服务端创建
netty服务端创建时序图:
关键步骤:
一、创建ServerBootstrap实例。此为Netty启动辅助类
二、设置并绑定Reactor线程池。 Netty的Reactor线程池是EventLoopGroup,它实际就是EventLoop的数组。
EventLoop的职责是处理注册到本线程多路复用器Selector上的Channel,Selector的轮询操作由绑定的EventLoop线程run方法驱动,在一个循环体内循环执行。值得说明的是,EventLoop的职责不仅仅是处理网络I/O事件,用户自定义的Task和定时任务Task也统一由EventLoop负责处理,这样线程模型就实现了统一。
EventLoopGroup即Reator线程池,一般有两个,一个负责调度和执行客户端的接入,另一个负责网络读写事件的处理、用户自定义任务和定时任务的执行。
三、 设置并绑定服务端Channel。 作为NIO服务端,需要创建ServerSocketChannel,Netty对原生的NIO类库进行了封装, 服务端监听端口往往只需要在系统启动时才会调用
四、链路建立的时候创建并初始化ChannelPipeline。ChannelPipeline并不是NIO服务端必需的,它本质就是一个负责处理网络事件的职责链,负责管理和执行ChannelHandler。网络事件以事件流的形式在ChannelPipeline中流转,由ChannelPipeline根据ChannelHandler的执行策略调度ChannelHandler的执行。
典型事件:链路注册;链路激活;链路断开;接收到请求消息;请求消息接收并处理完毕;发送应答消息;链路发生异常;发生用户自定义事件。
五、初始化ChannelPipeline完成之后,添加并设置ChannelHandler。ChannelHandler是Netty提供给用户定制和扩展的关键接口。利用ChannelHandler用户可以完成大多数的功能定制,例如消息编解码、心跳、安全认证、TSL/SSL认证、流量控制和流量整形等。Netty同时也提供了大量的系统ChannelHandler供用户使用,比较实用的系统ChannelHandler总结如下。
(1)系统编解码框架——ByteToBessageCodec;
(2)通用基于长度的半包解码器——LengthFieldBasedFrameDecoder;
(3)码流日志打印Handler——LoggingHandler;
(4)SSL安全认证Handler——SslHandler;
(5)链路空闲检测Handler——IdleStateHandler;
(6)流量整形Handler——ChannelTrafficShapingHandler;
(7)Base64编解码——Base64Decoder和Base64Encoder。
六、 绑定并启动监听端口。在绑定监听端口之前系统会做一系列的初始化和检测工作,完成之后,会启动监听端口,并将ServerSocketChannel注册到Selector上监听客户端连接。
七、Selector轮询。由Reactor线程NioEventLoop负责调度和执行Selector轮询操作,选择准备就绪的Channel集合。
八、当轮询到准备就绪的Channel之后,就由Reactor线程NioEventLoop执行ChannelPipeline的相应方法,最终调度并执行Channelhandler。
九、执行Netty系统的ChannelHandler和用户添加定制的ChannelHandler。ChannelPipeline根据网络事件的类型,调度并执行ChannelHadler。
客户端创建
netty客户端创建时序图:
关键步骤:
一、用户线程创建Bootstrap实例,通过API设置创建客户端相关的参数,异步发起客户端连接。
二、创建处理客户端连接、I/O读写的Reator线程组NioEventLoopGroup。可以通过构造函数指定I/O线程的个数,默认为CPU内核数的2倍。
三、通过Bootstrap的ChannelFactory和用户指定的Channel类型创建用于客户端连接的NioSocketChannel,它的功能类似于JDK NIO类库提供的SocketChannel。
四、创建默认的Channel Handler Pipeline,用于调度和执行网络事件。
五、异步发起TCP连接,判断连接是否成功。如果成功,则直接将NioSocketChannel注册到多路复用器上,监听读操作位,用于数据报读取和消息发送。如果没有立即连接成功,则注册连接监听位到多路复用器,等待连接结果。
六、注册对应的网络监听状态位到多路复用器。
七、由多路复用器在I/O现场中轮询Channel,处理连接结果。
八、如果连接成功,设置Future结果,发送连接成功事件,触发ChannelPipeline执行。
九、有ChannelPipeline调度执行系统和用户的ChannelHandler,执行业务逻辑。
ByteBuf
当我们进行数据传输的时候,往往需要用到缓冲区,对应NIO而言,主要用到的是ByteBuffer,但由于其长度固定,只有一个标识位置的指针,API功能有限,Netty自己实现了ByteBuf。只要功能有,7中基本类型、byte数组、ByteBuffer等的读写、缓冲区自身的copy和slice(切片)、设置网络字节序、构造缓冲区、操作位置指针等方法。
ByteBuf通过两个位置指针来协助缓冲区的读写操作(ByteBuffer只要一个),读操作用readerIndex,写操作用writerIndex。支持动态扩展,并且有多种API协助开发。
Channel
Channel是Netty抽象出来的网络I/O读写相关的接口,为什么不使用jdk nio原生的Channel呢?
-
JDK的SocketChannel和ServerSocketChannel没有统一的Channel接口供业务开发者使用
-
JDK的SocketChannel和ServerSocketChannel的主要职责就是网络I/O操作,集成使用难度大,直接实现与开发一个新的Channel工作量差不多。
-
Netty的Channel需要跟Netty整体架构融合在一起,例如I/O模型,基于ChannelPipeline的定制模型,元数据配置化的TCP参数等。
-
更加灵活
Channel的主要设计理念
-
在Channel接口层,采用Facade模式进行统一封装,将网络I/O操作、网络I/O相关联的其他操作封装起来,统一对外提供。
-
Channel接口定义尽量大而全,为SocketChannel和ServerSocketChannel提供统一的视图,不同子类实现不同功能,公共功能在父类中实现,最大程度地实现功能和接口的重用。
-
具体实现采用聚合而非包含的方式,将相关的功能类聚合在Channel中,由Channel统一负责分配和调度,功能实现更加灵活。
ChannelPipeline和ChannelHannel
Netty的Channel过滤器实现原理与Servlet Filter机制一致。它将Channel的管道抽象为ChannelPipeline,消息在ChannelPipeline中流动和传递。ChannelPipeline持有I/O时间拦截器ChannelHandler的连表,有ChannelHandler对I/O事件进行拦截和处理,可以方便的通过新增和删除ChannelHandler来实现不同的业务逻辑定制。这类拦截器实际上是责任链模式的一种变形,主要是为了方便事件的拦截和用户业务逻辑的定制。
ChannelPipeline支持运行态动态的添加或者删除ChannelHandler,在某些场景下这个特性非常实用,例如高峰对系统做拥塞保护。
ChannelPipeline是线程安全的,但是ChannelHandler却不是线程安全的,用户需要自己保证ChannelHandler的线程安全
inbound:从I/O线程流向用户业务Handler的inbound事件。调用HeadHandler对应的fireXXX方法或者执行事件相关的逻辑操作
outbound:由用户线程或者代码发起的I/O操作
由于TCP存在粘包和组包问题,通常情况下必须自己处理半包消息,利用LenthFieldBaseFrameDecoder解码器可以自动解决半包问题。
在pipeline中增加LineBasedFrameDecoder解码器,指定正确的参数组合,可以将Netty的ByteBuf 解码成单个的整包消息,后面的业务解码器拿到的就是个完成的数据报。
半包处理的细节
ByteToMessageDecoder解码器用于将ByteBuf解码成POJO对象。通过cumulation是否为空判断解码器是否缓存了没有解码完成的半包消息,如果为空,说明是首次解码或者最近一次已经处理完了半包消息,没有缓存的半包消息需要处理,直接将需要解码的ByteBuf赋值给cumulation。如果cumulation缓存有上次没有解码完成的ByteBuf,则进行复制操作,将需要解码的ByteBuf复制到 cumulation中,他的原理如下:
在复制前需要对cumulation的可写缓冲区进行判断,如果不足则需要动态扩展。
Reactor在下一章
ChannelFuture
由于Netty的Future都是与异步I/O操作相关的,命名为ChannelFuture。
在Netty中,所有的I/O操作都是异步的,这意味着任何I/O调用都会立即返回,而不是像传统BIO那样同步等待操作完成。
ChannelFuture有两种状态,uncompleted和completed,当开始一个I/O操作时,一个新的ChannelFuture被创建,此时属于uncompleted——非失败、非成功、非取消,成功便成completed,三种装填,失败、成功、取消。
ChannelFuture提供了一系列新的API,用于获取操作结果、添加事件监听器、取消I/O操作、同步等待等。
Netty强烈建议直接通过添加监听器的方式获取I/O操作结果。ChannelFuture 可以同时增加一个或者多个GenericFutureListener,也可以remove。当I/O操作完成后,I/O线程会回调ChannelFuture中的GenericFutureListener的完成方法,并把ChannelFuture对象当做方法的入参。如果用户需要做上线文相关操作,需要将上线文信息保存到对应的ChannelFuture中。
推荐通过GenericFutureListener代替ChannelFuture的get等方法,原因是时间无法预测,不设置超时时间,会导致线程阻塞,设置超时时间无法精准预测,利用异步通知机制回调GenericFutureListener最佳,性能最优。
也不要在ChannelHandler中调用ChannelFuture的await方法,会导致死锁。原因是发起I/O操作之后,有I/O线程负责异步通知发起I/O操作的用户线程,如果I/O线程和用户线程是一个线程,会导致I/O线程等待自己通知自己通知操作完成,这就导致死锁。
异步I/O操作有两类超时,TCP层面的I/O超时和业务逻辑层面的超时。业务逻辑时间应大于I/O超时时间。ChannelFuture超时并不代表I/O超时,这意味着ChannelFuture超时后,如果没有关闭连接资源,随后连接依旧可以成功,但会有严重的问题。
欢迎关注公众号,让我们一起学习探讨问题