【README】

1.本文总结自B站《netty-尚硅谷》,很不错;

2.文末有错误及解决方法;


【1】群聊需求

1)编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非
阻塞)
2)实现多人群聊;
3)服务器端:可以监测用户上线,离线,并实现消息转发功能;
4)客户端:通过channel 可以无阻塞发送消息给其它所有用户,同时可以接受
其它用户发送的消息(有服务器转发得到);


【2】概要设计

1)服务器端:

  • 服务器启动并监听 6667 ;
  • 服务器接收客户端消息,并实现转发,处理上线 与  离线;

2)客户端

  • 连接服务器;
  • 发送消息;
  • 接收服务器消息 ;

【3】代码实现及自测

【3.1】服务器端

/*** @Description 群聊服务器端 * @author xiao tang* @version 1.0.0* @createTime 2022年08月19日*/
public class NIOGchatServer {private Selector selector;private ServerSocketChannel listenChannel;private static final int PORT = 6667;/*** @description 构造器* @author xiao tang* @date 2022/8/19*/public NIOGchatServer() {try {// 得到选择器selector = Selector.open();// 初始化 ServerSocketChannellistenChannel = ServerSocketChannel.open();// 绑定端口listenChannel.socket().bind(new InetSocketAddress(PORT));// 设置非阻塞模式listenChannel.configureBlocking(false);// 把listenChannel注册到selector,事件为 ACCEPTlistenChannel.register(selector, SelectionKey.OP_ACCEPT);} catch (Exception e) {e.printStackTrace();System.out.println("群聊服务器构造异常");}}public static void main(String[] args) throws IOException {// 创建服务器对象,并监听端口new NIOGchatServer().listen();}/*** @description 监听* @param* @author xiao tang* @date 2022/8/19*/public void listen() throws IOException {while(true) {// 等待客户端请求连接selector.select();// 获取选择key集合Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();if (key.isAcceptable()) { // 通道发生连接事件SocketChannel sc = listenChannel.accept();sc.configureBlocking(false); // 设置为非阻塞// 将 sc 注册到 selector 上sc.register(selector, SelectionKey.OP_READ);// 提示System.out.println(sc.getRemoteAddress() + " connected successfully.");}if (key.isReadable()) { // 通道发生 read 事件// 处理读this.readData(key);}// 用完之后,要移除keyiterator.remove();}}}/** * @description 读取客户端消息* @param key 选择键* @return * @author xiao tang * @date 2022/8/19 */private void readData(SelectionKey key) {// 定义一个socketchannelSocketChannel channel = null;try {// 取到关联的channelchannel = (SocketChannel) key.channel();// 创建缓冲 bufferByteBuffer buffer = ByteBuffer.allocate(1024);int count = channel.read(buffer);// 根据count的值做处理if (count > 0) {// 把缓冲区的数据转为字符串并输出String msg = new String(buffer.array(), 0, count, StandardCharsets.UTF_8);// 输出该消息System.out.println(msg);// 向其他客户端转发消息this.forward2OtherClients(msg, channel);}} catch (IOException e) {e.printStackTrace();try {System.out.println(channel.getRemoteAddress() + " has been offline.");// 取消注册key.channel();// 关闭通道channel.close();} catch (IOException e2) {e2.printStackTrace();}}}/*** @description 消息转发给其他客户端* @param msg 消息* @param self 当前 SocketChannel* @author xiao tang* @date 2022/8/19*/private void forward2OtherClients(String msg, SocketChannel self) throws IOException {// 遍历所有注册到 selector 上的 SocketChannel 并排除自己for (SelectionKey key : selector.keys()) {// 排除自己if (key.equals(self.keyFor(selector))) continue;// 通过key 取出对应的 SocketChannelChannel targetChannel = key.channel();// 消息转发if (targetChannel instanceof SocketChannel) {SocketChannel dest = (SocketChannel) targetChannel;// 把 msg 存储到bufferByteBuffer buffer = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8));// 把buffer数据写入通道dest.write(buffer);}}}}

【3.2】客户端

/*** @Description NIO群聊客户端* @author xiao tang* @version 1.0.0* @createTime 2022年08月19日*/
public class NIOGchatClient {// 定义相关的属性private static final String HOST = "127.0.0.1"; // 服务器ip地址private static final int PORT = 6667; // 服务器端口private Selector selector;private SocketChannel socketChannel;private String userName;// 线程池private static ExecutorService THREAD_POOL = Executors.newCachedThreadPool();public static void main(String[] args) {try {// 启动客户端NIOGchatClient client = new NIOGchatClient();// 启动一个线程,每隔3秒读取服务器发送的数据THREAD_POOL.submit(new Runnable() {@Overridepublic void run() {while(true) {try {client.read();TimeUnit.SECONDS.sleep(3);} catch (Exception e) {e.printStackTrace();break;}}}});// 客户端接收控制台输入,并发送数据给服务器Scanner scanner = new Scanner(System.in);while(scanner.hasNextLine()) {client.send(scanner.nextLine());}} catch (Exception e) {e.printStackTrace();} finally {THREAD_POOL.shutdown();}}/*** @description 构造器* @author xiao tang* @date 2022/8/19*/public NIOGchatClient() throws IOException {this.selector = Selector.open();// 连接服务器socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", PORT));// 设置非阻塞socketChannel.configureBlocking(false);// 将 channel 注册到 selector,事件 READsocketChannel.register(this.selector, SelectionKey.OP_READ);// 得到userNameuserName = socketChannel.getLocalAddress().toString().substring(1);System.out.println(userName + " connected server successfully.");}/*** @description 发送消息到服务器* @param msg 消息* @author xiao tang* @date 2022/8/19*/private void send(String msg) {msg = userName + ":" + msg;ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes(StandardCharsets.UTF_8));try {socketChannel.write(buffer);} catch (IOException e) {e.printStackTrace();}}/*** @description 从通道读取数据并显示* @author xiao tang* @date 2022/8/20*/private void read() throws IOException {selector.select();// 存在可用通道,读取数据并显示 (注意这里是 selector.selectedKeys() 而不是 selector.keys() )Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();// 若可读通道,则读取if (key.isReadable()) {SocketChannel sc = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int count = sc.read(buffer);System.out.println(new String(buffer.array(), 0, count , StandardCharsets.UTF_8));}// 用完key要移除iterator.remove();}}
}

【3.3】测试效果


【4】报错及解决

1)问题1:为什么要移除key ?

// 用完之后,要移除key
iterator.remove();

refer2 Why the key should be removed in `selector.selectedKeys().iterator()` in java nio? - Stack Overflow

There are 2 tables in the selector:

  1. registration table: when we call channel.register, there will be a new item(key) into it. Only if we call key.cancel(), it will be removed from this table.

  2. ready for selection table: when we call selector.select(), the selector will look up the registration table, find the keys which are available, copy the references of them to this selection table. The items of this table won't be cleared by selector(that means, even if we call selector.select() again, it won't clear the existing items)

That's why we have to invoke iter.remove() when we got the key from selection table. If not, we will get the key again and again by selector.selectedKeys() even if it's not ready to use.

大意就是:选择器中有2个表,分别是 表1是注册表; 表2是就绪选择表

调用 selector.select() 时, 注册表1中对应通道有事件的key 会被拷贝到就绪选择表2;而 选择器不会清理表2的key;即便我们重复调用 selector.select() 时,它也不会清理表2的key;

这也就是为什么我们从选择表2中获得key后,会调用 it.remove() 清理掉key;如果不清理,我们重复调用 selector.selectedKeys() 时,还是会获取之前的key,即便这些key对应 通道没有事件,这就会导致报空指针

2)分清楚 selector.selectedKeys() 和 selector.keys() 的 区别:

  • selector.selectedKeys():获取有事件发生的通道对应的键集合,如 ACCEPT事件,READ事件;
  • selector.keys():获取注册到当前选择器的所有通道对应的key集合;(因为通道要先实现多路复用,就需要注册到选择器,选择器会产生一个key,与通道关联起来);

3)为什么客户端或服务器在读取缓冲区的内容时,我要通过offset + 长度去获取?如 代码:

// 若可读通道,则读取
if (key.isReadable()) {SocketChannel sc = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int count = sc.read(buffer);System.out.println(new String(buffer.array(), 0, count , StandardCharsets.UTF_8));
}

【代码解说】

  • 上述代码的最后一行,offset 等于0, 长度是count;
  • 因为如果不使用 count 限定buffer范围的话,打印出来有很多换行。(当然是我的测试案例里是有换行 ,有兴趣的同学可以自己试下);
  • 加了count,限定范围后,就没有换行了。

4.基于NIO的群聊系统相关推荐

  1. 手写一个NIO群聊系统

    一.浅谈NIO 1. 什么是NIO? ​​Java NIO​​:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 ​​I/ ...

  2. Java NIO 应用案例:实现一个简单的群聊系统

    1 案例要求 编写一个 NIO 多人群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞): 服务器端功能: 监测用户上线,离线: 实现客户端消息的转发功能(将该客户端的消息转发给其它客户端): ...

  3. Java NIO网络编程之群聊系统

    概述 对ServerSocketChannel.SocketChannel.SelectionKey有一定的理解和了解对应API. NIO非阻塞网络编程相关关系梳理: 以下概念: ServerSock ...

  4. Java NIO群聊系统

    实例要求: 1.编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞) 2.实现多人群聊 3.服务器端:可以监测用户上线,离线,并实现消息转发功能 4.客户端:通过 Channe ...

  5. Netty学习笔记:二、NIO网络应用实例-群聊系统

    实例要求: 编写一个NIO群聊系统,实现服务器端和多个客户端之间的数据简单通讯(非阻塞): 实现多人群聊: 服务器端:可以监测用户上线.离线,并实现消息转发功能: 客户端:通过channel可以无阻塞 ...

  6. 群聊系统项目(基于TCP、UDP实现)

    最近学习了网络编程后, 写了一个基于TCP.UDP协议的群聊系统. 技术栈 1.TCP.UDP通信       2.生产.消费模型, 支持并发       3.自定义协议封装数据 & json ...

  7. (六)Netty网络编程应用实例-群聊系统

    实例要求: 编写一个NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞) 实现多人群聊 服务器端:可以监测用户上线,离线,并实现消息转发功能 客户端:通过channel可以无阻塞发送消息给 ...

  8. 分享我用Qt写的游戏组队群聊系统

    #ifndef GETSERVERINFO_H #define GETSERVERINFO_H#include <QObject> #include <QtNetwork/QTcpS ...

  9. 4. 彤哥说netty系列之Java NIO实现群聊(自己跟自己聊上瘾了)

    你好,我是彤哥,本篇是netty系列的第四篇. 欢迎来我的公从号彤哥读源码系统地学习源码&架构的知识. 简介 上一章我们一起学习了Java中的BIO/NIO/AIO的故事,本章将带着大家一起使 ...

最新文章

  1. JavaScript中this的指向问题
  2. EasyUI学习笔记8:MIS开发利器_ datagrid插件(下)(终结篇)
  3. 真正的问题应该在我身上……
  4. HashMap如何在Java中工作
  5. I00005 打印直角三角形字符图案
  6. c语言程序设计实验指导实验报告,C语言程序设计实验指导及报告.doc
  7. 在向服务器发送请求时发生传输级错误。
  8. 卡尔曼滤波器工作原理
  9. 人工智能的利弊?好处和危害都有哪些
  10. 腾达路由器 远端服务器未响应,腾达路由器设置完成不能上网的解决办法
  11. win11最新bug修复合集(来源于微软官方)
  12. QT 获取键盘组合键
  13. (Java) 实现打印菱形图案
  14. 围绕开放标准改进WSO2 API Manager密钥管理体系结构
  15. 非隔离DCDC变换器的CCM分析
  16. seo是什么|怎么做好seo|seo视频教程
  17. 【R】【线性回归分析实验】
  18. weblogic服务器设置编码
  19. 在WORD中自动生成目录,页码
  20. DPlayer使用笔记

热门文章

  1. maven本地仓库设置
  2. Linux 下后台运行程序,查看和关闭后台运行程序(转载)
  3. 计算机系统声音出不来怎么办,win7系统电脑没有声音怎么办? 是什么原因如何解决...
  4. 电脑耳机没声音怎么设置?(win7/win10电脑耳机没声音的解决方法)
  5. Problem--133A--Codeforces--A. HQ9+
  6. TensorFlow batch
  7. Core Data概述
  8. 剖析环境加密与文档加密
  9. 【Python学习笔记】简单调用百度API应用
  10. Unity中的AI算法和实现1-Waypoint