硬核图解|tcp为何会粘包?背后的缘由让人暖心

尚码园 · · 120 次点击 · · 开始浏览    

事情从一个健身教练提及吧。程序员

李东,自称亚健康终结者,尝试使用互联网+的模式拓展本身的业务。在某款新开发的聊天软件琛琛上发布广告。golang

键盘说来就来。疯狂发送"李东",回车发送!,"亚健康终结者",再回车发送!面试

还记得四层网络协议长什么样子吗?算法

四层网络协议

四层网络模型每层各司其职,消息在进入每一层时都会多加一个报头,每多一个报头能够理解为数据报多戴一顶帽子。这个报头上面记录着消息从哪来,到哪去,以及消息多长等信息。好比,mac头部记录的是硬件的惟一地址,IP头记录的是从哪来和到哪去,传输层头记录到是到达目的主机后具体去哪一个进程网络

在从消息发到网络的时候给消息带上报头,消息和纷繁复杂的网络中经过这些信息在路由器间流转,最后到达目的机器上,接受者再经过这些报头,一步一步还原出发送者最原始要发送的消息。数据结构

四层网络协议 (1)

为何要将数据切片

软件琛琛是属于应用层上的。tcp

而"李东","亚健康终结者"这两条消息在进入传输层时使用的是传输层上的 TCP 协议。消息在进入传输层(TCP)时会被切片为一个个数据包。这个数据包的长度是MSS学习

能够把网络比喻为一个水管,是有必定的粗细的,这个粗细由网络接口层(数据链路层)提供给网络层,通常认为是的MTU(1500),直接传入整个消息,会超过水管的最大承受范围,那么,就须要进行切片,成为一个个数据包,这样消息才能正常经过“水管”。优化

数据分片

MTU 和 MSS 有什么区别

MSS和MTU的区别

  • 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。传输控制协议,是一种面向链接的、可靠的、基于字节流的传输层通讯协议。

tcp是什么

其中跟粘包关系最大的就是基于字节流这个特色。

字节流能够理解为一个双向的通道里流淌的数据,这个数据其实就是咱们常说的二进制数据,简单来讲就是一大堆 01 串。这些 01 串之间没有任何边界

二进制字节流

应用层传到 TCP 协议的数据,不是以消息报为单位向目的主机发送,而是以字节流的方式发送到下游,这些数据可能被切割和组装成各类数据包,接收端收到这些数据包后没有正确还原原来的消息,所以出现粘包现象。

为何要组装发送的数据

上面提到 TCP 切割数据包是为了能顺利经过网络这根水管。相反,还有一个组装的状况。若是先后两次 TCP 发的数据都远小于 MSS,好比就几个字节,每次都单独发送这几个字节,就比较浪费网络 io 。

正常发送数据包

好比小白爸让小白出门给买一瓶酱油,小白出去买酱油回来了。小白妈又让小白出门买一瓶醋回来。小白先后结结实实跑了两趟,影响了打游戏的时间。

优化的方法也比较简单。当小白爸让小白去买酱油的时候,小白先等待,继续打会游戏,这时候若是小白妈让小白买瓶醋回来,小白能够一次性带着两个需求出门,再把东西带回来。

上面说的其实就是TCPNagle 算法优化,目的是为了不发送小的数据包。

在 Nagle 算法开启的状态下,数据包在如下两个状况会被发送:

  • 若是包长度达到MSS(或含有Fin包),马上发送,不然等待下一个包到来;若是下一包到来后两个包的总长度超过MSS的话,就会进行拆分发送;
  • 等待超时(通常为200ms),第一个包没到MSS长度,可是又迟迟等不到第二个包的到来,则当即发送。

negle2

  • 因为启动了Nagle算法, msg1 小于 mss ,此时等待200ms内来了一个 msg2 ,msg1 + msg2 > MSS,所以把 msg2 分为 msg2(1) 和 msg2(2),msg1 + msg2(1) 包的大小为MSS。此时发送出去。
  • 剩余的 msg2(2) 也等到了 msg3, 一样 msg2(2) + msg3 > MSS,所以把 msg3 分为 msg3(1) 和 msg3(2),msg2(2) + msg3(1) 做为一个包发送。
  • 剩余的 msg3(2) 长度不足mss,同时在200ms内没有等到下一个包,等待超时,直接发送。
  • 此时三个包虽然在图里颜色不一样,可是实际场景中,他们都是一整个 01 串,若是处理开发者把第一个收到的 msg1 + msg2(1) 就当作是一个完整消息进行处理,就会看上去就像是两个包粘在一块儿,就会致使粘包问题

关掉 Nagle 算法就不会粘包了吗?

Nagle 算法实际上是个有些年代的东西了,诞生于 1984 年。对于应用程序一次发送一字节数据的场景,若是没有 Nagle 的优化,这样的包立马就发出去了,会致使网络因为太多的包而过载。

可是今天网络环境比之前好太多,Nagle 的优化帮助就没那么大了。并且它的延迟发送,有时候还可能致使调用延时变大,好比打游戏的时候,你操做如此丝滑,但却由于 Nagle 算法延迟发送致使慢了一拍,就问你难受不难受。

因此如今通常也会把它关掉

看起来,Nagle 算法的优化做用貌似不大,还会致使粘包"问题"。那么是否是关掉这个算法就能够解决掉这个粘包"问题"呢?

 TCP_NODELAY = 1

关闭Negle就不会粘包了吗

  • 接受端应用层在收到 msg1 时立马就取走了,那此时 msg1 没粘包问题
  • msg2 到了后,应用层在忙,没来得及取走,就呆在 TCP Recv Buffer 中了
  • msg3 此时也到了,跟 msg2msg3 一块儿放在了 TCP Recv Buffer
  • 这时候应用层忙完了,来取数据,图里是两个颜色做区分,但实际场景中都是 01 串,此时一块儿取走,发现仍是粘包。

所以,就算关闭 Nagle 算法,接收数据端的应用层没有及时读取 TCP Recv Buffer 中的数据,仍是会发生粘包。

怎么处理粘包

粘包出现的根本缘由是不肯定消息的边界。接收端在面对"一望无际"的二进制流的时候,根本不知道收了多少 01 才算一个消息。一不当心拿多了就说是粘包。其实粘包根本不是 TCP 的问题,是使用者对于 TCP 的理解有误致使的一个问题。

只要在发送端每次发送消息的时候给消息带上识别消息边界的信息,接收端就能够根据这些信息识别出消息的边界,从而区分出每一个消息。

常见的方法有

  • 加入特殊标志

    消息边界头尾标志

    能够经过特殊的标志做为头尾,好比当收到了0xfffffe或者回车符,则认为收到了新消息的头,此时继续取数据,直到收到下一个头标志0xfffffe或者尾部标记,才认为是一个完整消息。相似的像 HTTP 协议里当使用 chunked 编码 传输时,使用若干个 chunk 组成消息,最后由一个标明长度为 0 的 chunk 结束。

  • 加入消息长度信息

消息边界长度标志

这个通常配合上面的特殊标志一块儿使用,在收到头标志时,里面还能够带上消息长度,以此代表在这以后多少 byte 都是属于这个消息的。若是在这以后正好有符合长度的 byte,则取走,做为一个完整消息给应用层使用。在实际场景中,HTTP 中的Content-Length就起了相似的做用,当接收端收到的消息长度小于 Content-Length 时,说明还有些消息没收到。那接收端会一直等,直到拿够了消息或超时,关于这一点上一篇文章里有更详细的说明。

可能这时候会有朋友会问,采用0xfffffe标志位,用来标志一个数据包的开头,你就不怕你发的某个数据里正好有这个内容吗?

是的,,因此通常除了这个标志位,发送端在发送时还会加入各类校验字段(校验和或者对整段完整数据进行 CRC 以后得到的数据)放在标志位后面,在接收端拿到整段数据后校验下确保它就是发送端发来的完整数据。

消息边界头尾加校验标志

UDP 会粘包吗

TCP 同为传输层的另外一个协议,UDP,User Datagram Protocol。用户数据包协议,是面向无链接,不可靠的,基于数据报的传输层通讯协议。

UDP是什么

基于数据报是指不管应用层交给 UDP 多长的报文,UDP 都照样发送,即一次发送一个报文。至于若是数据包太长,须要分片,那也是IP层的事情,大不了效率低一些。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。而接收方在接收数据报的时候,也不会像面对 TCP 无穷无尽的二进制流那样不清楚啥时候能结束。正由于基于数据报基于字节流的差别,TCP 发送端发 10 次字节流数据,而这时候接收端能够分 100 次去取数据,每次取数据的长度能够根据处理能力做调整;但 UDP 发送端发了 10 次数据报,那接收端就要在 10 次收完,且发了多少,就取多少,确保每次都是一个完整的数据报。

咱们先看下IP报头

ip报头

注意这里面是有一个 16 位的总长度的,意味着 IP 报头里记录了整个 IP 包的总长度。接着咱们再看下 UDP 的报头

UDP报头

在报头中有16bit用于指示 UDP 数据报文的长度,假设这个长度是 n ,以此做为数据边界。所以在接收端的应用层能清晰地将不一样的数据报文区分开,从报头开始取 n 位,就是一个完整的数据报,从而避免粘包和拆包的问题。

固然,就算没有这个位(16位 UDP 长度),由于 IP 的头部已经包含了数据的总长度信息,此时若是 IP 包(网络层)里放的数据使用的协议是 UDP(传输层),那么这个总长度其实就包含了 UDP 的头部和 UDP 的数据。

由于 UDP 的头部长度固定为 8 字节( 1 字节= 8 位,8 字节= 64 位,上图中除了数据和选项之外的部分),那么这样就很容易的算出 UDP 的数据的长度了。所以说 UDP 的长度信息实际上是冗余的。

UDP数据长度

 UDP Data 的长度 = IP 总长度 - IP Header 长度 - UDP Header 长度

能够再来看下 TCP 的报头

tcp报头2

TCP首部里是没有长度这个信息的,跟UDP相似,一样能够经过下面的公式得到当前包的TCP数据长度。

 TCP Data 的长度 = IP 总长度 - IP Header 长度 - TCP Header 长度。

TCP数据长度

跟 UDP 不一样在于,TCP 发送端在发的时候就不保证发的是一个完整的数据报,仅仅当作一连串无结构的字节流,这串字节流在接收端收到时哪怕知道长度也没用,由于它极可能只是某个完整消息的一部分。

为何长度字段冗余还要加到 UDP 首部中

关于这一点,查了不少资料,《 TCP-IP 详解(卷2)》里说多是由于要用于计算校验和。也有的说是由于UDP底层使用的能够不是IP协议,毕竟 IP 头里带了总长度,正好能够用于计算 UDP 数据的长度,万一 UDP 的底层不是IP层协议,而是其余网络层协议,就不能继续这么计算了。

但我以为,最重要的缘由是,IP 层是网络层的,而 UDP 是传输层的,到了传输层,数据包就已经不存在IP头信息了,那么此时的UDP数据会被放在 UDP 的 Socket Buffer 中。当应用层来不及取这个 UDP 数据报,那么两个数据报在数据层面其实都是一堆 01 串。此时读取第一个数据报的时候,会先读取到 UDP 头部,若是这时候 UDP 头不含 UDP 长度信息,那么应用层应该取多少数据才算完整的一个数据报呢

所以 UDP 头的这个长度其实跟 TCP 为了防止粘包而在消息体里加入的边界信息是起同样的做用的。

为何UDP要冗余一个长度字段

面试的时候咱就把这些全说出去,显得咱好像通过了深深的思考同样,面试官可能会以为咱特别爱思考,加分加分

若是我说错了,请把个人这篇文章转发给更多的人,让你们记住这个满嘴胡话的人,在关注以后狠狠的私信骂我,拜托了!

IP 层有粘包问题吗

IP 层会对大包进行切片,是否是也有粘包问题?

先说结论,不会。首先前文提到了,粘包实际上是因为使用者没法正确区分消息边界致使的一个问题。

先看看 IP 层的切片分包是怎么回事。

P分包与重组

  • 若是消息过长,IP层会按 MTU 长度把消息分红 N 个切片,每一个切片带有自身在包里的位置(offset)一样的IP头信息
  • 各个切片在网络中进行传输。每一个数据包切片能够在不一样的路由中流转,而后在最后的终点汇合后再组装
  • 在接收端收到第一个切片包时会申请一块新内存,建立IP包的数据结构,等待其余切片分包数据到位。
  • 等消息所有到位后就把整个消息包给到上层(传输层)进行处理。

能够看出整个过程,IP 层从按长度切片到把切片组装成一个数据包的过程当中,都只管运输,都不须要在乎消息的边界和内容,都不在乎消息内容了,那就不会有粘包一说了。

IP 层表示:我只管把发送端给个人数据传到接收端就完了,我也不了解里头放了啥东西。

听起来就像 “我无论产品的需求傻不傻X,我实现了就行,我不问,也懒得争了”,这思路值得每一位优秀的划水程序员学习,respect

总结

粘包这个问题的根因是因为开发人员没有正确理解 TCP 面向字节流的数据传输方式,自己并非 TCP 的问题,是开发者的问题。

  • TCP 无论发送端要发什么,都基于字节流把数据发到接收端。这个字节流里可能包含上一次想要发的数据的部分信息。接收端根据须要在消息里加上识别消息边界的信息。不加就可能出现粘包问题。
  • TCP 粘包跟Nagle算法有关系,但关闭 Nagle 算法并不解决粘包问题。
  • UDP 是基于数据报的传输协议,不会有粘包问题。
  • IP 层也切片,可是由于不关心消息里有啥,所以有不会有粘包问题。
  • TCP 发送端能够发 10 次字节流数据,接收端能够分 100 次去取;UDP 发送端发了 10 次数据报,那接收端就要在 10 次收完。

数据包也只是按着 TCP 的方式进行组装和拆分,若是数据包有错,那数据包也只是犯了每一个数据包都会犯的错而已

最后,李东工做没了,而小白表示

文章推荐:

别说了,一块儿在知识的海洋里呛水吧

关注公众号:【golang小白成长记】

本文来自:尚码园

感谢作者:尚码园

查看原文:硬核图解|tcp为何会粘包?背后的缘由让人暖心

120 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传