[02][07][02] Netty 中的拆包粘包原理分析

1. ByteBuf 详解

Netty 的 ByteBuf 等同于 Java Nio 中的 ByteBuffer, 但是 ByteBuf 对 Nio 中的 ByteBuffer 的功能做了增强

下面这段代码演示了 ByteBuf 的创建以及内容的打印, ByteBuf 与 ByteBuffer 最大的区别之一是 ByteBuf 可以自动扩容, 默认长度是 256, 如果内容长度超过阈值时, 会自动触发扩容

public class NettyByteBufExample {

    public static void main(String[] args) {
        // 构建一个ByteBuf
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        System.out.println("=======before ======");
        LogUtils.log(buf);
        // 构建一个字符串
        StringBuilder stringBuilder = new StringBuilder();
        for (int i = 0; i < 400; i++) {
            stringBuilder.append("-" + i);
        }
        buf.writeBytes(stringBuilder.toString().getBytes());
        System.out.println("=======after ======");
        // 读取 2 个字节
        buf.readShort();
        // 读取 1 个字节
        buf.readByte();
        LogUtils.log(buf);
    }
}
public class LogUtils {
    public static void log(@org.jetbrains.annotations.NotNull ByteBuf buf) {
        StringBuilder sb = new StringBuilder()
                // 读索引
                .append("read index:").append(buf.readerIndex()).append(", ")
                // 写索引
                .append("write index:").append(buf.writerIndex()).append(", ")
                // 容量
                .append("capacity:").append(buf.capacity());
        System.out.println(sb);
        sb = new StringBuilder();
        ByteBufUtil.appendPrettyHexDump(sb, buf);
        System.out.println(sb);
        System.out.println("===================== 分隔符 =======================");
    }
}

输出结果

=======before ======
read index:0, write index:0, capacity:256

===================== 分隔符 =======================
=======after ======
read index:3, write index:1490, capacity:2048
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 31 2d 32 2d 33 2d 34 2d 35 2d 36 2d 37 2d 38 2d |1-2-3-4-5-6-7-8-|
|00000010| 39 2d 31 30 2d 31 31 2d 31 32 2d 31 33 2d 31 34 |9-10-11-12-13-14|
......
|000005b0| 33 39 32 2d 33 39 33 2d 33 39 34 2d 33 39 35 2d |392-393-394-395-|
|000005c0| 33 39 36 2d 33 39 37 2d 33 39 38 2d 33 39 39    |396-397-398-399 |
+--------+-------------------------------------------------+----------------+
===================== 分隔符 =======================

1.1 ByteBuf 的创建

  1. 创建基于堆内存的 ByteBuf
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);
  1. 创建基于直接内存 (堆外内存) 的 ByteBuf (默认情况下用的是这种)
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);

堆外内存就是把内存对象分配在 JVM 堆外的内存区域, 这部分内存不是虚拟机管理, 而是由操作系统来管理, 这样可以减少垃圾回收对应用程序的影响

直接内存的好处是读写性能会高一些, 如果数据存放在堆中, 此时需要把 Java 堆空间的数据发送到远程服务器, 首先需要把堆内部的数据拷贝到直接内存 (堆外内存), 然后再发送. 如果是把数据直接存储到堆外内存中, 发送的时候就少了一个复制步骤

由于缺少了 JVM 的内存管理, 所以需要我们自己来维护堆外内存, 防止内存溢出

ByteBuf 默认采用了池化技术来创建, 它的核心思想是实现对象的复用, 从而减少对象频繁创建销毁带来的性能开销. 池化功能是否开启, 可以通过下面的环境变量来控制, 其中 unpooled 表示不开启

-Dio.netty.allocator.type={unpooled|pooled}

1.2 ByteBuf 的存储结构

ByteBuf 的存储结构如图所示, 可以看到 ByteBuf 其实是一个字节容器, 该容器中包含三个部分

  • 已经丢弃的字节, 这部分数据是无效的
  • 可读字节, 这部分数据是 ByteBuf 的主体数据, 可写字节, 所有写到 ByteBuf 的数据都会存储到这一段
  • 可扩容字节, 表示 ByteBuf 最多还能扩容多少容量

xx

在 ByteBuf 中, 有三个关键参数

  • readerIndex: 读指针, 每读取一个字节, readerIndex 自增加 1. ByteBuf 里面总共有 witeIndex - readerIndex 个字节可读, 当 readerIndex 和 writeIndex 相等的时候, ByteBuf 不可读
  • writeIndex: 写指针, 每写入一个字节, writeIndex 自增加 1, 直到增加到 capacity 后, 可以触发扩容后继续写入
  • maxCapacity: 最大容量, 默认的值是 Integer.MAX_VALUE, 当 ByteBuf 写入数据时, 如果容量不足时, 会触发扩容, 直到 capacity 扩容到 maxCapacity

1.3 ByteBuf 的常用方法

  • 写入
  • 扩容
  • 读取

1.3.1 写入

对于 write 方法来说, ByteBuf 提供了针对各种不同数据类型的写入, 比如

  • writeChar, 写入 char 类型
  • writeInt, 写入 int 类型
  • writeFloat, 写入 flfloat 类型
  • writeBytes, 写入 nio 的 ByteBuffffer
  • writeCharSequence, 写入字符串

1.3.2 扩容

当向 ByteBuf 写入数据时, 发现容量不足时, 会触发扩容, 而具体的扩容规则是

  • 如果写入后数据大小未超过 512 个字节, 则选择下一个 16 的整数倍进行库容. 比如写入数据后大小为 12, 则扩容后的 capacity 是 16
  • 如果写入后数据大小超过 512 个字节, 则选择下一个 2n. 比如写入后大小是 512 字节, 则扩容后的 capacity 是 2^10=1024
  • 扩容不能超过 max capacity, 否则会报错

1.3.3 读取

针对不同数据类型提供了不同的操作方法

  • readByte, 读取单个字节
  • readInt, 读取一个 int 类型
  • readFloat, 读取一个 flfloat 类型

读完一个字节后, 这个字节就变成了废弃部分, 再次读取的时候只能读取未读取的部分数据, 如果想重复读取那些已经读完的数据, 这里提供了两个方法来实现标记和重置

public class ByteBufCreateExample {
    public static void main(String[] args) {
        // 使用堆内存,由JVM来管理内存
        ByteBuf buf = ByteBufAllocator.DEFAULT.heapBuffer();
        buf.writeBytes(new byte[]{1, 2, 3, 4});
        LogUtils.log(buf);
        buf.writeInt(5);
        LogUtils.log(buf);
        System.out.println("开始进行读取操作");
        // 标记索引位置
        buf.markReaderIndex();
        byte b = buf.readByte();
        System.out.println(b);
        // 重新回到标记位置
        buf.resetReaderIndex();
        LogUtils.log(buf);
    }
}

输出结果

read index:0, write index:4, capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04                                     |....            |
+--------+-------------------------------------------------+----------------+
===================== 分隔符 =======================
read index:0, write index:8, capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05                         |........        |
+--------+-------------------------------------------------+----------------+
===================== 分隔符 =======================
开始进行读取操作
1
read index:0, write index:8, capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 00 00 00 05                         |........        |
+--------+-------------------------------------------------+----------------+
===================== 分隔符 =======================

插入一个 int 类型的数据占 4 个字节

1.4 ByteBuf 的零拷贝机制

ByteBuf 中的零拷贝是减少数据复制提升性能, 假设有一个原始 ByteBuf, 想对这个 ByteBuf 其中的两个部分的数据进行操作. 按照正常的思路, 会创建两个新的 ByteBuf, 然后把原始 ByteBuf 中的部分数据拷贝到两个新的 ByteBuf 中, 但是这种会涉及到数据拷
贝, 在并发量较大的情况下, 会影响到性能

xx

ByteBuf 中提供了一个 slice 方法, 这个方法可以在不做数据拷贝的情况下对原始 ByteBuf 进行拆分, 使用方法如下

public class ByteBufCopyExample {
    public static void main(String[] args) {
        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        buf.writeBytes(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10});
        LogUtils.log(buf);
        // 从buf这个总的数据中,分别拆分 5 个字节保存到两个 ByteBuf 中
        // 零拷贝机制 (浅克隆)
        ByteBuf bb1 = buf.slice(0, 5);
        ByteBuf bb2 = buf.slice(5, 5);
        LogUtils.log(bb1);
        LogUtils.log(bb2);
        System.out.println("修改原始数据");
        buf.setByte(2, 8);
        LogUtils.log(bb1);
    }
}

输出结果

read index:0, write index:10, capacity:256
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 0a                   |..........      |
+--------+-------------------------------------------------+----------------+
===================== 分隔符 =======================
read index:0, write index:5, capacity:5
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05                                  |.....           |
+--------+-------------------------------------------------+----------------+
===================== 分隔符 =======================
read index:0, write index:5, capacity:5
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 06 07 08 09 0a                                  |.....           |
+--------+-------------------------------------------------+----------------+
===================== 分隔符 =======================
修改原始数据
read index:0, write index:5, capacity:5
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 08 04 05                                  |.....           |
+--------+-------------------------------------------------+----------------+
===================== 分隔符 =======================

通过 slice 对原始 buf 进行切片, 每个分片是 5 个字节, 为了证明 slice 是没有数据拷贝, 我们通过修改原始 buf 的索引 2 所在的值, 然后再打印第一个分片 bb1, 可以发现 bb1 的结果发生了变化, 说明两个分片和原始 buf 指向的数据是同一个

1.5 Unpooled

Unpooled 工具类是同了非池化的 ByteBuf 的创建, 组合, 复制等操作

假设有一个协议数据由头部和消息体组成, 这两个部分分别放在两个 ByteBuf 中, 我们希望把 header 和 body 合并成一个 ByteBuf, 通常的做法是

ByteBuf allBuf = Unpooled.buffer(header.readableBytes() + body.readableBytes());
allBuf.writeBytes(header);
allBuf.writeBytes(body);

在这个过程中, 我们把 header 和 body 拷贝到了新的 allBuf 中, 这个过程在无形中增加了两次数据拷贝操作. 那有没有更高效的方法减少拷贝次数来达到相同目的呢?

在 Netty 中, 提供了一个 CompositeByteBuf 组件, 它提供了这个功能

public class CompositeByteBufExample {
    public static void main(String[] args) {
        ByteBuf header = ByteBufAllocator.DEFAULT.buffer();
        header.writeBytes(new byte[]{1, 2, 3, 4, 5});
        ByteBuf body = ByteBufAllocator.DEFAULT.buffer();
        body.writeBytes(new byte[]{6, 7, 8, 9, 10});

        ByteBuf total = Unpooled.wrappedBuffer(header, body);
        LogUtils.log(total);
        header.setByte(2, 9);
        LogUtils.log(total);
    }
}

输出结果

read index:0, write index:10, capacity:10
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 03 04 05 06 07 08 09 0a                   |..........      |
+--------+-------------------------------------------------+----------------+
===================== 分隔符 =======================
read index:0, write index:10, capacity:10
         +-------------------------------------------------+
         |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
+--------+-------------------------------------------------+----------------+
|00000000| 01 02 09 04 05 06 07 08 09 0a                   |..........      |
+--------+-------------------------------------------------+----------------+
===================== 分隔符 =======================

CompositeByteBuf 是零拷贝, CompositeByteBuf 构建了一个逻辑整体, 里面仍然是两个真实对象, 也就是有一个指针指向了同一个对象

xx

1.6 内存释放

针对不同的 ByteBuf 创建, 内存释放的方法不同

  • UnpooledHeapByteBuf, 使用 JVM 内存, 只需要等待 GC 回收即可
  • UnpooledDirectByteBuf, 使用对外内存, 需要特殊方法来回收内存
  • PooledByteBuf 和它的之类使用了池化机制, 需要更复杂的规则来回收内存

如果 ByteBuf 是使用堆外内存来创建, 那么尽量手动释放内存, 那怎么释放呢?
Netty 采用了引用计数方法来控制内存回收, 每个 ByteBuf 都实现了 ReferenceCounted 接口. 每个 ByteBuf 对象的初始计数为 1
调用 release 方法时, 计数器减一, 如果计数器为 0, ByteBuf 被回收
调用 retain 方法时, 计数器加一, 表示调用者没用完之前, 其他 handler 即时调用了 release 也不会造成回收

2. 拆包和粘包问题

TCP 传输协议是基于数据流传输的, 而基于流化的数据是没有界限的, 当客户端向服务端发送数据时, 可能会把一个完整的数据报文拆分成多个小报文进行发送, 也可能将多个报文合并成一个大报文进行发送

在这样的情况下, 有可能会出现下所示的情况

xx

  1. 服务端恰巧读到了两个完整的数据包 A 和 B, 没有出现拆包/粘包问题
  2. 服务端接收到 A 和 B 粘在一起的数据包, 服务端需要解析出 A 和 B
  3. 服务端收到完整的 A 和 B 的一部分数据包 B-1, 服务端需要解析出完整的 A, 并等待读取完整的 B 数据包
  4. 服务端接收到 A 的一部分数据包 A-1, 此时需要等待接收到完整的 A 数据包
  5. 数据包 A 较大, 服务端需要多次才可以接收完数据包 A

由于存在拆包和粘包问题, 接收方很难界定数据包的边界在哪里, 所以可能会读取到不完整的数据导致数据解析出现问题

3. 应用层定义通信协议

一般会在应用层定义通信协议, 就是通信双方约定一个通信报文协议, 服务端收到报文之后, 按照约定的协议进行解码, 从而避免出现粘包和拆包问题

3.1 消息长度固定

数据报文都是固定的长度, 当接收方累计读取到固定长度的报文后, 就认为已经获得了一个完整的消息, 当发送方的数据小于固定长度时, 则需要空位补齐

如图所示, 假设我们固定消息长度是 4, 那么没有达到长度的报文, 需要通过一个空位来补齐, 从而使得消息能够形成一个整体

xx

这种方式很简单, 但是缺点也很明显, 对于没有固定长度的消息, 不清楚如何设置长度, 而且如果长度设置过大会造成字节浪费, 长度太小又会影响消息传输, 所以一般情况下不会采用这种方式

3.2 特定分隔符

既然没办法通过固定长度来分割消息, 那能不能在消息报文中增加一个分割符呢?然后接收方根据特定的分隔符来进行消息拆分. 比如我们采用\r\n 来进行分割

xx

对于特定分隔符的使用场景中, 需要注意分隔符和消息体中的字符不要存在冲突, 否则会出现消息拆分错误的问题

3.3 消息长度+消息内容+分隔符

基于消息长度+消息内容+分隔符的方式进行数据通信, Redis 的报文协议就是使用这个规则

*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$3\r\nmic

消息报文包含三个维度

  • 消息长度
  • 消息分隔符
  • 消息内容

这种方式在项目中是非常常见的协议, 首先通过消息头中的总长度来判断当前一个完整消息所携带的参数个数. 然后在消息体中, 再通过消息内容长度以及消息体作为一个组合, 最后通过\r\n 进行分割. 服务端收到这个消息后, 就可以按照该规则进行解析得到一个完整的命令进行执行

4. Netty 的编解码器

在 Netty 中, 默认帮我们提供了一些常用的编解码器用来解决拆包粘包的问题. 下面简单演示几种解码器的使用

4.1 FixedLengthFrameDecoder 解码器

固定长度解码器 FixedLengthFrameDecoder 的原理很简单, 就是通过构造方法设置一个固定消息大小 frameLength, 无论接收方一次收到多大的数据, 都会严格按照 frameLength 进行解码

如果累计读取的长度大小为 frameLength 的消息, 那么解码器会认为已经获取到了一个完整的消息, 如果消息长度小于 frameLength, 那么该解码器会一直等待后续数据包的达到, 知道获得指定长度后返回

使用方法如下, 在 Server 端的代码中增加一个 FixedLengthFrameDecoder, 长度为 10

ch.pipeline()
    // 增加解码器
    .addLast(new FixedLengthFrameDecoder(10)) 
    .addLast(new SimpleServerHandler());

4.2 DelimiterBasedFrameDecoder 解码器

DelimiterBasedFrameDecoder 是特殊分隔符解码器, 它有以下几个属性

  • delimiters, 指定特殊分隔符, 可以传递一个数组, 同时指定多个分隔符, 但最终会选择长度最短的分隔符进行拆分

接收方收到的消息体为 hello\nworld\r\n, 此时指定多个分隔符 \n\r\n, 最终会选择最短的分隔符解码

  • maxLength, 表示报文的最大长度限制, 如果超过 maxLength 还没检测到指定分隔符, 将会抛出 TooLongFrameException
  • failFast, 表示容错机制, 它与 maxLength 配合使用

如果 failFast=true, 当超过 maxLength 后会立刻抛出 TooLongFrameException, 不再进行解码. 如果 failFast=false, 那么会等到解码出一个完整的消息后才会抛出 TooLongFrameException

  • stripDelimiter, 它的作用是判断解码后的消息是否去除分隔符

如果 stripDelimiter=false, 而制定的特定分隔符是 \n, 那么 hello\nworld\r\n, 解码后的结果不会去除分隔符

4.3 LengthFieldBasedFrameDecoder 解码器

LengthFieldBasedFrameDecoder 是长度域解码器, 它是解决拆包粘包最常用的解码器, 基本上能覆盖大部分基于长度拆包的场景. 消息中间件 RocketMQ 就是使用该解码器进行解码的

首先来说明一下该解码器的核心参数

  • lengthFieldOffset, 长度字段的偏移量, 也就是存放长度数据的起始位置
  • lengthFieldLength, 长度字段锁占用的字节数
  • lengthAdjustment, 在一些较为复杂的协议设计中, 长度域不仅仅包含消息的长度, 还包含其他数据比如版本号, 数据类型, 数据状态等, 这个时候我们可以使用 lengthAdjustment 进行修正, 它的值= 包体的长度值- 长度域的值
  • initialBytesToStrip, 解码后需要跳过的初始字节数, 也就是消息内容字段的起始位置
  • lengthFieldEndOffset, 长度字段结束的偏移量, 该属性的值= lengthFieldOffset + lengthFieldLength

上面这些参数理解起来比较难, 通过几个案例来说明一下

4.3.1 消息长度+消息内容的解码

假设存在由长度和消息内容组成的数据包, 其中 length 表示报文长度, 用 16 进制表示, 共占用 2 个字节, 那么该协议对应的编解码器参数设置如下

  • lengthFieldOffset=0, 因为 Length 字段就在报文的开始位置
  • lengthFieldLength=2, 协议设计的固定长度为 2 个字节
  • lengthAdjustment=0, Length 字段质保函消息长度, 不需要做修正
  • initialBytesToStrip=0, 解码内容是 Length+content, 不需要跳过任何初始字节

xx

4.3.2 截断解码结果

如果我们希望解码后的结果中只包含消息内容, 其他部分不变, 如图 3-7 所示. 对应解码器参数组合如下

  • lengthFieldOffffset=0, 因为 Length 字段就在报文开始位置
  • lengthFieldLength=2, 协议设计的固定长度
  • lengthAdjustment=0, Length 字段只包含消息长度, 不需要做任何修正
  • initialBytesToStrip=2, 跳过 length 字段的字节长度, 解码后 ByteBuf 只包含 Content 字段

xx

4.3.3 长度字段包含消息内容

如果 Length 字段中包含 Length 字段自身的长度以及 Content 字段所占用的字节数, 那么 Length 的值为 0x00d(2+11=13 字节), 在这种情况下解码器的参数组合如下

  • lengthFieldOffffset=0, 因为 Length 字段就在报文开始的位置
  • lengthFieldLength=2, 协议设计的固定长度
  • lengthAdjustment=-2, 长度字段为 13 字节, 需要减 2 才是拆包所需要的长度
  • initialBytesToStrip=0, 解码后内容依然是 Length+Content, 不需要跳过任何初始字节

xx

4.3.4 基于长度字段偏移的解码

如图 3-9 所示, Length 字段已经不再是报文的起始位置, Length 字段的值是 0x000b, 表示 content 字段占 11 个字节, 那么此时解码器的参数配置如下:

  • lengthFieldOffffset=2, 需要跳过 Header 所占用的 2 个字节, 才是 Length 的起始位置
  • lengthFieldLength=2, 协议设计的固定长度
  • lengthAdjustment=0, Length 字段只包含消息长度, 不需要做任何修正
  • initialBytesToStrip=0, 解码后内容依然是 Length+Content, 不需要跳过任何初始字节

xx

4.3.5 基于长度偏移和长度修正解码

Length 字段前后分别有 hdr1 和 hdr2 字段, 各占据 1 个字节, 所以需要做长度字段的偏移, 还需要做 lengthAdjustment 的修正, 相关参数配置如下

  • lengthFieldOffffset=1, 需要跳过 hdr1 所占用的 1 个字节, 才是 Length 的起始位置
  • lengthFieldLength=2, 协议设计的固定长度
  • lengthAdjustment=1, 由于 hdr2+content 一共占了 1+11=12 字节, 所以 Length 字段值 (11 字节) 加上 lengthAdjustment(1) 才能得到 hdr2+Content 的内容 (12 字节)
  • initialBytesToStrip=3, 解码后跳过 hdr1 和 length 字段, 共 3 个字节

xx

4.3.6 解码器实战

比如定义如下消息头, 客户端通过该消息协议发送数据, 服务端收到该消息后需要进行解码

xx

先定义客户端, 其中 Length 部分, 可以使用 Netty 自带的 LengthFieldPrepender 来实现, 它可以计算当前发送消息的二进制字节长度, 然后把该长度添加到 ByteBuf 的缓冲区头中

public class LengthFieldBasedFrameDecoderClient {
    public static void main(String[] args) {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(eventLoopGroup)
                .channel(NioSocketChannel.class)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline()
                                // 传输消息的时候,在消息报文中增加 2 个字节的 length 在发送的 ByteBuf 之前
                                .addLast(new LengthFieldPrepender(2, 0, false))
                                .addLast(new StringEncoder())
                                .addLast(new ChannelInboundHandlerAdapter() {
                                    @Override
                                    public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                        ctx.writeAndFlush("i am request!");
                                        ctx.writeAndFlush("i am a another request!");
                                    }
                                });
                    }
                });
        try {
            ChannelFuture future = bootstrap.connect("localhost", 8080).sync();
            future.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}

上述代码运行时, 会得到两个报文

xx

下面是 Server 端的代码, 增加了 LengthFieldBasedFrameDecoder 解码器, 其中有两个参数的值如下

  • lengthFieldLength: 2, 表示 length 所占用的字节数为 2
  • initialBytesToStrip: 2, 表示解码后跳过 length 的 2 个字节, 得到 content 内容
public class LengthFieldBasedFrameDecoderServer {
    public static void main(String[] args) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            ch.pipeline()
                                    .addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 2, 0, 2))
                                    .addLast(new StringDecoder())
                                    .addLast(new ChannelInboundHandlerAdapter() {
                                        @Override
                                        public void channelRead(ChannelHandlerContext ctx, Object msg) {
                                            System.out.println("receive message:" + msg);
                                        }
                                    });
                        }
                    });
            // 绑定端口
            ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值