且构网

分享程序员开发的那些事...
且构网 - 分享程序员编程开发的那些事

Nio2Endpoint组件:Tomcat如何实现异步I/O?(上)

更新时间:2021-08-30 03:42:34

NIO是同步非阻塞,NIO已经足够好了,Java为什么还要NIO.2呢?


NIO和NIO.2最大的区别?


一个是同步一个是异步。而异步最大特点是,应用程序无需自己触发数据从内核空间到用户空间的拷贝


为何是应用程序去“触发”数据拷贝,而非直接从内核拷贝数据?



应用程序无法访问内核空间,数据拷贝必须由内核负责,问题是谁来触发?


  • 内核主动将数据拷贝到用户空间并通知应用程序
  • 还是等待应用程序通过Selector来查询,当数据就绪后,应用程序再发起一个read调用,这时内核再把数据从内核空间拷贝到用户空间。


数据从内核空间拷贝 =》 用户空间这段时间,应用程序还是阻塞的。所以异步效率高于同步,因为异步模式下应用程序始终不会被阻塞。



  • ServerSocket:用于在本机(Server端)开一个端口,被动的等待数据(用accept()方法),与 Client 端端建立连接后可以进行数据交换
  • Socket:用于连接远端机器(Server端)上的一个端口,主动发出数据,建立连接后也可以接收数据。


网络数据读取在异步模式下的工作过程

应用程序调用read API,同时告诉内核:


  • 数据准备好了后,拷贝到哪个Buffer
  • 调用哪个回调函数去处理这些数据


之后,内核接到该read指令,等待网卡数据到达。

数据到达后,产生硬件中断,内核在中断程序把数据从网卡拷贝到内核空间

接着做TCP/IP协议层的数据解包和重组,

再把数据拷贝到应用程序指定的Buffer,

最后调用应用程序指定的回调函数。


异步模式下,应用程序当了“需求甲方”,内核则忙前忙后,但最大限度提高了I/O通信效率。

Linux内核2.6的AIO都提供了异步I/O的支持,但还不完善,详情可以看这里:http://lse.sourceforge.net/io/aio.html

Java的NIO.2 API是对os异步I/O API的封装,通过epoll实现的。



Java NIO.2

服务端程序

Nio2Endpoint组件:Tomcat如何实现异步I/O?(上)



为什么需要创建一个线程池?



异步I/O模型下,应用程序不知道数据何时到达,因此向内核注册回调方法,当数据到达时,内核就会调用该回调方法。

同时为提高处理速度,会提供一个线程池给内核使用,这样不会耽误内核线程工作,内核只需把工作交给线程池就立即返回了。


回调类AcceptHandler



Nio2Endpoint组件:Tomcat如何实现异步I/O?(上)


它实现了CompletionHandler接口    

Nio2Endpoint组件:Tomcat如何实现异步I/O?(上)



两个模板参数V和A,分别表示



  • I/O调用的返回值
    比如accept的返回值就是AsynchronousSocketChannel
  • 附件类
    附件类由用户自己决定。


在accept的调用中,我们传入一个Nio2Server。因此AcceptHandler带有了两个模板参数:AsynchronousSocketChannel和Nio2Server。


CompletionHandler有两个方法:completed和failed,分别在I/O操作成功和失败时调用。completed方法有两个参数,其实就是前面说的两个模板参数。也就是说,Java的NIO.2在调用回调方法时,会把返回值和附件类当作参数传给NIO.2的使用者。



处理读的回调类ReadHandler

public class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {   
    // 读取到消息后的处理  
    @Override  
    public void completed(Integer result, ByteBuffer attachment) {  
        // attachment就是数据,调用flip操作,其实就是把读的位置移动最前面
        attachment.flip();  
        // 读取数据
        ... 
    }  

    void failed(Throwable exc, A attachment){
        ...
    }
}


read调用的返回值是一个整型,所以回调方法里:



  • 第一个参数是个整型
    表示有多少数据被读取到了Buffer中
  • 第二个参数是一个ByteBuffer
    因为调用read方法时,把用来存放数据的ByteBuffer当作附件类传进去了,所以在回调方法有ByteBuffer类型参数,直接从该ByteBuffer获取数据


Nio2Endpoint

Nio2Endpoint的组件

Nio2Endpoint组件:Tomcat如何实现异步I/O?(上)



总体工作流程类似NioEndpoint。

Nio2Acceptor扩展Acceptor,用异步I/O接收连接,跑在一个单独线程,也是一个线程组。