事情从一个健身教练说起吧。
李东,自称亚健康终结者,尝试使用互联网+的模式拓展自己的业务。在某款新开发的聊天软件琛琛上发布广告。
键盘说来就来。疯狂发送"李东",回车发送!,"亚健康终结者",再回车发送!
还记得四层网络协议长什么样子吗?
mac头部IP头
在从消息发到网络的时候给消息带上报头,消息和纷繁复杂的网络中通过这些信息在路由器间流转,最后到达目的机器上,接受者再通过这些报头,一步一步还原出发送者最原始要发送的消息。
为什么要将数据切片
软件琛琛是属于应用层上的。
MSS
MTU
MTU 和 MSS 有什么区别
- MTU: Maximum Transmit Unit,最大传输单元。 由网络接口层(数据链路层)提供给网络层最大一次传输数据的大小;一般 MTU=1500 Byte。 假设IP层有 <= 1500 byte 需要发送,只需要一个 IP 包就可以完成发送任务;假设 IP 层有> 1500 byte 数据需要发送,需要分片才能完成发送,分片后的 IP Header ID 相同。
- MSS:Maximum Segment Size 。 TCP 提交给 IP 层最大分段大小,不包含 TCP Header 和 TCP Option,只包含 TCP Payload ,MSS 是 TCP 用来限制应用层最大的发送字节数。 假设 MTU= 1500 byte,那么 MSS = 1500- 20(IP Header) -20 (TCP Header) = 1460 byte,如果应用层有 2000 byte 发送,那么需要两个切片才可以完成发送,第一个 TCP 切片 = 1460,第二个 TCP 切片 = 540。
什么是粘包
那么当李东在手机上键入"李东""亚健康终结者"的时候,在 TCP 中把消息分成 MSS 大小后,消息顺着网线顺利发出。
网络稳得很,将消息分片传到了对端手机 B 上。经过 TCP 层消息重组。变成"李东亚健康终结者"这样的字节流(stream)。
但由于聊天软件琛琛是新开发的,而且开发者叫小白,完了,是个臭名昭著的造 bug 工程师。经过他的代码,在处理字节流的时候消息从"李东","亚健康终结者"变成了"李东亚","健康终结者"。"李东"作为上一个包的内容与下一个包里的"亚"粘在了一起被错误地当成了一个数据包解析了出来。这就是所谓的粘包。
一个号称健康终结者的健身教练,大概运气也不会很差吧,就祝他客源滚滚吧。
为什么会出现粘包
那就要从 TCP 是啥说起。
TCP,Transmission Control Protocol。传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。
其中跟粘包关系最大的就是基于字节流这个特点。
字节流可以理解为一个双向的通道里流淌的数据,这个数据其实就是我们常说的二进制数据,简单来说就是一大堆 01 串。这些 01 串之间没有任何边界。
应用层传到 TCP 协议的数据,不是以消息报为单位向目的主机发送,而是以字节流的方式发送到下游,这些数据可能被切割和组装成各种数据包,接收端收到这些数据包后没有正确还原原来的消息,因此出现粘包现象。
为什么要组装发送的数据
上面提到 TCP 切割数据包是为了能顺利通过网络这根水管。相反,还有一个组装的情况。如果前后两次 TCP 发的数据都远小于 MSS,比如就几个字节,每次都单独发送这几个字节,就比较浪费网络 io 。
比如小白爸让小白出门给买一瓶酱油,小白出去买酱油回来了。小白妈又让小白出门买一瓶醋回来。小白前后结结实实跑了两趟,影响了打游戏的时间。
优化的方法也比较简单。当小白爸让小白去买酱油的时候,小白先等待,继续打会游戏,这时候如果小白妈让小白买瓶醋回来,小白可以一次性带着两个需求出门,再把东西带回来。
TCP
在 Nagle 算法开启的状态下,数据包在以下两个情况会被发送:
MSSFinMSS200msMSS
200msMSSmss200ms
关掉 Nagle 算法就不会粘包了吗?
Nagle 算法其实是个有些年代的东西了,诞生于 1984 年。对于应用程序一次发送一字节数据的场景,如果没有 Nagle 的优化,这样的包立马就发出去了,会导致网络由于太多的包而过载。
但是今天网络环境比以前好太多,Nagle 的优化帮助就没那么大了。而且它的延迟发送,有时候还可能导致调用延时变大,比如打游戏的时候,你操作如此丝滑,但却因为 Nagle 算法延迟发送导致慢了一拍,就问你难受不难受。
所以现在一般也会把它关掉。
看起来,Nagle 算法的优化作用貌似不大,还会导致粘包"问题"。那么是不是关掉这个算法就可以解决掉这个粘包"问题"呢?
TCP_NODELAY = 1
- 接受端应用层在收到 msg1 时立马就取走了,那此时 msg1 没粘包问题
- msg2 到了后,应用层在忙,没来得及取走,就呆在 TCP Recv Buffer 中了
- msg3 此时也到了,跟 msg2 和 msg3 一起放在了 TCP Recv Buffer 中
- 这时候应用层忙完了,来取数据,图里是两个颜色作区分,但实际场景中都是 01 串,此时一起取走,发现还是粘包。
因此,就算关闭 Nagle 算法,接收数据端的应用层没有及时读取 TCP Recv Buffer 中的数据,还是会发生粘包。
怎么处理粘包
粘包出现的根本原因是不确定消息的边界。接收端在面对"无边无际"的二进制流的时候,根本不知道收了多少 01 才算一个消息。一不小心拿多了就说是粘包。其实粘包根本不是 TCP 的问题,是使用者对于 TCP 的理解有误导致的一个问题。
只要在发送端每次发送消息的时候给消息带上识别消息边界的信息,接收端就可以根据这些信息识别出消息的边界,从而区分出每个消息。
常见的方法有
0xfffffe0xfffffe
Content-Length
0xfffffe
校验和CRC
UDP 会粘包吗
TCP
基于数据报是指无论应用层交给 UDP 多长的报文,UDP 都照样发送,即一次发送一个报文。至于如果数据包太长,需要分片,那也是IP层的事情,大不了效率低一些。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。而接收方在接收数据报的时候,也不会像面对 TCP 无穷无尽的二进制流那样不清楚啥时候能结束。正因为基于数据报和基于字节流的差异,TCP 发送端发 10 次字节流数据,而这时候接收端可以分 100 次去取数据,每次取数据的长度可以根据处理能力作调整;但 UDP 发送端发了 10 次数据报,那接收端就要在 10 次收完,且发了多少,就取多少,确保每次都是一个完整的数据报。
我们先看下IP报头
注意这里面是有一个 16 位的总长度的,意味着 IP 报头里记录了整个 IP 包的总长度。接着我们再看下 UDP 的报头。
16bit
当然,就算没有这个位(16位 UDP 长度),因为 IP 的头部已经包含了数据的总长度信息,此时如果 IP 包(网络层)里放的数据使用的协议是 UDP(传输层),那么这个总长度其实就包含了 UDP 的头部和 UDP 的数据。
数据和选项
UDP Data 的长度 = IP 总长度 - IP Header 长度 - UDP Header 长度
可以再来看下 TCP 的报头
TCP首部里是没有长度这个信息的,跟UDP类似,同样可以通过下面的公式获得当前包的TCP数据长度。
TCP Data 的长度 = IP 总长度 - IP Header 长度 - TCP Header 长度。
跟 UDP 不同在于,TCP 发送端在发的时候就不保证发的是一个完整的数据报,仅仅看成一连串无结构的字节流,这串字节流在接收端收到时哪怕知道长度也没用,因为它很可能只是某个完整消息的一部分。
为什么长度字段冗余还要加到 UDP 首部中
《 TCP-IP 详解(卷2)》
Socket Buffer
因此 UDP 头的这个长度其实跟 TCP 为了防止粘包而在消息体里加入的边界信息是起一样的作用的。
面试的时候咱就把这些全说出去,显得咱好像经过了深深的思考一样,面试官可能会觉得咱特别爱思考,加分加分。
如果我说错了,请把我的这篇文章转发给更多的人,让大家记住这个满嘴胡话的人,在关注之后狠狠的私信骂我,拜托了!
IP 层有粘包问题吗
IP 层会对大包进行切片,是不是也有粘包问题?
先说结论,不会。首先前文提到了,粘包其实是由于使用者无法正确区分消息边界导致的一个问题。
先看看 IP 层的切片分包是怎么回事。
IP层
IP 层
IP 层
听起来就像 “我不管产品的需求傻不傻X,我实现了就行,我不问,也懒得争了”,这思路值得每一位优秀的划水程序员学习,respect。
总结
粘包这个问题的根因是由于开发人员没有正确理解 TCP 面向字节流的数据传输方式,本身并不是 TCP 的问题,是开发者的问题。
TCP10 次100 次UDP10 次10 次
数据包也只是按着 TCP 的方式进行组装和拆分,如果数据包有错,那数据包也只是犯了每个数据包都会犯的错而已。
最后,李东工作没了,而小白表示
文章推荐:
别说了,一起在知识的海洋里呛水吧
关注公众号:【golang小白成长记】