概述
上篇博客我简单介绍了粘包、拆包问题出现的原因以及 Netty 如何解决该问题,在博客最后我给出通过换行符分割数据包解决粘包问题的方案。然而在实际业务场景中,数据包可能不适用换行符分割这种方式,因为实际数据中可能包含换行符。本篇博客我就来简单补充两种更完整的解决方案:
DelimiterBasedFrameDecoder
DelimiterBasedFrameDecoder 类可以自定义分割符,一般在代码中这样使用:
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
DelimiterBasedFrameDecoder decoder = new DelimiterBasedFrameDecoder(1024, buf);
上述代码就创建一种以 “$_” 为分隔符的缓冲池处理器,其中它的作用机制和 LineBasedFrameDecoder 类似,都是遍历 ByteBuf 中可读字节,根据关键字分割 TCP 包。其中1024表示最大长度,如果超过最大长度还没有出现分隔符就报错。
下面我们看一组具体示例,这里我们使用 Echo 服务器,其中我只列出核心代码:
Echo 服务端:
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
socketChannel.pipeline().addLast(
new DelimiterBasedFrameDecoder(1024, buf)
);
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new EchoServerHandler());
}
});
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("Time Server Receive message:[" + body + "] , and counter = " + ++counter);
String result = body + "$_";
ByteBuf buf = Unpooled.copiedBuffer(result.getBytes());
ctx.writeAndFlush(buf);
}
Echo 客户端:
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
socketChannel.pipeline().addLast(
new DelimiterBasedFrameDecoder(1024, buf)
);
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
private byte[] bytes = "Hi Netty!$_".getBytes();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = null;
for (int i = 0; i < 10; i++) {
buf = Unpooled.buffer(bytes.length);
buf.writeBytes(bytes);
ctx.writeAndFlush(buf);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String message = (String) msg;
System.out.println("Time Client Receive message:[" + message + "] , and counter = " + ++counter);
}
执行结果:
服务端:
Time Server Receive message:[Hi Netty!] , and counter = 1
Time Server Receive message:[Hi Netty!] , and counter = 2
...
Time Server Receive message:[Hi Netty!] , and counter = 10
客户端:
Time Client Receive message:[Hi Netty!] , and counter = 1
Time Client Receive message:[Hi Netty!] , and counter = 2
...
Time Client Receive message:[Hi Netty!] , and counter = 10
从执行结果就可以看出,TCP 消息包在缓冲区被 DelimiterBasedFrameDecoder 按我们预设的关键词分割为多个数据包。
FixedLengthFrameDecoder
FixedLengthFrameDecoder 类一般用作定长数据包,其中它在代码中一般这样使用:
FixedLengthFrameDecoder decoder = new FixedLengthFrameDecoder(20);
上述代码就创建了一个按20字节长度为一数据包的处理器,其中它的机制和 DelimiterBasedFrameDecoder 类似。
下面我们具体看示例,同样采用 Echo 服务器,只列出核心代码:
Echo 服务端:
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 1024)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
socketChannel.pipeline().addLast(
new FixedLengthFrameDecoder(20)
);
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new EchoServerHandler());
}
});
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String body = (String) msg;
System.out.println("Time Server Receive message:[" + body + "] , and counter = " + ++counter);
ByteBuf buf = Unpooled.copiedBuffer(body.getBytes());
ctx.writeAndFlush(buf);
}
Echo 客户端:
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
socketChannel.pipeline().addLast(
new FixedLengthFrameDecoder(20)
);
socketChannel.pipeline().addLast(new StringDecoder());
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
private byte[] bytes = "Hello Netty !!!!!!!!".getBytes();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ByteBuf buf = null;
for (int i = 0; i < 10; i++) {
buf = Unpooled.buffer(bytes.length);
buf.writeBytes(bytes);
ctx.writeAndFlush(buf);
}
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
String message = (String) msg;
System.out.println("Time Client Receive message:[" + message + "] , and counter = " + ++counter);
}
** 执行结果**:
服务端:
Time Server Receive message:[Hello Netty !!!!!!!!] , and counter = 1
Time Server Receive message:[Hello Netty !!!!!!!!] , and counter = 2
...
Time Server Receive message:[Hello Netty !!!!!!!!] , and counter = 10
客户端:
Time Client Receive message:[Hello Netty !!!!!!!!] , and counter = 1
Time Client Receive message:[Hello Netty !!!!!!!!] , and counter = 2
...
Time Client Receive message:[Hello Netty !!!!!!!!] , and counter = 10
上述示例中,我们每个数据包长度固定20字节,和 FixedLengthFrameDecoder 处理器配置长度相同,这样所有的数据包都可以完整分割,从执行结果来看也没有出现任何问题。
至此,Netty 常用的解决 TCP 粘包、拆包问题的 API 介绍完毕。