事件从一个健身教练说起吧。
李东,自称亚健康终结者,尝试应用互联网+的模式拓展本人的业务。在某款新开发的聊天软件琛琛上公布广告。
键盘说来就来。疯狂发送"李东",回车发送!,"亚健康终结者",再回车发送!
还记得四层网络协议长什么样子吗?
四层网络模型每层各司其职,音讯在进入每一层时都会多加一个报头,每多一个报头能够了解为数据报多戴一顶帽子。这个报头下面记录着音讯从哪来,到哪去,以及音讯多长等信息。比方,mac头部
记录的是硬件的惟一地址,IP头
记录的是从哪来和到哪去,传输层头记录到是达到目标主机后具体去哪个过程。
在从音讯发到网络的时候给音讯带上报头,音讯和纷繁复杂的网络中通过这些信息在路由器间流转,最初达到目标机器上,接受者再通过这些报头,一步一步还原出发送者最原始要发送的音讯。
为什么要将数据切片
软件琛琛是属于应用层上的。
而"李东","亚健康终结者"这两条音讯在进入传输层时应用的是传输层上的 TCP 协定。音讯在进入传输层(TCP)时会被切片为一个个数据包。这个数据包的长度是MSS
。
能够把网络比喻为一个水管,是有肯定的粗细的,这个粗细由网络接口层(数据链路层)提供给网络层,个别认为是的MTU
(1500),间接传入整个音讯,会超过水管的最大接受范畴,那么,就须要进行切片,成为一个个数据包,这样音讯能力失常通过“水管”。
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 算法优化,目标是为了防止发送小的数据包。
在 Nagle 算法开启的状态下,数据包在以下两个状况会被发送:
- 如果包长度达到
MSS
(或含有Fin
包),立即发送,否则期待下一个包到来;如果下一包到来后两个包的总长度超过MSS
的话,就会进行拆分发送; - 期待超时(个别为
200ms
),第一个包没到MSS
长度,然而又迟迟等不到第二个包的到来,则立刻发送。
- 因为启动了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
- 承受端应用层在收到 msg1 时立马就取走了,那此时 msg1 没粘包问题
- msg2 到了后,应用层在忙,没来得及取走,就呆在 TCP Recv Buffer 中了
- msg3 此时也到了,跟 msg2 和 msg3 一起放在了 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 都照样发送,即一次发送一个报文。至于如果数据包太长,须要分片,那也是IP层的事件,大不了效率低一些。UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。而接管方在接收数据报的时候,也不会像面对 TCP 无穷无尽的二进制流那样不分明啥时候能完结。正因为基于数据报和基于字节流的差别,TCP 发送端发 10 次字节流数据,而这时候接收端能够分 100 次去取数据,每次取数据的长度能够依据解决能力作调整;但 UDP 发送端发了 10 次数据报,那接收端就要在 10 次收完,且发了多少,就取多少,确保每次都是一个残缺的数据报。
咱们先看下IP报头
留神这外面是有一个 16 位的总长度的,意味着 IP 报头里记录了整个 IP 包的总长度。接着咱们再看下 UDP 的报头。
在报头中有16bit
用于批示 UDP 数据报文的长度,假如这个长度是 n ,以此作为数据边界。因而在接收端的应用层能清晰地将不同的数据报文辨别开,从报头开始取 n 位,就是一个残缺的数据报,从而防止粘包和拆包的问题。
当然,就算没有这个位(16位 UDP 长度),因为 IP 的头部曾经蕴含了数据的总长度信息,此时如果 IP 包(网络层)里放的数据应用的协定是 UDP(传输层),那么这个总长度其实就蕴含了 UDP 的头部和 UDP 的数据。
因为 UDP 的头部长度固定为 8 字节( 1 字节= 8 位,8 字节= 64 位,上图中除了数据和选项
以外的局部),那么这样就很容易的算出 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)》
里说可能是因为要用于计算校验和。也有的说是因为UDP底层应用的能够不是IP协定,毕竟 IP 头里带了总长度,正好能够用于计算 UDP 数据的长度,万一 UDP 的底层不是IP层协定,而是其余网络层协定,就不能持续这么计算了。
但我感觉,最重要的起因是,IP 层是网络层的,而 UDP 是传输层的,到了传输层,数据包就曾经不存在IP头信息了,那么此时的UDP数据会被放在 UDP 的 Socket Buffer
中。当应用层来不及取这个 UDP 数据报,那么两个数据报在数据层面其实都是一堆 01 串。此时读取第一个数据报的时候,会先读取到 UDP 头部,如果这时候 UDP 头不含 UDP 长度信息,那么应用层应该取多少数据才算残缺的一个数据报呢?
因而 UDP 头的这个长度其实跟 TCP 为了避免粘包而在音讯体里退出的边界信息是起一样的作用的。
面试的时候咱就把这些全说进来,显得咱如同通过了深深的思考一样,面试官可能会感觉咱特地爱思考,加分加分。
如果我说错了,请把我的这篇文章转发给更多的人,让大家记住这个满嘴胡话的人,在关注之后狠狠的私信骂我,托付了!
IP 层有粘包问题吗
IP 层会对大包进行切片,是不是也有粘包问题?
先说论断,不会。首先前文提到了,粘包其实是因为使用者无奈正确区分音讯边界导致的一个问题。
先看看 IP 层的切片分包是怎么回事。
- 如果音讯过长,
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,我还是没答对这道内存透露题
- 硬核!漫画图解HTTP知识点+面试题
- 程序员防猝死指南
别说了,一起在常识的陆地里呛水吧
关注公众号:【golang小白成长记】