天天看点

Netty 学习(七)常用的解码器前言一、固定长度解码器 FixedLengthFrameDecoder二、特殊分隔符解码器 DelimiterBasedFrameDecoder三、长度域解码器 LengthFieldBasedFrameDecoder总结

目录

  • 前言
  • 一、固定长度解码器 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 编码器是最常用的一种,只需要设置一些参数就可以轻松实现自定义协议。