m1kasaz的Netty笔记
BIO模型和NIO模型
两个模型都是很常见的Java网络模型
BIO模型:同步阻塞模型,一个线程专门负责一个连接,数据以流的形式传输
NIO模型:同步非阻塞模型,一个线程负责多个连接,其功能被多个组件分摊,Channel为双方提供双向的通信通道,读写数据都要通过Buffer,Selector可以实现高效的多路复用器
两个模型就好比不同餐厅的招待方式,BIO的餐厅就像一位服务员专门负责一桌客人,而且会一直等待客人的命令;NIO餐厅则是一位服务员负责多桌客人,而且只要你不点餐(数据没准备好),我就不会在你的桌前停留
BIO策略只适合连接数量不多,且连接时间较长的情况,过多的连接将导致线程数量过多导致OOM
NIO策略下,一个线程被赋予处理多个请求的能力,并发量大大提高
NIO性能很高,为什么我们不直接使用它呢?
答:开发效率不高,使用NIO直接进行开发,开发者需要应付这些问题:
-
Selector 的复杂性:
Selector是 NIO 的核心,但其使用方式反直觉。你需要在一个循环中轮询(select())事件,然后遍历 selected keys,并针对每个 key 判断其是什么事件(可连接、可读、可写),再进行相应的处理。这个循环的编写、超时处理、以及性能优化(如避免空轮询bug)都非常棘手。 -
Channel 和 Buffer 的繁琐管理:
Channel和Buffer需要紧密配合。从Channel读取数据到Buffer,或从Buffer写入数据到Channel后,你必须记得调用Buffer.flip()、Buffer.clear()、Buffer.compact()等方法来正确切换缓冲区的读写模式。忘记调用或调用错误会导致数据错乱,且这类错误很难调试。 -
非阻塞语义:所有 I/O 操作都是非阻塞的,立即返回。这意味着写入操作可能无法一次性写完所有数据,你需要自己维护一个队列,在
OP_WRITE事件触发时继续发送,这大大增加了状态管理的复杂度。 -
需要自行设计多线程方案:原生 NIO 只提供了基础的 I/O 多路复用,但它没有规定如何组织线程。你是用一个线程处理所有连接?还是用一个
Selector负责接收,再用一个线程池处理业务逻辑?或者为每个Channel分配一个线程?这就是 Reactor 单线程、多线程、主从多线程等模型,但 NIO 本身不提供这些,需要开发者自己从头实现,并处理随之而来的线程安全问题。 -
CPU 密集型任务会阻塞 I/O 线程:如果你在
Selector的同一线程中处理业务逻辑,一个耗时操作会阻塞整个Selector,导致所有其他连接的响应延迟。你必须自己小心地将耗时任务提交到额外的业务线程池中,并处理好线程间的任务交接和数据同步。
其实还有很多问题需要解决,这里列出了一些常见问题,我们只需要知道开发者在使用NIO开发时极度麻烦的,我们需要一个统一的框架将其进行封装,这就是Netty,它的底层基于NIO,但是对其进行了再封装与优化
对NIO模型的一些补充:
NIO模型的优点在于它的多路复用机制,我们只需要一个线程就能实现对多个socket的监听,这里有三个比较关键的函数,分别对应操作系统的三个内核函数。
1 | Selector.open() 调用 epoll_create |
讲一下三个内核函数的作用。
- epoll_create:创建一个红黑树,以每个fd为节点,每个fd都配置一个回调函数,当有数据到达fd时,就会触发回调函数,将这个fd放入就绪列表中。
- epoll_ctl:将所有fd插入红黑树。
- epoll_wait:遍历就绪列表,获取就绪的fd。
Netty
这是一个标准的Netty的服务端程序:
1 | public class NettyServer { |
我们就借助它来分析以下为什么Netty能如此高效的处理连接
Netty的高效源自于它各司其职却紧密配合的各个组件
- EventLoop:事件循环,他与线程是一对一的关系,与Channel是一对多的关系,用于循环处理一个连接的生命周期内所发生的事件(接收数据、异常处理等)。
- EventLoopGroup:事件循环组,就是一个EventLoop的池子,可以近似理解为一个线程池,一般创建一个bossGroup专门负责连接请求,一个workerGroup专门负责读写请求。
- ChannelHandler:通道处理器,用来处理出站和入站的数据,如
ChannelInboundHandler:处理入站事件和数据(如连接已建立、接收到消息、异常发生、连接关闭等)。ChannelOutboundHandler:处理出站操作(如打开连接、关闭连接、写入数据、刷新数据等)。 - ChannelPipeline:通道管道,一个存放了ChannelHandler的容器,是一个双向链表,每一个节点都是一个ChannelHandler。
- ChannelFuture:通道异步结果,Netty的所有IO操作都是异步完成的,也就是说类似connect,write等方法都会调用另一个线程去执行,我们可以通过ChannelFuture回调执行结果等信息。
如果说我们将这些组件模拟成一个邮局系统EventLoop就是邮政员,负责不断的收信,发信等;EventLoopGroup就是邮局,里面有许多邮政员;ChannelHandler模拟寄信的业务流程(贴邮票、封装、发送);ChannelPipeline规定了这些业务流程的顺序;ChannelFuture当你将一封信交给邮政员的时候,他会给你一张回执,让你等待消息,这张回执就是ChannelFuture。
下面是一个标准Netty客户端代码,我们可以对比一下他和服务端的区别。
1 | public class NettyClient { |
Client只创建一个线程循环组,而Server一般创建两个,bossGroup处理连接,workerGroup处理读写。
Client和Server的Channel是不同的,拥有各自的pipeline和handler,他们的结构大致如下。

下面对handler的具体功能展开讨论。
- 编码器与解码器
我们知道,数据在网络中以二进制的形式传播,客户端想要向服务端发送文本数据或者其他形式的数据,就需要编码器,客户端想要识别出数据代表的含义,就需要解码器。
我们可以在配置的时候为服务端添加编解码器,channel会根据进站和出站来调用handler。
以服务端为例,入站就是数据进入服务端,一般是读操作;出站就是数据离开客户端,一般是写操作。客户端同理。
1 |
|
NIO与Netty
我从两方面进行一个大致的对比
-
使用体验
Netty的使用体验远比NIO要好。体现在NIO需要手动调用selector,进行创建,监听,轮询;还需要使用非常反人类的bytebuffer,不断切换读写指针;NIO还需要自己编写译码解码的处理器。这些操作都可以使用Netty很快解决。
-
性能
Netty使用Reactor模型分别处理连接请求和读写请求,效率很高;Netty的零拷贝优化了数据流转的效率;面对需要频繁创建buffer的情况,Netty采用内存池化技术,提前将内存分为多个小块方便使用与回收
Netty的零拷贝包括:可以直接为对象分配堆外内存,可以在操作系统层面实现零拷贝,直接将byte数组包装成bytebuffer对象不需要复制
基于Netty如何实现聊天系统
系统架构图

实现群聊系统:

实现已读系统:





