JAVA NIO开发需要知道的Netty精粹

Netty是个高效的JAVANIO框架 , 总体框架基于异步非阻塞的设计 , 基于网络IO事件驱动 , 主要贡献在于可以让用户基于Netty提供的API快速开发高性能、高可靠性的网络应用 。 这篇文章主要是介绍Netty框架的基础技术——JAVANIO 。 这时候可能会有同学会有点小疑问 , 是异步IO(AIO)么?然而并不是 , 虽然JDK7也提供了异步IO(AIO)的接口 , 但是Netty曾经尝试过某个小版本 , 但是效果和NIO相比并没有什么优势 , 因此后面的版本Netty也把对AIO的支持废弃了 , 今天我们就来扒一下JAVANIO 。 四种IO模型简述
我们先从四种IO模型开始扒起 , 常见的IO模型有四种(这四种模型在网络上也有很多很多的资料 , 为较少篇幅本片将这部分内容压缩一下):
同步阻塞(BlockingIO):最简单的一种IO模型 , 用户线程在进行IO操作的时候通常是个系统调用 , 用户线程会由用户空间进入内核空间 , 内核空间数据包准备好后会将数据拷贝到用户空间 , 这个时候线程在用户态继续执行 。
同步非阻塞(Non-blockingIO):同步非阻塞IO即在同步阻塞的基础之上将socket设置为NONBLOCK 。 这样用户线程在发起IO操作之后可以立即返回 , 但是用户线程需要不断轮询来请求数据 。
IO多路复用(IOMultiplexing):即Reactor设计模式 , 多路复用模型从流程上和同步阻塞的区别不大 , 主要区别在于操作系统为用户提供了同时轮询多个IO句柄来查看是否有IO事件的接口(如select) , 这从根本上允许用户可以使用单个线程来管理多个IO句柄的问题 。
异步IO(AsynchronousIO):即Proactor设计模式 。 在异步IO模型中 , 用户不需要去轮询IO事件 , 然后才进行数据的读取 , 处理;在异步IO模型中 , IO事件就绪的时候 , 内核会开启一个独立的内核线程去执行执行IO操作 , 实现真正的异步IO 。 这个时候用户线程可以直接读取内核线程准备好的数据 。
多路复用IO模型和异步IO模型的区别主要是用户线程得知IO事件的时候在多路复用IO模型中 , 用户线程需要自己去处理IO , 而在异步IO模型中数据已经由内核线程为用户线程准备好了 。 在实际应用中 , 在高效的IO应用中 , 最常见的是第三种IO模型 , 异步IO目前操作系统方面的支持并不是很好而且在性能数据上并不是很好看 。
上面对四种IO模型进行了极其简单的概括 , 如多读者意犹未尽可以在网上查阅相关资料或者和作者联系 。 select、poll和epoll
JAVA对NIO的支持是从1.4版本开始的 , 是基于多路复用技术 , 而在linux操作系统方面多路复用技术有三种常用的机制:select、poll和epoll , epoll的支持也只是linux2.6版本之后才提供 , java在jdk5.0的update9之后才对epoll进行支持 。 这三种机制本质上都是同步IO , 主要是由于他们都需要在读写事件就绪的时候需要自己进行读写 , 也就是这个这个读写过程是阻塞的 。 下面对这三种机制进行简单总结:
select函数:该函数允许进程指示内核等待多个事件中的任何一个发生的时候或者在一定时间之后被唤醒 , select有个致命的缺点即在多路复用中文件描述符的数量有限制 , 如果需要突破限制需要重新编译操作系统内核 。
poll函数:poll机制与select机制类似 , 区别是poll没有最大描述符限制 。
epoll函数:epoll在linux2.6内核中被提出来 , 是之前的select和poll的增强版本 。 epoll也没有文件描述符数量限制 , 而且是用一个文件描述符来管理多个描述符 。 在性能上相比上面两种有了很大的优化 。
关于select、poll和epoll的详细介绍可以参考这里 。 JAVANIO
JAVA的NIO是基于IO多路复用模型 , 在不同平台上有不同的实现方式 。 Linux下面用的是poll和epoll , 在BSD上用kqueue , 在Windows上是重叠I/O 。
在JAVANIO中有三个核心的组件:Channels、Buffers和Selectors 。
JAVA NIO开发需要知道的Netty精粹
文章图片
JAVANIO核心组件
在JAVANIO中 , 基本上所有的IO都是从Channel开始的 , 读取操作即从Channel读到Buffer , 写操作即从Buffer写入Channel 。
JAVA NIO开发需要知道的Netty精粹
文章图片
NIO读写示意图Channel
在网络IO方面 , Channel的主要实现是ServerSocketChannel和SocketChannel 。 他们都代表一个面向流的可监听读写事件的socket 。 ServerSocketChannel是用于服务器端的socket , 他提供了一个静态工具方法open来为用户提供获取Channel的工具:publicstaticServerSocketChannelopen()throwsIOException{returnSelectorProvider.provider().openServerSocketChannel();}
其中涉及到的SelectorProvider用于创建具体的Channel , SelectorProvider的获取有三种途径 , 首先从系统属性中获取key为java.nio.channels.spi.SelectorProvider的值 , 如果没有则基于SPI机制来获取 , 如果再没有则最后提供默认的 , 这个默认值跟操作系统平台相关 , 比如我的mac系统 , JDK提供的默认Provider是KQueueSelectorProvider 。
JAVA NIO开发需要知道的Netty精粹
文章图片
ServerSocketChannel提供的接口
ServerSocketChannel的使用方式是面向服务器端的 , 一般的开发流程是:
获取一个ServerSocketChannel 。
设置网络操作 , 这些参数主要是和TCP协议有关 。
将ServerSocketChannel注册到Selector(多路复用器) 。
将ServerSocketChannel和某个具体的地址绑定 。
用户像多路复用器设置感兴趣的IO事件 。
用户线程以阻塞或非阻塞方式轮询Selector来查看是否有就绪的IO事件 。
用户针对不同的IO事件对Channel进行具体的IO操作 。
SocketChannel主要是面向客户端的开发的 , 也是以open方式获取channel , 客户端的开发流程大致如下:
获取一个SocketChannel 。
设置Channel为非阻塞方式 。
获取Selector 。
将channel注册到Selector , 并监听CONNECT事件 。
调用channel的connect方法连接指定的服务器和端口 。
如果连接成功则进行IO操作 , 如果没成功则轮询Selector处理CONNECT事件 。 Selector
Selector是JAVANIO中的多路复用器 , 配合SelectionKey使用 , SelectionKey代表着一个Channel和Selector的关系的抽象 , Channel向Selector注册的时候产生 , 由Selector维护 。 Selector维护着三个SelectionKey的集合:
keyset:这个集合包含所有向Selector注册的Channel产生的SelectionKey , 这个集合中的SelectionKey是不能直接被修改的 , 除非SelectionKey被channel , 并且发生select的时候SelectionKey才被移出 。
selectedkeyset:这个集合是keyset集合的子集 , 当有SelectionKey关联的Channel有Channel向Selector注册的IO事件就绪的时候并且有select操作 , 对应的SelectionKey会被放到selectedkeyset中 。 因为这个集合中的SelectionKey可以通过直接调用Set的remove将SelectionKey移除 。
cancelled-key:这个集合是也是keyset的子集 。 当有已经向Selector注册的Channel发生degistered的时候 , SelectionKey将被放到这个集合 , 并且在下一次select的时候被从所有的集合中移出 。
三种集合的流转我画个图表示一下:
JAVA NIO开发需要知道的Netty精粹
文章图片
Selector的SelectionKey集合流转图
在开发过程中 , 我们可以将多个Channel注册到一个Selector实例中 , 用一个线程来处理所有的IO事件 , 我们也可以将多个Channel注册到多个Selector实例中 , 结合高效的线程模型可以达到很好的效果 。 ByteBuffer
JAVANIO直接和Channel打交道的Buffer是ByteBuffer , ByteBuffer接口提供主要的内存分配、IO读写等相关接口 。 值得注意的是JAVANIO提供了两种Buffer内存分配机制 , 一种是堆内存 , 另一种是直接内存 , 主要区别:
堆内存分配和回收比较快 , 但是网络数据需要从内核copy到堆中 。
直接内存分配和回收比较慢 , 但是免去了从内核copy到堆中的一次copy 。
这两种内存各有千秋 , 使用的时候要根据实际情况去选择 。
总结:
这篇文章主要介绍一下JAVANIO涉及到的一些基础概念以及JAVA提供的NIO接口进行简单介绍 , JAVANIO提供的接口使用起来 , 略复杂 , 实际项目中不建议直接使用JDK提供的API进行开发 。 Netty是一个基于JAVANIO开发的可靠的JAVANIO工具 , Netty的精粹我认为除了IO模型之外还有下面的几个部分:
高效的线程模型
内存池技术
零copy技术
Netty是一个优秀的开源NIO框架 , 我们可以使用它来快速构建高性能的IO服务器 , 后面我会通过继续深入学习和大家一起分享Netty的实现和原理 。
作者:陆晨
【JAVA NIO开发需要知道的Netty精粹】来源:开源中国Float_Luuu


    推荐阅读