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,他们是ProtobufVarint32FrameDecoder
和ProtobufVarint32LengthFieldPrepender
。在讲解这两个类之前,我们需要了解一下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" 中字符串的长度。