Netty是一个高性能网络应用框架,应用也十分普遍,目前在Java领域中,Netty基本上可以成为网络程序的标配了。Netty框架功能丰富也十分复杂,此篇专栏主要会分析Netty框架中的线程模型,而线程模型也直接影响了网络程序的性能。
但是在我们理解这句话之前,有必要先去了解一下网络编程性能的瓶颈在哪里,Netty又是如何解决这个问题的。
网络编程的瓶颈
在BIO模型中,所有read()和write()操作都会阻塞当前线程,所以如果CS建立连接而不发送数据,那么read()就会一直阻塞,所以使用BIO模型,一般都会为每个socket分配一个独立的线程,这样就不会因为线程阻塞在一个socket上而影响对其他socket的读写BIO的线程模型,如下图所示,每一个socket都对应着一个独立的线程,为了避免频繁创建、消耗线程,可以采用线程池的思路,但是与socket和线程之间的对应关系不会变化。

BIO这种线程模型适用于socket连接不是很多的场景,但是现在的互联网场景没往往需要服务器能够支撑十万或者百万连接,而创建十万甚至上百万个线程显然不是很现实的,所以BIO线程模型无法解决百万连接的问题。
而如果仔细观察就会发现,虽然大并发场景的连接多,但是每一个连接的请求不频繁,所以大部分线程都是处于等待IO就绪的水平没所以大部分的线程都被阻塞了,浪费资源,如果能解决这个问题,就不需要那么多线程了。
顺着这个思路,就可以将线程模型优化一下了,用一个线程来处理多个连接,这样线程的利用率就上来了,同时所需要的线程数量就跟着下来了。
但是使用BIO相关的API是无法是实现的没这事因为BIO相关的socket都是读写阻塞的,一旦调用了阻塞式API,在IO就绪之前,调用线程会一直阻塞,也就无法处理其他的socket了。

不过好在java还提供了非阻塞式(NIO)API,利用非阻塞式API就能够实现一个线程处理多个连接了。那具体该如何实现呢?现在普遍都是采用Reactor模式,包括Netty的实现。所以,要想理解Netty的实现,接下来也需要讲一下Reactor模式。
Reactor模式
下面是Reactor 模式的类结构图,其中Handle指的是IO句柄,在Java网络编程中,本质就是一个网络连接。Event Handler处理一个IO Handle,get_handle()方法可以返回这个IO的Handle。Synchronous Event Demultiplexer可以理解为操作系统提供的IO多路复用的API,例如POSIX标准里的select()以及linux里面的epoll()。

Reactor的核心也自然是Reactor这个类,其中register_handler() 和remove_handler()这两个方法可以注册或者删除一个事件处理器,handle_events() 方式是核心,也是Reactor模式的发动机,这个方法的核心逻辑如下:首先通过同步事件多路选择器提供的select()方法监听网络事件,当有网络时间就绪后,就遍历事件处理器处理该网络时事件。由于网络事件是源源不断的,所以主程序中启动Reactor模式,需要以while(true){}的方式调用handle_events()方法。
void Reactor::handle_events(){
#通过同步事件多路选择器提供的
#select()方法监听网络事件
select(handles);
#处理网络事件
for(h in handlers){
h.handle_event();
}
}
#在出程序中启动事件循环
while(true){
handle_events();
}
Netty线程模型
Netty的设计实现虽然参考了Reactor模式,但是也没有完全照搬,Netty中最核心的概念是事件循环(EventLoop),其实也就是Reactor模式中的Reactor,负责监听网络时间并且调用事件处理器进行处理。在4.x版本中的Netty中,网络连接和EventLoop是最稳健的多对1关系,而EventLoop和Java线程是一对一的关系,这里的稳定指的是关系一旦确定就在不发生变化了。也就是说一个网络连接只会对应唯一的一个EventLoop,而一个EventLoop也只会对应到一个Java线程,所以一个网络连接只会对应到一个Java线程。
一个线程连接对应到了一个Java线程上,有什么好处呢?最大的好处就是对于一个网络连接的事件处理是单线程的,这样就避免了各种并发问题。
Netty中的线程模型可以参考下图,这个图和前面我们之前提到的理想的线程模型图很相似,核心目标都是一个线程处理多个网络连接。

Netty中还有一个核心概念是EventLoopGroup,顾名思义,一个EventLoopGroup由一组EventLoop组成。实际使用中,一般都会创建两个EventLoopGroup,一个成为bossGroup,一个称为workerGroup。为什么会有两个EventLoopGroup呢?
其实这个问题和Socket处理网络请求的机制有关,socket处理TCP网络连接请求,是在一个独立的socket中,每当有一个TCP连接成功建立,都会创建一个新的socket,之后对TCP连接的读写都是由新创建的socket完成的。
也就是说处理TCP连接请求和读写请求是通过两个不同的socket完成的。
但是上面我们也直说了读写请求,而没有连接请求的讨论。
在Netty中,bossGroup就是来处理连接请求的,而workerGroup是用来处理读写请求的。 bossGroup处理完连接请求之后,会将这个连接提交给workerGroup来处理,workerGroup里面有多个EventLoop,那新的连接会交给哪个EventLoop来处理呢?这就需要一个负载均衡 算法,而我们知道负载均衡有三种方法,这里Netty主要采用的是轮询算法。
下面我们就用Netty重新实现以下echo程序的服务端,近距离感受下Netty。
Netty实现Echo程序服务端
实现服务端的思路是:首先创建一个事件处理器(等同于Reactor模式中的事件处理器),然后创建了bossGroup和workerGroup,再之后创建并且初始化了ServerBootstrap,代码还是很简单的,不过有两个地方要注意:
-
如果NettybossGroup只监听一个端口,那么bossGroup只需要一个EventLoop就可以了,多了就浪费了。
-
第二个,默认情况下,Netty会创建“2*CPU核数”个EventLoop,由于网络连接与EventLoop有稳定的联系,所以事件处理器在处理网络时间的时候是不能有阻塞操作的,否则很容易导致请求大面积超时,如果实在无法避免阻塞,就用线程池异步处理。
相关Server端代码已上传至github: https://github.com/2NaCl/netty-demo

本文深入探讨了Netty框架的线程模型,对比BIO模型,解释了Netty如何利用Reactor模式和NIO API实现高效网络编程,特别关注其在处理高并发连接时的性能优势。
360

被折叠的 条评论
为什么被折叠?



