Netty核心指南——Java IO 与 NIO基础
Netty系列文章
目录
1. I/O基础入门
1.1 Linux网络I/O模型简介
- Linux的内核将所有外部设备都看作一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个文件描述符fd。
- UNIX提供了5种I/O模型:
- 阻塞IO模型
在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间会一直等待。
- 非阻塞IO模型
如果该缓冲区没有数据就返回错误,轮询检查状态,看内核有无数据到来。
- IO复用模型
Linux 提供了select和poll系统调用用于检测多个文件描述符(fd)的就绪状态,但它们采用顺序扫描方式,效率低且支持的 fd 数量有限。为提升性能,Linux 引入了基于事件驱动机制的epoll,替代顺序轮询,实现高效的就绪事件通知。
- 信号驱动IO模型

- 异步IO:
与信号驱动的主要区别是——信号驱动IO由内核通知我们何时可以开始一个IO操作;异步IO模型由内核通知我们IO操作何时已经完成。
- 阻塞IO模型
1.2 I/O多路复用技术
- 该技术把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统多线程/多进程模型比,最大的优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行。
- 目前支持IO多路复用的系统调用有select、pselect、poll、epoll。在Linux网络编程中,很长一段时间都用select做轮询和网络事件通知,但select有固有缺陷,后面着了epoll作为替代方案。epoll的改进如下:
- 支持一个进程打开的socket描述符不受限制
- I/O效率不会随着FD数目的增加而线性下降
- 使用mmap加速内核与用户空间的消息传递
- API更加简单
2. 传统的BIO编程
网络编程的基本模型是Client/Server模型——服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,三次握手建立连接后,通过网络套接字进行通信。
2.1 BIO通信模型图
服务端通常由一个独立的Acceptor线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成后通过输出流返回应答给客户端,线程销毁。
- 问题:缺乏弹性伸缩能力,客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系。随访问量增大,可能发生线程堆栈溢出、创建新线程失败等问题。
2.2 同步阻塞式I/O创建的TimeServer源码分析
-
根据传入参数设置监听端口。

-
通过无限循环来监听客户端连接,如果没有客户端接入,主线程阻塞在
ServerSocket的accept操作上; -
当有新客户端接入时,以
Socket为参数构造TimeServerHandler对象,以它为构造函数参数创建一个新的客户端线程处理这条Socket链路。
-
通过
BufferedReader读取一行,如果非空则对内容判断是否是“QUERY TIME ORDER”,是则获取系统时间,发送给客户端。
-
释放输入流、输出流和Socket套接字句柄资源,最后线程自动销毁并被虚拟机回收。


2.3 同步阻塞式I/O创建的TimeClient源码分析
- 客户端通过
Socket创建,发送查询时间服务器的“QUERY TIME ORDER”指令,然后读取服务器的响应并将结果打印出来,随后关闭连接,释放资源,程序退出执行。
3. 伪异步I/O编程
为了解决同步阻塞I/O的一个链路一个线程处理的问题,线程模型优化—>后端通过一个线程池来处理多个客户端的请求介入(客户端M个,线程池最大线程数N个,M可以远远大于N),通过线程池灵活调配线程资源。
3.1 伪异步I/O模型图
采用线程池和任务队列实现伪异步的I/O通信框架。
新客户端接入时,将客户端Socket封装成一个Task投递到后段的线程池中进行处理,JDK线程池维护一个消息队列和N个活跃线程,对消息队列中的任务进行处理。线程池可以设置消息队列大小和最大线程数,因此它的资源占用是可控的,无论多少个客户端并发访问都不会导致资源耗尽和宕机。
3.2 伪异步I/O创建的TimeServer源码分析
- 服务端:首先创建一个时间服务器处理类的线程池,当接收到新的客户端连接时,将请求Socket封装成一个Task,然后调用线程池的execute方法执行。
- 客户端:代码不变。
- 它底层的通信依然采用同步阻塞模型。
3.3 伪异步I/O弊端分析
- 对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞,如果对方要60s才能将数据发送完成,读取一方的I/O线程也将会被同步阻塞60s,在此期间其他接入消息只能在消息队列中排队。
- (根据输入和输出流的API文档)读和写操作都是同步阻塞的,阻塞时间取决于对方I/O线程的处理速度和网络I/O的传输速度。
- 所以它无法从根本上解决同步I/O导致的通信线程阻塞的问题。
4. NIO编程
- 在这称之为Non-block I/O。与Socket类和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel套接字通道实现。
- 这两个新增通道都支持阻塞和非阻塞两种模式,阻塞模式简单但性能低不可靠,非阻塞模式相反。
- 低负载低并发的应用程序–>选择同步阻塞I/O以降低编程复杂度;高负载高并发–>用NIO的非阻塞模式。
4.1 NIO类库简介
-
缓冲区Buffer
- Buffer是一个对象,包含要写入或者读出的数据,在NIO库中,所有数据都是用缓冲区处理的。
- 它实质上是一个数组,通常是一个字节数组(也可以使用其他种类)。

- 每个Buffer类都是Buffer接口的一个子实例。ByteBuffer在具有一般缓冲区的操作之外还提供了一些特有的操作,以方便网络读写。
-
通道Channel
- 网络数据通过Channel读取和写入。通道与流不同之处是通道是双向的,可以读、写或者二者同时进行,是全双工的。
- Channel的类继承关系

自顶向下看,前三层主要是Channel接口,用于定义它的功能,后面是一些具体的功能类(抽象类)。实际上Channel可以分为两大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel。
-
多路复用器Selector
- 它提供选择已经就绪的任务的能力。Selector不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
4.2 NIO服务器端序列图

4.3 NIO客户端序列图

- NIO编程的优点:
- 客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果。
- SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他链路。
- 线程模型的优化:JDK的Selector在Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制,这意味着一个Selector线程可以同时处理成千上万个客户端连接。
5. AIO编程
- 也就是NIO 2.0,引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。它不需要通过Selector对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。
- 它是真正的异步I/O,在异步I/O操作时可以传递信号变量,当操作完成后会回调相关的方法。
6. 四种I/O的对比
6.1 概念澄清
- NIO类库支持非阻塞读和写操作,相比于之前的同步阻塞读和写,它是异步的,因此很多人习惯称NI为异步非阻塞I/O。但实际上它只能被称为非阻塞I/O。NIO 2.0(AIO)才是真正的异步I/O。
- 伪异步I/O概念完全来源于实践,它通过线程池 + 队列模拟出的异步效果。本质上还是同步阻塞 I/O,只是隔离了 I/O 线程和业务线程,避免阻塞业务线程。
6.2 不同I/O模型对比


7. 选择Netty的理由
- 如果想直接使用JDK的NIO类库进行开发–>JDK NIO的bug,比如epoll bug,它会导致Selector空轮询,最终导致CPU 100%。
- Netty定制能力强,可通过ChannelHandler对通信框架进行灵活地扩展。
- 性能高,与其他NIO框架对比,综合性能最优。
- 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦恼。
- 社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时更多的新功能会加入。
- 经历了大规模的商业应用考验,质量得到验证。
因为这些优点,Netty逐渐成为了Java NIO编程的首选框架。
更多推荐

所有评论(0)