目录
一、前言
要理解 Netty 的 Reactor 线程模型,需要先了解 NIO 和 EventLoop 等一些基础知识,Netty 是对 NIO 的一些基础 API 进行了封装,而 EventLoop 的设计思想被运用在 Redis、Nginx、Node.js 等众多高性能框架中。理解了这两个基础知识后,对 Netty 的 Reactor 模式就会有比较深刻的认识。
二、背景知识
1.Java NIO
要点:
Selector
一般称为选择器
,当然你也可以翻译为 多路复用器
。它是Java NIO核心组件中的一个,用于检查一个或多个NIO Channel(通道)的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。
通道事件
- NIO Channel(通道)在通讯过程中会通过状态(事件)表明是否已经就绪,比如某个Channel成功连接到另一个服务器称为“ 连接就绪 ”,NIO 的事件用 SecletionKey的四个常量来表示:
Key | 事件 | Value |
---|---|---|
连接就绪 | SelectionKey.OP_CONNECT | 8 |
接收就绪 | SelectionKey.OP_ACCEPT | 16 |
读就绪 | SelectionKey.OP_READ | 1 |
写就绪 | SelectionKey.OP_WRITE | 4 |
通道与 Selector 绑定
一个 SelectionKey 对象表示了一个特定的通道对象和一个特定的选择器对象之间的注册关系。同时包括了监控的 IO 事件。
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
- key.channel(); 返回该 SelectionKey 对应的 channel。
- key.interestOps(); 返回代表需要 Selector 监控的 IO 操作的 bit mask。
- key.interestOps(int ops); 设置新的监控事件 bit mask。
- key.selector(); 返回该 SelectionKey 对应的 Selector。
- key.attachment(); 返回 SelectionKey 的attachment,attachment 可以在注册 channel 的时候指定。
- key.readyOps(); 返回一个 bit mask,代表在相应 channel 上可以进行的 IO 操作。
NIO 服务端的模板
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress("localhost", 8080));
ssc.configureBlocking(false);
Selector selector = Selector.open();
ssc.register(selector, SelectionKey.OP_ACCEPT);
// 参考 Netty 源码,采用反射方式将 Selector 的 selectedKeys 字段与本地变量关联,这样变量会随着 Selector 的 select 方法的执行发生变化。
Set<SelectionKey> selectedKeys = new HashSet<>();
Field selectedKeysField = SelectorImpl.class.getDeclaredField("selectedKeys");
selectedKeysField.setAccessible(true);
selectedKeysField.set(selector, selectedKeys);
while(true) {
int readyNum = selector.select();
if (readyNum == 0) {
continue;
}
Iterator<SelectionKey> it = selectedKeys.iterator();
while(it.hasNext()) {
SelectionKey key = it.next();
if(key.isAcceptable()) {
// 接受连接
} else if (key.isReadable()) {
// 通道可读
} else if (key.isWritable()) {
// 通道可写
}
it.remove();
}
}
2. EventLoop
要点:
主线程就让 Event Loop 线程去处理耗时较长的任务,然后接着往后运行。等到耗时任务完成操作,Event Loop 线程再把结果返回主线程。主线程就调用事先设定的回调函数,完成整个任务。
三、Netty 中的 EventLoop
1. 主从多线程模型
- 主从多线程模型由多个 Reactor 线程组成,每个 Reactor 线程
都有独立的 Selector 对象
。 - MainReactor 仅负责处理客户端连接的 Accept 事件,连接建立成功后将新创建的连接对象注册至 SubReactor。
- SubReactor 分配线程池中的 I/O 线程与连接绑定,它将负责连接生命周期内所有的 I/O 事件。
2. 事件处理机制
NioEventLoop 的事件处理机制采用的是无锁串行化的设计思路。
- BossEventLoopGroup 负责监听客户端的 Accept 事件,当事件触发时,将事件注册至 WorkerEventLoopGroup 中的一个 NioEventLoop 上。
- WorkerEventLoopGroup 负责处理客户端的 Read / Write 事件。数据准备完成时,会被传递到 ChannelPipeline 的第一个 ChannelHandler 中。数据处理完成后,将加工完成的数据再传递给下一个 ChannelHandler,整个过程是串行化执行,不会发生线程上下文切换的问题。
3. EventLoop 最佳实践
- 网络连接建立过程中三次握手、安全认证的过程会消耗不少时间。这里建议采用 Boss 和 Worker 两个 EventLoopGroup,有助于分担 Reactor 线程的压力。
- 如果业务逻辑执行时间较短,建议直接在 ChannelHandler 中执行。耗时较长的 ChannelHandler 可以考虑维护一个业务线程池,将编解码后的数据封装成 Task 进行异步处理,避免 ChannelHandler 阻塞而造成 EventLoop 不可用。
- 在设计业务架构的时候,需要明确业务分层和 Netty 分层之间的界限。不要一味地将业务逻辑都添加到 ChannelHandler 中。
四、总结
Netty Reactor 线程模型的核心处理引擎是 EventLoop, Netty EventLoop 的功能用处可以简单的归纳总结:
- MainReactor 线程: 处理客户端请求接入。
- SubReactor 线程: 数据读取、I/O事件分发与执行。
附录
NIO 示例
NIO 服务端示例代码
class NioServer {
public static void main(String[] args) {
ServerSocketChannel ssc = ServerSocketChannel.open()
ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000))
ssc.configureBlocking(false)
Selector selector = Selector.open()
ssc.register(selector, SelectionKey.OP_ACCEPT)
def readBuf = ByteBuffer.allocate(1024)
def writeBuf = ByteBuffer.allocate(128)
writeBuf.put("received".getBytes())
writeBuf.flip()
for(;;){
int nReady = selector.select()
if(nReady == 0){
Thread.sleep(1000)
continue
}
def keys = selector.selectedKeys()
def it = keys.iterator()
while(it.hasNext()){
def key = it.next()
it.remove()
if(key.isAcceptable()){
def channel = ssc.accept()
channel.configureBlocking(false)
channel.register(selector, SelectionKey.OP_READ)
}else if(key.isReadable()){
def chanel = key.channel() as SocketChannel
readBuf.clear()
chanel.read(readBuf)
readBuf.flip()
byte[] a = new byte[readBuf.limit()]
readBuf.get(a)
println("received ${ new String(a)}")
key.interestOps(SelectionKey.OP_WRITE)
}else if(key.isWritable()){
writeBuf.rewind()
def channel = key.channel() as SocketChannel
channel.write(writeBuf)
key.interestOps(SelectionKey.OP_READ)
}
}
}
}
}
NIO 客户端示例代码
class NioClient {
static void main(String[] args) {
def channel = SocketChannel.open()
channel.connect(new InetSocketAddress("127.0.0.1", 8000))
def writeBuf = ByteBuffer.allocate(32)
def readBuf = ByteBuffer.allocate(32)
writeBuf.put("hello".getBytes())
writeBuf.flip()
while (true) {
writeBuf.rewind()
channel.write(writeBuf)
readBuf.clear()
channel.read(readBuf)
readBuf.flip()
byte[] a = new byte[readBuf.limit()]
readBuf.get(a)
println("received: ${new String(a)}")
}
}
}