前言
Netty從底層Java通道讀取ByteBuf二進制數據,傳入Netty通道的流水線,隨后開始入站處理。在入站處理過程中,需要將ByteBuf二進制類型解碼成Java POJO對象。這個解碼過程可以通過Netty的Decoder(解碼器)去完成。
在出站處理過程中,業務處理后的結果(出站數據)需要從某個Java POJO對象編碼為最終的ByteBuf二進制數據,然后通過底層Java通道發送到對端。在編碼過程中,需要用到Netty的Encoder(編碼器)去完成數據的編碼工作。
解碼器:入站處理過程中,將ByteBuf二進制類解碼為Java POJO對象;
編碼器:出站處理過程中,將Java POJO對象編碼為ByteBuf二進制數據。
Decoder原理與實戰
Netty解碼器是什么?
(1)它是一個InBound入站處理器,負責處理“入站數據”。
(2)它能將上一站Inbound入站處理器傳過來的輸入(Input)數據進行解碼或者格式轉換,然后發送到下一站Inbound入站處理器。
一個標準的解碼器的職責為:將輸入類型為ByteBuf的數據進行解碼,輸出一個一個的Java POJO對象。Netty內置了yteToMessageDecoder解碼器。
Netty中的解碼器都是Inbound入站處理器類型,都直接或者間接地實現了入站處理的超級接口ChannelInboundHandler。
ByteToMessageDecoder解碼器處理流程
ByteToMessageDecoder是一個非常重要的解碼器基類,是一個抽象類,實現了解碼處理的基礎邏輯和流程。ByteToMessageDecoder繼承自ChannelInboundHandlerAdapter適配器,是一個入站處理器,用于完成從ByteBuf到Java POJO對象的解碼功能。
ByteToMessageDecoder解碼的流程大致如圖所示。
自定義Byte2IntegerDecoder整數解碼器
Byte2IntegerDecoder.java
由于解碼器的功能僅僅是完成ByteBuf的解碼,不做其他業務處理,所以還需要編寫一個業務處理器,用于在讀取解碼后的Java POJO對象之后完成具體的業務處理。
IntegerProcessHandler.class
ReplayingDecoder解碼器
使用上面的Byte2IntegerDecoder整數解碼器會面臨一個問題:需要對ByteBuf的長度進行檢查,有足夠的字節才能進行整數的讀取。這種長度的判斷是否可以由Netty來幫忙完成呢?答案是可以的,可以使用Netty的ReplayingDecoder類省去長度的判斷。
ReplayingDecoder對輸入的ByteBuf進行了“偷梁換柱”,在將外部傳入的ByteBuf緩沖區傳給子類之前,換成了自己裝飾
過的ReplayingDecoderBuffer緩沖區。也就是說,在示例程序中,Byte2IntegerReplayDecoder中的decode()方法所得到的實參in的直接類型并不是原始的ByteBuf類型,而是ReplayingDecoderBuffer類型。
實質上,ReplayingDecoder的作用遠遠不止于進行長度判斷,它更重要的作用是用于分包傳輸的應用場景。
整數的分包解碼器的實戰案例
通道接收到的ByteBuf數據包和發送端發送的數據包不完全一致:
Netty通過什么樣的解碼器對圖中接收端的3個ByteBuf緩沖數據進行解碼,而后得到和發送端一模一樣的4個字符串呢?理論上可以使用ReplayingDecoder來解決。在進行數據解析時,如果發現當前ByteBuf中所有可讀的數據不夠,那么ReplayingDecoder會一直等待,直到可讀數據是足夠的。這一切都是在ReplayingDecoder內部,通過與緩沖區裝飾器ReplayingDecoderBuffer相互配合完成的。
Byte2IntegerReplayDecoderTester
字符串的分包解碼器的實戰案例
在原理上,字符串分包解碼和整數分包解碼是一樣的,所不同的是:整數的長度是固定的,目前在Java中是4字節;字符串的長度是不固定的,是可變的。
如何獲取字符串的長度信息呢?這是一個小小的難題,和程序所使用的具體傳輸協議是強相關的。一般來說,在Netty中進行字符串的傳輸可以采用普通的Head-Content內容傳輸協議。該協議的規則很簡單:
(1)在協議的Head部分放置字符串的字節長度,可以用一個整數類型來描述。
(2)在協議的Content部分,放置字符串的字節數組。
MessageToMessageDecoder解碼器
與前面不同的是,解碼器需要繼承一個新的Netty解碼器基類MessageToMessageDecoder<I>。在繼承它的時候,需要明確的泛型實參<I>,用于指定入站消息的Java POJO類型。
為什么繼承MessageToMessageDecoder<I>時需要指定入站數據的類型,而在前面繼承ByteToMessageDecoder解碼ByteBuf時不需要指定泛型實參呢?原因很簡單:ByteToMessageDecoder的入站消息類型是十分明確的,就是二進制緩沖區ByteBuf類型;MessageToMessageDecoder<I>的入站消息類型是不明確的,可以是任何POJO類型,所以需要指定。
常用的內置Decoder
Netty提供了不少開箱即用的Decoder(解碼器),能夠滿足很多編解碼應用場景的需求。
(1)固定長度數據包解碼器——FixedLengthFrameDecoder
(2)行分割數據包解碼器——LineBasedFrameDecoder
(3)自定義分隔符數據包解碼器——DelimiterBasedFrameDecoder
(4)自定義長度數據包解碼器——LengthFieldBasedFrameDecoder
LineBasedFrameDecoder解碼器
LineBasedFrameDecoder,它是一個最為基礎的Netty內置解碼器。這個解碼器的工作原理很簡單,依次遍歷ByteBuf數據包中的可讀字節,判斷在二進制字節流中是否存在換行符"\n"或者"\r\n"的字節碼。如果有,就以此位置為結束位置,把從可讀索引到結束位置之間的字節作為解碼成功后的ByteBuf數據包。
LineBasedFrameDecoder,它是一個最為基礎的Netty內置解碼器。這個解碼器的工作原理很簡單,依次遍歷ByteBuf數據包中的可讀字節,判斷在二進制字節流中是否存在換行符"\n"或者"\r\n"的字節碼。如果有,就以此位置為結束位置,把從可讀索引到結束位置之間的字節作為解碼成功后的ByteBuf數據包。
DelimiterBasedFrameDecoder解碼器
DelimiterBasedFrameDecoder解碼器不僅可以使用換行符,還可以使用其他特殊字符作為數據包的分隔符,例如制表符"\t"。
LengthFieldBasedFrameDecoder解碼器
傳輸內容中的Length(長度)字段的值是指存放在數據包中要傳輸內容的字節數。普通的基于Head-Content協議的內容傳輸盡量用內置的LengthFieldBasedFrameDecoder來解碼。
多字段Head-Content協議數據包解析的實戰案例
NettyOpenBoxDecoder
Encoder原理與實戰
在Netty的業務處理完成后,業務處理的結果往往是某個Java POJO對象需要編碼成最終的ByteBuf二進制類型,通過流水線寫入底層的Java通道,這就需要用到Encoder(編碼器)。
在Netty中,什么叫編碼器?首先,編碼器是一個Outbound出站處理器,負責處理“出站”數據;其次,編碼器將上一站Outbound出站處理器傳過來的輸入(Input)數據進行編碼或者格式轉換,然后傳遞到下一站ChannelOutboundHandler出站處理器。
MessageToByteEncoder編碼器
MessageToByteEncoder是一個非常重要的編碼器基類,位于Netty的io.netty.handler.codec包中。MessageToByteEncoder的功能是將一個Java POJO對象編碼成一個ByteBuf數據包。
Integer2ByteEncoderTester
MessageToMessageEncoder編碼器
能夠通過Netty的編碼器將某種POJO對象編碼成另外一種POJO對象呢?答案是肯定的。需要繼承另外一個Netty的重要編碼器——MessageToMessageEncoder編碼器,并實現它的encode()抽象方法。在子類的encode()方法實現中,完成原POJO類型到目標POJO類型的轉換邏輯。在encode()實現方法中,編碼完成后,將解碼后的目標對象加入encode()方法中的實參list輸出容器即可。
解碼器和編碼器的結合
在實際的開發中,由于數據的入站和出站關系緊密,因此編碼器和解碼器的關系很緊密。
前面講到編碼器和解碼器是分開實現的。例如,通過繼承ByteToMessageDecoder基類或者其子類,完成ByteBuf數據包到POJO的解碼工作;通過繼承基類MessageToByteEncoder或者其子類,完成POJO到ByteBuf數據包的編碼工作。總之,具有相反邏輯的編碼器和解碼器分開實現在兩個不同的類中,導致的一個結果是相互配套的編碼器和解碼器在加入通道的流水線時常常需要分兩次添加。
ByteToMessageCodec編解碼器
現在的問題是:具有相互配套邏輯的編碼器和解碼器能否放在同一個類中呢?答案是肯定的,這需要用到Netty的新類型——Codec(編解碼器)。
編解碼器ByteToMessageCodec同時包含了編碼encode()和解碼decode()兩個抽象方法,這兩個方法都需要我們自己實現:
(1)編碼方法——encode(ChannelHandlerContext, I,ByteBuf)。
(2)解碼方法——decode(ChannelHandlerContext, ByteBuf,List)。
CombinedChannelDuplexHandler組合器
前面的編碼器和解碼器相結合是通過繼承完成的。繼承的不足之處在于:將編碼器和解碼器的邏輯強制性地放在同一個類中,在只需要編碼或者解碼單邊操作的流水線上,邏輯上不大合適。
編碼器和解碼器如果要結合起來,除了繼承的方法之外,還可以通過組合的方式實現。與繼承相比,組合會帶來更大的靈活性:編碼器和解碼器可以捆綁使用,也可以單獨使用。