从历史发展来看,一个新方法的出现必然先出现一个效率较低的方法,然后人们才会对其进行改进。只有首先了解效率较低的方法,才能了解新技术的本质。所以我们需要了解什么是BIO?传统的BIO是流式传输的,会造成一个问题:当客户端发送消息太慢,耗时太长时,接收方会不断阻塞。也就是说如果发送端需要60s来完成数据传输,那么接收端会阻塞60s。为了方便理解,我们看一下通常的BIO伪代码写法:服务端无限循环创建一个ServerSocket对象,不断调用ServerSocket.accept,等待Socket对象启动一个线程,读取输入通过Socket对象流,读取和处理数据,写入响应数据客户端创建一个Socket对象,通过Socket对象获取输出流,写入数据通过伪代码可知。当有新的客户端连接时,服务端会创建一个新的线程来处理客户端的请求(所谓的伪异步I/O),当客户端有几万个并发请求时,BIO肯定无法处理支持它。由于BIO底层最终是调用linux内核函数recvfrom实现的,返回数据的节点是数据包到达复制到应用进程缓冲区或者出错时,所以这期间会阻塞等待时期。这种I/O模型是UNIX中定义的五种I/O模型中的阻塞I/O模型NIO。于是,基于epoll的复用技术的NIO诞生了,就是为了解决BIO这个大问题。NIO不是一个已建立的scoket连接拿到文件描述符(fd)后,直接调用recvfrom函数读取数据。它将fd提供给epoll函数。epoll函数是基于自身的事件驱动机制。当检测到socket对应的fd的数据可读时,触发回调函数告诉用户进程读取数据。题外话,select/poll也属于I/O多路复用模型(epoll当然有),但是JavaNIO的底层为什么不用它们来实现呢?java训练的原因是select是基于轮询机制检测所有的fd,只有有可读的fd才返回。如果没有可读的fd,就会阻塞在select中。它的时间复杂度是O(n),select是基于效率和性能的考虑。有一个最大限制,默认是1024。epoll的最大限制受系统中最大文件句柄数的限制。poll的实现机制和select类似。以上就是对NIO底层原理的简单分析。基于这样的机制,JavaNIO封装了一套类库供开发者使用。但是原生的JavaNIO类库还是过于繁琐,不利于使用,所以Netty后来诞生了。要想了解Netty,就必须熟悉Java原生NIO类库的一些概念。JAVA原生NIOAPI的服务端和客户端的编写一般步骤如下:服务器端:创建一个ServerSocketChannel对象,同时将其接收事件注册到多路复用器(Selector)和启动线程(Reactor)不断轮询准备就绪的频道。当有就绪的Channel集合时,多路复用器会返回SelectionKey集合,轮询SelectionKey集合,确定对应的事件类型4.1如果是接收事件,通过ServerSocketChannel.accept()创建一个SocketChannel(相当于完成TCP三-way握手并建立物理链路),并将SocketChannel的读取事件注册到多路复用器。4.2如果是读事件,读取Channel的数据。4.3如果是写事件,写数据。写入后,写入通道事件从多路复用器中删除。如果TCPbuffer写了半个包就满了,不能再写了,它会继续把Channel的写事件注册到multiplexer,等待下一次轮询。客户端创建一个SocketChannel对象并接收事件,注册到多路复用器上,启动Reactor线程,不断轮询就绪的Channel。当有就绪的Channelset时,multiplexer会返回SelectionKeyset,轮询SelectionKeyset来确定对应的。如果事件类型是接收事件(表示TCP已经建立),将Channel的读事件注册到多路复用器。写数据的简单总结就是在JavaNIO中,Channel的所有接收事件,读写事件都注册到Multiplexer上,然后通过不断的轮询Multiplexer上是否有就绪的Channel,然后判断事件根据事件处理对应就绪Channel的type。
