3.5 TCP

本节的内容十分重要,请务必反复温习确保精通相关的知识。

本节将详细介绍 TCP 连接的性质与原理。TCP 扩展了网络层的 IP 协议。在提供最基础的进程到进程的传输服务与报文段的差错检测服务之外,TCP 实现了两个应用进程之间 面向连接的点到点的可靠的字节流式的管道化的流水线式的 传输服务。

概述

前面已经无数次提到过,TCP 被称作是 面向连接的(connection-oriented)。即两个进程在能够发送数据之前必须先相互“握手”,准备某些预备报文段,以建立确保数据传输的参数。此外,TCP 连接是一条 逻辑连接,其共同状态仅仅保留在两个通信端系统的 TCP 程序中。主机之间的网络元素(如路由器与交换机)无法也不会知道这条连接的存在,它们只负责传输 IP 数据报。连接建立与拆除的详细过程见下面 连接管理

除此之外,TCP 连接总是 点到点的,即每条 TCP 连接只能有一个发送方与一个接收方,无法做到一个发送方向多个发送方发送数据,或是从多个接收方接收数据。

TCP 连接提供的是 全双工服务(Full-duplex Service),即数据可以在两个进程建立的 TCP 连接中双向流动。

应用层的应用通过套接字向下层的 TCP 实体传送数据流,穿过套接字的数据被存放到对应连接的 发送缓存 中。TCP 会不时从发送缓存中取出一块数据配上 TCP 头部后封装为 TCP 报文段并交付给网络层。TCP 单次可以取出数据的字节数受限于 最大报文段长度(Maximum Segment Size,MSS),MSS 一般根据物理网络发送的最大链路层帧长度(即 最大传输单元(Maximum Transmission Unit,MTU))设置,一般要保证 TCP 报文段加上首部长度能够放在一个 MTU 中。以太网的 MTU 为 1500 字节,去除 20 字节 IP 头部与 20 字节 TCP 头部后,MSS 的典型值一般为 1460 字节。

TCP 建立的字节流式通信指的是,TCP 将数据看作一个 无结构但有序 的字节流进行发送,其不会对发送的数据分别属于哪个应用层报文做区分,这需要上面的应用进程自行区分。

TCP 同样实现了 流量控制拥塞控制,会根据接收方进程接收数据的能力与网络上路径的拥塞程度控制向网络中发送报文段的速度。具体分别会在 流量控制3.6 拥塞控制原理3.7 TCP 的拥塞控制 中讲解。

报文段结构

TCP 报文段由 20 字节的首部与长度不超过 MSS 的数据字段构成。TCP 首段的结构如下图所示:

Pasted image 20250514202647.png

TCP 相比 UDP 新增的字段的部分解释如下:

前面提过,TCP 将数据看作一个无结构但有序的字节流。序号字段与确认号字段分别代表了该报文数据部分的 首字节在字节流中的编号 与期望从对方收到的 下一个字节的编号。确认号隐含了 累积确认 的思想,即 TCP 只会确认到第一个未接收到的字节为止的字节。当连接建立时,发送方与接收方 协商并随机选择字节流起始序号,以防止滞留在网络中的来自相同两台主机的一条旧连接中发送的报文被错误接收。

TCP 规范并没有规定当接收方接收到失序报文时会怎么做,不过实践中常采用的做法是接收方保留失序字节一段时间,向发送方发送报文要求中间缺少的字节并等待其到达。

需要注意的是,服务器对来自客户端数据的确认是捎带封装在服务器向客户端发送的报文段中的。

往返时间估计与超时

TCP 采取超时重传机制处理报文段的丢失问题,但如何选择超时时间是一个重要的问题。时间过短会引发大量不必要的重发;时间过长会导致接收方对真正发生丢失的分组做出反应时会比较迟钝。显然超时间隔应当大于连接的 RTT,但在当今互联网的环境下 RTT 并非一成不变,而会不断波动。因此有必要根据当前链路的环境自适应的计算出超时时间。其具体机制如下:

TCP 实体会选定一些用于测量往返延迟的报文段,测量该报文段的 RTT,记为 tsample。需要注意的是 TCP 实体在某一时刻只会选择一个用于测量的报文段(这是因为 TCP 是一种流水线协议,同一时刻可能会发送多个报文段);此外,如果该报文段触发了重传,则该次测量会被忽略。

SampleRTT 的会随当时的网络状况出现较大幅度的波动,我们对其做 指数加权移动平均(Exponential Weighted Moving Average,EWMA)。具体而言,设 t¯ 为先前计算出的 SampleRTT 均值 EstimatedRTT,最新一次 SampleRTT 测量的结果为 tsample,则新的 EstimatedRTT 值为

t¯=(1α)t¯+αtsample

将该递推式展开,容易得到在已经测量了 n 个 SampleRTT 时,EstimatedRTT 为

t¯=i=1nα(1α)nitsample

可以发现,旧的 SampleRTT 在整个 EstimatedRTT 中的权重会呈指数级快速衰减,而最近的 SampleRTT 所占权重更高。RFC 6298 给出的 α 推荐值为 α=0.125

不同的网络给 SampleRTT 造成的波动幅度不同,若网络连接稳定 SampleRTT 波动很小,则最终的超时时间也不应有大幅的变化。我们使用当次 SampleRTT 与 EstimatedRTT 的绝对偏差来衡量 RTT 的波动,DevRTT 同样使用 EWMA 来计算,即:

d=(1β)d+β|tsamplet¯|

RFC 6298 给出的 β 推荐值为 β=0.25

综合 EstimatedRTT 与 SampleRTT,超时定时器的设定时间为:

ttimeout=t¯+4d

超时定时器的初始默认时间一般为 1s。

一旦某个报文发送超时,超时定时器的设定时间将加倍,以免后续报文段过早出现超时。然而一旦收到了对某个报文段的确认或接收到了来自上层应用的新数据,加倍机制就会取消,超时计时器设定时间重新按照上面的方式计算得出。

可靠数据传输机制

前面也无数次提到过,TCP 在不可靠的网络层 IP 服务上实现了可靠的数据传递。IP 的服务是 尽力而为 的,其不保证数据的成功交付、按序交付或是完整交付。而 TCP 在此之上保证了数据以无损坏、无间隙、无冗余、按序的字节流方式传送。

TCP 的可靠数据传输机制是 3.4 可靠数据传输的原理 里介绍的 GBNSR 的混合体。具体如下:

由于每当超时定时器触发超时时都会导致超时定时加倍,这可能会导致当某一个段连续超时时,接下来发送的数据所设置的超时周期过长,增加了对重发丢失报文的响应延迟。因此 TCP 通过利用 冗余 ACK,即发送方已经收到的确认来触发 快速重传 机制。具体而言,如果接收方收到了对某个数据的 3 个冗余 ACK(注意需要加上第一个正常确认的 ACK,一共是 4 个 ACK),则立刻重发冗余 ACK 指示的丢失报文段。

此外,接收方并不是在接收到一个报文段后立刻发送对该报文段的确认。可以推断出,在发送方一次发送大量报文段的情况下,这种做法会增加冗余 ACK 的数量。RFC 5681 对发送方生成 ACK 的策略建议如下:

Pasted image 20250514202740.png

流量控制

前面的讨论都集中于应对网络中有可能发生的各种问题。然而,端系统本身也有一些需要注意的问题。有时发送方会快速发送大量数据,尽管网络畅通,接收方可以快速收到这些报文段,但接收方上层的应用程序可能无法在短时间内读取并处理这些数据,这会导致接收方的接收缓存溢出造成数据丢失。针对这种情况,TCP 引入了 流量控制服务(Flow-control Service) 用于抑制发送方发送数据的速率,使得发送方的发送速率与接收方应用的读取速率相匹配。

与拥塞控制的区分

流量控制与拥塞控制虽然结果都是抑制发送方发送数据的速率,但针对的情况完全不同,绝不能混为一谈:

  • 流量控制用于处理 接收方应用程序 读取速度慢导致接收缓存溢出的问题
  • 拥塞控制用于处理 IP 网络的拥塞 导致数据无法正常传输的问题

发送方维护如下两个变量:

接收方维护如下两个变量:

显然,LastByteSent 减去 LastByteAcked 即为仍未被确认的数据量,记接收方接收缓存的大小为 RcvBuffer,则接收缓存的空闲空间为 RcvBuffer - (LastByteRcvd - LastByteRead),记为 接收窗口 rwnd,被接收方捎带发送给发送方。发送方需要保证未被确认的数据量总不大于接收缓存的空余空间,这保证了在最坏情况下(所有未被确认的数据均暂未被发送方接收)发送缓存也不会溢出。

然而,当 rwnd 为 0,即接收缓存已满时,发送方仍然会持续发送不含任何有效数据的报文段,以确认接收缓存何时产生了空闲。

TCP 的流量控制与拥塞控制存在联动。一般而言,发送方发送的未确认的字节数为接收窗口与拥塞窗口二者的小值。

连接管理

TCP 是面向连接的。两个应用程序在发送数据之前需要先发送一些特殊的报文段协商建立连接相关的各种参数,这一个过程被称作 三次握手,指一条 TCP 连接的建立需要至少三个报文段的往来,具体如下:

三次握手避免了两次握手可能造成的只有服务器维护半连接与老的连接的数据对新连接造成影响的问题。

虽然 TCP 连接允许数据双向传输,但是当通信完成需要拆除连接时,一次只会拆除一个方向的连接,如下:

然而,由于两军问题的存在。TCP 这一拆除连接的机制本质上是不可靠的。

当然,有时一台主机会收到与某个没有应用程序监听的端口建立连接的请求,此时该主机将发送一个 RST 标志位置位的报文段指示发送方没有报文段对应的套接字。

在 UDP 中遇到类似情况时,会发送一个特殊的 ICMP 数据报,见 5.6 ICMP