UDP拆包粘包问题

3.7 HTTP/3 强势来袭 | 小林coding (xiaolincoding.com)

TCP粘包是什么? 为什么UDP不粘包?为什么UDP要冗余长度字段? - 知乎 (zhihu.com)

四层网络

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

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

  • 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粘包

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

基于数据报是指无论应用层交给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 层表示:我只管把发送端给我的数据传到接收端就完了,我也不了解里头放了啥东西。

QUIC协议

特点

UDP是一个简单、不可靠的传输协议,而且UDP包之间是无序的,也没有依赖关系。

而且,UDP是不需要连接的,也就不需要握手和挥手的过程,所以天然就比TCP快。

当然,HTTP/3不仅仅只是简简单单将传输协议替换成了UDP,还基于UDP协议在应用层实现了QUIC协议,它具有类似TCP的连接管理、拥塞窗口、流量控制的网络特性,相当于将不可靠传输的UDP变成可靠的了,所以不同担心数据包丢失的问题。

QUIC的优点有很多,这里列举几个,比如

  • 无队头阻塞
  • 更快的连接建立
  • 连接迁移

无队头阻塞

QUIC协议也有类似HTTP/2 Stream与多路复用的概念,也是可以再同一条连接上并发传输多个Stream,Stream可以被认为就是一条HTTP请求。

由于QUIC使用的传输协议是UDP,UDP不关心数据包的顺序,如果数据包丢失,UDP也不关心。

不过QUIC协议会保证数据包的可靠性,每个数据包都有一个序号唯一标识。当某个流中的一个数据包丢失了,即使该流的其他数据包到达了,数据页无法被HTTP/3读取,直到QUIC重传丢失的报文,数据才会交给HTTP/3

而其他流的数据报文只要被完整接受,HTTP/3就可以读取到数据,这与HTTP2不同,HTTP2只要某个流中的数据包丢失了,其他流也会因此受影响。

所以QUIC连接上的多个Stream之间没有相互依赖,都是独立的,某个流发生丢包了,只会影响该留,其他流不受影响。

更快的连接建立

对于 HTTP/1 和 HTTP/2 协议,TCP 和 TLS 是分层的,分别属于内核实现的传输层、OpenSSL 库实现的表示层,因此它们难以合并在一起,需要分批次来握手,先 TCP 握手,再 TLS 握手。

HTTP/3 在传输数据前虽然需要 QUIC 协议握手,这个握手过程只需要 1 RTT,握手的目的是为确认双方的「连接 ID」,连接迁移就是基于连接 ID 实现的。

但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层,而是 QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS 1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果

如下图右边部分,HTTP/3 当会话恢复时,有效负载数据与第一个数据包一起发送,可以做到 0-RTT:

连接迁移

在前面我们提到,基于 TCP 传输协议的 HTTP 协议,由于是通过四元组(源 IP、源端口、目的 IP、目的端口)确定一条 TCP 连接。

那么当移动设备的网络从 4G 切换到 WiFi 时,意味着 IP 地址变化了,那么就必须要断开连接,然后重新建立连接,而建立连接的过程包含 TCP 三次握手和 TLS 四次握手的时延,以及 TCP 慢启动的减速过程,给用户的感觉就是网络突然卡顿了一下,因此连接的迁移成本是很高的。

而 QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID 来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。

HTTP3

了解完QUIC协议的特点后,我们再来看看HTTP这一层做了什么变化。

HTTP/3同HTTP/2一样采用二进制帧的结构,不同的地方在于HTTP/2的二进制帧里需要定义Stream,而HTTP/3自身不需要再定义Stream,直接用QUIC里的Stream,于是HTTP/3的帧结构也变简单了。

从上图可以看到,HTTP/3帧头只有俩个字段:类型和长度

根据帧类型的不同,大体上分为数据帧和空指帧俩类,Headers帧(HTTP头部)和DATA帧(HTTP包体)属于数据帧。

HTTP/3在头部压缩算法这一方面也做了升级,升级成了QPACK,与HTTP/2中的HTPACK编码方式相似,HTTP/3中的QPACK也采用了静态表 ,动态表和Huffman编码。

对于静态表的变化,HTTP/2 中的 HPACK 的静态表只有 61 项,而 HTTP/3 中的 QPACK 的静态表扩大到 91 项。

HTTP/2 和 HTTP/3 的 Huffman 编码并没有多大不同,但是动态表编解码方式不同。

所谓的动态表,在首次请求-响应后,双方会将未包含在静态表中的Header项更新各自的动态表,接着后续传输时仅用一个数字表示,然后对方可以根据这一个数字从动态表里查到对应的数据,就不必每次都传输常常的数据,大大提升了编码效率。

可以看到,动态表是具有时序性的,如果首次请求发生了丢包,后续的收到请求,对方就无法解码出HPACK头部,因为对方还没建立好动态表,因此后续的请求解码会阻塞到首次请求中丢失的数据包重传过来。

HTTP/3的QPACK解决了这一问题,那它是如何解决的呢?

QUIC会有俩个特殊的双向流,所谓的单向流只有一端可以发送消息,双向则指俩端都可以发送消息,传输HTTP消息时用的是双向流,这俩个单向流的用法:

  • 一个叫 QPACK Encoder Stream,用于将一个字典(Key-Value)传递给对方,比如面对不属于静态表的 HTTP 请求头部,客户端可以通过这个 Stream 发送字典;
  • 一个叫 QPACK Decoder Stream,用于响应对方,告诉它刚发的字典已经更新到自己的本地动态表了,后续就可以使用这个字典来编码了。

这俩个特殊的单向流是用来同步双方的动态表,编码放收到解码放更新确认的通知后,才使用动态表编码HTTP头部。


总结

HTTP/2 虽然具有多个流并发传输的能力,但是传输层是 TCP 协议,于是存在以下缺陷:

  • 队头阻塞,HTTP/2 多个请求跑在一个 TCP 连接中,如果序列号较低的 TCP 段在网络传输中丢失了,即使序列号较高的 TCP 段已经被接收了,应用层也无法从内核中读取到这部分数据,从 HTTP 视角看,就是多个请求被阻塞了;
  • TCP 和 TLS 握手时延,TCP 三次握手和 TLS 四次握手,共有 3-RTT 的时延;
  • 连接迁移需要重新连接,移动设备从 4G 网络环境切换到 WiFi 时,由于 TCP 是基于四元组来确认一条 TCP 连接的,那么网络环境变化后,就会导致 IP 地址或端口变化,于是 TCP 只能断开连接,然后再重新建立连接,切换网络环境的成本高;

HTTP/3 就将传输层从 TCP 替换成了 UDP,并在 UDP 协议上开发了 QUIC 协议,来保证数据的可靠传输。

QUIC 协议的特点:

  • 无队头阻塞,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,也不会有底层协议限制,某个流发生丢包了,只会影响该流,其他流不受影响;
  • 建立连接速度快,因为 QUIC 内部包含 TLS 1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与 TLS 密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。
  • 连接迁移,QUIC 协议没有用四元组的方式来“绑定”连接,而是通过「连接 ID 」来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本;

另外 HTTP/3 的 QPACK 通过两个特殊的单向流来同步双方的动态表,解决了 HTTP/2 的 HPACK 队头阻塞问题。

KCP

概述

KCP是一个开源的快速可靠ARQ协议

为了实现可靠传输,需要对传输信道的差错进行处理。处理方式一般有俩种,一种是ARQ(Automatic Repeat-request),让接受方对接收到的数据进行确认,发送回执,以指示请求重传出错的报文;另一种是FEC。

QUIC的ARQ是在TCP的基础上做了一些改进,一方面ACK报文会携带数据包的接收时间和ACK的时间,以方便计算RTT,另一方面QUIC的ACK是基于包的序号,并支持更多的ACK块,在乱序发送方面比TCP更灵活。

TCP保证数据准确交付,UDP保证数据快速到达,KCP是俩种协议的折中。

KCP的设计目标是为了解决在网络拥堵的情况下,TCP传输慢的问题

KCP是一个快速可靠协议,能以比 TCP浪费10%-20%的带宽的代价,换取平均延迟降低 30%-40%,且最大延迟降低三倍的传输效果。纯算法实现,并不负责底层协议(如UDP)的收发,需要使用者自己定义下层数据的发送方式,以 callback的方式提供给 KCP。连时钟都需要外部传递进来,内部不会有任何一次系统调用。
TCP是为流量设计的(每秒内可以传输多少KB的数据),讲究的是充分利用带宽。而 KCP是为流速设计的(单个数据从一端发送到一端需要多少时间),以10%-20%带宽浪费的代价换取了比 TCP快30%-40%的传输速度。

KCP力求在保证可靠性的情况下提高传输速度。KCP没有规定下层传输协议,但通常使用UDP来实现。

传输协议

在TCP/IP五层模型协议中,传输协议处在第四层,提供端到端的接口,所以不论是TCP还是UDP,其数据段中都包含了端口号。

传输层数据叫做段(segment)网络层的数据叫做包(packet),数据链路层叫做帧(frame),物理层的数据叫做流(stream)

在网络中,我们认为传输是不可靠的,而在很多场景下我们需要的是可靠数据,所谓的可靠,指的是数据能够正常收到,且能够顺序的收到,于是就有了ARQ协议,TCP之所以可靠就是基于此。

ARQ协议有俩种模式

停等ARQ模式

同步请求响应模式,基于超时重传保证可靠。

  1. A会为每个即将发送的数据编号,编号的目的是为了标识数据和给数据排序
  2. A发送完数据之后,会给这次发送的数据设置一个超时计时器
  3. B收到数据,将会返回一个确认,该确认也有自己的编号
  4. A收到确认,将删除副本且取消超时计时器,保留副本的原因是传输可能出错
  5. B收到错误的数据,或者数据在传输过程中出错,总之就是说B没有收到想要的数据
  6. A在超时计时器的设置时间内没有收到确认,此时重发数据

所以可靠的TCP有32位序列号和32位确认号,TCP和UDP都有16位校验和,序列号和确认号的目的是为了保证传输可靠,即有序且收到,而校验和的目的是保证传输的数据没有被篡改。

连续ARQ协议

可以连续发送多个分组,而不必每发完一个分组就停下来等待对方确认。

是不是想到了HTTP1.1中的管道模式与HTTP1.0停等模式,但这里有些许区别,HTTP1.1是中服务器按照顺序响应客户端请求,但连续ARQ协议不会响应每个数据段,而是仅仅响应编号最大的这个数据段,表示之前的数据都收到了,这个叫做UNA模式,而停等ARQ协议可以看作是ACK模式。

现在已经能够在不可靠的网络中传输可靠的数据,但这不意味着可以随意发送数据,带宽是有限的,接收方的负载也是有限的,所以引入了窗口协议,做流量控制。

传输协议有俩种

拥塞窗口

防止过多的数据注入到网络中,这样可以使网络中的路由器和链路不至于过载。与拥塞控制相关的有慢启动、退半避让、快重传、快恢复等。慢启动是在刚开始发送数据时让窗口缓慢扩张(慢是说将初始的cwnd置1,然后以2倍的速度指数增长,到ssthresh时再+1线性增长,这样看其实也不慢),退半避让是在网络拥堵时窗口大小减半(同时让ssthresh置为减半的窗口,但不能小于2),快重传(累计确认重传)是在网络恢复时及时给予响应,与之配合的就是快恢复(快恢复和退半避让差不多,与慢启动做对照)。

滑动窗口

接收方告知发送方自己可以接收的缓冲区大小,通常与ARQ协议配合使用。

TCP协议中的16位窗口大小就是为窗口协议提供支持的。而UDP协议的目标是尽最大努力交付,不管你收到没有,所以没有该字段。

TCP协议是面向连接的协议,在数据传输前通过三次握手建立连接,传输完成后通过四次挥手断开连接,整个过程表示一次完整的数据传输,所以需要4位头长告知哪些是正在传输的数据(排除头数据就是要使用的数据或片段)。

UDP协议是无连接的,两次数据传输没有任何联系,所以需要16位长度告知本次传输的数据有多少。同时注意,UDP协议每次传输的最大数据量并不是2^16 - 1 - 8 - 20(8表示UDP头长,20表示IP头长),而是与MTU有关,即数据链路层的最大传输单元(Maximum Transmission Unit),值是1500。

TCP协议中的8位标志位表示不同的功能,例如当SYN = 1时表示建立连接时让ack = seq + 1而不做任何验证,当URG = 1时16位紧急指针生效,紧急指针表示紧急数据的偏移量,而偏移量之后的数据则是正常数据,紧急数据可以不经过缓冲区尽快被应用层处理。

当清楚TCP和UDP的工作流程,KCP就很容易理解了。

KCP协议的特征

RTO不翻倍

RTO(Retransmission-TimeOut)即重传超时时间,TCP是基于ARQ协议实现的可靠性,KCP也是基于ARQ协议实现的可靠性,但TCP的超时计算是RTO2,而KCP的超时计算是RTO1.5,也就是说假如连续丢同一个包3次,TCP第3次重传是RTO8,而KCP则是RTO3.375,意味着可以更快地重新传输数据。通过4字节ts计算RTT(Round-Trip-Time)即往返时延,再通过RTT计算RTO,ts(timestamp)即当前segment发送时的时间戳。

选择性重传

与TCP相同,都是通过累计确认实现的,发送端发送了1,2,3,4,5几个包,然后收到远端的ACK:1,3,4,5,当收到ACK = 3时,KCP知道2被跳过1次,收到ACK = 4时,知道2被跳过了2次,此时可以认为2号丢失,不用等超时,直接重传2号,大大改善了丢包时的传输速度。1字节cmd = 81时,sn相当于TCP中的seq,cmd = 82时,sn表示收到的但不连续的序号。cmd相当于WebSocket协议中的openCode,即操作码。

非延迟ACK

TCP在连续ARQ协议中,不会将一连串的每个数据都响应一次,而是延迟发送ACK,即上文所说的UNA模式,目的是为了充分利用带宽,但是这样会计算出较大的RTT时间,延长了丢包时的判断过程,而KCP的ACK是否延迟发送可以调节,当配置了非延迟ACK时,收到数据立即响应(或得知发送端的wmd = 0时立即响应,比较容易理解,wnd = 0,必然是对方超时没有收到确认猜想网络拥堵从而做出了慢启动补偿,所以此时就需要立即给出响应)。

UNA + ACK

参考特征2和特征3,cmd = 82 时,una + sn 配合;

非退让流控

在传输及时性要求很高的小数据时,可以通过配置忽略上文所说的窗口协议中的拥塞窗口机制(退半避让),而仅仅依赖于滑动窗口。就是说当丢包的情况出现时,网络可能发生了拥堵,本该ssthresh减半,cwnd=1 | = ssthresh,但KCP不做处理,这样对其他做传输的服务是不公平的,如果网络真的拥堵,KCP如此将导致网络里增加更多的未被收到的数据(更多的丢包),牺牲了带宽利用率。

2字节wnd与TCP协议中的16位窗口大小意义相同,值得一提的是,KCP协议的窗口控制还有其它途径,当cmd = 83时,表示询问远端窗口大小,当cmd = 84时,表示告知远端窗口大小。

4字节conv表示会话匹配数字,同TCP中的conv,类似于HTTP协议中的SessionID,表示两端的编号身份。

1字节frg表示拆数据时的编号,4字节len表示整个数据的长度,相当于WebSocket协议中的len。

Last modification:January 11, 2024
如果觉得我的文章对你有用,请随意赞赏