TCP 是基于流传输的协议,请求数据在其传输的过程中是没有界限区分,所以我们在读取请求的时候,不一定能获取到一个完整的数据包。如果一个包较大时,可能会切分成多个包进行多次传输。同时,如果存在多个小包时,可能会将其整合成一个大包进行传输。这就是 TCP 协议的粘包/拆包概念。本文基于 Netty5 进行分析粘包/拆包描述 假设当前有
123
和 abc
两个数据包,那么他们传输情况示意图如下:
-
I 为正常情况,两次传输两个独立完整的包。
-
II 为粘包情况,
123
和abc
封装成了一个包。 -
III 为拆包情况,图中的描述是将
123
拆分成了1
和23
,并且1
和abc
一起传输。123
和abc
也可能是abc
进行拆包。甚至123
和abc
进行多次拆分也有可能。
-
/**
-
* 服务端网络事件的读写操作类
-
*
-
* Created by YangTao.
-
*/
-
public class ServerHandler extends ChannelHandlerAdapter {
-
// 接收消息计数器
-
private int i = 0;
-
-
// client端消息
-
@Override
-
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
-
i++;
-
-
System.out.print(msg);
-
-
// 对每条读取到的消息进行打数标记
-
System.out.println("================== ["+ i +"]");
-
// 发送应答消息给客户端
-
ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i).getBytes());
-
ctx.write(rmsg);
-
}
-
-
// 其他操作 .......
-
}
-
/**
-
* 客户端发送数据
-
*
-
* Created by YangTao.
-
*/
-
public class NettyClient {
-
-
public void send() {
-
Bootstrap bootstrap = new Bootstrap();
-
NioEventLoopGroup group = new NioEventLoopGroup();
-
-
try {
-
bootstrap.group(group)
-
.channel(NioSocketChannel.class)
-
.option(ChannelOption.TCP_NODELAY, true)
-
.handler(new ChannelInitializer<NioSocketChannel>() {
-
@Override
-
protected void initChannel(NioSocketChannel ch) {
-
ChannelPipeline pipeline = ch.pipeline();
-
pipeline.addLast(new StringDecoder());
-
pipeline.addLast("logger", new LoggingHandler(LogLevel.INFO));
-
pipeline.addLast(new ClientHandler());
-
}
-
});
-
Channel channel = bootstrap.connect(HOST, PORT).channel();
-
int i = 1;
-
while (i <= 300){
-
channel.writeAndFlush(String.format("【时间 %s: \t%s】", new Date(), i));
-
// 打印发送请求的次数
-
System.out.println(i);
-
i++;
-
}
-
}catch (Exception e){
-
e.printStackTrace();
-
}finally {
-
if (group != null)
-
group.shutdownGracefully();
-
}
-
}
-
}
i
和服务端的接收消息计数 i
应该是相同的数。那么下面通过运行程序,查看打印结果。
【】
中的最后一个数字与 []
中数字对上的是已独立完整的包接收到(粘包/拆包示意图中的情况 I)。但是 【】
中为 37
和 38
的出现了粘包情况(粘包/拆包示意图中的情况 II),两条数据粘合在一起。
【】
中 167
的数据被拆分为了两部分(图中画绿线数据),该情况为拆包(粘包/拆包示意图中的情况 III)。上面程序没有考虑到 TCP 的粘包/拆包问题,所以如果是我们实际应用的程序的话,不能保证数据的正常情况,就会导致程序异常。Netty 解决粘包/拆包问题
LineBasedFrameDecoder 换行符处理
Netty 的强大,方便,简单使用的优势,在粘包/拆包问题上也提供了多种编解码解决方案,并且很容易理解和掌握。这里使用 LineBasedFrameDecoder 和 StringDecoder(将接收到的对象转换成字符串) 来解决粘包/拆包问题。只需在服务端和客户端分别添加 LineBasedFrameDecoder 和 StringDecoder解码器,因为是双向会话,所以两端都要添加,由于我一开始就添加 StringDecoder 编码器,所以只需添加 LineBasedFrameDecoder 就够了。服务端:

-
/**
-
* 服务端网络事件的读写操作类
-
*
-
* Created by YangTao.
-
*/
-
public class ServerHandler extends ChannelHandlerAdapter {
-
// 接收消息计数器
-
private int i = 0;
-
-
// client端消息
-
@Override
-
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
-
i++;
-
-
System.out.print(msg);
-
-
// 对每条读取到的消息进行打数标记
-
System.out.println("================== ["+ i +"]");
-
// 发送应答消息给客户端
-
ByteBuf rmsg = Unpooled.copiedBuffer(String.valueOf(i + System.getProperty("line.separator")).getBytes());
-
ctx.write(rmsg);
-
}
-
-
// 其他操作 .......
-
}
-
/**
-
* 客户端发送数据
-
*
-
* Created by YangTao.
-
*/
-
public class NettyClient {
-
-
public void send() {
-
// 连接操作 .......
-
-
try {
-
// 获取 channel
-
Channel channel = channel();
-
int i = 1;
-
ByteBuf buf = null;
-
while (i <= 300){
-
String str = String.format("【时间 %s: \t%s】", new Date(), i) + System.getProperty("line.separator");
-
byte[] bytes = str.getBytes();
-
// 写入缓冲区
-
buf = Unpooled.buffer(bytes.length);
-
buf.writeBytes(bytes);
-
channel.writeAndFlush(buf);
-
// 打印发送请求的次数
-
System.out.println(i);
-
i++;
-
}
-
}catch (Exception e){
-
e.printStackTrace();
-
}
-
-
// 退出操作 .......
-
}
-
}

DelimiterBasedFrameDecoder 自定义分隔符
自定义分隔符和换行分隔符差不多,只需将发送的数据后换行符换成你自己设定的分割符即可。服务端和客户端均在 pipeline 添加 DelimiterBasedFrameDecoder:-
// 指定的分隔符
-
public static final String DELIMITER = "$@$";
-
-
// 如果当前数据2048个字节中没有分隔符,就会抛出异常,避免内存溢出。也可以自定义预检查当前读取的数据,自定义这里超过的规则
-
pipeline.addLast(new DelimiterBasedFrameDecoder(
-
2048,
-
Unpooled.wrappedBuffer(DELIMITER.getBytes())) // 分割符缓冲对象
-
);
FixedLengthFrameDecoder 根据固定长度
设定固定长度,进行数据传输,如果不达固定长度,使用空格补全。服务端和客户端均在 pipeline 添加 FixedLengthFrameDecoder:-
// 100为指定的固定长度
-
ch.pipeline().addLast(new FixedLengthFrameDecoder(100));
动态指定长度
动态指定长度就是说,每条消息的长度都是随着消息头进行指定,这里使用的编码器为 LengthFieldBasedFrameDecoder。-
pipeline().addLast(
-
new LengthFieldBasedFrameDecoder(
-
2048, // 帧的最大长度,即每个数据包最大限度
-
0, // 长度字段偏移量
-
4, // 长度字段所占的字节数
-
0, // 消息头的长度,可以为负数
-
4) // 需要忽略的字节数,从消息头开始,这里是指整个包
-
);
-
// 创建 byteBuf
-
ByteBuf buf = getBuf();
-
-
// .....
-
-
// 设置该条消息内容长度
-
buf.writeInt(msg.length());
-
// 设置消息内容
-
buf.writeBytes(msg.getBytes("UTF-8"));
Netty 极大的为使用者提供了多种解决粘包/拆包方案,并且可以很愉快的对多种消息进行自动解码,在使用过程中也极容易掌握和理解,很大程度上提升开发效率和稳定性。