粘包和拆包
写在前面
粘包、拆包是 Socket 编程中最常遇见的一个问题,本文只对粘包、拆包现象及发生的原因做简要分析,具体如何解决粘包和拆包的问题,在后续文章中会详细介绍。
什么是粘包、拆包
TCP 是个"流"协议,所谓流,就是没有界限
的一串数据(无论你上层是如何封装的数据,到通信层都会转换成“流”的形式,比如 Netty 的 ByteBuf),它会根据 TCP 缓冲区的实际情况进行包的划分,所以实际场景可能是:
-
当 TCP 发送缓冲区剩余空间不足时,一个完整的包可能会被拆分为多个包进行发送,即可能发生拆包情况。
-
当 TCP 发送缓冲区剩余空间足够时,多个小的包也有可能被封装成一个大的包进行发送,即可能发生粘包情况。
粘包、拆包产生的原因
上面我们详细了解了 TCP 粘包与拆包,那么为什么会发生粘包和拆包呢,大致上有三个方面的原因:
-
即上文描述的那种情况。
-
Nagle
算法,TCP 默认开启 Nagle 算法,Nagle 算法主要做两件事情:只有上一个分组得到确认,才发送下一个分组,收集多个小分组,在一个确认到来时一起发送,Nagle 算法可能造成发送方粘包。 -
进行
MSS(Max Segment Size)
TCP 包,MSS 是最大 TCP 分段,是 TCP 报文段中的数据字段
最大长度,MSS = TCP 报文段长度 - TCP 首部长度,同样 MSS = MTU - IP header头大小 - TCP 头大小。
注意,MSS 是 TCP 传输层的概念,TCP 为了避免被发送方分片,会主动把数据分割成小段再交给网络层,最大的分段大小称之为 MSS(Max Segment Size)。
- 以太网的 Payload 大于
MTU
,进行 IP 分片(MTU
概念不清楚的可以看一下这篇文章)。
MTU 是数据链路层概念。
如何处理 TCP 粘包和 TCP 拆包问题?
无论是 TCP 拆包还是 TCP 粘包本质问题都在于无法区分包的边界
,一般有三种区分包边界的方式:
-
消息数据固定长度,实际应用中基本不可能做到,即时做到了,也是很浪费存储和网络资源。
-
使用分割符来区分包的界限
-
数据包的头部中增加数据包长度字段
UDP 存在粘包和拆包的问题吗?
TCP 之所以存在拆包和粘包问题,本质就是 TCP 是面向字节流的协议,字节流协议即无边界协议;而像 UDP 是面向报文的,当客户端连续发送多个包,并不会发生粘包现象,每一个包都是独立的,发送的时候也是以一个一个包为单位。
那么问题来了,不会发生粘包,如果应用程序 write 一个大的包,那么到底层进行发送的时候会不会发生拆包呢?
答案是:不会。UDP 协议发送时,用 sendto 函数最大能发送数据的长度为:65535- IP 头(20) - UDP 头(8) = 65507 字节
。用 sendto 函数发送数据时,如果发送数据长度大于该值,则函数直接返回错误,不会发生拆包,而 TCP 流协议是会发生拆包的。
sendto 扩展
sendto
是一个计算机函数,指向一指定目的地发送数据,sendto 适用于发送未建立连接的 UDP 数据包 (参数为SOCK_DGRAM)。sendto 发送数据必需注意数据长度不应超过通讯子网的 IP 包最大长度。IP 包最大长度在 WSAStartup() 调用返回的 WSAData 的 iMaxUdpDg 元素中。如果数据太长无法自动通过下层协议,则返回 WSAEMSGSIZE
错误,数据不会被发送。
WSAEMSGSIZE
:套接口为 SOCK_DGRAM 类型,且数据报大于 WINDOWS 套接口实现所支持的最大值。
int PASCAL FAR sendto(SOCKET s, const char FAR* buf, int len, int flags, const struct sockaddr FAR* to, int tolen);
s:一个标识套接口的描述字
buf:含待发送数据的缓冲区
len:buf 缓冲区中数据的长度
flags:调用方式标志位
to:(可选)指针,指向目的套接口的地址
tolen:to 所指地址的长度
总结
到这里关于 TCP 粘包和拆包是什么,产生的原因是什么,以及 UDP 是否也会发生粘包和拆包的问题做了简要分析。这只是关于 TCP 粘包和拆包问题的第一篇文章,后面会详细分析常用的解决方案,以及市面上常用通信框架的解决方案是什么。
参考