粘包和拆包

写在前面

粘包、拆包是 Socket 编程中最常遇见的一个问题,本文只对粘包、拆包现象及发生的原因做简要分析,具体如何解决粘包和拆包的问题,在后续文章中会详细介绍。

什么是粘包、拆包

TCP 是个"流"协议,所谓流,就是没有界限的一串数据(无论你上层是如何封装的数据,到通信层都会转换成“流”的形式,比如 Netty 的 ByteBuf),它会根据 TCP 缓冲区的实际情况进行包的划分,所以实际场景可能是:

  • 当 TCP 发送缓冲区剩余空间不足时,一个完整的包可能会被拆分为多个包进行发送,即可能发生拆包情况。

  • 当 TCP 发送缓冲区剩余空间足够时,多个小的包也有可能被封装成一个大的包进行发送,即可能发生粘包情况。

粘包、拆包产生的原因

上面我们详细了解了 TCP 粘包与拆包,那么为什么会发生粘包和拆包呢,大致上有三个方面的原因:

  1. 即上文描述的那种情况。

  2. Nagle 算法,TCP 默认开启 Nagle 算法,Nagle 算法主要做两件事情:只有上一个分组得到确认,才发送下一个分组,收集多个小分组,在一个确认到来时一起发送,Nagle 算法可能造成发送方粘包。

  3. 进行 MSS(Max Segment Size) TCP 包,MSS 是最大 TCP 分段,是 TCP 报文段中的 数据字段 最大长度,MSS = TCP 报文段长度 - TCP 首部长度,同样 MSS = MTU - IP header头大小 - TCP 头大小。

注意,MSS 是 TCP 传输层的概念,TCP 为了避免被发送方分片,会主动把数据分割成小段再交给网络层,最大的分段大小称之为 MSS(Max Segment Size)。

  1. 以太网的 Payload 大于 MTU,进行 IP 分片(MTU 概念不清楚的可以看一下这篇文章)。

MTU 是数据链路层概念。

如何处理 TCP 粘包和 TCP 拆包问题?

无论是 TCP 拆包还是 TCP 粘包本质问题都在于无法区分包的边界,一般有三种区分包边界的方式:

  1. 消息数据固定长度,实际应用中基本不可能做到,即时做到了,也是很浪费存储和网络资源。

  2. 使用分割符来区分包的界限

  3. 数据包的头部中增加数据包长度字段

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 粘包和拆包问题的第一篇文章,后面会详细分析常用的解决方案,以及市面上常用通信框架的解决方案是什么。

参考

posted @ 2020-11-12 13:05  我们都是小白鼠  阅读(995)  评论(0编辑  收藏  举报