netty系列之:netty中的懶人編碼解碼器

flydean發表於2021-08-20

簡介

netty之所以強大,是因為它內建了很多非常有用的編碼解碼器,通過使用這些編碼解碼器可以很方便的搭建出非常強大的應用程式,今天給大家講講netty中最基本的內建編碼解碼器。

netty中的內建編碼器

在對netty的包進行引入的時候,我們可以看到netty有很多以netty-codec開頭的artifactId,統計一下,有這麼多個:

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

總共10個codec包,其中netty-codec是最基礎的一個,其他的9個是對不同的協議包進行的擴充套件和適配,可以看到netty支援常用的和流行的協議格式,非常的強大。因為codec的內容非常多,要講解他們也不是很容易,本文將會以netty-codec做一個例子,講解其中最基本的也是最通用的編碼解碼器。

使用codec要注意的問題

雖然netty提供了很方便的codec編碼解碼器,但是正如我們在前一篇文章中提到的,有些codec是需要和Frame detection一起配合使用的,先使用Frame detection將ByteBuf拆分成一個個代表真實資料的ByteBuf,再交由netty內建的codec或者自定義的codec進行處理,這樣才能起到應有的效果。

netty內建的基本codec

netty中基本的codec有base64、bytes、compression、json、marshalling、protobuf、serialization、string和xml這幾種。

下面將會一一進行講解。

base64

這個codec是負責ByteBuf和base64過後的ByteBuf之間的轉換。雖然都是從ByteBuf到ByteBuf,但是其中的內容發生了變化。

有兩個關鍵的類,分別是Base64Encoder和Base64Decoder。因為Base64Decoder是一個MessageToMessageDecoder,所以需要使用一個DelimiterBasedFrameDecoder提前進行處理,常用的例子如下:

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

bytes

bytes是將bytes陣列和ByteBuf之間進行轉換,同樣的在decode之前,也需要使用FrameDecoder,通常的使用方式如下:

   ChannelPipeline pipeline = ...;
  
   // Decoders
   pipeline.addLast("frameDecoder",
                    new LengthFieldBasedFrameDecoder(1048576, 0, 4, 0, 4));
   pipeline.addLast("bytesDecoder",
                    new ByteArrayDecoder());
  
   // Encoder
   pipeline.addLast("frameEncoder", new LengthFieldPrepender(4));
   pipeline.addLast("bytesEncoder", new ByteArrayEncoder());
   

compression

compression這個包的內容就比較豐富了,主要是對資料的壓縮和解壓縮服務。其支援的演算法如下:

brotli
Bzip2
FastLZ
JdkZlib
Lz4
Lzf
Snappy
Zlib
Zstandard

compression對於大資料量的傳輸特別有幫助,通過壓縮可以節省傳輸的資料量,從而提高傳輸速度。

但是壓縮是使用特定的演算法來計算的,所以它是一個高CPU的操作,我們在使用的時候需要兼顧網路速度和CPU效能,並從中得到平衡。

json

json這個包裡面只有一個JsonObjectDecoder類,主要負責將Byte流的JSON物件或者陣列轉換成JSON物件和陣列。

JsonObjectDecoder直接就是一個ByteToMessageDecoder的子類,所以它不需要FrameDecoder,它是根據括號的匹配來判斷Byte陣列的起始位置,從而區分哪些Byte資料是屬於同一個Json物件或者陣列。

我們如果希望使用JSON來傳輸資料的話,這個類就非常有用了。

marshalling

Marshalling的全稱叫做JBoss Marshalling,它是JBoss出品的一個物件序列化的方式,但是JBoss Marshalling的最新API還是在2011-04-27,已經有10年沒更新了,是不是已經被廢棄了?

所以這裡我們不詳細介紹這個序列化的內容,感興趣的小夥伴可以自行探索。

protobuf

protobuf大家應該都很熟悉了,它是google出品的一種資訊交換格式,可以將其看做是一種序列化的方式。它是語言中立、平臺中立、可擴充套件的結構化資料序列化機制,和XML類似,但是比XML更小、更快、更簡單。

netty對protobuf的支援在於可以將protobuf中的message和MessageLite物件跟ByteBuf進行轉換。

protobuf的兩個編碼器也是message到message直接的轉換,所以也需要使用frame detection。當然你也可以使用其他的frame detection比如LengthFieldPrepender和LengthFieldBasedFrameDecoder如下所示:

   ChannelPipeline pipeline = ...;
  
   // 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 

也就是:

10 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)  |
   +--------+---------------+      +---------------+

serialization

序列化就是把物件轉換成二進位制資料,事實上所有的codec都可以成為序列化。他們提供了物件和byte之間的轉換方法。

netty也提供了兩個物件的轉換方法:ObjectDecoder和ObjectEncoder。

要注意的是,這兩個物件和JDK自帶的ObjectInputStream和ObjectOutputStream,是不相容的,如果要相容,可以使用CompactObjectInputStream、CompactObjectOutputStream和CompatibleObjectEncoder。

string

String是我們最常使用到的物件,netty為string提供了StringDecoder和StringEncoder。

同樣的,在使用這兩個類之前,需要將訊息進行轉換,通常使用的是 LineBasedFrameDecoder按行進行轉換:

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

xml

xml也是一個非常常用的格式,但是它的體積會比較大,現在應該用的比較少了。netty提供了一個XmlFrameDecoder來進行解析。

因為xml有自己的開始和結束符,所以不需要再做frame detection,直接轉換即可,如:

   +-----+-----+-----------+
   | <an | Xml | Element/> |
   +-----+-----+-----------+
轉換成:
   +-----------------+
   | <anXmlElement/> |
   +-----------------+
   +-----+-----+-----------+-----+----------------------------------+
   | <an | Xml | Element/> | <ro | ot><child>content</child></root> |
   +-----+-----+-----------+-----+----------------------------------+
   轉換成:
   +-----------------+-------------------------------------+
   | <anXmlElement/> | <root><child>content</child></root> |
   +-----------------+-------------------------------------+

都是可以的。

總結

netty提供了很多優秀的codec來適配各種應用協議,大家可以多用用,找找不同協議的不同之處。

本文已收錄於 http://www.flydean.com/16-netty-buildin-codec-common/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章