副标题#e#

接上篇《TCP 粘包和半包 介绍及解决(上)》

上一篇介绍了粘包和半包及其通用的解决方案,今天重点来看一下 Netty 是如何实现封装成帧(Framing)方案的。

Netty - 粘包和半包(下)

解码核心流程

之前介绍过三种解码器FixedLengthFrameDecoder、DelimiterBasedFrameDecoder、LengthFieldBasedFrameDecoder,它们都继承自ByteToMessageDecoder,而ByteToMessageDecoder继承自ChannelInboundHandlerAdapter,其核心方法为channelRead。因此,我们来看看ByteToMessageDecoder的channelRead方法:

  1. @Override 
  2. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 
  3.  if (msg instanceof ByteBuf) { 
  4.  CodecOutputList out = CodecOutputList.newInstance(); 
  5.  try { 
  6.  // 将传入的消息转化为data 
  7.  ByteBuf data = (ByteBuf) msg; 
  8.  // 最终实现的目标是将数据全部放进cumulation中 
  9.  first = cumulation == null; 
  10.  // 第一笔数据直接放入 
  11.  if (first) { 
  12.  cumulation = data; 
  13.  } else { 
  14.  // 不是第一笔数据就进行追加 
  15.  cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data); 
  16.  } 
  17.  // 解码 
  18.  callDecode(ctx, cumulation, out); 
  19.  } 
  20.  // 以下代码省略,因为不属于解码过程 
  21.  } 

再来看看callDecode方法:

  1. protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { 
  2.  try { 
  3.  while (in.isReadable()) { 
  4.  int outoutSize = out.size(); 
  5.  if (outSize > 0) { 
  6.  // 以下代码省略,因为初始状态时,outSize 只可能是0,不可能进入这里 
  7.  } 
  8.  int oldInputLength = in.readableBytes(); 
  9.  // 在进行 decode 时,不执行handler的remove操作。 
  10.  // 只有当 decode 执行完之后,开始清理数据。 
  11.  decodeRemovalReentryProtection(ctx, in, out); 
  12.  // 省略以下代码,因为后面的内容也不是解码的过程 

再来看看decodeRemovalReentryProtection方法:

  1. final void decodeRemovalReentryProtection(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) 
  2.  throws Exception { 
  3.  // 设置当前状态为正在解码 
  4.  decodeState = STATE_CALLING_CHILD_DECODE; 
  5.  try { 
  6.  // 解码 
  7.  decode(ctx, in, out); 
  8.  } finally { 
  9.  // 执行hander的remove操作 
  10.  boolean removePending = decodeState == STATE_HANDLER_REMOVED_PENDING; 
  11.  decodeState = STATE_INIT; 
  12.  if (removePending) { 
  13.  handlerRemoved(ctx); 
  14.  } 
  15.  } 
  16. // 子类都重写了该方法,每种实现都会有自己特殊的解码方式 
  17. protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception; 

从上面的过程可以总结出,在解码之前,需要先将数据写入cumulation,当解码结束后,需要通过 handler 进行移除。

具体解码过程

刚刚说到decode方法在子类中都有实现,那针对我们说的三种解码方式,一一看其实现。

1. FixedLengthFrameDecoder

其源码为:

  1. @Override 
  2. protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { 
  3.  Object decodedecoded = decode(ctx, in); 
  4.  if (decoded != null) { 
  5.  out.add(decoded); 
  6.  } 
  7. protected Object decode( 
  8.  @SuppressWarnings("UnusedParameters") ChannelHandlerContext ctx, ByteBuf in) throws Exception { 
  9.  // 收集到的数据是否小于固定长度,小于就代表无法解析 
  10.  if (in.readableBytes() < frameLength) { 
  11.  return null; 
  12.  } else { 
  13.  return in.readRetainedSlice(frameLength); 
  14.  } 

就和这个类的名字一样简单,就是固定长度进行解码,因此,在设置该解码器的时候,需要在构造方式里传入frameLength。

2. DelimiterBasedFrameDecoder

#p#副标题#e#

其源码为:

  1. @Override 
  2. protected final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { 
  3.  Object decodedecoded = decode(ctx, in); 
  4.  if (decoded != null) { 
  5.  out.add(decoded); 
  6.  } 
  7. protected Object decode(ChannelHandlerContext ctx, ByteBuf buffer) throws Exception { 
  8.  // 当前的分割符是否是换行分割符(\n或者\r\n) 
  9.  if (lineBasedDecoder != null) { 
  10.  return lineBasedDecoder.decode(ctx, buffer); 
  11.  } 
  12.  // Try all delimiters and choose the delimiter which yields the shortest frame. 
  13.  int minFrameLength = Integer.MAX_VALUE; 
  14.  ByteBuf minDelim = null; 
  15.  // 其他分割符进行一次切分 
  16.  for (ByteBuf delim: delimiters) { 
  17.  int frameLength = indexOf(buffer, delim); 
  18.  if (frameLength >= 0 && frameLength < minFrameLength) { 
  19.  minFrameLength = frameLength; 
  20.  minDelim = delim; 
  21.  } 
  22.  } 
  23.  // 以下代码省略 

根据它的名字可以知道,分隔符才是它的核心。它将分割符分成两类,只有换行分割符(n或者rn)和其他。因此,需要注意的是,你可以定义多种分割符,它都是支持的。

3. LengthFieldBasedFrameDecoder

该类比较复杂,如果直接看方法容易把自己看混乱,因此我准备结合类上的解释,先看看其私有变量。

  1. * BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes) 
  2. * +------+--------+------+----------------+ +------+----------------+ 
  3. * | HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content | 
  4. * | 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" | 
  5. * +------+--------+------+----------------+ +------+----------------+ 
  • lengthFieldOffset :该字段代表 Length 字段是从第几个字节开始的。上面的例子里,Length 字段是从第1个字节开始(HDR1 是第0个字节),因此该值即为0。
  • lengthFieldLength:该字段代表 Length 字段所占用的字节数。上面的例子里,Length 字段占用2个字节,因此该值为2。
  • lengthAdjustment:该字段代表 Length 字段结束位置到真正的内容开始位置的距离。上面例子里,因为 Length 字段的含义是整个消息(包括 HDR1、Length、HDR2、Actual Content,一般 Length 指的只是 Actual Content),所以 Length 末尾到真正的内容开始位置(HDR1的开始处),相当于减少3个字节,所以是-3。
  • initialBytesToStrip: 展示时需要从 Length 字段末尾开始跳过几个字节。上面例子里,因为真正的内容是从 HDR1 开始的,最终展示的内容是从 HDR2 开始的,所以中间差了3个字节,所以该值是3。

该类的解码方法比较复杂,有兴趣的同学可以试着自己分析一下。

总结

这一篇主要是结合 Netty 里的源代码讲解了 Netty 中封装成帧(Framing)的三种方式,相信你一定有了不一样的理解。

dawei

【声明】:北京站长网内容转载自互联网,其相关言论仅代表作者个人观点绝非权威,不代表本站立场。如您发现内容存在版权问题,请提交相关链接至邮箱:bqsm@foxmail.com,我们将及时予以处理。