ChannelPipeline和ChannelHandler

Channel过滤器实现原理与Servlet filter机制一致,它将Channel的数据管道抽象为ChannelPipeline,消息在ChannelPipeline中流动和传递。ChannelPipeline持有I/O事件拦截器ChannelHandler的链表,由ChannelHandler对I/O事件进行拦截和处理,可以方便的通过新增和删除ChannelHandler来实现不同的业务逻辑定制,不需要对已有的ChannelHandler进行修改,能够实现对修改封闭和对扩展的支持。

1、ChannelPipeline功能说明

ChannelPipeline是ChannelHandler的容器,他负责ChannelHandler的管理和时间拦截与调度。

2、ChannelPripeline的事件处理

1)底层的SocketChannel read()方法去读ByteBuf,触发ChannelRead事件,由I/O线程NioEventloop调用ChannelPipeline的fireChannelRead(Object msg)方法,将消息传输到ChannelPipeline中

2)消息依次被HeadHandler、ChannelHandler1、ChannelHandler2.……TailHandler拦截和处理,在这个过程中,任何ChannelHandler都可以中断当前的流程,结束消息的传递

3)调用ChannelHandlerContext的write方法发送消息,消息从TailHandler开始,途经ChannelHandlerN……ChannelHandler1,HeadHandler,最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递

Netty的事件分为:inbound事件和outbound事件。

inbound事件通常由I/O线程触发,上图的左半部分;触发inbound事件的方法

①ChannelHandlerContext.fireChannelRegistered():Channel注册事件

②ChannelHandlerContext.fireChannelActive():TCP链路建立成功,Channel激活事件

③ChannelHandlerContext.fireChannelRead(Object):读事件

④ChannelHandlerContext.fireChannelReadComplete():读操作完成通知事件

⑤ChannelHandlerContext.fireExceptionCaught(Throwable):异常通知事件

⑥ChannelHandlerContext.fireUserEventTriggered(Object):用户自定义事件

⑦ChannelHandlerContext.fireChannelWritabilityChanged():Channel的可写状态变化通知事件

⑧ChannelHandlerContext.fireChannelInactive():TCP连接关闭,链路不可用通知事件

Outbound事件通常是由用户主动发起的网络I/O操作,上图对应的右半部分。触发Outbound事件的方法如下:

① ChannelHandlerContext.bind(SocketAddress, ChannelPromise):绑定本地地址事件

②ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise):连接服务端

③ChannelHandlerContext.write(Object, ChannelPromise):发送事件

④ChannelHandlerContext.flush():刷新事件

⑤ChannelHandlerContext.read():读事件

⑥ChannelHandlerContext.disconnect(ChannelPromise):断开连接事件

⑦ChannelHandlerContext.close(ChannelPromise):关闭当前Channel事件

3、自定义拦截器

ChannelPipeline通过ChannelHandler接口来实现事件的拦截和处理,由于ChannelHandler中的事件种类繁多,不同的ChannelHandler可能需要关心其中的某一个或者几个事件。

4、构建pipeline

Netty中Channel创建的时候,会创建一个独立的pipeline。对于使用者而言,只需要将自定义的拦截器加入到pipeline中即可

5、ChannelPipeline的主要特性

ChannelPipeline支持运行态动态的添加或者删除ChannelHandler。

ChannelPipeline是线程安全的。

但是ChannelHandler不是线程安全的。

6、ChannelPipeline源码分析

  • 类图
  • ChannelPipeline对ChannelHandler的管理

负责ChannelHandler的查询、添加、替换和删除。

ChannelPipeline的线程安全性,需要通过线程安全容器或者锁来保证并发访问的安全,此处Netty使用的synchronized关键字,保证同步块儿内的所有操作的原子性。

对新增的ChannelHandler名进行重复性校验,如果已经有同名的ChannelHandler存在,则不允许覆盖,抛出IllegalArgumentException。校验通过之后,使用新增的ChannelHandler等参数构造一个新的DefaultChannelHandlerContext实例。

将新创建的DefaultChannelHandler添加到当前的pipeline中,

首选需要对添加的ChannelHandlerContext做重复性校验

加入成功之后,缓冲ChannelHandlerContext,发送新增的ChannelHandlerContext通知消息。

  • ChannelPipeline的inbound事件

pipeline中以fireXX命名的方法都是从I/O线程流向用户业务Handler的inbound事件。

  • ChannelPipeline的outbound事件

Pipeline本身并不直接进行I/O操作。Pipeline负责将I/O事件通过TailHandler进行调度和传播,最终调用Unsafe的I/O方法进行I/O操作。

7、ChannelHandler功能说明

ChannelHandler类似于Servlet的filter过滤器,可以拦截和处理自己感兴趣的事件,也可以透传和终止事件的传递。业务逻辑定制,如打印日志、统一封装异常信息、性能统计和消息编解码。

ChannelHandler支持注解

1)Sharable:多个ChannelPipeline共用同一个ChannelHandler

2)Skip:被skip注解的方法不会被调用,直接被忽略

  • ChannelHandlerAdapter功能说明

用户ChannelHandler必须实现ChannelHandler的所有接口,包括不关心的那些事件处理接口,这会导致用户代码冗余和臃肿,代码可维护性变差。为了解决这个问题Netty提供了ChannelHandlerAdapter基类,它的所有接口实现都是事件透传,如果用户ChannelHandler关心某个事件,只需要覆盖ChannelHandlerAdapter对应的方法即可。

  • ByteToMessageDecoder功能说明

将读取到的字节数组或者字节缓冲区解码为业务可以使用的POJO对象。为了方便业务将ByteBuf解码成业务POJO对象,Netty提供了ByteToMessageDecoder抽象工具解码类。

用户的解码器继承ByteToMessageDecoder,只需要实现void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) 抽象方法接口完成ByteBuf到POJO对象的解码。但是ByteToMessageDecoder并没有考虑到TCP粘包和组包场景,读半包需要用户解码器自己负责处理。

  • MessageToMessageDecoder功能说明

是Netty的二次解码器,它的职责是将一个对象二次解码为其他对象。SocketChannel读取到的TCP数据报是ByteBuf,实际就是字节数组,首先需要将ByteBuffer缓冲区中的数据报读取出来,并将其解码为Java对象;然后对Java对象根据某些规则做二次解码,将其解码为另一个POJO对象。因为MessageToMessageDecoder在ByteToMessageDecoder之后,所以称为二次解码器。

二次解码器实际应用,HTTP+xml协议栈的应用。

  • LengthFieldBasedFrameDecoder功能说明

半包解码器有LineBasedFrameDecoder、DelimiterBasedFrameDecoder,现在提供第三种就是LengthFieldBasedFrameDecoder.如何区分整包消息:

①固定长度。

②通过回车换行符区分

③通过分隔符区分整包

④通过指定长度来标识整包

利用以以下参数祖新进行解码

lengthFieldOffset=0

lengthFieldLength=2

lengthAdjustment=0

initialBytesToStrip=0

1)消息的第一个字段是长度字段,后面是消息体,消息头中只包含一个长度字段。

2)长度包含消息头的长度,这种需要使用lengthAdjustment进行修正。

3)不是所有的协议都将长度字段放在消息头的首位,当标识消息长度的字段位于消息头的中间或者尾部时,需要使用lengthFieldOffset字段进行标识。

4)长度字段夹在两个消息头之间或者长度字段位于消息头的中间,前后都有其他消息头字段,在这种场景下如果想忽略长度字段以及其前面的其他消息头字段,则可以通过initialBytesToStrip参数来跳过要忽略的字节长度。

  • MessageToByteEncoder功能说明

负责将POJO对象编码成ByteBuf,用户的编码器继承MessageToByteEncoder,实现void encode(ChannelHandlerContext ctx, I msg, ByteBuf out)即可。

  • MessageToMessageEncoder功能说明

讲一个POJO对象编码成另一个对象。用户解码器继承MessageToMessageEncoder解码器,实现void encode(ChannelHandlerContext ctx, I msg, List<Object> out)方法即可。

  • LengthFieldPreoender功能说明

如果协议中的第一个字段为长度字段,Netty提供的LengthFieldPrepender编码器,可以计算当前待发送消息的二进制字节长度,将该长度添加到ByteBuf的缓冲区头中。

8、ChannelHandler源码分析

  • ChannelHandler的类继承图

ChannelHandler主要分类:

①ChannelPipeline的系统ChannelHandler,用于I/O操作和对事件进行预处理,对于用户不可见,这类ChannelHandler主要包括HeadHandler和TailHandler

②编解码ChannelHandler,包括ByteToMessageCodec、MessageToMessageDecoder等

③其他系统功能性ChannelHandler,保罗榴莲整型Handler、读写超时Handler、日志Handler

  • ByteToMessageDecoder源码分析
  • MessageToMessageDecoder
  • LengthFieldBasedFrameDecoder

①discardingTooLongFrame标识,是否需要丢弃当前可读的字节缓冲区,为true,执行丢弃。

判断需要丢弃的字节长度,由于丢弃的字节数不能大于当前缓冲区可读的字节数,所以需要通过Math.min(bytesToDiscard, in.readableBytes())函数进行选择,取bytesToDiscard和缓冲区可读字节数中的最小值。调用ByteBuf的skipBytes方法跳过需要忽略的字节长度,然后bytesToDiscard减去需要忽略的字节长度,最后判断是否已经达到需要忽略的数,达到的话对discardingTooLongFrame进行置位

②对当前缓冲区的可读字节数和长度偏移量进行对比,如果小于长度偏移量,则说明当前缓冲区的数据报不够,返回空,由线程继续读取后续的数据报。

③通过读索引和lengthFieldOffset计算获取实际的长度字段索引,然后通过索引值获取消息报文的长度字段。根据长度字段自身的字节长度进行判断,共有以下几种可能的取值:

a、长度所占字节为1,buf.getUnsignedByte(offset)获取长度值

b、长度所占字节为2,buf.getUnsignedShort(offset)获取长度值

c、长度所占字节为3,buf.getUnsignedMedium(offset)获取长度值

d、长度所占字节为4,buf.getUnsignedInt(offset)获取长度值

e、长度所占字节为8,buf.getLong(offset)获取长度值

f、 其他长度不支持,抛出DecoderException

④对获取的长度进行合法性判断,同时根据其他解码参数进行长度调整。

如果长度小于0,报文非法,跳过lengthFieldEndOffset字节,抛出CorruptedFrameException异常;

如果长度大于等于0, 根据lengthAdjustment + lengthFieldEndOffset和修正长度

修正后如果frameLength < lengthFieldEndOffset,非法数据报,抛出异常

修正后如果frameLength > maxFrameLength,接收到的消息长度大于系统允许的最大长度上线,需要设置discardingTooLongFrame,计算需要丢弃的字节数

丢弃策略:frameLength-ByteBuf的可读字节数=丢弃字节长度(discard)

如果丢弃字节数discard小于缓冲区可读字节数,丢弃整包消息。

如果丢弃字节数大于当前的可读字节数,说明即便当前的所欲偶可读字节数都丢弃,也无法完成任务,则设置discardingTooLongFrame为true,下次解码的时候继续丢弃,丢弃操作完成之后,调用failIfNecessary。

⑤如果当前的可读字节数小于frameLengthInt,说明是个半包消息,需要返回空,线程继续读取后续的数据报,等待下次解码。

⑥对需要忽略的消息头字段进行判断,如果大于frameLengthInt,码流非法,需要忽略数据报,

⑦extractFrame获取解码后的整包消息缓冲区

根据消息的实际长度分配一个新的ByteBuf对象,将需要解码的ByteBuf可写缓冲区复制到新创建ByteBuf中并返回,返回之后跟新原解码缓冲区ByteBuf为原读索引+消息报文的实际长度actualFrameLength

  • MessageToByteEncoder

如果支持不支持发送的消息直接透传;如果支持判断缓冲区的类型,对于直接内存分配ioBuffer(堆外内存),对于堆内存通过headBuffer方法分配。

  • MessageToMessageEncoder
  • LengthFieldPrepender

负责在待发送的ByteBuf消息头中增加一个长度字段来标识消息的长度,用户不需要额外去设置这个长度字段。

猜你喜欢

转载自blog.csdn.net/sunshine052697/article/details/87969167