Netty 粘包拆包 编解码器简介与使用

1、netty中的编解码器

netty编解码包:

  • netty-codec
  • netty-codec-http
  • netty-codec-http2
  • netty-codec-memcache
  • netty-codec-redis
  • netty-codec-socks
  • netty-codec-stomp
  • netty-codec-mqtt
  • netty-codec-haproxy
  • netty-codec-dns
    等等

在这里插入图片描述

netty-codec是最基础的一个,其他的是对不同的协议包进行的扩展和适配,可以看到netty支持常用的和流行的协议格式,非常的强大。因为codec的内容非常多,要讲解他们也不是很容易,本文将会以netty-codec做一个例子,讲解其中最基本的也是最通用的编码解码器。

1.1、base64

这个codec是负责ByteBuf和base64过后的ByteBuf之间的转换。虽然都是从ByteBuf到ByteBuf,但是其中的内容发生了变化。

有两个关键的类,分别是Base64Encoder和Base64Decoder。因为Base64Decoder是一个MessageToMessageDecoder,所以需要使用一个DelimiterBasedFrameDecoder提前进行处理

 // Decoders
   pipeline.addLast("frameDecoder", new DelimiterBasedFrameDecoder(80, Delimiters.nulDelimiter()));
   pipeline.addLast("base64Decoder", new Base64Decoder());
  
   // Encoder
   pipeline.addLast("base64Encoder", new Base64Encoder());

1.2、compression

主要是对数据的压缩和解压缩服务。其支持的算法如下

  • brotli
  • Bzip2
  • FastLZ
  • JdkZlib
  • Lz4
  • Lzf
  • Snappy
  • Zlib
  • Zstandard

compression对于大数据量的传输特别有帮助,通过压缩可以节省传输的数据量,从而提高传输速度。

但是压缩是使用特定的算法来计算的,所以它是一个高CPU的操作,我们在使用的时候需要兼顾网络速度和CPU性能,并从中得到平衡。

1.3、JSON

json这个包里面只有一个JsonObjectDecoder类,主要负责将Byte流的JSON对象或者数组转换成JSON对象和数组。

JsonObjectDecoder直接就是一个ByteToMessageDecoder的子类,所以它不需要FrameDecoder,它是根据括号的匹配来判断Byte数组的起始位置,从而区分哪些Byte数据是属于同一个Json对象或者数组。

我们如果希望使用JSON来传输数据的话,这个类就非常有用了。

1.4、Protobuf (重点哦!!!!!!)

protobuf大家应该都很熟悉了,它是google出品的一种信息交换格式,可以将其看做是一种序列化的方式。它是语言中立、平台中立、可扩展的结构化数据序列化机制,和XML类似,但是比XML更小、更快、更简单。

netty对protobuf的支持在于可以将protobuf中的message和MessageLite对象跟ByteBuf进行转换。

protobuf的两个编码器也是message到message直接的转换,所以也需要使用frame detection。当然你也可以使用其他的frame detection比如LengthFieldPrepender和LengthFieldBasedFrameDecoder如下所示

   // Decoders
   pipeline.addLast("frameDecoder",
                    new LengthFieldBasedFrameDecoder(1048576, 0, 4, 0, 4));
   pipeline.addLast("protobufDecoder",
                    new ProtobufDecoder(MyMessage.getDefaultInstance()));
  
   // Encoder
   pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
   pipeline.addLast("protobufEncoder", new ProtobufEncoder());

其中LengthFieldPrepender会自动给字段前面加上一个长度字段:

之前:
   +----------------+
   | "HELLO, WORLD" |
   +----------------+

之后:
   +--------+----------------+
   + 0x000C | "HELLO, WORLD" |
   +--------+----------------+

当然netty为protobuf准备了两个专门的frame detection,他们是ProtobufVarint32FrameDecoderProtobufVarint32LengthFieldPrepender。在讲解这两个类之前,我们需要了解一下protobuf中的Base 128 Varints。

什么叫Varints呢?
就是序列化整数的时候,占用的空间大小是不一样的,小的整数占用的空间小,大的整数占用的空间大,这样不用固定一个具体的长度,可以减少数据的长度,但是会带来解析的复杂度。

那么怎么知道这个数据到底需要几个byte呢?
在protobuf中,每个byte的最高位是一个判断位,如果这个位被置位1,则表示后面一个byte和该byte是一起的,表示同一个数,如果这个位被置位0,则表示后面一个byte和该byte没有关系,数据到这个byte就结束了。

举个例子,一个byte是8位,如果表示的是整数1,那么可以用下面的byte来表示:

0000 0001

如果一个byte装不下的整数,那么就需要使用多个byte来进行连接操作,比如下面的数据表示的是300:

1010 1100 0000 0010

为什么是300呢?首先看第一个byte,它的首位是1,表示后面还有一个byte。再看第二个byte,它的首位是0,表示到此就结束了。我们把判断位去掉,变成下面的数字:

010 1100 000 0010

这时候还不能计算数据的值,因为在protobuf中,byte的位数是反过来的,所以我们需要把上面的两个byte交换一下位置:

000 0010 010 1100 

= 256 + 32 + 8 + 4 = 300

在protobuf中一般使用Varint作为字段的长度位,所以netty提供了ProtobufVarint32LengthFieldPrepender和ProtobufVarint32FrameDecoder对ByteBuf进行转换。

比如为ByteBuf添加varint的length:

   BEFORE ENCODE (300 bytes)       AFTER ENCODE (302 bytes)
   +---------------+               +--------+---------------+
   | Protobuf Data |-------------->| Length | Protobuf Data |
   |  (300 bytes)  |               | 0xAC02 |  (300 bytes)  |
   +---------------+               +--------+---------------+

解码的时候删除varint的length字段:

   BEFORE DECODE (302 bytes)       AFTER DECODE (300 bytes)
   +--------+---------------+      +---------------+
   | Length | Protobuf Data |----->| Protobuf Data |
   | 0xAC02 |  (300 bytes)  |      |  (300 bytes)  |
   +--------+---------------+      +---------------+

说白了:tcp底层传输数据是无差别传输,byteBuf缓冲区接收的数据是无逻辑性,合理性的。发送数据前,对数据编码,并加上数据长度,那么对于接收方而言,解码数据,通过数据长度,读取对应的数据字节数组,进行编码,获取的数据就是具有逻辑性,合理性的。编解码的目的就是为了让数据更易理解处理和传输

1.5、serialization

序列化就是把对象转换成二进制数据,事实上所有的codec都可以成为序列化。他们提供了对象和byte之间的转换方法。

netty也提供了两个对象的转换方法:ObjectDecoder和ObjectEncoder。

要注意的是,这两个对象和JDK自带的ObjectInputStream和ObjectOutputStream,是不兼容的,如果要兼容,可以使用CompactObjectInputStream、CompactObjectOutputStream和CompatibleObjectEncoder。

1.6、string

String是我们最常使用到的对象,netty为string提供了StringDecoder和StringEncoder。

同样的,在使用这两个类之前,需要将消息进行转换,通常使用的是 LineBasedFrameDecoder按行进行转换:

   // Decoders
   pipeline.addLast("frameDecoder", new LineBasedFrameDecoder(80));
   pipeline.addLast("stringDecoder", new StringDecoder(CharsetUtil.UTF_8));
  
   // Encoder
   pipeline.addLast("stringEncoder", new StringEncoder(CharsetUtil.UTF_8));

做IM可以常用,嘻嘻!!!

1.7、xml

xml也是一个非常常用的格式,但是它的体积会比较大,现在应该用的比较少了。netty提供了一个XmlFrameDecoder来进行解析。

因为xml有自己的开始和结束符,所以不需要再做frame detection,直接转换即可,如:

   +-----+-----+-----------+
   | <an | Xml | Element/> |
   +-----+-----+-----------+
转换成:
   +-----------------+
   | <anXmlElement/> |
   +-----------------+

2、FrameDecoder 和 FrameEncode

netty中的数据是通过ByteBuf来进行传输的,一个ByteBuf中可能包含多个有意义的数据,这些数据可以被称作frame,也就是说一个ByteBuf中可以包含多个Frame。(就是粘包拆包处理,嘿嘿)

对于消息的接收方来说,接收到了ByteBuf,还需要从ByteBuf中解析出有用而数据,那就需要将ByteBuf中的frame进行拆分和解析。

一般来说不同的frame之间会有有些特定的分隔符,我们可以通过这些分隔符来区分frame,从而实现对数据的解析。

netty为我们提供了一些合适的frame解码器,通过使用这些frame解码器可以有效的简化我们的工作。下图是netty中常见的几个frame解码器:

在这里插入图片描述

2.1、LineBasedFrameDecoder

LineBasedFrameDecoder从名字上看就是按行来进行frame的区分。根据操作系统的不同,换行可以有两种换行符,分别是 “\n” 和 “\r\n” 。

LineBasedFrameDecoder的基本原理就是从ByteBuf中读取对应的字符来和"\n" 跟 “\r\n”,可以了可以准确的进行字符的比较,这些frameDecoder对字符的编码也会有一定的要求,一般来说是需要UTF-8编码。因为在这样的编码中,“\n"和”\r"是以一个byte出现的,并且不会用在其他的组合编码中,所以用"\n"和"\r"来进行判断是非常安全的。

LineBasedFrameDecoder中有几个比较重要的属性:

  • 一个是maxLength的属性,用来检测接收到的消息长度,如果超出了长度限制,则会抛出TooLongFrameException异常。

  • 还有一个stripDelimiter属性,用来判断是否需要将delimiter过滤掉。

  • 还有一个是failFast,如果该值为true,那么不管frame是否读取完成,只要frame的长度超出了maxFrameLength,就会抛出TooLongFrameException。如果该值为false,那么TooLongFrameException会在整个frame完全读取之后再抛出。

LineBasedFrameDecoder的核心逻辑是先找到行的分隔符的位置,然后根据这个位置读取到对应的frame信息,这里来看一下找到行分隔符的findEndOfLine方法:

    private int findEndOfLine(final ByteBuf buffer) {
        int totalLength = buffer.readableBytes();
        int i = buffer.forEachByte(buffer.readerIndex() + offset, totalLength - offset, ByteProcessor.FIND_LF);
        if (i >= 0) {
            offset = 0;
            if (i > 0 && buffer.getByte(i - 1) == '\r') {
                i--;
            }
        } else {
            offset = totalLength;
        }
        return i;
    }

使用了一个ByteBuf的forEachByte对ByteBuf进行遍历。我们要找的字符是:ByteProcessor.FIND_LF。

2.2、DelimiterBasedFrameDecoder

LineBasedFrameDecoder只对行分隔符有效,如果我们的frame是以其他的分隔符来分割的话LineBasedFrameDecoder就用不了了,所以netty提供了一个更加通用的DelimiterBasedFrameDecoder,这个frameDecoder可以自定义delimiter:

public class DelimiterBasedFrameDecoder extends ByteToMessageDecoder {

        public DelimiterBasedFrameDecoder(int maxFrameLength, ByteBuf delimiter) {
        this(maxFrameLength, true, delimiter);
    }

DelimiterBasedFrameDecoder的逻辑和LineBasedFrameDecoder差不多,都是通过对比bufer中的字符来对bufer中的数据进行截取,但是DelimiterBasedFrameDecoder可以接受多个delimiters,所以它的用处会根据广泛

2.3、FixedLengthFrameDecoder

除了进行ByteBuf中字符比较来进行frame拆分之外,还有一些其他常见的frame拆分的方法,比如根据特定的长度来区分,netty提供了一种这样的decoder叫做FixedLengthFrameDecoder。从缓冲区中读取固定长度数据返回

2.4、LengthFieldBasedFrameDecoder

ByteBuf中还有一些frame中包含了特定的长度字段,这个长度字段表示ByteBuf中有多少可读的数据,这样的frame叫做LengthFieldBasedFrame。

netty中也提供了一个对应的处理decoder:

public class LengthFieldBasedFrameDecoder extends ByteToMessageDecoder 

读取的逻辑很简单,首先读取长度,然后再根据长度再读取数据。为了实现这个逻辑,LengthFieldBasedFrameDecoder提供了4个字段,分别是 lengthFieldOffset,lengthFieldLength,lengthAdjustment和initialBytesToStrip。

lengthFieldOffset指定了长度字段的开始位置,lengthFieldLength定义的是长度字段的长度,lengthAdjustment是对lengthFieldLength进行调整,initialBytesToStrip表示是否需要去掉长度字段。

   BEFORE DECODE (14 bytes)         AFTER DECODE (14 bytes)
   +--------+----------------+      +--------+----------------+
   | Length | Actual Content |----->| Length | Actual Content |
   | 0x000C | "HELLO, WORLD" |      | 0x000C | "HELLO, WORLD" |
   +--------+----------------+      +--------+----------------+

要编码的消息有个长度字段,长度字段后面就是真实的数据,0x000C是一个十六进制,表示的数据是12,也就是"HELLO, WORLD" 中字符串的长度。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

白鸽呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值