TCP 粘包拆包说明
TCP 是个“流”协议(流 -> 没有界限的一串数据)。
TCP 底层并不了解上层数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被tcp拆分成多个包发送,也有可能把多个小的包封装成一个大的数据包发送。
示例分析
客户端分别发送了两个数据包 D1 和 D2 给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下 5 种情况:
- 图第一行:服务端分别读取到两个独立的数据包,没有粘包和拆包
- 图第二行:服务端一次接收到两个数据包,D1 和 D2 粘合在一起,被称为TCP 粘包
- 图第三行:服务端第一次读取到 D1 的完整内容和 D2 的部分内容,第二次读取到了 D2 的剩余部分,这被称为 TCP 拆包
- 图第四行:服务端第一次读取到了 D1 的部分内容, 第二次读取到了 D1 的剩余部分和 D2 的整包
- 如果服务端tcp 接收滑窗非常小,而数据包 D1 和 D2 比较大,则服务端需要多次才能将D1 和 D2 包接收完全,期间发生多次拆包。
发生原因
问题产生的原因有三,如下:
1. 应用程序 write 写入的字节大小大于套接字缓冲区大小
2. 待发送数据大于MSS(指TCP报文能够携带的最大数据长度),TCP在传输前将进行拆包
3. 以太网帧的 payload 大于 MTU(最大传输单元)而进行 IP 分片
解决策略
由于 tcp 无法理解上层业务数据,所以这个问题只能通过上层的设计来解决,解决方案归纳如下:
- 消息定长。规定每个报文为固定长度,如果不够,空位补空格。
- 在包尾增加回车换行符进行分割,例如 FTP 协议。
- 将消息分为头部和消息主体,头部包含表示消息长度的字段
LineBasedFrameDecoder 解决 TCP 粘包
原理分析
对应于上面解决策略其二,在包尾添加回车换行符,然后在解码时以回车换行符作为包结束标志。
// 包尾添加换行符
msg= Unpooled.copiedBuffer(("QUERY TIME ORDER"+System.getProperty("line.separator")).getBytes());
// 添加解码器 LineBasedFrameDecoder
ch.pipeline().addLast(new LineBasedFrameDecoder(1024));
LineBasedFrameDecoder 的工作原理是它依次遍历ByteBuf中的可读字节,判断看是否有“\n” 或“\r\n”,如果有,就以此位置作为结束位置。
DelimiterBasedFrameDecoder 分隔符解码器
原理分析
以自定义分隔符作为包结束标志。
// 1. 定义分隔符$_
// 2. 添加分隔符解码器 DelimiterBasedFrameDecoder
ByteBuf delemiter= Unpooled.copiedBuffer("$_".getBytes());
ch.pipeline().addLast(new DelimiterBasedFrameDecoder(1024,delemiter));
// 3.包尾添加$_
msg= Unpooled.copiedBuffer(("QUERY TIME ORDER$_").getBytes());
ctx.writeAndFlush(msg);
FixedLengthFrameDecoder 定长解码器
原理说明
FixedLengthFrameDecoder 是固定长度解码器,它能够按照指定的长度对消息进行自动解码,开发者不需要考虑TCP的粘包拆包问题。
// 添加定长为20的解码器
ch.pipeline().addLast(new FixedLengthFrameDecoder(20));
无论一次接收到多少数据包,它都会按照设置的定长进行解码。
例如:设置定长为5,接收到数据包abcdefg, 解码器按照定长5进行解码,对abcde进行处理,剩余的fg缓存起来,等待下个包到来再进行拼包。