高并发编程系列(三)Netty服务端启动流程源码分析

1 服务端ServerBootstrap

在分析客户端的代码时,我们已经对Bootstrap启动Netty有了一个大致的认识,接下来在分析服务端时,就会相对简单一些了。下面来看服务端的启动代码。

服务端基本写法与客户端的代码相比,没有很大的差别,基本上也是进行如下几个部分的初始化。

(1)EventLoopGroup:无论是服务端还是客户端,都必须指定EventLoopGroup。在上面的代码中,指定了NioEventLoopGroup,表示一个NIO的EventLoopGroup,不过服务端需要指定两个EventLoopGroup,一个是bossGroup,用于处理客户端的连接请求;另一个是workerGroup,用于处理与各个客户端连接的I/O操作。

(2)指定Channel的类型。这里是服务端,所以使用了NioServerSocketChannel。

(3)配置自定义的业务处理器Handler。

1.1 NioServerSocketChannel的创建

我们在分析客户端Channel的初始化过程时已经提到,Channel是对Java底层Socket连接的抽象,并且知道客户端Channel的具体类型是NioSocketChannel,由此可知,服务端Channel的类型就是NioServerSocketChannel。

我们按照分析客户端的流程,对服务端的代码也同样分析一遍,这样会方便我们对比服务端和客户端有哪些不同。通过7.1节的分析,我们知道,在客户端中,Channel类型的指定是在初始化时通过Bootstrap的channel()方法设置的,服务端也是同样的方式。

再看服务端代码,我们调用ServerBootstarap的channel(NioServerSocketChannel.class)方法,传入的参数是NioServerSocketChannel.class对象。如此,按照与客户端代码同样的流程,我们可以确定NioServerSocketChannel的实例化也是通过ReflectiveChannelFactory工厂类来完成的,而ReflectiveChannelFactory中的clazz属性被赋值为NioServerSocketChannel.class,因此当调用ReflectiveChannelFactory的newChannel()方法时,就能获取一个NioServerSocketChannel的实例。newChannel()方法的代码如下。

下面总结一下。

(1)ServerBootstrap中的ChannelFactory的实现类是ReflectiveChannelFactory类。

(2)创建的Channel具体类型是NioServerSocketChannel。

Channel的实例化过程,其实就是调用ChannelFactory的newChannel()方法,而实例化的Channel具体类型就是初始化ServerBootstrap时传给channel()方法的实参。因此,上面代码案例中的服务端ServerBootstrap创建的Channel实例就是NioServerSocketChannel的实例。

1.2 服务端Channel的初始化

我们来分析NioServerSocketChannel的实例化过程,先看一下NioServerSocketChannel的类层次结构,如下图所示。

首先,我们来看一下NioServerSocketChannel的默认构造器。与NioSocketChannel类似,构造器都是调用newSocket()方法来打开一个Java NIO Socket。不过需要注意的是,客户端的newSocket()方法调用的是openSocketChannel,而服务端的newSocket()方法调用的是openServerSocketChannel。顾名思义,一个是客户端的Java SocketChannel,另一个是服务端的Java ServerSocketChannel,代码如下。

然后,调用重载构造方法,代码如下。

在这个构造方法中,调用父类构造方法时传入的参数是SelectionKey.OP_ACCEPT。作为对比,我们回顾一下,在客户端的Channel初始化时,传入的参数是SelectionKey.OP_READ。在服务启动后需要监听客户端的连接请求,因此在这里我们设置SelectionKey.OP_ACCEPT,也就是通知Selector我们对客户端的连接请求感兴趣。

接下来,和客户端对比分析一下,逐级调用父类的构造器,首先调用NioServerSocketChannel的构造器,其次调用AbstractNioMessageChannel的构造器,然后调用AbstractNioChannel的构造器,最后调用AbstractChannel的构造器。同样地,在AbstractChannel中实例化一个Unsafe和Pipeline,代码如下。

不过,这里需要注意的是,客户端的Unsafe是AbstractNioByteChannel.NioByteUnsafe的实例,而服务端的Unsafe是AbstractNioMessageChannel.AbstractNioUnsafe的实例。AbstractNioMessageChannel重写了newUnsafe()方法,代码如下。

最后,总结一下在NioServerSocketChannel实例化过程中执行的逻辑。

(1)调用NioServerSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER)方法创建一个新的Java NIO原生的ServerSocketChannel对象。

(2)实例化AbstractChannel对象并给属性赋值,具体赋值的属性如下。

parent:设置为默认值null。

unsafe:通过调用newUnsafe()方法,实例化一个Unsafe对象,其类型是AbstractNioMessageChannel.AbstractNioUnsafe内部类。

pipeline:赋值的是DefaultChannelPipeline的实例。

(3)实例化AbstractNioChannel对象并给属性赋值,具体赋值的属性如下。

ch:被赋值为Java NIO原生的ServerSocketChannel对象,通过调用NioServerSocketChannel的newSocket()方法获取。

readInterestOp:被赋值为默认值SelectionKey.OP_ACCEPT。

Ch:被设置为非阻塞,也就是调用ch.configureBlocking(false)方法。

(4)给NioServerSocketChannel对象的config属性赋值为new NioServerSocketChannelConfig(this,javaChannel().socket())。

1.3 服务端ChannelPipeline的初始化

服务端ChannelPipeline的初始化和客户端一致,也可以参考7.1.5节的内容,不再单独分析。

1.4 将服务端Channel注册到Selector

服务端Channel的注册过程和客户端一致,也可以参考7.1.7节的内容,不再单独分析。

1.5 bossGroup与workerGroup

在客户端初始化的时候,我们初始化了一个EventLoopGroup对象,而在服务端的初始化时,我们设置了两个EventLoopGroup:一个是bossGroup,另一个是workerGroup。那么这两个EventLoopGroup都是干什么用的呢?接下来我们详细探究一下。其实,bossGroup只用于服务端的accept,也就是用于处理客户端新连接接入的请求。我们可以把Netty比作一个餐馆,bossGroup就像一个大堂经理,当客户来到餐馆吃饭时,大堂经理就会引导顾客就座,为顾客端茶送水等。而workerGroup就是实际干活的厨师,它们负责客户端连接通道的I/O操作:当大堂经理接待顾客后,顾客可以稍做休息,而此时后厨里的厨师们(workerGroup)就开始忙碌地准备饭菜了。bossGroup与workerGroup的关系如下图所示,前面的章节我们也分析过,这里再巩固一下。

首先,服务端的bossGroup不断地监听是否有客户端的连接,当发现有一个新的客户端连接到来时,bossGroup就会为此连接初始化各项资源;然后,从workerGroup中选出一个EventLoop绑定到此客户端连接中;接下来,服务端与客户端的交互过程将全部在此分配的EventLoop中完成。下面我们结合代码进行分析。

在ServerBootstrap初始化时调用了b.group(bossGroup,workerGroup),并设置了两个EventLoopGroup,代码如下。

显然,这个方法初始化了两个属性。一个是group=parentGroup,它是在super.group(parentGroup)中完成初始化的;另一个是childGroup=childGroup。接着从应用程序的启动代码看,调用了b.bind()方法来监听一个本地端口。bind()方法会触发如下调用链。

代码看到这里,我们发现对于AbstractBootstrap的initAndRegister()方法,我们已经很熟悉,在分析客户端程序时和它打过很多交道,现在再来回顾一下这个方法。

这里group()方法返回的是上面我们提到的bossGroup,而这里的Channel其实就是NioServerSocketChannel的实例,因此可以猜测group().register(channel)将bossGroup和NioServerSocketChannel关联起来了。那么workerGroup具体是在哪里与NioServerSocketChannel关联的呢?继续看init(channel)方法。

实际上,init()方法在ServerBootstrap中被重写了,从上面的代码片段中我们看到,它为Pipeline添加了一个ChannelInitializer,而这个ChannelInitializer中添加了一个非常关键的ServerBootstrapAcceptor的Handler。关于Handler的添加与初始化的过程,我们留到之后的章节再详细分析。现在,先来关注ServerBootstrapAcceptor类。在ServerBootstrapAcceptor中重写了channelRead()方法,其主要代码如下。

ServerBootstrapAcceptor中的childGroup是构造此对象时传入的currentChildGroup,也就是workerGroup对象。而这里的Channel是NioSocketChannel的实例,因此childGroup的register()方法就是将workerGroup中的某个EventLoop和NioSocketChannel进行关联。那么,ServerBootstrapAcceptor的channelRead()方法是在哪里被调用的呢?其实当一个Client连接到Server时,Java底层NIO的ServerSocketChannel就会有一个SelectionKey.OP_ACCEPT的事件就绪,接着就会调用NioServerSocketChannel的doReadMessages()方法,代码如下。

在doReadMessages()方法中,通过调用javaChannel().accept()方法获取客户端新连接的SocketChannel对象,紧接着实例化一个NioSocketChannel,并且传入NioServerSocketChannel对象(即this)。由此可知,我们创建的NioSocketChannel的父类Channel就是NioServerSocketChannel实例。接下来利用Netty的ChannelPipeline机制,将读取事件逐级发送到各个Handler中,于是就会触发ServerBootstrapAcceptor的channelRead()方法。

1.6 服务端Selector事件轮询

回到服务端ServerBootStrap的启动代码,它是从bind()方法开始的。ServerBootStrapt的bind()方法实际上就是其父类AbstractBootstrap的bind()方法,来看代码。

在doBind0()方法中,调用EventLoop的execute()方法,代码如下。

execute()方法主要就是创建线程,将线程添加到EventLoop的无锁化串行任务队列。重点关注startThread()方法,代码如下。

我们发现startThread()方法最终调用的是SingleThreadEventExecutor.this.run()方法,这个this就是NioEventLoop对象,代码如下。

终于看到似曾相识的代码。上面代码主要就是用一个死循环不断地轮询SelectionKey的。select()方法主要用来解决JDK空轮询Bug,而processSelectedKeys()就是针对不同的轮询事件进行处理。如果客户端有数据写入,最终也会调用AbstractNioMessageChannel的doReadMessages()方法。下面我们来总结一下Selector的轮询流程。

(1)Selector事件轮询是从EventLoop的execute()方法开始的。

(2)在EventLoop的execute()方法中,会为每一个任务都创建一个独立的线程,并保存到无锁化串行任务队列。

(3)线程任务队列的每个任务实际调用的是NioEventLoop的run()方法。

(4)在run()方法中调用processSelectedKeys()处理轮询事件。

1.7 Netty解决JDK空轮询Bug

大家应该早就听说过臭名昭著的Java NIO epoll的Bug,它会导致Selector空轮询,最终导致CPU使用率达到100%。官方声称JDK 1.6的update18修复了该问题,但是直到JDK 1.7该问题仍旧存在,只不过该Bug发生概率降低了一些而已,并没有被根本解决。出现此Bug是因为当Selector轮询结果为空时,没有进行wakeup或对新消息及时进行处理,导致发生了空轮询,CPU使用率达到了100%。我们来看一下这个问题在issue中的原始描述。

This is an issue with poll (and epoll) on Linux.If a file descriptor for a connected socket is polled with a request event mask of 0,and if the connection is abruptly terminated (RST) then the poll wakes up with the POLLHUP (and maybe POLLERR) bit set in the returned event set.The implication of this behaviour is that Selector will wakeup and as the interest set for the SocketChannel is 0 it means there aren't any selected events and the select method returns 0.

具体解释为:在部分Linux Kernel 2.6中,poll和epoll对于突然中断的Socket连接会对返回的EventSet事件集合置为POLLHUP,也可能是POLLERR,EventSet事件集合发生了变化,这就可能导致Selector会被唤醒。

这是与操作系统机制有关系的,JDK虽然仅仅是一个兼容各个操作系统平台的软件,但遗憾的是在JDK 5和JDK 6最初的版本中,这个问题并没有得到解决,而将这个“帽子”抛给了操作系统方,这就是这个Bug一直到2013年才最终修复的原因。

在Netty中最终的解决办法是:创建一个新的Selector,将可用事件重新注册到新的Selector中来终止空轮询。我们来回顾一下事件轮询的关键代码。

前面我们提到select()方法解决了JDK空轮询的Bug,那么它到底是如何解决的呢?下面我们来一探究竟,先来看一下select()方法的源码。

从上面的代码中可以看出,Selector每一次轮询都计数selectCnt++,开始轮询会将系统时间戳赋值给timeoutMillis,轮询完成后再将系统时间戳赋值给time,这两个时间会有一个时间差,而这个时间差就是每次轮询所消耗的时间。从上面的逻辑可以看出,如果每次轮询消耗的时间为0s,且重复次数超过512次,则调用rebuildSelector()方法,即重构Selector,具体实现代码如下。

实际上,在rebuildSelector()方法中,主要做了以下三件事情。

(1)创建一个新的Selector。

(2)将原来Selector中注册的事件全部取消。

(3)将可用事件重新注册到新的Selector,并激活。就这样,Netty完美解决了JDK的空轮询Bug。看到这里,是不是感觉没那么神秘了?

1.8 Netty对Selector中KeySet的优化

分析完Netty对JDK空轮洵Bug的解决方案,接下来看一个很有意思的细节。Netty对Selector中存储SelectionKey的HashSet也做了优化。在前面的分析中,Netty对Selector有重构,创建一个新的Selector就会调用openSelector()方法,来看代码。

我们再来看一下openSelector()方法的代码。

上面代码的主要功能就是利用反射机制,获取JDK底层的Selector的Class对象,用反射方法从Class对象中获得两个属性:selectedKeys和publicSelectedKeys,这两个属性就是用来存储已注册事件的。然后,将这两个对象重新赋值为Netty创建的SelectedSelectionKeySet,是不是有种“偷梁换柱”的感觉?

我们先看selectedKeys和publicSelectedKeys到底是什么类型,打开SelectorImpl的源码,看其构造方法的代码。

我们发现selectedKeys和publicSelectedKeys就是HashSet。我们再来看Netty创建的SelectedSelectionKeySet对象的源代码。

源码篇幅不长,但很精辟。SelectedSelectionKeySet同样继承了AbstractSet,因此赋值给selectedKeys和publicSelectedKeys不存在类型强制转换的问题。细心的小伙伴应该已经发现在SelectedSelectionKeySet中禁用了remove()方法、contains()方法和iterator()方法,只保留了add()方法,而且底层存储结构用的是数组SelectionKey[] keys。那么,Netty为什么要这样设计呢?主要目的还是简化我们在轮询事件时的操作,不需要每次轮询都移除Key。

1.9 Handler的添加过程

服务端Handler的添加过程和客户端的有点区别,跟EventLoopGroup一样,服务端的Handler也有两个:一个是通过handler()方法设置的Handler,另一个是通过childHandler()方法设置的childHandler。通过前面的bossGroup和workerGroup的分析,其实我们可以在这里大胆地猜测:Handler与accept过程有关,即Handler负责处理客户端新连接接入的请求;而childHandler就是负责和客户端连接的I/O交互。那么实际上是不是这样的呢?我们继续用代码来验证。在前面章节我们已经了解到ServerBootstrap重写了init()方法,在这个方法中也添加了Handler,代码如下。

在上面代码的initChannel()方法中,首先通过handler()方法获取一个Handler,如果获取的Handler不为空,则添加到Pipeline中,然后添加一个ServerBootstrapAcceptor的实例。这里的handler()方法返回的是哪个对象呢?其实它返回的是Handler属性,而这个属性就是我们在服务端的启动代码中设置的。

这个时候,Pipeline中的Handler情况如下图所示。

根据对原来客户端代码的分析,将Channel绑定到EventLoop(这里是指NioServerSocketChannel绑定到bossGroup)后,会在Pipeline中触发fireChannelRegistered事件,接着会触发对ChannelInitializer的initChannel()方法的调用。因此在绑定完成后,此时的Pipeline的内容如下图所示。

在我们分析bossGroup和workerGroup时,已经知道ServerBootstrapAcceptor的channelRead()方法会为新建的Channel设置Handler并注册到一个EventLoop中。

而这里的childHandler就是我们在服务端启动代码中设置的Handler。

后续的步骤我们基本上已经清楚了,在客户端连接Channel注册后,就会触发ChannelInitializer的initChannel()方法的调用。最后我们总结一下服务端Handler与childHandler的区别与联系。

(1)在服务端NioServerSocketChannel对象的Pipeline中添加了Handler对象和ServerBootstrapAcceptor对象。

(2)当有新的客户端连接请求时,会调用ServerBootstrapAcceptor的channelRead()方法创建此连接对应的NioSocketChannel对象,并将childHandler添加到NioSocketChannel对应的Pipeline中,而且将此Channel绑定到workerGroup中的某个EventLoop中。

(3)Handler对象只在accept()阻塞阶段起作用,它主要处理客户端发送过来的连接请求。

(4)childHandler在客户端连接建立以后起作用,它负责客户端连接的I/O交互。

最后来看一张图,加深理解。下图描述了服务端从启动初始化到有新连接接入的变化过程。

展开阅读全文

页面更新:2024-04-12

标签:服务端   赋值   初始化   源码   客户端   实例   属性   流程   对象   事件   代码   方法   系列

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2008-2024 All Rights Reserved. Powered By bs178.com 闽ICP备11008920号-3
闽公网安备35020302034844号

Top