目錄
- 前言
- 一、固定長度解碼器 FixedLengthFrameDecoder
- 二、特殊分隔符解碼器 DelimiterBasedFrameDecoder
- 三、長度域解碼器 LengthFieldBasedFrameDecoder
-
- 1. 長度域解碼器特有屬性
- 2. 與固定長度解碼器和特定分隔符解碼器相似的屬性
- 3. 示例
-
- 示例 1:典型的基于消息長度 + 消息内容的解碼
- 示例 2:解碼結果需要截斷
- 示例 3:長度字段包含消息長度和消息内容所占的位元組
- 示例 4:基于長度字段偏移的解碼
- 示例 5:長度字段與内容字段不再相鄰
- 示例 6:基于長度偏移和長度修正的解碼
- 示例 7:長度字段包含除 Content 外的多個其他字段
- 總結
前言
Netty 已經封裝好了網絡通信的底層實作,應用開發隻需要擴充 ChannelHandler 實作自定義編解碼邏輯即可。Netty 提供了很多開箱即用的解碼器,這些解碼器基本覆寫了 TCP 拆包/粘包解決方案。
一、固定長度解碼器 FixedLengthFrameDecoder
固定長度解碼器 FixedLengthFrameDecoder 非常簡單,直接通過構造函數設定固定長度的大小 frameLength。
- 累積讀取的長度大小為 frameLength, 那麼解碼器認為已經擷取到一個完整的消息。
- 讀取的長度小于 frameLength, 解碼器将等待後續資料包的到達。
-
如果一次接收到的資料包遠大于 frameLength,解碼器會按該長度對消息進行拆分。
Netty 使用 FixedLengthFrameDecoder 的示例如下:
class EchoServer {
void startEchoServer(int port) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup()
EventLoopGroup workerGroup = new NioEventLoopGroup()
try {
ServerBootstrap b = new ServerBootstrap()
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new FixedLengthFrameDecoder(10));
ch.pipeline().addLast(new EchoServerHandler());
}
})
def f = b.bind(port).sync()
f.channel().closeFuture().sync()
} finally {
bossGroup.shutdownGracefully()
workerGroup.shutdownGracefully()
}
}
static void main(String[] args) {
new EchoServer().startEchoServer(8088)
}
}
@Sharable
class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
byte[] bytes = new byte[10]
msg.readBytes(bytes)
println("Receive client : ${new String(bytes)}")
}
}
}
上述服務的代碼中使用了 10 位元組的解碼器,并在解碼之後通過 EchoServerHandler 列印結果。
Receive client : 1234567890
二、特殊分隔符解碼器 DelimiterBasedFrameDecoder
使用特殊分隔符解碼器 DelimiterBasedFrameDecoder 之前需要了解以下幾個屬性的作用。
- delimiters 特殊分隔符,delimiters 的類型是 ByteBuf 數組,可以同時指定多個分隔符,但是最終會選擇
的分隔符進行消息拆分。長度最短
- maxLength 封包最大長度限制,如果超過 maxLength 還沒有找到指定的分隔符,将會抛出 TooLongFrameException。maxLength 是對程式在極端情況下的一種保護措施。
- failFast 通過設定 failFast 可以控制抛出 TooLongFrameException 的時機。 如果 failFast = true, 那麼在超出 maxLength 立即抛出 TooLongFrameException,不再繼續解碼。如果 failFast = false, 那麼會等到解碼出完整消息才會抛出 TooLongException。
- stripDelimiter 是否去除分隔符, 解碼後得到的消息是否去除分隔符。 如果 stripDelimiter = false,那麼結果不去除分隔符。
Netty 使用 DelimiterBasedFrameDecoder的示例如下:
b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
protected void initChannel(Channel ch) throws Exception {
ByteBuf delimiter = Unpooled.copiedBuffer("&".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(100, true, true, delimiter));
ch.pipeline().addLast(new EchoServerHandler());
}
})
輸入:
1234567890&saghasdoghj&sagh&1243
輸出:
Receive client : 1234567890
Receive client : saghasdoghj
Receive client : sagh
三、長度域解碼器 LengthFieldBasedFrameDecoder
長度域解碼器 LengthFieldBasedFrameDecoder 是解決 TCP 拆包和粘包問題最常用的解碼器,LengthFieldBasedFrameDecoder 相對來說會複雜些,它的屬性可以分成兩大類:長度域解碼器持有屬性以及其他解碼器(如特定分隔符解碼器)的相似的屬性。
1. 長度域解碼器特有屬性
- lengthFieldOffset 長度字段的偏移量,也就是存放長度資料的起始位置。
- lengthFieldLength 長度字段所占用的位元組數。
- lengthAdjustment 消息長度的修正值,lengthAdjustment = 包體的長度值 - 長度域的值·。
- initialBytesToStrip 解碼後需要跳過的初始位元組數,也就是消息内容字段的起始位置。
- lengthFieldEndOffset 長度字段結束的偏移量,lengthFieldEndOffset = lengthFieldOffset + lengthFieldLength
2. 與固定長度解碼器和特定分隔符解碼器相似的屬性
- maxFrameLength 封包最大限制長度
- failFast 是否立即抛出 TooLongFrameException,與 maxFrameLength 搭配使用
- discardingTooLongFrame 是否處于丢棄模式
- tooLongFrameLength 需要丢棄的位元組數
- bytesToDiscard 累計丢棄的位元組數
使用 LengthFieldBasedFrameDecoder 的示例:
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer() {
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(100, 0, 2, 0, 2, true))
.addLast(new EchoServerHandler())
}
})
輸入
00 04 41 41 41 41
輸出
AAAA
3. 示例
示例 1:典型的基于消息長度 + 消息内容的解碼
封包隻包含消息長度 Length 和消息内容 Content 字段,其中 Length 為 16 進制表示,共占用 2 位元組,Length 的值 0x000C 代表 Content 占用 12 位元組
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000C | "HELLO, WORLD" | | 0x000C | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
該協定對應的解碼器參數組合如下:
lengthFieldOffset = 0,Length 字段就在封包的開始位置。
lengthFieldLength = 2,Length 字段占用 2 位元組。
lengthAdjustment = 0,Length 字段隻包含消息長度,不需要做任何修正。
initialBytesToStrip = 0,解碼後内容依然是 Length + Content,不需要跳過任何初始位元組。
示例 2:解碼結果需要截斷
示例 2 與示例 1 的差別在于,解碼後的結果隻包含消息内容。
BEFORE DECODE (14 bytes) AFTER DECODE (12 bytes)
+--------+----------------+ +----------------+
| Length | Actual Content |----->| Actual Content |
| 0x000C | "HELLO, WORLD" | | "HELLO, WORLD" |
+--------+----------------+ +----------------+
該協定對應的解碼器參數組合如下:
lengthFieldOffset = 0,Length 字段就在封包的開始位置。
lengthFieldLength = 2,Length 字段占用 2 位元組。
lengthAdjustment = 0,Length 字段隻包含消息長度,不需要做任何修正。
initialBytesToStrip = 2,跳過 Length 字段的位元組長度,解碼後 ByteBuf 中隻包含 Content字段。
示例 3:長度字段包含消息長度和消息内容所占的位元組
Length 字段包含 Length 字段自身的固定長度(2 位元組)以及 Content 字段(12 位元組)所占用的位元組數,Length 的值為 0x000E(2 + 12 = 14 位元組),在 Length 字段值的基礎上做 lengthAdjustment(-2)的修正,才能得到真實的 Content 字段長度。
BEFORE DECODE (14 bytes) AFTER DECODE (14 bytes)
+--------+----------------+ +--------+----------------+
| Length | Actual Content |----->| Length | Actual Content |
| 0x000E | "HELLO, WORLD" | | 0x000E | "HELLO, WORLD" |
+--------+----------------+ +--------+----------------+
對應的解碼器參數組合如下:
lengthFieldOffset = 0,因為 Length 字段就在封包的開始位置。
lengthFieldLength = 2,Length 字段占用 2 位元組。
lengthAdjustment = -2,長度字段為 14 位元組,需要減 2 才是拆包所需要的長度。
initialBytesToStrip = 0,解碼後内容依然是 Length + Content,不需要跳過任何初始位元組。
示例 4:基于長度字段偏移的解碼
封包增加了魔數字段,Length 字段不再是封包的起始位置,Length 字段的值為 0x00000C(長度為 3 位元組),表示 Content 字段占用 12 位元組,
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Header 1 | Length | Actual Content |----->| Header 1 | Length | Actual Content |
| 0xCAFE | 0x00000C | "HELLO, WORLD" | | 0xCAFE | 0x00000C | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
該協定對應的解碼器參數組合如下:
lengthFieldOffset = 2,Length 字段需要跳過 Header 1 所占用的 2 位元組。
lengthFieldLength = 3,Length 字段值為 0x00000C,占用 3位元組。
lengthAdjustment = 0, Length 字段隻包含消息長度,不需要做任何修正。
initialBytesToStrip = 0,解碼後内容依然是完整的封包,不需要跳過任何位元組。
示例 5:長度字段與内容字段不再相鄰
Length 字段之後是 Header 1, 與 Content 字段不相鄰。Length 的值為 0x00000C (12),不包含 header 1 字段,所有也需要修正才能得到 Header + Content 的内容。
BEFORE DECODE (17 bytes) AFTER DECODE (17 bytes)
+----------+----------+----------------+ +----------+----------+----------------+
| Length | Header 1 | Actual Content |----->| Length | Header 1 | Actual Content |
| 0x00000C | 0xCAFE | "HELLO, WORLD" | | 0x00000C | 0xCAFE | "HELLO, WORLD" |
+----------+----------+----------------+ +----------+----------+----------------+
該協定對應的解碼器參數組合如下:
lengthFieldOffset = 0,因為 Length 字段就在封包的開始位置。
lengthFieldLength = 3,Length 字段值為 0x00000C,占用 3位元組。
lengthAdjustment = 2, Length 字段(12) + lengthAdjustment 修正字段(2) = Header(2 位元組) + Content 的内容(12 位元組)。
initialBytesToStrip = 0,解碼後内容依然是完整的封包,不需要跳過任何位元組。
示例 6:基于長度偏移和長度修正的解碼
Length 字段前後分為别 HDR1 和 HDR2 字段,各占用 1 位元組,是以既需要做長度字段的偏移,也需要做 lengthAdjustment 修正。
BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x000C | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
該協定對應的解碼器參數組合如下:
lengthFieldOffset = 1, 需要跳過 HDR1 所占用的 1 位元組,才是 Length 的起始位置。
lengthFieldLength = 2,Length 字段值為 0x000C,占用 2 位元組。
lengthAdjustment = 1, Length 字段(12) + lengthAdjustment 字段(1) = HDR2(1 位元組) + Content 的内容(12 位元組)。
initialBytesToStrip = 3,解碼後跳過 HDR1 (1 位元組)和 Length 字段(2 位元組),共占用 3 位元組。
示例 7:長度字段包含除 Content 外的多個其他字段
Length 字段記錄了整個封包的長度,包含 Length 自身所占位元組、HDR1 、HDR2 以及 Content 字段的長度,解碼器需要知道如何進行 lengthAdjustment 調整,才能得到 HDR2 和 Content 的内容。
BEFORE DECODE (16 bytes) AFTER DECODE (13 bytes)
+------+--------+------+----------------+ +------+----------------+
| HDR1 | Length | HDR2 | Actual Content |----->| HDR2 | Actual Content |
| 0xCA | 0x0010 | 0xFE | "HELLO, WORLD" | | 0xFE | "HELLO, WORLD" |
+------+--------+------+----------------+ +------+----------------+
該協定對應的解碼器參數組合如下:
lengthFieldOffset = 1, 需要跳過 HDR1 所占用的 1 位元組,才是 Length 的起始位置。
lengthFieldLength = 2,Length 字段值為 0x0010,占用 2 位元組。
lengthAdjustment = -3, Length 字段(16) + lengthAdjustment 字段(- 3) = HDR2(1 位元組) + Content 的内容(12 位元組)。
initialBytesToStrip = 3,解碼後跳過 HDR1 (1 位元組)和 Length 字段(2 位元組),共占用 3 位元組。
總結
本文學習了三種常用的解碼器,LengthFieldBasedFrameDecoder 編碼器是最常用的一種,隻需要設定一些參數就可以輕松實作自定義協定。