从历史发展角度看,一个新方法的出现,必然是先出现一种不太高效的方法,人们再加以改进。只有先理解了不太高效的方法,才能够理解新技术的本质。所以我们需要先了解一下什么是BIO?
传统的BIO采用流的方式进行传输,会造成一个问题:当客户端发送消息过于缓慢耗时太长,那么接收一方就会持续阻塞下去。也就是说如果发送方需要60s才能将数据传输完毕,那么接受方将会阻塞60s。为方便理解我们先看看一惯的BIO伪代码写法:
服务端
1. 创建ServerSocket对象
2. 死循环不断调用ServerSocket.accept,等待拿到Socket对象
3. 开启线程 通过Socket对象读取输入流
4. 读取并处理数据
5. 写出响应数据
客户端
1. 创建Socket对象
2. 通过Socket对象获取输出流,写入数据
通过伪代码可以知道,当新接入一个客户端后服务端会新建一个线程来处理客户端请求(所谓的伪异步I/O),当有成千上万客户端并发请求,BIO必定支撑不了。由于BIO 底层最终是调用linux内核函数recvfrom实现的,它返回数据的节点是在数据包到达且被复制到应用进程缓冲区中或者发生错误,所以在此期间它会一直阻塞等待着。
此I/O模型为UNIX中定义的5种I/O模型的阻塞I/O模型
NIO
因此基于epoll的多路复用技术实现的NIO横空出世解决了BIO这一大病垢,NIO并不是建立scoket连接拿到文件描述符(fd)后就直接调用recvfrom函数进行数据读取,它则是将 fd给到epoll函数,由epoll函数基于自身的事件驱动机制,当检测到socket对应的fd 的数据可读时,触发回调函数告诉用户进程进行数据读取。
题外,select/poll也属于I/O复用模型(当然epoll也是),但为什么Java NIO底层不用它们进行实现呢?原因是 select 它们是基于轮询的机制检测所有fd,发现有可读fd才返回,如果没有则阻塞于select,它的时间复杂度 O(n),且select 基于效率和性能的考虑,是有最大限制,默认为1024。而epoll的最大限制是受限于系统最大文件句柄数。poll的实现机制和select类似。
以上是对NIO底层原理的一个浅析,Java NIO 是基于这样的一种机制封装出一套类库来,供开发人员使用。不过原生的JavaNIO 类库还是过于繁琐不利于使用,因此才有了后面Netty的出世。如果要了解Netty,那必须要对原生Java NIO 类库的一些概念有一定熟悉。
JAVA原生NIO API 编写服务端和客户端大致步骤如下:
服务端:
1. 创建ServerSocketChannel对象,同时将它的接收事件,注册到多路复用器(Selector)上
2. 开启线程(Reactor)不断轮询就绪Channel
3. 当存在就绪Channel集合时,多路复用器会返回SelectionKey 集合
4. 轮询SelectionKey集合判断对应的事件类型
4.1 如果为接收事件,则通过 ServerSocketChannel.accept() 创建SocketChannel(相当于了完成TCP三次握手,建立物理链路),并将该SocketChannel的读事件注册到多路复用器上
4.2 如果为读事件,则对该Channel进行数据读取
4.3 如果为写事件,则进行写数据,写完毕将该Channel写事件从多路复用器上移除。若写半包后TCP缓冲区已满,无法再写时,会继续将该Channel的写事件继续注册到多路复用器上,等待下次轮询。
客户端
1. 创建SocketChannel对象同时它接收事件,注册到多路复用器上
2. 开启Reactor线程,不断轮询就绪Channel
3. 当存在就绪Channel集合时,多路复用器会返回SelectionKey 集合
4. 轮询SelectionKey集合判断对应的事件类型
如果为接收事件(表示已建立TCP),则将Channel的读事件注册到多路复用器上。
5. 写数据
简单的总结就是 Java NIO中将通道的接收事件、读、写事件全部注册到多路复用器之上,然后通过不断的轮询多路复用器上是否已经有就绪Channel,然后判断对应就绪Channel的事件类型,根据事件进行处理。
相关java培训开发技术知识,关注我,有更多精彩内容云您分享!