TCP数据粘包问题

前言

本文记录了TCP数据粘包问题的前因后果,只为记录学习使用。以下内容大部分转载于

[网络坦白局] TCP粘包 数据包:我只是犯了每个数据包都会犯的错 |硬核图解 (qq.com)

网络协议传输

我们先来看四层网络协议分层

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

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

数据切片

我们通常在浏览器中传输数据时使用的应用层协议就是http协议,假如我们在网页上键入了一个url,就是表示我们对这个url进行请求获取信息,接下来协议栈就会把我们的请求进行层层封装进行发送,但是数据的传输是有限制的,在一次数据传输过程中,我们不可能传输过大的数据,因此需要有一个数据传输的最大限制,也就是我们通常所说的MTU

也就是说,如果 HTTP 请求消息比较长,超过了 MSS 的长度,这时 TCP 就需要把 HTTP 的数据拆解成一块块的数据发送,而不是一次性发送所有数据。

  • MTU:一个网络包的最大长度,以太网中一般为 1500 字节。
  • MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度。

数据会被以 MSS 的长度为单位进行拆分,拆分出来的每一块数据都会被放进单独的网络包中。也就是在每个被拆分的数据加上 TCP 头信息,然后交给 IP 模块来发送数据。

什么是粘包

我们进行已经例子进行问题说明。

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

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

那么当李东在手机上键入"李东""亚健康终结者"的时候,在 TCP 中把消息分成 MSS 大小后,消息顺着网线顺利发出。

网络稳得很,将消息分片传到了对端手机 B 上。经过 TCP 层消息重组。变成"李东亚健康终结者"这样的字节流(stream)

 

但由于聊天软件琛琛是新开发的,而且开发者叫小白,完了,是个臭名昭著的造 bug 工程师。经过他的代码,在处理字节流的时候消息从"李东","亚健康终结者"变成了"李东亚","健康终结者"。"李东"作为上一个包的内容与下一个包里的"亚"粘在了一起被错误地当成了一个数据包解析了出来。这就是所谓的粘包

 

为什么会出现粘包

TCP,Transmission Control Protocol。传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。

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

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

应用层传到 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算法也并不意味着粘包问题就解决了

  • 接受端应用层在收到 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 结束。

但是采用0xfffffe标志位,用来标志一个数据包的开头,也可能会出现发的某个数据里正好有这个内容,所以一般除了这个标志位,发送端在发送时还会加入各种校验字段(校验和或者对整段完整数据进行 CRC 之后获得的数据)放在标志位后面,在接收端拿到整段数据后校验下确保它就是发送端发来的完整数据。

  • 加入消息长度信息

这个一般配合上面的特殊标志一起使用,在收到头标志时,里面还可以带上消息长度,以此表明在这之后多少 byte 都是属于这个消息的。如果在这之后正好有符合长度的 byte,则取走,作为一个完整消息给应用层使用。在实际场景中,HTTP 中的Content-Length就起了类似的作用,当接收端收到的消息长度小于 Content-Length 时,说明还有些消息没收到。那接收端会一直等,直到拿够了消息或超时

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 的长度信息其实是冗余的。

 可以再来看下 TCP 的报头

TCP首部里是没有长度这个信息的,跟UDP类似,同样可以通过下面的公式获得当前包的TCP数据长度。

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

IP 层有粘包问题吗

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

先说结论,不会。首先前文提到了,粘包其实是由于使用者无法正确区分消息边界导致的一个问题。

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

  • 如果消息过长,IP层会按 MTU 长度把消息分成 N 个切片,每个切片带有自身在包里的位置(offset)同样的IP头信息

  • 各个切片在网络中进行传输。每个数据包切片可以在不同的路由中流转,然后在最后的终点汇合后再组装

  • 在接收端收到第一个切片包时会申请一块新内存,创建IP包的数据结构,等待其他切片分包数据到位。

  • 等消息全部到位后就把整个消息包给到上层(传输层)进行处理。

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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值