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
服务端程序
为什么需要创建一个线程池?
异步I/O模型下,应用程序不知道数据何时到达,因此向内核注册回调方法,当数据到达时,内核就会调用该回调方法。
同时为提高处理速度,会提供一个线程池给内核使用,这样不会耽误内核线程工作,内核只需把工作交给线程池就立即返回了。
回调类AcceptHandler
它实现了CompletionHandler接口
两个模板参数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的组件
总体工作流程类似NioEndpoint。
Nio2Acceptor扩展Acceptor,用异步I/O接收连接,跑在一个单独线程,也是一个线程组。