概述

TCP (Transmission Control Protocol) 是一个面向连接的协议,为应用层提供了可靠、按序、面向字节流的服务,拥有流量控制、拥塞控制的能力。

  • 面向连接:在数据传输前后,需要分别创建和销毁连接。
  • 可靠传输:分组差错和丢失检测、重传纠错。
  • 流量控制:发送方根据接收方的反馈,调整自身发送窗口大小。
  • 拥塞控制:发送方根据网络拥塞情况,调整自身发送窗口大小。

(未完待续)

面向连接

“连接”是传统有线电话的概念,拨号方在拨号成功时就已经与被叫方建立了可靠的通信线路。该线路由通信双方独占资源,提供了高质量、可靠的通信服务。在通话结束时该线路会进行关闭连接的操作,释放被占用的资源。TCP的“面向连接”就是模仿传统电信网,为通信双方建立起一条连接用于通信,并在结束时释放相关资源。当然该连接只是一条虚拟的、概念上的连接而非真实的物理连接。

面向连接意味着可靠传输,也意味着包含了“建立连接 → 数据传输 → 释放连接”的生命周期。与之相对的是“无连接”(如UDP),既不提供可靠传输服务,也无需创建连接。

把“提供可靠服务”的任务留给运输层的TCP实现,是基于计算机的处理差错的能力而做出的决策。而IP层只需提供“尽最大努力交付”的服务即可,此举提高了互联网的灵活性、对互联网的发展有重要意义。

面向字节流

TCP是面向字节流的协议,也被称为流式协议。理解“面向字节流”的含义,对TCP通信逻辑的理解有重要意义。

概念上,可以简单地理解为TCP只管按字节流的顺序/序号进行收发就完事。发送方TCP将应用程序发送过来的字节流,追加到当前序号后方并执行发送,而不关心这些字节流对应用程序有什么含义。例如TCP接收到n个KB的字节流,它只负责发送。至于这n个KB的字节是属于一个或多个应用层报文,TCP并不关心。反之亦然,接收方TCP也是把收到的字节流按序交付应用程序处理即可——至于交付的字节流是不是多个应用层报文,TCP并不关心。与之对应的是面向报文的UDP协议,每个UDP分组对于应用程序来说都是一个具体的消息。

从具体的通信过程来看,每个报文的序号seq和确认号ack两个字段是非常关键的。

面向字节流的另一个含义是,TCP本身没有对单个报文的长度进行限制(区别于IP数据报有具体的对payload长度的限制)。因此需要考虑消息体过大或过小的问题(Nagle算法)。

可靠传输

TCP 使用 sequence 字段进行分组丢失检测、使用 checksum 字段进行差错校验以及通过重传机制进行纠错,并由此实现可靠传输。

差错校验

IP层和运输层协议均通过checksum字段进行差价校验,但IP层仅对单个IP帧首部进行校验,而将payload部分的验证交给上层处理。因此运输层的协议包括TCP和UDP在内,需要对该层的报文进行整体校验。校验的内容包含三部分:伪首部header, payload. 其中伪首部由源IP、目的地IP、协议号、数据长度等信息组成,长度为12字节。

当checksum校验失败时,TCP接收方会丢弃该报文并等待发送方超时自动重传,而不会主动通知发送方报文校验失败。这是为了减少协议的复杂度,否则还要考虑报错报文的丢失、滞留的问题,甚至需要增加header-flags.

自动重传

停止-等待:发送方每发送一次报文后就等待直到收到接收方发回的ACK报文后再发送后续报文。

信道利用率:如果把发送方发送的时间记为Td,报文发送到接收方处理并传回的时间记为RTT (Round-Trip Time),接收方处理ACK报文的时间记为Ta,信道使用率为U,则发送方从发送报文到接收并处理ACK报文的这段时间内,信道使用率为:

U = Td / (Td + RTT + Ta)

由于有效数据传输 Td 只占少部分时间,因此使用停止-等待策略的传输协议的信道使用率较低。

自动重传请求 ARQ (Automatic Repeat reQuest): 发送方自行维护一个超时计时器,在超时后仍未收到接收方的ACK报文则自发进行重传操作。当发生重传时,信道的使用率会进一步降低。

连续ARQ: 将一段较大的数据/字节流划分为多个部分/报文,一次性地将多个报文全部发送,并且对超时未确认的报文段进行主动重传,此举可提升信道使用率。

发送窗口

发送窗口是TCP发送方的发送缓冲区里的一个逻辑区域,可以把它想象成在序号范围 [0, 232 - 1] 上的一个滑块。

发送窗口在TCP连接发送的过程中不断往高位移动,且在到达最大值后继续从0开始循环/绕回,因此在高速网络中需要在 header 使用防止序号绕回 PAWS (Protect Against Wrapped Sequence numbers) 选项。

在 2.5Gbit/s 网络中,序号在14秒内就会发生重复:4.29GB ÷ (2.5Gb/s ÷ 8) < 14s (序号的单位是Byte)

发送窗口由三个索引值维持:

I0 和 I2 之间为发送窗口,为可以发送的范围。I0 和 I1 之间为已经发送,但未收到确认的部分,I1 和 I2 之间为未发送的部分。当发送方收到ACK帧时,会根据其 ack 值将发送窗口往右移动。

发送窗口的大小会根据流量控制拥塞控制两个方面的反馈进行控制。

RTT和超时重传时间RTO的计算

TCP 发送方自动重传使用重传计数器 (Retransmission Timer) 进行计时,一旦超时则主动发起重传。这段时间被称为RTO (retransmission timeout).

RTO 选值是个复杂的问题,如果选择较小值,会导致轻易触发重传,增大整体网络的负载。进入网络的分组多了,容易引起路由器丢弃分组,导致恶性循环。如果选择较大值,则会导致发送方在接收方需要重传时不能及时响应,使信道处于空闲状态,降低了通信效率/信道使用率。RTO 应略大于 RTT,因为发送方处理 ACK 也要花费时间。最麻烦的是网络环境并不稳定,因此 RTO 必须要有自适应性。

根据 RFC 6298, RTO 使用两个变量 sRTT (s short for smoothed) 和 RTTvar, 以及各自系数 αβ, 通过以下过程进行计算:

  1. 首次测量 RTT 之前,RTO 初始值设为1秒
  2. 首次测量到 RTT 的值记作 R,则设置
    a. sRTT = R
    b. RTTvar = R/2
    c. RTO = sRTT + 4 * RTTvar
  3. 后续测量到的 RTT 值记作 R’
    a. RTTvar = (1 - β) * RTTvar + β * | sRTT - R’ |
    b. sRTT = (1 - α) * sRTT + α * R’
    c. RTO = sRTT + 4 * RTTvar

两个系数的取值分别为 α = 1/8, β = 1/4.

该 RFC 强调了第3步 RTTvar 中使用的 sRTT 值必须是更新前的值,即必须按 3a 到 3b 的顺序(如果先 3b 后 3a,则计算 RTTvar 使用的 sRTT 为更新后的值)。

该计算过程表明 RTO 的取值是对 RTT 的新值和历史值之间的加权求和。由于 α = 1/8, (1 - α) = 7/8, 可看出历史 RTT 占很大权重。而 RTTvar (往返时间偏差值, RTT variation) 则计算了历史 RTT 与当前 RTT 之间的差值并进行权重求和,目的是使 RTO 略大于 sRTT。

TODO ACK 帧会消耗发送方的序号吗?如果重传后的 ACK 能找到对应的重发记录吗?

流量控制

TCP的流量控制机制是指,发送方通过 ACK 帧的窗口字段将自身的接收缓冲区的可用空间大小告知发送方,使发送方调整发送窗口的大小,避免发送过快而接收方由于缓冲区已满而无法接收数据。

如果接收方缓冲区已满,则 ACK 帧的发送窗口值为0,此时发送方将暂停发送操作,并在一段时间后发送至少一字节的报文给接收方用于探测是否可接收后续数据。该行为被称为 Zero-Window Probing (ZWP).

拥塞控制

拥塞控制与流量控制看起来很像,从表现上来看都是对发送方的发送窗口大小的反馈,但是本质上二者完全不同。流量控制是 TCP 接收方发起的,因自身接收缓冲区不足而通知发送方需要放慢甚至暂停发送,此时二者之间的网络是畅通的。而拥塞控制则是通信双方的网络处于拥塞、高负载的状态,此时接收方的接收压力很小甚至没有。

根据 RFC 9293, 虽然流量控制和拥塞控制可以使发送窗口被动地缩减,但强烈反对 TCP peer 主动地缩减自身发送窗口。

慢开始 Slow Start、拥塞避免 Congestion Avoidance

3-ACK

Nagle算法

建立连接与关闭连接

报文格式

参考资料