【Netty权威指南】13-ChannelPipeline和ChannelHandler

Netty的 ChannelPipeline和 Channelhandler机制类似于 Servlet和 Filter过滤器,这类拦截器实际上是职责链模式的一种变形,主要是为了方便事件的拦截和用户业务逻辑的定制。
Netty的 Channel过滤器实现原理与 Servlet Filter机制一致,它将 Channe的数据管道抽象为 ChannelPipeline,消息在ChannelPipeline中流动和传递。 ChannelPipeline持有IO事件拦截器 ChannelHandler的链表,由 ChannelHandler对I/O事件进行拦截和处理,可以
方便地通过新增和删除 ChannelHandler来实现不同的业务逻辑定制,不需要对已有的ChannelHandler进行修改,能够实现对修改封闭和对扩展的支持。

1、ChannelPipeline功能说明

ChannelPipeline是 ChannelHandler的容器,它负责 ChannelHandler的管理和事件拦截与调度。

1.1、ChannelPipeline的事件处理

下图展示了一个消息被 ChannelPipeline的 ChannelHandler链拦截和处理的全过程,消息的读取和发送处理全流程描述如下。
(1)底层的 SocketChannel.read()方法读取 ByteBuf,触发 ChannelRead事件,由IO线程 NioEventLoop调用 ChannelPipeline的 fireChanneIRead(Object msg)方法,将消息(ByteBuf)传输到 ChannelPipeline中。
(2)消息依次被 HeadHandler、 ChannelHandler1、 ChannelHandler2…… TailHandler拦截和处理,在这个过程中,任何ChannelHandler都可以中断当前的流程,结束消息的传递
(3)调用 ChannelHandlerContext的 write方法发送消息,消息从 TailHandler开始,途经 ChannelHandlerN… ChannelHandler1、HeadHandler,最终被添加到消息发送缓冲区中等待刷新和发送,在此过程中也可以中断消息的传递,例如当编码失败时,就需要中断流
程,构造异常的 Future返回。

Netty中的事件分为 inbound事件和 outbound事件。 inbound事件通常由IO线程触发,例如TCP链路建立事件、链路关闭事件、读事件、异常通知事件等,它对应上图的左半部分。
触发 inbound事件的方法如下。
(1) ChannelHandlerContext.fireChannelRegistered(): Channel注册事件;
(2) ChannelHandlerContext.fireChannelActive():TCP链路建立成功, Channel激活事件;
(3) ChannelHandlerContext.fireChannelRead(Object):读事件;
(4) ChannelHandlerContext.fireChannelReadComplete(:读操作完成通知事件;
(5) ChannelHandlerContext.fireExceptionCaught(Throwable):异常通知事件;
(6) ChannelHandlerContext.fireUserEventTriggered(Object):用户自定义事件;
(7) ChannelHandlerContext.fireChannelWritabilityChanged(): Channel的可写状态变化通知事件;
(8) ChannelHandlerContext.fireChannelInactive():TCP连接关闭,链路不可用通知事件。
outbound事件通常是由用户主动发起的网络IO操作,例如用户发起的连接操作、绑定操作、消息发送等操作,它对应上图的右半部分。
触发 outbound事件的方法如下:
(1) ChannelHandlerContext.bind(SocketAddress, Channelpromise):绑定本地地址事件;
(2) ChannelHandlerContext.connect(SocketAddress, SocketAddress, ChannelPromise):连接服务端事件;
(3) ChannelhandlerContext.write(Object, ChannelPromise):发送事件;
(4) ChannelHandlerContext.flush():刷新事件;
(5) ChannelHandlerContext.read():读事件;
(6) ChannelhandlerContext.disconnect(ChannelPromise):断开连接事件
(7) ChannelHandlerContext.close(ChannelPromise):关闭当前 Channel事件。

1.2、自定义拦截器

ChannelPipeline通过 ChannelHandler接口来实现事件的拦截和处理,由于ChannelHandler中的事件种类繁多,不同的 ChannelHandler可能只需要关心其中的某一个或者几个事件,所以,通常 ChannelHandler只需要继承 ChannelHandlerAdapter类覆盖自已关心的方法即可。
例如,下面的例子展示了拦截 ChannelActive事件,打印TCP链路建立成功日志,代码如下。

下面的例子展示了如何在链路关闭的时候释放资源,示例代码如下。

1.3、构建 pipeline

事实上,用户不需要自己创建 pipeline,因为使用 ServerBootstrap或者 Bootstrap启动服务端或者客户端时,Netty会为每个 Channel连接创建一个独立的 pipeline。对于使用者而言,只需要将自定义的拦截器加入到 pipeline中即可。相关的代码如下

对于类似编解码这样的 ChannelHandler,它存在先后顺序,例如 MessageToMessageDecoder,在它之前往往需要有 ByteToMessageDecoder将 ByteBuf解码为对象,然后对对象做二次解码得到最终的POJO对象。 Pipeline支持指定位置添加或者删除拦截器,相关接口定义如图17-2所示。

1.4、ChannelPipeline的主要特性

ChannelPipeline支持运行态动态的添加或者删除 ChannelHandler,在某些场景下这个特性非常实用。例如当业务高峰期需要对系统做拥塞保护时,就可以根据当前的系统时间进行判断,如果处于业务高峰期,则动态地将系统拥塞保护 ChannelHandler添加到当前的
ChannelPipeline中,当高峰期过去之后,就可以动态删除拥塞保护 ChannelHandler了。
ChannelPipeline是线程安全的,这意味着N个业务线程可以并发地操作 ChannelPipeline而不存在多线程并发问题。但是, ChannelHandler却不是线程安全的,这意味着尽管ChannelPipeline是线程安全的,但是用户仍然需要自己保证 ChannelHandler的线程安全。

2、ChannelPipeline源码分析

ChannelPipeline的代码相对比较简单,它实际上是一个 ChannelHandler的容器,内部维护了一个 ChannelHandler的链表和迭代器,可以方便地实现 Channelhandler查找、添加替换和删除。

2.1、ChannelPipeline的类继承关系图

ChannelPipeline的类继承关系比较简单,如图17-3所示。

2.2、ChannelPipeline对 ChannelHandler的管理

ChannelPipeline是 ChannelHandler的管理容器,负责 Channelhandler的查询、添加、替换和删除。由于它与Map等容器的实现非常类似,所以我们只简单抽取新增接口进行源码分析,其他方法读者可以自行阅读和分析。在 ChannelPipeline中添加 Channelhandler
方法如图17-4所示。

直接调用 addBefore(ChannelhandlerInvoker invoker, String baseName, final String name,Channelhandler handler)方法,代码如图17-5所示。

由于 ChannelPipeline支持运行期动态修改,因此存在两种潜在的多线程并发访问场景。
◎IO线程和用户业务线程的并发访问;
◎用户多个线程之间的并发访问。
为了保证 ChannelPipeline的线程安全性,需要通过线程安全容器或者锁来保证并发访问的安全,此处Netty直接使用了 synchronized关键字,保证同步块内的所有操作的原子性。首先根据 baseName获取它对应的 DefaultchannelHandlerContext, ChannelPipeline维
护了 Channelhandler名和 ChannelHandlerContext实例的映射关系,代码如图17-6所示。

对新增的 ChannelHandler名进行重复性校验,如果已经有同名的 ChannelHandler存在,则不允许覆盖,抛出 IllegalArgumentException("Duplicate handler name:"+name)异常。校验通过之后,使用新增的 ChannelHandler等参数构造一个新的 DefaultchannelHandlerContext实例,代码如图17-7所示。

将新创建的 DefaultChannelHandlerContext添加到当前的 pipeline中,代码如图17-8所示。

首先需要对添加的 ChannelHandlerContext做重复性校验,校验代码如图17-9所示。

如果 ChannelHandlerContext不是可以在多个 ChannelPipeline中共享的,且已经被添加到 ChannelPipeline中,则抛出 ChannelPipelineException异常。 Handler指针修改如图17-10所示。

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

2.3、ChannelPipeline的 inbound事件

当发生某个oO事件的时候,例如链路建立、链路关闭、读取操作完成等,都会产生个事件,事件在 pipeline中得到传播和处理,它是事件处理的总入口。由于网络IO相关的事件有限,因此Netty对这些事件进行了统一抽象,Netty自身和用户的 ChannelHandler会对感兴趣的事件进行拦截和处理。
pipeline中以 fireXXX命名的方法都是从l/O线程流向用户业务 Handler的 inbound事件,它们的实现因功能而异,但是处理步骤类似,总结如下。
(1)调用 HeadHandler对应的 fireXXX方法;
(2)执行事件相关的逻辑操作。
以 fireChannelActive方法为例,调用 head.fireChannelActive()之后,判断当前的 Channel配置是否自动读取,如果为真则调用 Channel的read方法,代码如图17-11所示。

2.4、ChannelPipeline的 outbound事件

由用户线程或者代码发起的IO操作被称为 outbound事件,事实上 inbound和 outbound是Netty自身根据事件在 pipeline中的流向抽象出来的术语,在其他NIO框架中并没有这个概念。
inbound事件相关联的操作如图17-12所示。

Pipeline本身并不直接进行IO操作,在前面对 Channel和 Unsafe的介绍中我们知道最终都是由 Unsafe和 Channel来实现真正的IO操作的。 Pipeline负责将IO事件通过TailHandler进行调度和传播,最终调用 Unsafe的IO方法进行IO操作,相关代码实现如图17-13所示。

它直接调用 TailHandler的 connect方法,最终会调用到 HeadHandler的 connect方法代码如图17-14所示。

最终由 Headhandler调用 Unsafe的 connect方法发起真正的连接, pipeline仅仅负责事件的调度。

3、ChannelHandler功能说明

Channelhandler类似于 Servlet的 Filter过滤器,负责对l/O事件或者l/O操作进行拦截和处理,它可以选择性地拦截和处理自已感兴趣的事件,也可以透传和终止事件的传递。
基于 ChannelHandler接口,用户可以方便地进行业务逻辑定制,例如打印日志、统封装异常信息、性能统计和消息编解码等。
Channelhandler支持注解,目前支持的注解有两种
◎Sharable:多个 ChannelPipeline共用同一个 ChannelHandler;
◎Skip:被@Skip注解的方法不会被调用,直接被忽略。

3.1、ChannelHandlerAdapter功能说明

对于大多数的 ChannelHandler会选择性地拦截和处理某个或者某些事件,其他的事件会忽略,由下一个ChannelHandler进行拦截和处理。这就会导致一个问题:用户ChannelHandler必须要实现 ChannelHandler的所有接口,包括它不关心的那些事件处理接口,这会导致用户代码的冗余和臃肿,代码的可维护性也会变差。
为了解决这个问题,Netty提供了 ChannelHandlerAdapter基类,它的所有接口实现都是事件透传,如果用户 ChannelHandler关心某个事件,只需要覆盖 ChannelHandlerAdapter对应的方法即可,对于不关心的,可以直接继承使用父类的方法,这样子类的代码就会非常简
洁和清晰。前面几章样例代码中,我们的 ChannelHandler都是直接继承自 ChannelHandlerAdapter,开发起来非常简单和高效。
ChannelHandlerAdapter相关的代码实现如图17-15所示。

从图17-15的源码中我们发现这些透传方法被@Skip注解了,这些方法在执行的过程中会被忽略,直接跳到下一个 ChannelHandler中执行对应的方法。

3.2、ByteToMessageDecoder功能说明

利用NIO进行网络编程时,往往需要将读取到的字节数组或者字节缓冲区解码为业务可以使用的POJO对象。为了方便业务将 ByteBuf解码成业务POJO对象, Netty提供了ByteToMessageDecoder抽象工具解码类。
用户的解码器继承 ByteToMessageDecoder,只需要实现 void decode(ChannelHandlerContext ctx,ByteBuf in,List<Object> out)抽象方法即可完成 ByteBuf到POJO对象的解码。
由于 ByteDecoder并没有考虑TCP粘包和组包等场景,读半包需要用户解码器自己负责处理。正因为如此,对于大多数场景不会直接继承 ByteToMessageDecoder,而是继承另外一些更高级的解码器来屏蔽半包的处理,下面的小节我们会对它们进行介绍。

3.3、MessageToMessageDecoder功能说明

MessageToMessageDecoder实际上是Netty的二次解码器,它的职责是将一个对象次解码为其他对象。
为什么称它为二次解码器呢?我们知道,从 SocketChannel读取到的TCP数据报是ByteBuffer,实际就是字节数组,我们首先需要将ByteBuffer缓冲区中的数据报读取出来,并将其解码为Java对象;然后对Java对象根据某些规则做二次解码,将其解码为另一个POJO对象。因为 MessageToMessageDecoder在 ByteToMessageDecoder之后,所以称之为二次解码器。
二次解码器在实际的商业项目中非常有用,以HTTP+XML协议栈为例,第一次解码往往是将字节数组解码成 HttpRequest对象,然后对 HttpRequest消息中的消息体字符串进行二次解码,将XML格式的字符串解码为POJO对象,这就用到了二次解码器。类似这样的场景还有很多,不再一一枚举。
事实上,做一个超级复杂的解码器将多个解码器组合成一个大而全的 MessageToMessageDecoder解码器似乎也能解决多次解码的问题,但是采用这种方式的代码可维护性会非常差。例如,如果我们打算在HTTP+XML协议栈中增加一个打印码流的功能,即首次解码获取 HttpRequest对象之后打印XML格式的码流。如果采用多个解码器组合,在中间插入一个打印消息体的 Handler即可,不需要修改原有的代码;如果做一个大而全的解码器,就需要在解码的方法中增加打印码流的代码,可扩展性和可维护性都会变差。
用户的解码器只需要实现 void decode(ChannelHandlerContext ctx,I msg,List<Object> out)抽象方法即可,由于它是将一个POJO解码为另一个POJO,所以一般不会涉及到半包的处理,相对于 ByteToMessageDecoder更加简单些。

3.4、LengthFieldFrameDecoder功能说明

如何区分一个整包消息,通常有如下4种做法。
◎固定长度,例如每120个字节代表一个整包消息,不足的前面补零。解码器在处理这类定常消息的时候比较简单,每次读到指定长度的字节后再进行解码。
◎通过回车换行符区分消息,例如FTP协议。这类区分消息的方式多用于文本协议。
◎通过分隔符区分整包消息
◎通过指定长度来标识整包消息
如果消息是通过长度进行区分的, LengthFieldBasedFrameDecoder都可以自动处理粘包和半包问题,只需要传入正确的参数,即可轻松搞定“读半包”问题。
下面我们看看如何通过参数组合的不同来实现不同的“半包”读取策略。第一种常用的方式是消息的第一个字段是长度字段,后面是消息体,消息头中只包含一个长度字段。
它的消息结构定义如图17-16所示

使用以下参数组合进行解码

  • lengthFieldOffset =0;
  • lengthFieldLength=2;
  • lengthAdjustment=0;
  • initialBytesToStrip=0。

解码后的字节缓冲区内容如图17-17所示。

因为通过 ByteBuf.readableBytes()方法我们可以获取当前消息的长度,所以解码后的字节缓冲区可以不携带长度字段,由于长度字段在起始位置并且长度为2,所以将initialBytesToStrip设置为2,参数组合修改为

  • lengthFieldoffset=0;
  • lengthFieldLength=2;
  • lengthAdjustment =0;
  • initialBytesToStrip= 2。

解码后的字节缓冲区内容如图17-18所示。

从图17-18的解码结果看,解码后的字节缓冲区丢弃了长度字段,仅仅包含消息体,不过通过 ByteBuf.readableBytes()方法仍然能够获取到长度字段的值。
在大多数的应用场景中,长度仅用来标识消息体的长度,这类协议通常由消息长度字段+消息体组成,如图17-18所示的例子。但是,对于一些协议,长度还包含了消息头的长度。在这种应用场景中,往往需要使用 lengthAdjustment进行修正,修正后的参数组合方式如下。由于整个消息的长度往往都大于消息体的长度,所以, lengthAdjustment为负数,图17-19展示了通过指定 lengthAdjustment字段来包含消息头的长度。

  • lengthFieldoffset =0;
  • lengthFieldLength= 2;
  • lengthAdjustment =-2;
  • initialBytesToStrip =0。

由于协议种类繁多,并不是所有的协议都将长度字段放在消息头的首位,当标识消息长度的字段位于消息头的中间或者尾部时,需要使用 length FieldOffset字段进行标识,下面的参数组合给出了如何解决消息长度字段不在首位的问题。

  • lengthFieldoffset= 2;
  • lengthFieldLength =3;
  • lengthAdjustment =0;
  • initialBytesToStrip=0。

由于消息头1的长度为2,所以长度字段的偏移量为2;消息长度字段 Length为3,所以 lengthFieldLength值为3。由于长度字段仅仅标识消息体的长度,所以 lengthAdjustment和 initialBytesToStrip都为0。
最后一种场景是长度字段夹在两个消息头之间或者长度字段位于消息头的中间,前后都有其他消息头字段,在这种场景下如果想忽略长度字段以及其前面的其他消息头字段,则可以通过 initialBytesToStrip参数来跳过要忽略的字节长度,它的组合效果如下。

  • lengthFieldOffset=1;
  • lengthFieldLength =2;
  • lengthAdjustment= 1;
  • initialBytesToStrip=3。

首先,由于HDR1的长度为1,所以长度字段的偏移量 lengthFieldOffset为1:长度字段为2个字节,所以 lengthFieldLength为2。由于长度字段是消息体的长度,解码后如果携带消息头中的字段,则需要使用 lengthAdjustment进行调整,此处它的值为1,表示的是HDR2的长度,最后由于解码后的缓冲区要忽略长度字段和HDR1部分,所以lengthAdjustment为3。解码后的结果为13个字节,HDRl和 Length字段被忽略。
事实上,通过4个参数的不同组合,可以达到不同的解码效果,用户在使用过程中可以根据业务的实际情况进行灵活调整。
由于TCP存在粘包和组包问题,所以通常情况下必须自己处理半包消息。利用LengthFieldBasedFrameDecoder解码器可以自动解决半包问题,它通常的用法如下。

在 pipeline中增加 LineBasedFrameDecoder解码器,指定正确的参数组合,它可以将Netty的ByteBuf解码成单个的整包消息,后面的业务解码器拿到的就是个完整的数据报,正常进行解码即可,不再需要额外考虑“读半包”问题,方便了业务消息的解码。

3.5、MessageToByteEncoder功能说明

MessageToByteEncoder负责将POJO对象编码成 ByteBuf,用户的编码器继承 MessageToByteEncoder,实现 void encode(Channelhandler Context ctx,I msg, ByteBuf out)接口接口,示例代码如下。

3.6、MessageToMessageEncoder功能说明

将一个POJO对象编码成另一个对象,以HTTP+XML协议为例,它的一种实现方式是:先将POJO对象编码成XML字符串,再将字符串编码为HTTP请求或者应答消息。
对于复杂协议,往往需要经历多次编码,为了便于功能扩展,可以通过多个编码器组合来实现相关功能。
用户的解码器继承 MessageToMessageEncoder解码器,实现 void encode(ChannelHandlerContext ctx,I msg,List<Object> out)方法即可。注意,它与 MessageToByteEncoder的区别是输出是对象列表而不是 ByteBuf,示例代码如下。

3.7、LengthFieldPrepender功能说明

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

通过 LengthFieldPrepender可以将待发送消息的长度写入到 ByteBuf的前2个字节,码后的消息组成为长度字段+原消息的方式。
通过设置 LengthFieldPrepender为true,消息长度将包含长度本身占用的字节数,打开 LengthFieldPrepender后,图17-22示例中的编码结果如图17-23所示。

4、ChannelHandler源码分析

4.1、ChannelHandler的类继承关系图

相对于ByteBuf和 Channel, ChannelHandler的类继承关系稍微简单些,但是它的子类非常多。由于 ChannelHandler是Netty框架和用户代码的主要扩展和定制点,所以它的子类种类繁多、功能各异,系统 Channelhandler主要分类如下。
◎ ChannelPipeline的系统 Channelhandler,用于IO操作和对事件进行预处理,对于用户不可见,这类 ChannelHandler主要包括 HeadHandler和 TailHandler;
◎ 编解码 ChannelHandler,包括 ByteToMessageCodec、MessageToMessageDecoder等,这些编解码类本身又包含多种子类,如图17-24所示。
◎ 其他系统功能性 ChannelHandler,包括流量整型 Handler、读写超时 Handler、日志 Handler等

4.2、ByteToMessageDecoder源码分析

顾名思义, ByteToMessageDecoder解码器用于将 ByteBuf解码成POJO对象,下面起看它的实现。
首先看 channelRead方法的源码,如图17-25所示。

首先判断需要解码的msg对象是否是 ByteBuf,如果是 ByteBuf才需要进行解码,否则直接透传。
通过 cumulation是否为空判断解码器是否缓存了没有解码完成的半包消息,如果为空,说明是首次解码或者最近一次已经处理完了半包消息,没有缓存的半包消息需要处理,直接将需要解码的 Byte Buf赋值给 cumulation;如果 cumulation缓存有上次没有解码完成的 ByteBuf,则进行复制操作,将需要解码的 ByteBuf复制到 cumulation中,它的原理如下。

在复制之前需要对 cumulation的可写缓冲区进行判断,如果不足则需要动态扩展,扩展的代码如图17-26所示。

扩展的代码很简单,利用字节缓冲区分配器重新分配一个新的 ByteBuf,将老的cumulation复制到新的 ByteBuf中,释放 cumulation。需要注意的是,此处内存扩展没有采用倍增或者步进的方式,分配的缓冲区恰恰够用,此处的算法可以优化下,以防止连续半包导致的频繁缓冲区扩张和内存复制。
复制操作完成之后释放需要解码的 ByteBuf对象,调用 callDecode方法进行解码,代码如图17-27所示。
对 ByteBuf进行循环解码,循环的条件是解码缓冲区对象中有可读的字节,调用抽象decode方法,由用户的子类解码器进行解码,方法定义如图17-28所示。
解码后需要对当前的 pipeline状态和解码结果进行判断,代码如图17-29所示。

如果当前的 ChannelHandlerContext己经被移除,则不能继续进行解码,直接退出循环;
如果输出的out列表长度没变化,说明解码没有成功,需要针对以下不同场景进行判断。
(1)如果用户解码器没有消费 Byte Buf,则说明是个半包消息,需要由IO线程继续读取后续的数据报,在这种场景下要退出循环。
(2)如果用户解码器消费了 Byte Buf,说明可以解码可以继续进行。从图17-29所示代码可以看出,业务解码器需要遵守Netty的某些契约,解码器才能正常工作,否则可能会导致功能错误,最重要的契约就是:如果业务解码器认为当前的字节缓冲区无法完成业务层的解码,需要将 readIndex复位,告诉Netty解码条件不满足应当退出解码,继续读取数据报。
(3)如果用户解码器没有消费 ByteBuf,但是却解码出了一个或者多个对象,这种行为被认为是非法的,需要抛出 DecoderException异常。
(4)最后通过 isSingleDecode进行判断,如果是单条消息解码器,第一次解码完成之后就退出循环。

4.3、MessageToMessageDecoder源码分析

MessageToMessageDecoder负责将一个PoJO对象解码成另一个POJO对象,下面起看下它的源码实现。
首先看 channelRead方法的源码,如图17-30所示。
先通过 RecyclableArrayList创建一个新的可循环利用的 RecyclableArrayList,然后对解码的消息类型进行判断,通过类型参数校验器看是否是可接收的类型,如果是则校验通过,参数类型校验的代码如图17-31所示。
校验通过之后,直接调用 decode抽象方法,由具体实现子类进行消息解码,解码抽象方法定义如图17-32所示。
解码完成之后,调用 ReferenceCountUtil的 release方法来释放被解码的msg对象如果需要解码的对象不是当前解码器可以接收和处理的类型,则将它加入到RecyclableArrayList中不进行解码。

最后,对RecyclableList进行遍历,循环调用ChannelHandlerContext的fireChannelRead方法,通知后续的 ChannelHandler继续进行处理。循环通知完成之后,通过 recycle方法释放 RecyclableList对象。

4.4、LengthFieldBasedFrameDecoder源码分析

本节我们一起来学习最通用和重要的解码器——基于消息长度的半包解码器,首先看它的入口方法,源码如图17-33所示。

调用内部的 decode(ChannelHandlerContext ctx, ByteBuf in)方法,如果解码成功,将其加入到输出的 List<Object> out列表中。
下面继续看 decode(ChannelHandlerContext ctx, ByteBuf in)的实现,如图17-34所示。

判断 discardingTooLongFrame标识,看是否需要丢弃当前可读的字节缓冲区,如果为真,则执行丢弃操作,具体如下。
判断需要丢弃的字节长度,由于丢弃的字节数不能大于当前缓冲区可读的字节数,所以需要通过 Math.min(bytesToDiscard,in.readableBytes())函数进行选择,取 bytesToDiscard和缓冲区可读字节数之中的最小值。计算获取需要丢弃的字节数之后,调用 ByteBuf的skipBytes方法跳过需要忽略的字节长度,然后 bytesToDiscard减去已经忽略的字节长度。
最后判断是否已经达到需要忽略的字节数,达到的话对 discardingTooLongFrame等进行置位,代码如图17-35所示。

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

通过读索引和lengthFieldOffset计算获取实际的长度字段索引,然后通过索引值获取消息报文的长度字段,代码如图17-37所示。


根据长度字段自身的字节长度进行判断,共有以下6种可能的取值

  • 长度所占字节为1,通过 ByteBuf的 getUnsignedByte方法获取长度值
  • 长度所占字节为2,通过 ByteBuf的 getUnsignedShort方法获取长度值;
  • 长度所占字节为3,通过 Byte Buf的 getUnsignedMedium方法获取长度值
  • 长度所占字节为4,通过 Byte Buf的 getUnsignedInt方法获取长度值;
  • 长度所占字节为8,通过 Byte Buf的 geeLong方法获取长度值;
  • 其他长度不支持,抛出 DecoderException异常。

获取长度之后,就需要对长度进行合法性判断,同时根据其他解码参数进行长度调整,代码如图17-38所示。

如果长度小于0,说明报文非法,跳过 lengthFieldEndOffset个字节,抛出 CorruptedFrameException异常。
根据 lengthFieldEndOffset和 lengthAdjustment字段进行长度修正,如果修正后的报文长度小于 lengthFieldEndOffset,则说明是非法数据报,需要抛出 CorruptedFrameException异常。
如果修正后的报文长度大于 ByteBuf的最大容量,说明接收到的消息长度大于系统允许的最大长度上限,需要设置 discardingTooLongFrame,计算需要丢弃的字节数,根据情况选择是否需要抛出解码异常。
丢弃的策略如下: frameLength减去 ByteBuf的可读字节数就是需要丢弃的字节长度,如果需要丢弃的字节数 discard小于缓冲区可读的字节数,则直接丢弃整包消息。如果需要丢弃的字节数大于当前的可读字节数,说明即便将当前所有可读的字节数全部丢弃,也无法完成任务,则设置 discardingTooLongFrame标识为true,下次解码的时候继续丢弃。
丢弃操作完成之后,调用 faillfNecessary方法根据实际情况抛出异常。如图17-39所示。

如果当前的可读字节数小于 frameLength,说明是个半包消息,需要返回空,由IO线程继续读取后续的数据报,等待下次解码。
对需要忽略的消息头字段进行判断,如果大于消息长度 frameLength,说明码流非法需要忽略当前的数据报,抛出 CorruptedFrameException异常。通过 ByteBuf的 skipBytes方法忽略消息头中不需要的字段,得到整包 ByteBuf。
通过 extractFrame方法获取解码后的整包消息缓冲区,代码如图17-40所示。

根据消息的实际长度分配一个新的 ByteBuf对象,将需要解码的 ByteBuf可写缓冲区复制到新创建的 ByteBuf中并返回,返回之后更新原解码缓冲区 ByteBuf为原读索引+消息报文的实际长度( actualFrameLength)。
至此,基于长度的半包解码器介绍完毕,对于使用者而言,实际不需要对 LengthFieldBasedFrameDecoder进行定制。只需要了解每个参数的用法,再结合用户的业务场景进行参数设置,即可实现半包消息的自动解码,后面的业务解码器得到的是个完整的整包消息,不用再额外考虑如何处理半包。这极大地降低了开发难度,提升了开发效率。

4.5、MessageToByteEncoder源码分析

MessageToEncoder负责将用户的POJO对象编码成 ByteBuf,以便通过网络进行传输。下面一起看它的源码实现,如图17-41所示。

首先判断当前编码器是否支持需要发送的消息,如果不支持则直接透传:如果支持则判断缓冲区的类型,对于直接内存分配 ioBuffer(堆外内存),对于堆内存通过 heapBuffer方法分配。
编码使用的缓冲区分配完成之后,调用 encode抽象方法进行编码,方法定义如图17-42所示。

编码完成之后,调用 ReferenceCountutil的 release方法释放编码对象msg。对编码后的 ByteBuf进行以下判断。
◎如果缓冲区包含可发送的字节,则调用 ChannelHandlerContext的 write方法发送ByteBuf;
◎如果缓冲区没有包含可写的字节,则需要释放编码后的 ByteBuf,写入一个空的ByteBuf到 ChannelHandlerContext中
发送操作完成之后,在方法退出之前释放编码缓冲区 ByteBuf对象。

4.6、MessageToMessageEncoder源码分析

MessageToMessageEncoder负责将一个POJO对象编码成另一个POJO对象,例如将XMLDocument对象编码成XML格式的字符串。下面一起看它的源码实现,如图17-43所示

与之前的编码器类似,创建 RecyclableArrayList对象,判断当前需要编码的对象是否是编码器可处理的类型,如果不是,则忽略,执行下一个 ChannelHandler的 write方法具体的编码方法实现由用户子类编码器负责完成,如果编码后的 RecyclableArrayList为空,说明编码没有成功,释放 RecyclableArrayList引用。
如果编码成功,则通过遍历 RecyclableArrayList,循环发送编码后的POJO对象,代码如图17-44所示。

4.7、LengthFieldPrepender源码分析

LengthFieldPrepender负责在待发送的 ByteBuf消息头中增加一个长度字段来标识消息的长度,它简化了用户的编码器开发,使用户不需要额外去设置这个长度字段。下面我们来看下它的实现,如图17-45所示。


首先对长度字段进行设置,如果需要包含消息长度自身,则在原来长度的基础之上再加上 lengthFieldLength的长度。

如果调整后的消息长度小于0,则抛出参数非法异常。对消息长度自身所占的字节数进行判断,以便采用正确的方法将长度字段写入到 Byte Buf中,共有以下6种可能。

  1. 长度字段所占字节为1:如果使用1个Byte字节代表消息长度,则最大长度需要小于256个字节。对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的 Byte Buf并通过 write Byte将长度值写入到 ByteBuf中
  2. 长度字段所占字节为2:如果使用2个Byte字节代表消息长度,则最大长度需要小于65536个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的 ByteBuf并通过 write short将长度值写入到 ByteBuf中。
  3. 长度字段所占字节为3:如果使用3个Byte字节代表消息长度,则最大长度需要小于16777216个字节,对长度进行校验,如果校验失败,则抛出参数非法异常;若校验通过,则创建新的 Byte Buf并通过 write Medium将长度值写入到 ByteBuf中。
  4. 长度字段所占字节为4:创建新的 Byte Buf,并通过 writelnt将长度值写入到ByteBuf中
  5. 长度字段所占字节为8:创建新的 Byte Buf,并通过 writeLong将长度值写入到 ByteBuf中。
  6. 其他长度值:直接抛出 Error。

最后将原需要发送的 ByteBuf复制到 List<object> out中,完成编码。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值