13. 服务端创建
13.1 Netty服务端创建流程
-
步骤1: 创建ServerBootstrap实例时
-
ServerBootstrap只有一个无参的构造函数,根本原因是因为它的参数太多了,而且未来也可能会发生变化
-
为了解决这个问题,就需要引入Builder模式。建议遇到多个构造器参数时要考虑用构建器。
-
-
步骤2:设置并绑定 Reactor 线程池。
-
Netty 的 Reactor 线程池是EventLoopGroup。EventLoop的职责是处理所有注册到本线程多路复用器Selector上的Channel
-
Selector的轮询操作由绑定的EventLoop线程run方法驱动,在一个循环体内循环执行。1
-
-
步骤3:设置并绑定服务端Channel。
-
作为NIO服务端,需要创建ServerSocketChannel。
-
Netty 对原生的NIO类库进行了封装,对应实现是NioServerSocketChannel。
-
-
步骤4:链路建立的时候创建并初始化ChannelPipeline。
-
ChannelPipeline并不是NIO服务端必需的,它本质就是一个负责处理网络事件的职责链,负责管理和执行 ChannelHandler。
-
网络事件以事件流的形式在ChannelPipeline 中流转,由ChannelPipeline根据ChannelHandler的执行策略调度ChannelHandler的执行。
-
-
步骤5:初始化ChannelPipeline完成之后,添加并设置ChannelHandler。
-
ChannelHandler是Netty提供给用户定制和扩展的关键接口。
-
利用ChannelHandler可以完成大多数的功能定制,例如消息编解码、心跳、安全认证、TSL/SSL认证、流量控制和流量整形等。
-
-
步骤6:绑定并启动监听端口。在绑定监听端口之前系统会做一系列的初始化和检测工作,完成之后,会启动监听端口,并将ServerSocketChannel注册到Selector上监听客户端连接。
-
步骤7: Selector轮询。由Reactor线程NioEventLoop负责调度和执行Selector轮询操作,选择准备就绪的Channel集合。
-
步骤8:当轮询到准备就绪的Channel之后,就由Reactor线程NioEventLoop执行ChannelPipeline的相应方法,最终调度并执行ChannelHandle。
-
步骤9:执行 Netty系统ChannelHandler和用户添加定制的ChannelHandler。
-
ChannelPipeline根据网络事件的类型,调度并执行ChannelHandler。
-
分析
-
通过构造函数创建 ServerBootstrap 实例后,通常会创建两个EventLoopGroup。
-
NioEventLoopGroup实际就是Reactor线程池,负责调度和执行客户端的接入、网络读写事件的处理、用户自定义任务和定时任务的执行。
-
线程组和线程类型设置完成后,需要设置服务端Channel用于端口监听和客户端链路接入。Netty通过Chanel工厂类来创建不同类型的Channel,对于服务端,通过指定Channel类型的方式创建Channel工厂
-
指定NioServerSocketChannel后,需要设置TCP的一些参数,作为服务端,主要是要设置TCP的 backlog参数。backlog指定了内核为此套接口排队的最大连接个数。
-
服务端启动的最后一步,就是绑定本地端口,启动服务,
-
Netty服务端监听的相关资源已经初始化完毕后,最后注册NioServerSocketChannel到Reactor线程的多路复用器上,然后轮询客户端连接事件。
14. 客户端创建
Netty为了向使用者屏蔽NIO通信的底层细节,在和用户交互的边界做了封装,目的就是为了减少用户开发工作量,降低开发难度。Bootstrap是Socket客户端创建工具类,用户通过Bootstrap可以方便地创建Netty的客户端并发起异步TCP连接操作。
14.1 Netty客户端创建流程
步骤1:用户线程创建Bootstrap实例,通过API设置创建客户端相关的参数,异步发起客户端连接。
步骤2:创建处理客户端连接、I/O读写的Reactor线程组NioEventLoopGroup。可以通过构造函数指定I/O线程的个数
步骤3:通过Bootstrap的ChannelFactory和用户指定的Channel类型创建用于客户端连接的NioSocketChannel,它的功能类似于 JDK NIO 类库提供的SocketChannel;
步骤4:创建默认的Channel Handler Pipeline,,用于调度和执行网络事件;
步骤5:异步发起TCP连接,判断连接是否成功。如果成功,则直接将NioSocketChannel注册到多路复用器上,监听读操作位,用于数据报读取和消息发送;如果没有立即连接成功,则注册连接监听位到多路复用器,等待连接结果;
步骤6:注册对应的网络监听状态位到多路复用器;
步骤7:由多路复用器在I/O现场中轮询各Channel,处理连接结果;
步骤8:如果连接成功,设置Future 结果,发送连接成功事件,触发ChannelPipeline执行;
步骤9:由ChannelPipeline调度执行系统和用户的ChannelHandler,执行业务逻辑。
18. Netty线程模型
18.1 Reactor单线程模型
Reactor单线程模型,是指所有的I/O操作都在同一个NIO线程上面完成。Reactor单线程模型如图18-1所示。
由于Reactor模式使用的是异步非阻塞I/O,所有的I/O操作都不会导致阻塞,理论上一个线程可以独立处理所有I/O相关的操作。
-
例如,通过Acceptor类接收客户端的TCP连接请求消息,当链路建立成功之后,通过Dispatch将对应的ByteBuffer派发到指定的Handler上,进行消息解码。用户线程消息编码后通过NIO线程将消息发送给客户端。
-
在一些小容量应用场景下,可以使用单线程模型。但是这对于高负载、大并发的场景不合适
18.2 Reactor多线程模型
Rector多线程模型与单线程模型最大的区别就是有一组NIO线程来处理1/0操作,它的原理如图18-2所示。
Reactor多线程模型的特点如下。
-
有专门一个 NIO 线程—Acceptor 线程用于监听服务端,接收客户端的TCP 连接请求。
-
网络I/O操作——读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现,它包含一个任务队列和N个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送。
-
一个NIO线程可以同时处理N条链路,但是一个链路只对应一个NIO线程,防止发生并发操作问题。
在绝大多数场景下,Reactor多线程模型可以满足性能需求。但是,在个别特殊场景中,一个NO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。
在这类场景下,单独一个Acceptor线程可能会存在性能不足的问题,为了解决性能问题,产生了主从Reactor多线程模型。
18.3 主从Reacotr多线程模型
主从Reactor线程模型的特点是:服务端用于接收客户端连接的不再是一个单独的NIO线程,而是一个独立的NIO线程池。
-
Acceptor接收到客户端TCP连接请求并处理完成后,将新创建的SocketChannel注册到I/O线程池(sub reactor线程池)的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。
-
Acceptor线程池仅仅用于客户端的登录、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的!/O线程上,由1/0线程负责后续的1/0操作。
利用主从NIO线程模型,可以解决一个服务端监听线程无法有效处理所有客户端连接的性能不足问题。因此,在Netty的官方Demo中,推荐使用该线程模型。
18.4 Netty线程模型
Netty的线程模型并不是一成不变的,它实际取决于用户的启动参数配置。通过设置不同的启动参数,Netty可以同时支持Reactor单线程模型、多线程模型和主从Reactor多线层模型。
服务端启动的时候,创建了两个NioEventLoopGroup,它们实际是两个独立的Reactor线程池。一个用于接收客户端的TCP连接,另一个用于处理!/0相关的读写操作,或者执行系统Task、定时任务Task等。
Netty用于接收客户端请求的线程池职责如下。
-
接收客户端TCP连接,初始化Channel参数;
-
链路状态变更事件通知给ChannelPipeline。
Netty处理I/O操作的Reactor线程池职责如下。
-
异步读取通信对端的数据报,发送读事件到ChannelPipeline;
-
异步发送消息到通信对端,调用ChannelPipeline的消息发送接口;
-
执行系统调用Task以及定时任务Task,例如链路空闲状态监测定时任务。
Netty在很多地方进行了无锁化的设计,例如在I/O线程内部进行串行操作,避免多线程竞争导致的性能下降问题。
表面上看,串行化设计似乎CPU利用率不高,并发程度不够。但是,通过调整NIO线程池的线程参数,可以同时启动多个串行化的线程并行运行,这种局部无锁化的串行线程设计相比一个队列一多个于作线程的模刑性能面优
18.5 最佳实践
Netty的多线程编程最佳实践如下。
-
创建两个NioEventLoopGroup,用于逻辑隔离NIO Acceptor和NIO 1/0线程。
-
尽量不要在ChannelHandler中启动用户线程(解码后用于将POJO消息派发到后端业务线程的除外)。
-
解码要放在NIO线程调用的解码Handler中进行,不要切换到用户线程中完成消息的解码。
-
如果业务逻辑操作非常简单,可以直接在NIO线程上完成业务逻辑编排,不需要切换到用户线程。
-
如果业务逻辑处理复杂,不要在NIO线程上完成,建议将解码后的POJO消息封装成Task,派发到业务线程池中由业务线程执行,以保证NIO线程尽快被释放,处理其他的I/O操作。