思维导图
文章已收录Github精选,欢迎Star:https://github.com/yehongzhi/learningSummary
一、什么是粘包和拆包
TCP是一种面向连接的、可靠的、基于字节流的传输层通信协议。(来自百度百科)
发送端为了将多个发给接收端的数据包,更有效地发送到接收端,会使用Nagle算法。Nagle算法会将多次时间间隔较小且数据量小的数据合并成一个大的数据块进行发送。虽然这样的确提高了效率,但是因为面向流通信,数据是无消息保护边界的,就会导致接收端难以分辨出完整的数据包了。
所谓的粘包和拆包问题,就是因为TCP消息无保护边界导致的。
1.1 图解粘包和拆包
正常发送消息是三次发送三个数据包,这种情况没有问题。
粘包,则是其中有多个数据包合并成一个数据包进行发送,也就是上图的第二种情况。
拆包,则是其中一个数据包被拆成了多段,发送的数据包只包含了一个完整数据包的一部分。也就是上图的第三种情况。
1.2 程序演示
首先准备客户端负责发送消息,连续发送5次消息,代码如下:
for (int i = 1; i <= 5; i++) {
ByteBuf byteBuf = Unpooled.copiedBuffer("msg No" + i + " ", Charset.forName("utf-8"));
ctx.writeAndFlush(byteBuf);
}
然后服务端作为接收方,接收并且打印结果:
//count变量,用于计数
private int count = 0;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
byte[] bytes = new byte[msg.readableBytes()];
//把ByteBuf的数据读到bytes数组中
msg.readBytes(bytes);
String message = new String(bytes, Charset.forName("utf-8"));
System.out.println("服务器接收到数据:" + message);
//打印接收的次数
System.out.println("接收到的数据量是:" + (++this.count));
}
启动服务端,再启动两个客户端发送消息,服务端的控制台可以看到这样:
粘包的问题其实是随机的,所以每次结果都不太一样。
二、解决方案
总体思路可以分为三种:
- 在数据的末尾添加特殊的符号标识数据包的边界。通常会加\n\r、\t或者其他的符号。
- 在数据的头部声明数据的长度,按长度获取数据。
- 规定报文的长度,不足则补空位。读取时按规定好的长度来读取。
2.1 使用LineBasedFrameDecoder
这是Netty内置的一个解码器,对应的编码器是LineEncoder。
原理是上面讲的第一种思路,在数据末尾加上特殊符号以标识边界。默认是使用换行符\n。
用法很简单,发送方加上编码器:
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//添加编码器,使用默认的符号\n,字符集是UTF-8
ch.pipeline().addLast(new LineEncoder(LineSeparator.DEFAULT, CharsetUtil.UTF_8));
ch.pipeline().addLast(new TcpClientHandler());
}
接收方加上解码器:
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//解码器需要设置数据的最大长度,我这里设置成1024
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
//给pipeline管道设置业务处理器
ch.pipeline().addLast(new TcpServerHandler());
}
然后在发送方,发送消息时在末尾加上标识符:
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 1; i <= 5; i++) {
//在末尾加上默认的标识符\n
ByteBuf byteBuf = Unpooled.copiedBuffer