TCP可靠传输机制

Note
This article was last updated on 2022-08-15, the content may be out of date.

TCP是一个有状态的,面向连接的协议,其在下层不可靠传输的基础上提供了一层抽象, 使连接双方可将TCP看作一个能够可靠传输数据的端到端通道, 保证接收方从TCP层收到的数据与发送方送往TCP层的数据的内容和顺序一致。

理论上讲,TCP/IP五层模型的任何一层都可以提供可靠的数据传输,但由于层间通信仍有可能会引入错误, 使得以提供可靠传输服务的下层为基础的传输协议仍有可能是非可靠的传输, 因此在紧邻应用层的传输层提供可靠传输服务将使得该服务的价值最大化。 另一方面,也可以基于UDP(不提供可靠传输服务)直接在应用层实现数据的可靠传输。

“functions placed at the lower levels may be redundant or of litle value when compared to the cost of providing them at the higher level.” 1

可靠传输服务主要包括以下三部分

  • 乱序重组
  • 错误检测
  • 丢包处理

针对这三种需求,TCP分别使用对应的机制来实现。

TCP使用序号(Sequence Number)对发送的字节流进行编号,接收方可以根据序号对收到的数据重新排序。 接收方收到数据后会发送确认包(Acknowledgment Segment), 其中包含声明自己已经无误接收到的数据流的确认号(Acknowledgment Number)。 通过使用序号和确认号可以确保发送方送入TCP层的数据与接收方从TCP层收到的数据顺序一致。

具体来说,发送方为每个数据包附上一个序号SEG.SEQ,其表示发送数据的开头在数据流中的字节位置; 接收方接收到该数据包后会将序号加上数据长度(Payload Length)作为确认号, 并回应一个含有确认号SEG.ACK的确认包。

考虑一个单向传输数据的TCP连接

  1. 发送方维护两个变量SND.NXTSND.UNA并初始化为相同的序号初始值(Initial Send Sequence number, ISS)。 ISS通常会随机选取2以防御TCP序号预测攻击。 把这两个变量的值减去ISS的结果作为相对值,从而能够将ISS当作相对0来考虑, 相对值只与发送的字节数有关而与ISS的取值无关。其中
    • SND.NXT表示发送方下个数据包的序号,其相对值表示尚未发送的数据在数据流中的字节位置
    • SND.UNA表示尚未被确认的最小序号,其相对值表示尚未被确认的数据在数据流中的字节位置
  2. 接收方维护一个变量RCV.NXT并使用收到的第一个数据包的SEG.SEQ作为初始值(即ISS)
  3. TCP连接建立完成后数据流的第一个字节的序号为ISS+1,详见TCP连接
  4. 发送方发送序号为SND.NXT,包含数据长度为n的报文,并将SND.NXT更新为SND.NXT+n
  5. 接受方收到序号为RCV.NXT的数据包并确认无误后将RCV.NXT更新为RCV.NXT+n 并延时等待最多500ms3 (若已处于等待状态则结束等待并立即发送所有等待中的确认包), 然后发送确认号为RCV.NXT的确认包,表示下个数据包的期望序号为RCV.NXT
  6. 当发送方收到的确认包满足SEG.ACK>SND.UNA时将SND.UNA更新为SEG.ACK

当接收方接收到SEG.SEQRCV.NXT不一致的数据包时(out-of-order segment)4

  • SEG.SEQ<RCV.NXT,则重新发送确认号为RCV.NXT的确认包并丢掉接受到的内容
  • SEG.SEQ>RCV.NXT,TCP标准没有规定暂存还是丢掉该数据包, 但通常情况会暂存该数据包并记录SEG.SEQRCV.NXT的间隔(Gap)。 若后续数据包完全或部分填补间隔(Gap), 则立即发送确认号为间隔最左端(lower end of the gap)的确认包

实际上,TCP通信双方都会各自维护发送方和接收方两套变量, 因此在TCP数据传输中会有两个序号和两个响应号。

为了确保通信双方发送和接受到的内容一致,TCP使用校验和(Checksum)机制来对TCP数据包进行校验。

发送方首先对以下三部分进行求和(以16 bits为单位,不足16 bits的右端补0)

  • IP伪首部
  • TCP的首部字段(Checksum字段初始为0x0000)
  • TCP的数据内容(Data Payload)

再对求和结果求其反码,就得到了校验和,并将其填入数据包的Checksum字段中。 接收者在收到数据包后连同Checksum字段按相同的算法再计算一次校验和。 如果计算结果为0xFFFF,那么就表明数据包没有检测出错误和完整性缺失。

当检测出错误时:

  • 发送方重新发送序号为SND.UNA的数据包
  • 接收方重新发送确认号为RCV.NXT的确认包

IPv4的IP伪首部(96 bits)包括

  • Source Address: 发送方的IPv4地址(32 bits)
  • Destination Address: 接收方的IPv4地址(32 bits)
  • Zeros: 固定填充位0x00(8 bits)
  • Protocol: 上层协议编号(8 bits)
  • Upper-Layer Packet Length: 上层协议报文字节长度,此处为TCP报文长度(16 bits)

IPv4的校验和具体计算方式定义在 RFC 793 - Section 3.1

IPv6的IP伪首部(320 bits)包括

  • Source Address: 发送方的IPv6地址(128 bits)
  • Destination Address: 接收方的IPv6地址(128 bits)
  • Upper-Layer Packet Length: 上层协议报文字节长度,此处为TCP报文长度(32 bits)
  • Zeros: 固定填充位0x000000(24 bits)
  • Next Header: 上层协议编号(8 bits)

IPv6的校验和具体计算方式定义在 RFC 2460 - Section 8.1

IP伪首部不是真正的IP首部,而只是IP首部的一部分字段,因此叫做"伪"首部(IP Pseudo-header)

协议编号详见[IANA]Assigned Internet Protocol Numbers

关于TCP校验和的校验强度

由于Checksum字段仅使用了简单的求和方式,校验和发生碰撞的概率较大, 因此现代TCP通信通常会在数据链路层额外使用CRC来增强校验能力。关于CRC算法详见 CRC算法的原理、实现、以及常用模式

观察表明,即使在受到CRC校验保护的转发和层间跳转过程中软件和硬件仍会引入错误, 因此TCP的校验和作为端到端检验仍有其存在价值

TCP使用超时(Timeout)机制检测丢包,当发送一个数据包后在重传时长(Retransmission TimeOut, RTO)内没有收到对应的确认包, 便视为该数据包或确认包丢失,此时会重新发送一次该数据包。

RTO过小时会导致由于网络拥塞或路由等延迟因素没有及时收到的报文被当作丢失处理, 从而增加重传的数据量,增大网络负载,可能会导致延迟更加严重,也会导致发送方带宽利用率降低; 过大时会使等待确认的时间过长从而降低带宽利用率。 由于网络因素时刻在发生变化, 因此RTO不适合使用静态值,而应根据网络状况动态计算出最适合的取值。

发送方维护两个变量SRTT(Smoothed Round-Trip Time)和RTTVAR(Round-Trip Time Variation), 并且选择G作为时钟粒度(Clock Granularity),则有

$$ \mathrm{RTO} = \mathit{max}\{\mathrm{SRTT} + \mathit{max} \{\mathrm{G}, \mathrm{K}\cdot \mathrm{RTTVAR}\},\;1\} $$

其中$\mathrm{K}=4$

当第一次测出RTT后,将SRTT的初始化为RTTRTTVAR的初始化为RTT/2; 由于首次通信发生之前无法测出RTT的值,因此通常选择1s6作为RTO的初始值。

在后续通信过程中,每次测量出新RTT后按以下顺序更新SRTTRTTVAR,最后再更新RTO

$$ \begin{matrix} \mathrm{RTTVAR}&=&(1-\boldsymbol\beta)\cdot \mathrm{RTTVAR}+\boldsymbol\beta \cdot |\mathrm{SRTT}-\mathrm{RTT}|\\ \mathrm{SRTT}&=&(1-\boldsymbol\alpha)\cdot \mathrm{SRTT}+\boldsymbol\alpha \cdot \mathrm{RTT} \end{matrix} $$

其中$\boldsymbol{\alpha}=0.125, \;\boldsymbol{\beta}=0.25$7

  • 在发送方发送一个数据包后(包括重传),若定时器尚未运行,则启动定时器
  • 当收到预期的确认包(即SEG.ACK>SND.UNA)后重置定时器
  • SND.UNA==SND.NXT时,关闭定时器
  • 当定时器超时后,重新发送序号为SND.UNA的数据包,将RTO设置为原来的2倍(binary exponential backoff) 但不能超过上限(上限最小为60s),然后重启定时器。若超时发生在连接建立时期,即(SYN-ACK)超时, 且该TCP实现的RTO初始值小于3s,则在连接建立后需将RTO重新初始化为3s
  • 若发生重传后收到了其他数据包的确认,则按照公式重新计算RTO。 由于发生超时可能意味着网络状况发生变化,因此TCP实现可能会在超时后重新初始化SRTTRTTVAR

注意,由于确认包表示位于SEG.ACK及之前的数据均已被无误接收, 因此即使传输过程中一部分确认包发生丢失, 只要发送方仍能收到后续的确认包, 即可认为丢失确认包的数据包没有丢失,无需重传

由于TCP使用流水线(Pipeline)的方式发送数据包,当定时器超时后通常已经发送了许多数据包, 因此使用超时机制并不能及时的检测到丢包。针对这种情况, TCP使用一种快速重传(Fast Retransmit and Recovery, FRR)机制, 即当发送方连续收到三个重复的确认包(不包括原始确认包)且SEG.ACK=SND.UNA时视为发生了丢包, 随即重传序号为SND.UNA的数据包而无需等到定时器超时。此时需按照超时重传机制重置定时器。

当发生单独的数据包丢失时,快速重传机制效率最高; 当多个数据包在短时间内丢失时快速重传则无法很有效的工作。

TCP数据包的用于储存SEG.SEQ的空间长度只有32 bits,因此当发送的数据量足够大时(大于4GB), 网络中可能同时存在两个SEG.SEQ相同但携带不同数据的合法数据包, 这种情况称为序号混迭(Sequence number warp-around)

为了解决这个问题,需要根据TCP的传输速率规定TCP数据包的最大存活周期(Maximum Segment Lifetime, MSL), 也可以对储存序号的空间进行扩容(涉及到更改TCP首部字段, 实施困难)或使用Option字段扩大SEG.SEQ的数值空间

RFC 7323 讨论了TCP高速传输数据时可能出现的问题以及对应的解决方案。