共计 5906 个字符,预计需要花费 15 分钟才能阅读完成。
事件从一个健身教练说起吧。
李东,自称亚健康终结者,尝试应用互联网 + 的模式拓展本人的业务。在某款新开发的聊天软件 琛琛 上公布广告。
键盘说来就来。疯狂发送 ” 李东 ”,回车发送!,” 亚健康终结者 ”,再回车发送!
还记得 四层网络协议 长什么样子吗?
四层网络模型每层各司其职,音讯在进入每一层时都会多加一个 报头 ,每多一个报头能够了解为 数据报多戴一顶帽子 。这个报头下面记录着音讯从哪来,到哪去,以及音讯多长等信息。比方,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 小白成长记】