一、前言
Netty是一个异步、基于事件驱动的网络应用程序框架,其对 Java NIO进行了封装,大大简化了 TCP 或者 UDP 服务器的网络编程。其应用还是比较广泛的,比如Apache Dubbo 、Apache RocketMq、Zuul 2.0服务网关、Spring WebFlux、Sofa-Bolt 底层网络通讯都是基于 Netty 来实现的,本节我们谈谈Netty4中的线程模型。
二、 Netty的线程模型
如上图下侧为Netty Server端,当NettyServer启动时候会创建两个NioEventLoopGroup线程池组,其中boss组用来接受客户端发来的连接,worker组则负责对完成TCP三次握手的连接进行处理;如上图每个NioEventLoopGroup里面包含了多个NioEventLoop,每个NioEventLoop中包含了一个NIO Selector、一个队列、一个线程;其中线程用来做轮询注册到Selector上的Channel的读写事件和对投递到队列里面的事件进行处理。
当NettyServer启动时候会注册监听套接字通道NioServerSocketChannel到boss线程池组中的某一个NioEventLoop管理的Selector上,然后其对应的线程则会负责轮询该监听套接字上的连接请求;当客户端发来一个连接请求时候,boss线程池组中注册了监听套接字的NioEventLoop中的Selector会读取读取完成了TCP三次握手的请求,然后创建对应的连接套接字通道NioSocketChannel,然后把其注册到worker线程池组中的某一个NioEventLoop中管理的一个NIO Selector上,然后该连接套接字通道NioSocketChannel上的所有读写事件都由该NioEventLoop管理。当客户端发来多个连接时候,NettyServer端则会创建多个NioSocketChannel,而worker线程池组中的NioEventLoop是有个数限制的,所以Netty有一定的策略把很多NioSocketChannel注册到不同的NioEventLoop上,也就是每个NioEventLoop中会管理好多客户端发来的连接,然后通过循环轮询处理每个连接的读写事件。
如上图上侧部分为Netty Client部分,当NettyClient启动时候会创建一个NioEventLoopGroup,用来发起请求并对建立TCP三次连接的套接字的读写事件进行处理。当调用Bootstrap的connect方法发起连接请求后内部会创建一个NioSocketChannel用来代表该请求,并且会把该NioSocketChannel注册到NioSocketChannel管理的某个NioEventLoop的Selector上,然后该NioEventLoop的读写事件都有该NioEventLoop负责处理。
Netty之所以说是异步非阻塞网络框架是因为通过NioSocketChannel的write系列方法向连接里面写入数据时候是非阻塞的,马上会返回的,即使调用写入的线程是我们的业务线程,这是Netty通过在ChannelPipeline中判断调用NioSocketChannel的write的调用线程是不是其对应的NioEventLoop中的线程来实现的,如果发现不是则会把写入请求封装为WriteTask投递到其对应的NioEventLoop中的队列里面,然后等其对应的NioEventLoop中的线程轮询连接套接字的读写事件时候捎带从队列里面取出来执行;总结说就是每个NioSocketChannel对应的读写事件都是在其对应的NioEventLoop管理的单线程内执行,对同一个NioSocketChannel不存在并发读写,所以无需加锁处理。
另外当从NioSocketChannel中读取数据时候,并不是使用业务线程来阻塞等待,而是等NioEventLoop中的IO轮询线程发现Selector上有数据就绪时候,通过事件通知方式来通知我们业务数据已经就绪,可以来读取并处理了。
总结一句话就是使用Netty框架进行网络通信时候,当我们发起请求后请求会马上返回,而不会阻塞我们的业务调用线程;如果我们想要获取请求的响应结果,也不需要业务调用线程使用阻塞的方式来等待,而是当响应结果出来时候使用IO线程异步通知业务的方式,可知在整个请求-响应过程中业务线程不会由于阻塞等待而不能干其他事情。
下面我们讨论两个细节,第一是完成TCP三次握手的套接字应该注册到worker线程池中的哪一个NioEventLoop的Selector上,第二个是NioEventLoop中的线程负责监听注册到Selector上的所有连接的读写事件和处理队列里面的消息,那么会不会导致由于处理队列里面任务耗时太长导致来不及处理连接的读写事件?
对于第一个问题NioEventLoop的分配,Netty默认使用的是PowerOfTwoEventExecutorChooser,其代码如下:
private final class PowerOfTwoEventExecutorChooser implements EventExecutorChooser {
@Override
public EventExecutor next() {
return children[childIndex.getAndIncrement() & children.length - 1];
}
}
可知是采用的轮询取模方式来进行分配。
对于第二个问题,Netty默认是采用时间均分策略来避免某一方处于饥饿状态:
//1.记录开始处理时间
final long ioStartTime = System.nanoTime();
try {//1.1处理连接套接字的读写事件
processSelectedKeys();
} finally {
// 1.2计算连接套接字处理耗时,ioRatio默认为50
final long ioTime = System.nanoTime() - ioStartTime;
//1.3运行队列里面任务
runAllTasks(ioTime * (100 - ioRatio) / ioRatio);
}
如上代码1.1处理所有注册到当前NioEventLoop的Selector上的所有连接套接字的读写事件,代码1.2用来统计其耗时,由于默认情况下ioRatio为50,所以代码1.3尝试使用与代码1.2执行相同的时间来运行队列里面的任务。
也就是处理套接字读写事件与运行队列里面任务是使用时间片轮转方式轮询执行。
三、总结
Netty的异步非阻塞基于事件驱动的模型大大简化了我们编写网络应用程序的成本。