大家好!今天来一起聊聊 Netty 中如何解决 TCP 粘包拆包问题。这个问题算是 Java 网络编程中的一个"障碍",不少小伙伴在这里栽过跟头,甚至有经验的开发者也会在这个问题上踩坑。本文从问题本质出发,结合具体案例,手把手教你搞定 Netty 粘包拆包问题。
一、什么是 TCP 粘包拆包问题?
在深入 Netty 的解决方案前,先得搞清楚问题到底是什么。
1.1 问题描述
想象一下这个场景:你用 TCP 发送了两条消息"Hello"和"World",但接收方可能收到的是:
- "HelloWorld"(粘包)
- "Hel"和"loWorld"(拆包)
- "HelloWor"和"ld"(粘包+拆包)
这就是所谓的 TCP 粘包拆包现象。就像快递员把两个包裹捆在一起送(粘包),或者把一个大包裹分成几次送(拆包)。

1.2 为什么会发生?
这个问题主要有几个原因:
-
TCP 是流协议:TCP 只关心字节流的传输,不会在数据包之间保留边界信息,就像水管中的水流,你无法区分哪些水是上一秒的,哪些是这一秒的。
-
操作系统的缓冲区机制 :TCP 为提高网络效率,会将数据放入缓冲区。这时 Nagle 算法可能会将多个小数据包合并成一个再发送。Nagle 算法主要优化小分组传输,通过延迟发送减少网络小包数量,可通过设置
TCP_NODELAY
选项关闭它。 -
MSS 限制:如果数据超过最大分段大小(Maximum Segment Size,简称 MSS),TCP 会将其拆分成多个包发送。MSS = MTU(最大传输单元,通常为 1500 字节)- IP 头(20 字节)- TCP 头(20 字节),所以标准以太网的 MSS 通常为 1460 字节。
二、Netty 解决粘包拆包的核心思路
Netty 解决这个问题的核心思想很简单:在消息中添加边界信息。这就像在信件中加上页码和总页数,让收信人知道这是第几页,总共有几页。
具体实现有以下几种方式:

2.1 基于分隔符的解码器
这种方式类似我们在文本处理中使用分隔符分割数据。比如我们用 Excel 导出 CSV 文件时,用逗号分隔每个字段,用换行符分隔每一行。
Netty 提供了:
- LineBasedFrameDecoder:使用换行符作为分隔符,就像聊天记录一样,一行一条消息
- DelimiterBasedFrameDecoder:使用自定义分隔符,可以自己决定用什么符号来区分消息
2.2 基于长度字段的解码器
这种方式在消息头部包含"长度字段",明确指出消息体的长度。类似于快递包裹上的重量标签,告诉你这个包裹有多重。
Netty 提供了:
- LengthFieldBasedFrameDecoder:通过解析长度字段确定消息边界,灵活性最高
2.3 基于固定长度的解码器
如果所有消息都是固定长度,可以直接按固定长度切分,就像固定规格的信封,每个都一样大。
Netty 提供了:
- FixedLengthFrameDecoder:将入站数据按固定长度切分
三、四种解码器详解与使用案例
下面通过具体案例详解四种解码器的使用方法。
3.1 LineBasedFrameDecoder(基于行分隔符)
这个解码器使用\n
或\r\n
作为分隔符,非常适合基于文本的协议,比如聊天应用、日志传输等。
工作原理:

使用案例:
java
public class LineBasedServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 添加行解码器,最大行长度为8192
pipeline.addLast(new LineBasedFrameDecoder(8192));
// 将ByteBuf转为String
pipeline.addLast(new StringDecoder());
// 业务处理器
pipeline.addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println("收到消息: " + msg);
// 回复消息
ctx.writeAndFlush("已收到: " + msg + "\n");
}
});
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
客户端测试:
java
public class LineBasedClient {
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LineBasedFrameDecoder(8192));
pipeline.addLast(new StringDecoder());
// 明确添加StringEncoder避免依赖默认行为
pipeline.addLast(new StringEncoder());
}
});
Channel ch = b.connect("localhost", 8888).sync().channel();
// 发送三条消息,每条消息以\n结尾
ch.writeAndFlush("你好,Netty!\n");
ch.writeAndFlush("这是第二条消息\n");
ch.writeAndFlush("这是最后一条消息\n");
ch.closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
注意事项:
- 如果消息内容中本身包含
\n
,会导致误判为多条消息 - 必须确保发送端添加正确的换行符
- 8192 是最大行长度,超过会抛出异常,可根据业务需求调整
3.2 DelimiterBasedFrameDecoder(基于自定义分隔符)
这个解码器允许我们使用自定义的分隔符,比如$_$
、##END##
等,更加灵活。但注意,分隔符越长,扫描查找的开销就越大,对性能影响也越明显。对于长分隔符(超过 10 字节),建议优先使用长度字段解码器,避免扫描时间线性增长导致性能下降。
使用案例 :使用"\_\"作为分隔符
java
public class DelimiterBasedServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 定义分隔符
ByteBuf delimiter = Unpooled.copiedBuffer("$_$", CharsetUtil.UTF_8);
// 添加基于分隔符的解码器
pipeline.addLast(new DelimiterBasedFrameDecoder(8192, delimiter));
pipeline.addLast(new StringDecoder());
pipeline.addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println("收到消息: " + msg);
ctx.writeAndFlush("已收到: " + msg + "$_$");
}
});
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
解决分隔符冲突问题: 如果分隔符可能出现在消息内容中,有两种常见的解决方法:
- 使用转义字符:比如要发送"Hello*World* "*,可以将* 转义为\_\
- 使用二进制分隔符:选择极少出现在原始数据中的字节序列,如 0xFF,0xFF
实际应用场景:
- 自定义文本协议
- 遗留系统集成,需要兼容已有的协议格式
3.3 FixedLengthFrameDecoder(固定长度解码器)
这个解码器适用于所有消息都是固定长度的场景,比如有些老旧工控系统的通信协议。由于不需要扫描查找分隔符,它的性能通常是最高的,内存占用也最稳定。
使用案例:假设所有消息长度为 20 字节
java
public class FixedLengthServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 固定长度为20字节的解码器
pipeline.addLast(new FixedLengthFrameDecoder(20));
// 增加ReadTimeoutHandler处理接收不足情况
pipeline.addLast(new ReadTimeoutHandler(60)); // 60秒超时
pipeline.addLast(new StringDecoder());
pipeline.addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println("收到固定长度消息: " + msg);
}
});
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
客户端发送固定长度消息示例:
java
// 填充消息到固定长度的示例
public void sendFixedLengthMessage(Channel channel, String message) {
byte[] content = message.getBytes();
// 创建20字节的缓冲区
ByteBuf buf = Unpooled.buffer(20);
// 写入原始内容
buf.writeBytes(content);
// 如果不足20字节,用0填充
// 注意:填充字符(0或空格)必须与服务端解析逻辑一致
buf.writeZero(20 - content.length);
channel.writeAndFlush(buf);
}
注意事项:
- 必须确保发送的消息正好是 20 字节,不足需要填充(通常用空格或 0 填充)
- 填充字符的选择(0 或空格)需要客户端和服务端达成一致,避免解析错误
- 如果接收不到完整的 20 字节,解码器会一直等待,可能导致超时
- 添加
ReadTimeoutHandler
可以防止永久等待导致的资源泄露
3.4 LengthFieldBasedFrameDecoder(基于长度字段的解码器)
这是最灵活也是最常用的解码器,适用于各种复杂协议。它通过解析消息中的长度字段来确定消息体的长度,算法时间复杂度为 O(1),不需要像分隔符解码器那样扫描整个缓冲区(O(n)),所以性能更高。
工作原理:
参数说明:
参数名 | 含义 | 如何理解(生活类比) |
---|---|---|
maxFrameLength | 最大帧长度 | 快递公司规定单个包裹最大重量,超过不收 |
lengthFieldOffset | 长度字段偏移量 | 快递单上"重量"字段前面有收件人、地址等信息,需要跳过这些才能找到重量 |
lengthFieldLength | 长度字段长度 | 重量标签占用多少空间(1 位数、2 位数还是更多) |
lengthAdjustment | 长度调整值 | 重量标签表示的是净重还是毛重,可能需要加上或减去包装重量 |
initialBytesToStrip | 解码后跳过的字节数 | 收件人拿到快递后是否需要撕掉快递单后才能用内容 |
常见协议格式:
scss
| 长度字段(4字节) | 实际内容 |
使用案例:
java
public class LengthFieldServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 添加基于长度字段的解码器
// 参数分别是:最大帧长度,长度字段偏移,长度字段长度,长度调整值,跳过的初始字节数
pipeline.addLast(new LengthFieldBasedFrameDecoder(
1024 * 1024, // 设置1MB的最大帧长度,防止内存溢出
0, 4, 0, 4
) {
// 增加异常处理
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {
try {
return super.decode(ctx, in);
} catch (TooLongFrameException e) {
System.err.println("消息太长,关闭连接: " + e.getMessage());
ctx.close();
return null;
}
}
});
// 添加自定义处理器
pipeline.addLast(new SimpleChannelInboundHandler<ByteBuf>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) {
byte[] bytes = new byte[msg.readableBytes()];
msg.readBytes(bytes);
System.out.println("收到消息: " + new String(bytes, CharsetUtil.UTF_8));
}
});
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
客户端发送示例:
java
public class LengthFieldClient {
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
// LengthFieldPrepender自动添加长度字段
ch.pipeline().addLast(new LengthFieldPrepender(4));
}
});
Channel ch = b.connect("localhost", 8888).sync().channel();
// 方式1:通过LengthFieldPrepender自动处理长度字段
String message = "Hello, Netty with LengthField!";
ByteBuf buf = Unpooled.copiedBuffer(message, CharsetUtil.UTF_8);
ch.writeAndFlush(buf);
// 方式2:手动构造包含长度字段的二进制消息
String message2 = "Manual length field message";
byte[] content = message2.getBytes(CharsetUtil.UTF_8);
ByteBuf buf2 = Unpooled.buffer();
buf2.writeInt(content.length); // 4字节长度字段(大端)
buf2.writeBytes(content);
ch.writeAndFlush(buf2);
ch.closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
字节序处理:
长度字段在网络传输中通常使用大端序(网络字节序),这是 TCP/IP 协议的标准约定,Java 默认也使用大端序。大端序指高位字节在前,低位字节在后,便于跨平台兼容。如果需要兼容一些使用小端序的老系统,可以明确指定使用小端序方法:
java
// 读取小端序长度字段
int length = in.readIntLE();
// 编码小端序长度字段
out.writeIntLE(contentLength);
一个完整的 LengthFieldBasedFrameDecoder 应用实例
下面是一个更复杂但更实用的例子,我们设计一个简单的协议:
scss
| 魔数(2字节) | 版本(1字节) | 长度字段(4字节) | 消息类型(1字节) | 消息内容 | CRC校验(4字节) |
协议结构图:

这里选择0x9527
作为魔数,而不是0x0000
或0xFFFF
这样的常见值,是为了降低与其他数据误判的概率。很多知名协议都有自己的魔数,如 HTTP/2 使用"PRI "(ASCII 码为 0x50524920)作为魔数,Elasticsearch 使用 0xEs 开头的魔数。版本号放在魔数之后,便于将来协议升级时,根据版本号选择不同的解码逻辑。
LengthFieldBasedFrameDecoder 参数计算表:
参数名 | 含义 | 计算方式 | 当前协议值 | 备注 |
---|---|---|---|---|
lengthFieldOffset | 长度字段偏移 | 魔数(2)+版本(1) | 3 | 跳过前 3 字节才是长度字段 |
lengthFieldLength | 长度字段长度 | 固定大小 | 4 | 长度字段占 4 字节(int) |
lengthAdjustment | 长度调整值 | 长度字段后其他头部字段长度 | 1 | 长度值仅表示内容长度,不包括消息类型(1 字节) |
initialBytesToStrip | 跳过字节数 | 0 表示保留完整消息 | 0 | 保留完整消息体,包括头部 |
服务端代码:
java
public class ComplexProtocolServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
// 魔数(2字节) + 版本(1字节) = 3字节偏移
// 长度字段为4字节
// 长度字段后还有消息类型(1字节),所以调整值为1
// 我们保留协议头,所以不跳过任何字节
pipeline.addLast(new LengthFieldBasedFrameDecoder(
65535, 3, 4, 1, 0));
// 添加自定义协议解码器
pipeline.addLast(new ProtocolDecoder());
// 业务处理器
pipeline.addLast(new SimpleChannelInboundHandler<ProtocolMessage>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, ProtocolMessage msg) {
System.out.println("收到消息: " + msg);
// 业务处理...
}
});
// 添加心跳检测(可选)
pipeline.addLast(new IdleStateHandler(30, 0, 0));
pipeline.addLast(new ChannelInboundHandlerAdapter() {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
if (evt instanceof IdleStateEvent) {
System.out.println("30秒未收到数据,关闭连接");
ctx.close();
}
}
});
}
});
ChannelFuture f = b.bind(8888).sync();
f.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
// 协议消息类
static class ProtocolMessage {
private short magic; // 魔数
private byte version; // 版本
private int length; // 长度
private byte type; // 消息类型
private byte[] content; // 消息内容
private int crc; // CRC校验
// 省略getter/setter和toString
}
// 协议解码器
static class ProtocolDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
// 步骤1: 确保有足够的字节可读
if (in.readableBytes() < 12) { // 头部固定部分长度
return;
}
// 步骤2: 记录读索引,便于重置
in.markReaderIndex();
// 步骤3: 解析头部各字段
short magic = in.readShort();
byte version = in.readByte();
int length = in.readInt(); // 默认大端序(网络字节序)
byte type = in.readByte();
// 步骤4: 校验魔数
if (magic != 0x9527) {
in.resetReaderIndex();
throw new CorruptedFrameException("非法的魔数: " + magic);
}
// 步骤5: 协议版本处理
if (version != 0x01) {
// 根据版本号选择不同的解码逻辑
if (version == 0x02) {
System.out.println("使用V2版本解码器");
// V2版本解码逻辑
} else {
throw new CorruptedFrameException("不支持的协议版本: " + version);
}
}
// 步骤6: 确保消息体和CRC都可读
if (in.readableBytes() < length + 4) {
in.resetReaderIndex();
return;
}
// 步骤7: 读取消息体
byte[] content = new byte[length];
in.readBytes(content);
// 步骤8: 读取和验证CRC
int crc = in.readInt();
// 计算CRC
CRC32 crc32 = new CRC32();
crc32.update(content);
int calculatedCrc = (int) crc32.getValue();
if (crc != calculatedCrc) {
throw new CorruptedFrameException("CRC校验失败");
}
// 步骤9: 创建消息对象并传递
ProtocolMessage message = new ProtocolMessage();
message.magic = magic;
message.version = version;
message.length = length;
message.type = type;
message.content = content;
message.crc = crc;
// 将消息传递给下一个处理器
out.add(message);
}
}
}
客户端编码示例:
java
// 协议编码器
class ProtocolEncoder extends MessageToByteEncoder<ProtocolMessage> {
@Override
protected void encode(ChannelHandlerContext ctx, ProtocolMessage msg, ByteBuf out) {
// 写入魔数
out.writeShort(msg.getMagic());
// 写入版本
out.writeByte(msg.getVersion());
// 写入长度
out.writeInt(msg.getContent().length);
// 写入类型
out.writeByte(msg.getType());
// 写入内容
out.writeBytes(msg.getContent());
// 计算并写入CRC
CRC32 crc32 = new CRC32();
crc32.update(msg.getContent());
out.writeInt((int) crc32.getValue());
}
}
这个例子展示了如何使用 LengthFieldBasedFrameDecoder 处理复杂的自定义协议,实现了可靠的消息边界识别,同时通过 CRC 校验增强了数据完整性验证。
四、解码器选型对比
在实际项目中,如何选择合适的解码器?下面是一个简单的对比:

各解码器使用场景对比:
解码器类型 | 适用场景 | 优点 | 缺点 | 内存占用 | 字节序处理 | 支持协议升级 | 推荐消息格式 | GC 压力 | 代表性应用 |
---|---|---|---|---|---|---|---|---|---|
LineBasedFrameDecoder | 基于行的文本协议 | 简单直观 | 分隔符冲突 | 中等(扫描缓冲区) | 不涉及 | 有限 | 文本 | 中 | HTTP 头解析、日志传输 |
DelimiterBasedFrameDecoder | 自定义分隔符协议 | 灵活可定制 | 分隔符冲突 | 中等(扫描缓冲区) | 不涉及 | 有限 | 文本 | 高(多次扫描) | 自定义文本协议 |
FixedLengthFrameDecoder | 固定长度消息 | 性能最佳,简单 | 浪费空间,不灵活 | 低(固定缓冲区) | 不涉及 | 否 | 文本/二进制 | 低(固定缓冲区) | 古老工控协议、银行交易系统 |
LengthFieldBasedFrameDecoder | 带长度字段的协议 | 灵活高效 | 配置复杂 | 中(按需分配) | 需指定大端/小端 | 是(通过版本号) | 二进制 | 中(按需分配) | RPC 框架、大多数二进制协议 |
五、解码器性能与异常处理
5.1 性能对比
解码器性能差异主要体现在处理算法复杂度上:
- FixedLengthFrameDecoder: O(1),直接按固定长度切分,性能最高
- LengthFieldBasedFrameDecoder: O(1),直接读取长度字段,性能高
- DelimiterBasedFrameDecoder/LineBasedFrameDecoder: O(n),需要扫描整个缓冲区查找分隔符,性能较低
在相同硬件条件下,基于长度字段的解码器处理 100 字节消息的吞吐量通常比基于分隔符的高出 40%-60%。特别是在高并发场景下,这种差异会更加明显。
5.2 异常处理示例
解码器可能抛出的异常及处理方式:
java
// 异常处理示例
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4) {
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) {
try {
return super.decode(ctx, in);
} catch (TooLongFrameException e) {
// 帧太长异常
logger.error("消息长度超过限制: {}", e.getMessage());
// 可以返回错误响应
ctx.writeAndFlush(Unpooled.copiedBuffer("消息长度超出限制", CharsetUtil.UTF_8));
// 关闭连接防止攻击
ctx.close();
return null;
} catch (CorruptedFrameException e) {
// 数据损坏异常
logger.error("数据格式错误: {}", e.getMessage());
ctx.close();
return null;
}
}
});
5.3 内存分配与流量控制
不同解码器的内存分配方式有很大差异:
- FixedLengthFrameDecoder:使用固定大小缓冲区,内存占用稳定,适合资源受限环境
- LengthFieldBasedFrameDecoder:根据长度字段按需分配内存,适合变长消息
- DelimiterBasedFrameDecoder:在搜索过程中可能产生内存碎片,增加 GC 压力
在高流量场景下,可以结合 Netty 的流量控制机制避免内存溢出:
java
// 设置高水位线和低水位线,单位为字节
b.option(ChannelOption.WRITE_BUFFER_HIGH_WATER_MARK, 64 * 1024); // 64KB
b.option(ChannelOption.WRITE_BUFFER_LOW_WATER_MARK, 32 * 1024); // 32KB
// 对于使用池化内存的场景,可以调整JVM参数
// -Dio.netty.allocator.type=pooled
// -XX:MaxDirectMemorySize=256m
六、总结
解码器类型 | 适用场景 | 优点 | 缺点 | 代表性应用 | 实现难度 | 扩展性 |
---|---|---|---|---|---|---|
LineBasedFrameDecoder | 基于行的文本协议 | 简单直观 | 分隔符冲突 | HTTP 头解析 | 低 | 低 |
DelimiterBasedFrameDecoder | 自定义分隔符协议 | 灵活可定制 | 分隔符冲突 | 自定义文本协议 | 低 | 中 |
FixedLengthFrameDecoder | 固定长度消息 | 性能最佳,实现简单 | 浪费空间,不灵活 | 固定格式传输 | 最低 | 最低 |
LengthFieldBasedFrameDecoder | 带长度字段的协议 | 灵活性最高,性能好 | 配置复杂 | RPC 框架,大多数二进制协议 | 中 | 高 |