非阻塞式IO通信

一、简介

非阻塞IO(NIO)弥补了原来同步阻塞IO的不足,NIO有三个重要概念:

  • 缓冲区Buffer:缓冲待读写处理的数据,NIO是读写数据操作的就是Buffer;

  • 通道Channel:数据通过的双向通道;

  • 多路复用器Selector:负责多路复用;

二、NIO服务端&客户端流程

下面是NIO服务端的时序图:

  1. 打开ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道(对应BIO的ServerSocket);

  2. 绑定监听端口,设置连接方式为非阻塞模式;

  3. 创建Reactor线程,创建多路复用器Selector并启动线程;

  4. 将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听ACCEPT事件;

  5. 多路复用器在线程run方法的无限循环体内轮询准备就绪的Key;

  6. 多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路;

  7. 设置客户端链路为非阻塞模型;

  8. 将新接入的客户端连接注册到Reactor线程的多路复用器上,监听读操作,读取客户端发送的网络消息;

  9. 异步读取客户端消息到缓冲区;

  10. 对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排;

  11. 将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端。

注意:如果发送区TCP缓冲区满,会导致写半包,此时,需要注册监听写操作位,循环写,直到整包消息写入TCP缓冲区。

下面是NIO客户端的时序图:

  1. 打开SocketChannel,绑定客户端本地地址(可选,默认系统会随机分配一个可用的本地地址);

  2. 设置SocketChannel为非阻塞模式,同时设置客户端连接的TCP参数;

  3. 异步连接服务端;

  4. 判断是否连接成功,如果连接成功则直接注册读状态位到多路复用器中(步骤10),如果当前没有连接成功(异步连接,返回false,说明客户端已经发送sync包,服务端没有返回ack包,物理链路还没有建立),向多路复用器注册连接状态位(步骤5);

  5. 向Reactor线程的多路复用器注册OP_CONNECT状态位,监听服务端的TCP ACK应答;

  6. 创建Reactor线程,创建多路复用器并启动线程;

  7. 多路复用器在线程run方法的无限循环体内轮询准备就绪的key;

  8. 接收connect事件并进行处理;

  9. 判断连接是否完成,如果完成执行步骤10;

  10. 注册读事件到多路复用器;

  11. 异步读客户端请求到缓冲区;

  12. 对ByteBuffer进行编解码,如果有半包消息接收缓冲区Reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排。

  13. 将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端。

三、代码实例

3.1 服务端

TimeServer.java

MultiplexerTimeServer.java

3.2 客户端

TimeClient.java

TimeClientHandle.java

3.3 运行服务端和客户端

服务端运行结果:

客户端运行结果:

四、NIO的好处

  1. 客户端发起的链接操作是异步的,可以通过多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞;

  2. SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样IO通信线程就可以处理其他的链路,不需要同步等待这个链路可用;

  3. 线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者单个进程的句柄数限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而性能下降。因此,它非常适合做高性能、高负载的网络服务器。

参考:

《Netty权威指南》

Last updated

Was this helpful?