skaiuijing

引言

要想理解计算机底层架构,最重要的是就是理解相关文档和手册。

这些文档和手册永远是最权威、最可靠、最值得学习的资料。

TCP协议是网络协议栈中最重要、最复杂的部分,可以这么说,实现一个网络协议栈,TCP的难度占了百分之40以上,而相关代码一般都会占该协议栈的一半篇幅。

现在让我们阅读文档:RFC9293和RFC793

重点是RFC9293,因为RFC793其实是非常老的文档了,但是笔者使用的4.4FreeBSD代码也非常老。

参考链接:RFC 9293:传输控制协议 (TCP)

本文概述

互联网上已经有许多直接翻译的文章了,笔者的本篇文章主要是摘自其中的重点部分并进行润色,另外,最重要的一点是:本系列TCP协议的核心在于实现,也就是说,笔者关心的是RFC文档中TCP协议栈的实现指导。

本文会参考4.4FreeBSD内核(也就是TCP/IP三卷)与rfc文档,尽可能描述对应的伪代码。

这些伪代码是TCP协议栈实现的重要参考。

先看看文档目录:

RFC9293

参考文档,让我们先简单理解TCP模型。

头部结构

3.1 标头格式

TCP 段作为互联网数据报发送。IP 首部包含源地址和目标地址等信息,TCP 首部紧随其后,提供 TCP 专用的控制信息。这种分层设计使得在主机层可以存在不同的传输协议,而不仅仅是 TCP。在互联网协议套件早期,IP 首部中的部分字段曾经是 TCP 的一部分。

本文档描述了 TCP 使用的首部格式。TCP 首部之后是用户数据,其整体结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
 0                   1                   2                   3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |C|E|U|A|P|R|S|F| |
| Offset| Rsrvd |W|C|R|C|S|S|Y|I| Window |
| | |R|E|G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| [Options] |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :
: Data :
: |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

Note that one tick mark represents one bit position.

注:图中每个刻度代表 1 位。

字段说明

  • 源端口(16 位):发送端口号。
  • 目标端口(16 位):接收端口号。
  • 序列号(32 位):本段第一个数据字节的序列号;若设置了 SYN,则为初始序列号 ISN,第一个数据字节为 ISN+1。
  • 确认号(32 位):若 ACK 标志置位,则表示期望接收的下一个序列号。连接建立后该字段始终有效。
  • 数据偏移量(4 位):TCP 首部长度,以 32 位字为单位,指示数据部分的起始位置。
  • 保留位(4 位):保留供将来使用,必须置零。
  • 控制位(Flags,1 位各占):
    • CWR:拥塞窗口减少
    • ECE:ECN-Echo
    • URG:紧急指针有效
    • ACK:确认号有效
    • PSH:Push 功能
    • RST:复位连接
    • SYN:同步序列号
    • FIN:发送端无更多数据
  • 窗口大小(16 位):发送方愿意接收的数据量。支持窗口缩放时需结合扩展计算。
  • 校验和(16 位):对首部、数据及伪首部进行校验,确保完整性。TCP 校验和必须生成并验证。
  • 紧急指针(16 位):指向紧急数据后第一个字节的序列号,仅在 URG 标志置位时有效。
  • 选项(可变长):用于扩展功能,长度为 8 位的倍数,必要时填充对齐。常见选项包括:
    • 0:选项列表结束(EOL)
    • 1:无操作(NOP)
    • 2:最大报文段长度(MSS,4 字节)
  • 数据(可变长):用户数据。

伪首部(Pseudo Header)

在计算校验和时,TCP 还需包含一个“伪首部”,以防止报文被错误路由。

IPv4 伪首部(96 位):

1
2
3
4
5
6
7
+--------+--------+--------+--------+
| 源地址(32 位) |
+--------+--------+--------+--------+
| 目标地址(32 位) |
+--------+--------+--------+--------+
| 保留 | 协议号 | TCP 长度 |
+--------+--------+--------+--------+
  • 源地址:IPv4 源地址
  • 目标地址:IPv4 目标地址
  • 保留:置零
  • 协议号:IP 首部中的协议字段
  • TCP 长度:TCP 首部 + 数据的总长度(不含伪首部本身)

IPv6 伪首部(320 位): 包含源地址、目标地址、上层数据长度、下一个首部值等,定义见 RFC 8200。

(FreeBSD的校验和计算是通过原地重组头部结构,这个结构就是伪首部。但Linux则是重新构造)

连接

TCP是面向连接的协议,可能有很多人疑问,为什么三次握手和四次挥手呢?

其实很简单,就算确保双方都能互相确认:

A->B,B知道了A可以向他发送消息,但是A还不知道这一点。

B->A,A知道B可以向他发送消息,但是B还不知道。

A->B,B知道A知道B可以向他发送消息。

好了,现在双方都知道了可以互相发送消息了。

这就是三次握手。

至于四次挥手,

A->B:B知道A要关闭了,但目前A还不知道B知道A要关闭,所以B要发消息给A。

B->A:A知道B知道他要关闭了,所以A可以直接关闭了。但是B还不知道A知不知道,所以A最后还要发消息给B

因为四次挥手本质上也是与握手一样,都是双方的确认,所以在Linux等操作系统中,B->A的两次操作往往被合并为一次。

也就是说,其实握手和挥手都可以是三次。

三次是双方互相确认的最小次数。

最后A->B:现在双方都确认了,可以安心关闭了。

3.3.1 关键连接状态变量

在深入讨论 TCP 的实现细节之前,需要先介绍一些核心术语。
TCP 为了维护连接状态,会在 传输控制块(Transmission Control Block, TCB) 中保存一系列变量。

TCB 中通常包含以下信息:

  • 本地和远程的 IP 地址与端口号
  • IP 安全级别和连接范围(参见附录 A.1)
  • 指向用户发送缓冲区和接收缓冲区的指针
  • 指向重传队列和当前处理段的指针
  • 与发送和接收序列号相关的多个变量

表 2:发送序列相关变量

变量名 描述
SND.UNA 已发送但尚未确认的最小序列号(Send Unacknowledged)
SND.NXT 下一个将要发送的序列号(Send Next)
SND.WND 发送窗口大小(Send Window)
SND.UP 发送紧急指针(Send Urgent Pointer)
SND.WL1 上次窗口更新时使用的段序列号
SND.WL2 上次窗口更新时使用的段确认号
ISS 初始发送序列号(Initial Send Sequence Number)

表 3:接收序列相关变量

变量名 描述
RCV.NXT 下一个期望接收的序列号(Receive Next)
RCV.WND 接收窗口大小(Receive Window)
RCV.UP 接收紧急指针(Receive Urgent Pointer)
IRS 初始接收序列号(Initial Receive Sequence Number)

图 3:发送序列空间

1
2
3
     1         2          3          4
----------|----------|----------|----------
SND.UNA SND.NXT SND.UNA+SND.WND
  • 1:已被确认的旧序列号
  • 2:已发送但未确认的数据序列号
  • 3:允许发送的新数据序列号(发送窗口)
  • 4:尚未允许使用的未来序列号

发送窗口即图中标记为 3 的部分。

图 4:接收序列空间

1
2
3
    1          2          3
----------|----------|----------
RCV.NXT RCV.NXT+RCV.WND
  • 1:已确认的旧序列号
  • 2:允许接收的新数据序列号(接收窗口)
  • 3:尚未允许接收的未来序列号

接收窗口即图中标记为 2 的部分。

表 4:当前段相关变量

变量名 描述
SEG.SEQ 段的序列号
SEG.ACK 段的确认号
SEG.LEN 段的长度
SEG.WND 段的窗口大小
SEG.UP 段的紧急指针

TCP状态机

TCP一共有十一种状态,一个连接在其生命周期中会经历一系列的状态。
这些状态包括:LISTEN、SYN-SENT、SYN-RECEIVED、ESTABLISHED、FIN-WAIT-1、FIN-WAIT-2、CLOSE-WAIT、CLOSING、LAST-ACK、TIME-WAIT,以及一个虚构的状态 CLOSED。
其中 CLOSED 是虚构的,因为它表示没有 TCB(传输控制块)的情况,也就是没有连接存在。

让我们看看每个状态要做的事情:

状态

LISTEN:表示等待来自任意远程 TCP 对等端口的连接请求。
SYN-SENT:表示在发送连接请求后,等待一个匹配的连接请求。
SYN-RECEIVED:表示在收到并发送连接请求后,等待对方对连接请求的确认。
ESTABLISHED:表示连接已建立,接收到的数据可以交付给用户,这是连接数据传输阶段的正常状态。
FIN-WAIT-1:表示等待远程 TCP 发来的连接终止请求,或等待之前发送的终止请求的确认。
FIN-WAIT-2:表示等待远程 TCP 发来的连接终止请求。
CLOSE-WAIT:表示等待本地用户发出连接终止请求。
CLOSING:表示等待远程 TCP 对终止请求的确认。
LAST-ACK:表示等待对先前发送的终止请求的确认(该请求中已包含对远程 TCP 终止请求的确认)。
TIME-WAIT:表示等待足够的时间,以确保远程 TCP 收到其终止请求的确认,并避免先前连接的延迟报文影响新的连接。
CLOSED:表示没有任何连接。
TCP 连接会根据不同事件从一个状态转换到另一个状态。
这些事件包括:用户调用(OPEN、SEND、RECEIVE、CLOSE、ABORT、STATUS)、收到的 TCP 报文段(尤其是带有 SYN 和 FIN 标志的报文段)、以及超时。
其中,OPEN 调用可以指定是主动建立连接,还是被动等待连接。
• 被动 OPEN 表示进程希望接受传入的连接请求。
• 主动 OPEN 表示进程尝试主动发起连接。

握手与挥手

让我们从三次握手和四次挥手开始:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
         主动开启者               被动开启者
LISTEN
被动打开
SYN_SENT ------SYN K-----------> SYN_RCVD
/
ESTABLISHED <---SYN L, ACK K + 1--
----ACK K + 1--------> ESTABLISHED
数据传输发生在ESTABLISHED状态
FIN_WAIT_1 -------FIN M------> CLOSED_WAIT
其实Linux内核中真正的挥手要发送FIN + ACK才能触发,最后的数据确认和结束信号进行了合并
/
FIN_WAIT_2 <-----ACK M + 1---------
LAST_ACK 其实这两次操作往往也都会被合并为一次
/
TIME_WAIT <------FIN N---------
| -------ACK N +1 --------> \
|2MSL计时器 CLOSED
CLOSED

我们经常说三次握手和四次挥手,其实不一定都会遵循该流程,现代操作系统中增加了非常多的机制,比如快速打开这些。

状态机:

每一个主机在开始都有两种选择:发送或者接收,顺着脉络解读即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
                            +---------+ ---------\      active OPEN
| CLOSED | \ -----------
+---------+<---------\ \ create TCB
| ^ \ \ snd SYN
passive OPEN | | CLOSE \ \
------------ | | ---------- \ \
create TCB | | delete TCB \ \
V | \ \
rcv RST (note 1) +---------+ CLOSE | \
-------------------->| LISTEN | ---------- | |
/ +---------+ delete TCB | |
/ rcv SYN | | SEND | |
/ ----------- | | ------- | V
+--------+ snd SYN,ACK / \ snd SYN +--------+
| |<----------------- ------------------>| |
| SYN | rcv SYN | SYN |
| RCVD |<-----------------------------------------------| SENT |
| | snd SYN,ACK | |
| |------------------ -------------------| |
+--------+ rcv ACK of SYN \ / rcv SYN,ACK +--------+
| -------------- | | -----------
| x | | snd ACK
| V V
| CLOSE +---------+
| ------- | ESTAB |
| snd FIN +---------+
| CLOSE | | rcv FIN
V ------- | | -------
+---------+ snd FIN / \ snd ACK +---------+
| FIN |<---------------- ------------------>| CLOSE |
| WAIT-1 |------------------ | WAIT |
+---------+ rcv FIN \ +---------+
| rcv ACK of FIN ------- | CLOSE |
| -------------- snd ACK | ------- |
V x V snd FIN V
+---------+ +---------+ +---------+
|FINWAIT-2| | CLOSING | | LAST-ACK|
+---------+ +---------+ +---------+
| rcv ACK of FIN | rcv ACK of FIN |
| rcv FIN -------------- | Timeout=2MSL -------------- |
| ------- x V ------------ x V
\ snd ACK +---------+delete TCB +---------+
-------------------->|TIME-WAIT|------------------->| CLOSED |
+---------+ +---------+

序列号与数据传输

把网络数据包看成一个大数组,现在双方都要通信,我们要怎么确保对方都接收到了呢?

可以把这个大数组按照下标切割,比如:

0:A数据 1:B数据 2:C数据

现在,主机sb 和另一台主机daSB进行通信。

sb:发送A数据

daSB接收到A数据后,发送一个确认号,也就是A数据的下标0,表示A数据已经接收到了。

sb看到后,这样就知道daSB拿到A数据了,于是发送下标可以更新到B数据这里了。

下标本质上是给每个数据分配了独一无二的标识,接收方返回标识,也就是意味着数据包被接收。

这就是序列号的思想。

3.4 序列号

基本概念

  • TCP 连接中传输的 每个字节 都会分配一个 序列号
  • 序列号用于保证数据的有序性和可靠交付。
  • TCP 使用 累积确认:当确认号为 X 时,表示 所有序列号 ≤ X 的字节 都已成功接收。
  • 序列号空间范围为 0 ~ 2³² - 1,采用 模 2³² 算术,即序列号会循环回绕。

序列号比较的典型场景

TCP 实现中常见的序列号比较包括:

  1. 判断一个确认号是否引用了已发送但未确认的数据。
  2. 判断某个段的数据是否已被确认(例如是否可从重传队列移除)。
  3. 判断传入段的序列号是否落在接收窗口内。

发送端相关变量

  • SND.UNA:最早的未确认序列号
  • SND.NXT:下一个要发送的序列号
  • SEG.ACK:对端返回的确认号
  • SEG.SEQ:段的第一个序列号
  • SEG.LEN:段中数据字节数(包括 SYN/FIN 占用的 1 个序列号)
  • SEG.SEQ + SEG.LEN - 1:段的最后一个序列号

可接受的确认条件:
SND.UNA < SEG.ACK =< SND.NXT

接收端相关变量

  • RCV.NXT:期望接收的下一个序列号(接收窗口左边界)
  • RCV.NXT + RCV.WND - 1:接收窗口右边界
  • SEG.SEQ:段的第一个序列号
  • SEG.SEQ + SEG.LEN - 1:段的最后一个序列号

可接受的条件:

  • 段起点在窗口内,或
  • 段终点在窗口内

特殊情况:零窗口与零长度段

当接收窗口为 0 时,只有 ACK 段 可以被接受。
TCP 规范定义了四种情况(表 5):

段长度 接收窗口 可接受性条件
0 0 SEG.SEQ = RCV.NXT
0 >0 RCV.NXT ≤ SEG.SEQ < RCV.NXT+RCV.WND
>0 0 不可接受
>0 >0 段起点或终点落在窗口内

控制位与序列号

  • SYNFIN 也会占用一个序列号。
  • SYN 被认为发生在段中第一个数据字节之前。
  • FIN 被认为发生在段中最后一个数据字节之后。
  • 因此,SEG.LEN 包括数据字节数 + SYN/FIN 标志。

3.4.1 初始序列号(ISN)的选择

  • 每个连接的实例(化身)必须有唯一的初始序列号,以避免旧段被误认为新段。
  • ISN 由一个 单调递增的 32 位计数器生成,通常每 4 微秒加 1,约 4.55 小时循环一次,远大于 MSL(最大报文段生存期)。
  • 为防止攻击者预测 ISN,ISN = **计数器值 M + 伪随机函数 F(…)**,其中 F 结合了本地/远程 IP、端口和一个秘密密钥。

3.4.2 静默时间(Quiet Time)

  • 如果主机重启并丢失了序列号历史,可能会重用旧的序列号,导致混淆。
  • 为避免这种情况,TCP 规范要求在重启后 至少等待一个 MSL(2 分钟) 才能发送新段。
  • 如果实现能保留序列号历史,则无需等待。

3.4.3 高速网络与 PAWS

  • 在高速链路(如 1Gbps、10Gbps、100Gbps)下,序列号空间可能在几秒甚至几百毫秒内循环。
  • 为避免旧段混淆,TCP 引入了 时间戳选项PAWS(Protect Against Wrapped Sequence numbers)机制,用于检测并丢弃过期段。

额外讲一句

我们可以看见,序列号是递增的,那么会不会溢出呢?

答案是肯定的。上面的3.4.3就说明了这一点,那么怎么解决这个问题呢?

4.4FreeBSD系统使用的是比较相对大小,而不是绝对大小。这与Linux内核的调度算法动态优先级比较的思想是一样的。

3.5 建立连接

TCP 使用 三次握手(Three-Way Handshake, 3WHS) 来建立连接。通常情况下,该过程由一方主动发起,另一方被动响应。如果双方同时发起连接请求,就会出现 同时打开(Simultaneous Open) 的情况。在这种情况下,每个 TCP 对等体都会收到一个不带确认的 SYN 段。若接收端收到旧的重复 SYN 段,也可能被误认为是同时打开。此时,正确使用 RST(重置)段 可以消除歧义。

虽然以下示例未展示带数据的 SYN 段,但这是允许的。只要接收端在连接进入 ESTABLISHED 状态之前不将数据交付给应用层即可(通常会先缓存数据)。三次握手的主要作用是减少错误连接的可能性。

基本三次握手流程

图 6 展示了最简单的三次握手过程:

1
2
3
4
5
6
7
8
9
10
11
    TCP Peer A                                           TCP Peer B

1. CLOSED LISTEN

2. SYN-SENT --> <SEQ=100><CTL=SYN> --> SYN-RECEIVED

3. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED

4. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED

5. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> ESTABLISHED
  • 第 2 行:A 发送 SYN,序列号为 100。
  • 第 3 行:B 回复 SYN+ACK,确认号为 101,表示已收到 A 的 SYN。
  • 第 4 行:A 回复 ACK,确认 B 的 SYN。
  • 第 5 行:A 开始发送数据。注意 ACK 本身不占用序列号空间。

同时打开

当双方几乎同时发起连接时,会出现 同时打开,如图 7:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    TCP Peer A                                       TCP Peer B

1. CLOSED CLOSED

2. SYN-SENT --> <SEQ=100><CTL=SYN> ...

3. SYN-RECEIVED <-- <SEQ=300><CTL=SYN> <-- SYN-SENT

4. ... <SEQ=100><CTL=SYN> --> SYN-RECEIVED

5. SYN-RECEIVED --> <SEQ=100><ACK=301><CTL=SYN,ACK> ...

6. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED

7. ... <SEQ=100><ACK=301><CTL=SYN,ACK> --> ESTABLISHED
  • 双方状态依次经历 CLOSED → SYN-SENT → SYN-RECEIVED → ESTABLISHED
  • TCP 实现必须支持同时打开(MUST-10)。
  • 实现还必须区分连接是由 被动 OPEN 还是 主动 OPEN 发起的(MUST-11)。

防止旧 SYN 干扰

三次握手的另一个重要作用是防止旧的重复 SYN 段引起混淆。
如果接收端收到旧的 SYN,它会正常响应,但发送端会发现 ACK 不正确,从而发送 RST 来重置连接。

图 8 展示了从旧 SYN 中恢复的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    TCP Peer A                                           TCP Peer B

1. CLOSED LISTEN

2. SYN-SENT --> <SEQ=100><CTL=SYN> ...

3. (duplicate) ... <SEQ=90><CTL=SYN> --> SYN-RECEIVED

4. SYN-SENT <-- <SEQ=300><ACK=91><CTL=SYN,ACK> <-- SYN-RECEIVED

5. SYN-SENT --> <SEQ=91><CTL=RST> --> LISTEN

6. ... <SEQ=100><CTL=SYN> --> SYN-RECEIVED

7. ESTABLISHED <-- <SEQ=400><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED

8. ESTABLISHED --> <SEQ=101><ACK=401><CTL=ACK> --> ESTABLISHED

3.5.1 半开连接与异常情况

半开连接(Half-Open Connection) 指一方认为连接仍然存在,而另一方已关闭或丢失状态(如重启)。

  • 如果一方尝试发送数据,另一方会返回 RST,从而中止连接。
  • 半开连接通常较少见,但必须被正确处理。

图 9–11 展示了几种典型的半开场景:

  • 图 9:一方重启后重新发起连接,另一方仍认为连接存在。
  • 图 10:重启方收到旧连接的数据,因无效而返回 RST。
  • 图 11:两个被动监听的端口因旧 SYN 干扰而触发复位。

3.5.2 重置(RST)的生成

RST 可由应用主动发出,也可由协议在异常情况下自动生成。

  • CLOSED 状态:收到任何非 RST 段 → 回复 RST。
  • 非同步状态(LISTEN、SYN-SENT、SYN-RECEIVED):收到不可接受的 ACK 或安全级别不匹配 → 回复 RST。
  • 同步状态(ESTABLISHED、FIN-WAIT、CLOSE-WAIT 等):收到不可接受的段 → 回复一个空 ACK;若安全级别不符 → 回复 RST 并进入 CLOSED。

3.5.3 重置的处理

  • SYN-SENT 外,RST 段必须通过检查其序列号是否在窗口内来验证。
  • SYN-SENT 状态下,若 RST 的 ACK 字段确认了初始 SYN,则接受该 RST。
  • 处理规则:
    • LISTEN 状态:忽略 RST。
    • SYN-RECEIVED 状态:若之前来自 LISTEN,则回到 LISTEN,否则进入 CLOSED。
    • 其他状态:中止连接,进入 CLOSED,并通知用户。

TCP 实现应允许 RST 段携带诊断数据(SHLD-2),但目前尚无标准格式。

3.6 关闭连接

CLOSE 操作表示“我没有更多数据要发送”。
TCP 的关闭涉及全双工连接,因此存在一定的歧义:关闭的一方是否还能继续接收数据?在 TCP 的设计中,关闭仅表示发送方向的终止,接收方向仍然可以继续,直到远端也关闭为止。

因此,一个应用程序可以先执行多次 SEND,然后调用 CLOSE,但仍然继续 RECEIVE,直到收到远端关闭的信号。TCP 实现会在远端关闭时通知用户,即使没有挂起的 RECEIVE 调用。这样,应用可以优雅地终止连接。

TCP 保证在关闭前,所有已发送的数据都会可靠交付。因此,应用在调用 CLOSE 后,只需等待确认连接关闭,就能确保数据已送达对端。用户必须继续读取,直到 TCP 明确告知“没有更多数据”。

三种典型关闭情况

  1. 本地用户发起关闭
    • 用户调用 CLOSE,TCP 构造一个 FIN 段并进入 FIN-WAIT-1 状态。
    • 在此状态下仍可接收数据。FIN 及之前的数据会被重传,直到收到确认。
    • 当对端确认 FIN 并发送自己的 FIN,本端再发送 ACK,进入 TIME-WAIT
  2. 远端发起关闭
    • 本端收到未经请求的 FIN → 回复 ACK,并通知用户“连接正在关闭”。
    • 用户调用 CLOSE 后,本端发送剩余数据并发出 FIN。
    • 等待对端确认 FIN 后,连接删除。若超时未确认,则中止连接。
  3. 双方同时关闭
    • 双方几乎同时发送 FIN,进入 CLOSING 状态。
    • 双方交换 ACK 后,进入 TIME-WAIT,最终 CLOSED。

正常关闭序列(单方主动关闭)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    TCP Peer A                                           TCP Peer B

1. ESTABLISHED ESTABLISHED

2. (Close)
FIN-WAIT-1 --> <SEQ=100><ACK=300><CTL=FIN,ACK> --> CLOSE-WAIT

3. FIN-WAIT-2 <-- <SEQ=300><ACK=101><CTL=ACK> <-- CLOSE-WAIT

4. (Close)
TIME-WAIT <-- <SEQ=300><ACK=101><CTL=FIN,ACK> <-- LAST-ACK

5. TIME-WAIT --> <SEQ=101><ACK=301><CTL=ACK> --> CLOSED

6. (2 MSL)
CLOSED

同时关闭序列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    TCP Peer A                                           TCP Peer B

1. ESTABLISHED ESTABLISHED

2. (Close) (Close)
FIN-WAIT-1 --> <SEQ=100><ACK=300><CTL=FIN,ACK> ... FIN-WAIT-1
<-- <SEQ=300><ACK=100><CTL=FIN,ACK> <--
... <SEQ=100><ACK=300><CTL=FIN,ACK> -->

3. CLOSING --> <SEQ=101><ACK=301><CTL=ACK> ... CLOSING
<-- <SEQ=301><ACK=101><CTL=ACK> <--
... <SEQ=101><ACK=301><CTL=ACK> -->

4. TIME-WAIT TIME-WAIT
(2 MSL) (2 MSL)
CLOSED CLOSED

关闭方式

TCP 连接可以通过两种方式终止:

  1. 正常关闭:通过 FIN 握手完成(如图 12、13)。
  2. 中止关闭:通过发送一个或多个 RST 段立即终止,连接状态直接丢弃。

应用必须被告知关闭是 正常结束 还是 异常中止(MUST-12)。

3.6.1 半关闭连接

TCP 的关闭是 单向独立 的,因此可能出现 半关闭(Half-Close)

  • 一方关闭发送方向(发出 FIN),但仍能接收数据。
  • 另一方仍可继续发送,直到也关闭。

一些实现可能选择 半双工关闭:调用 CLOSE 后,应用不能再接收数据(MAY-1)。

  • 如果此时仍有未读数据,或在 CLOSE 后收到新数据,TCP 应发送 RST 表示数据丢失(SHLD-3)。

TIME-WAIT 状态

  • 主动关闭的一方必须进入 TIME-WAIT,并保持 2×MSL(最大报文段寿命)(MUST-13)。
  • 在此期间,若收到新的 SYN,可能允许直接建立新连接(MAY-2),前提是:
    1. 新连接的序列号大于上一个连接使用的最大序列号;
    2. SYN 不是旧的重复报文。

现代实现通常启用 TCP 时间戳选项,结合 PAWS(防止序列号回绕保护),以支持更高的连接建立速率,并缩短 TIME-WAIT 对繁忙服务器的影响(SHLD-4)。

3.7 分段(Segmentation)

分段是指 TCP 将应用层传来的字节流切分为一个个 TCP 段的过程。
需要注意的是:

  • 单个 TCP 段 不一定 对应应用层的一次发送调用(如一次 write())。
  • TCP 不保证应用层写入的边界与 TCP 段的边界一致。
  • 在某些特殊协议中(如 RDMA 使用的 DDP/MPA),TCP 段与应用数据单元之间的对应关系可以被控制,用于性能优化。

通常,TCP 在分段时需要在 大段小段 之间权衡:

发送大段的目标

  • 减少网络中传输的数据包数量
  • 降低中断和协议栈层间交互,提高效率
  • 降低 TCP 首部开销

但大段并非总是更优。例如,在某些实现中,1025 字节的段可能比 1024 字节更慢,仅仅因为数据对齐问题。

发送小段的目标

  • 避免超过路径最小 MTU,防止分片或丢包
  • 降低应用等待延迟(避免 TCP 为凑够大段而延迟发送)
  • 在某些链路层(如小帧链路)上实现“命运共享”

为平衡这些目标,TCP 提供了多种机制:

  • 最大段大小(MSS)选项
  • 路径 MTU 发现(PMTUD/PLPMTUD)
  • Nagle 算法
  • IPv6 巨型报文支持

3.7.1 最大段大小(MSS)选项

  • TCP 必须支持 MSS 选项的发送与接收(MUST-14)。
  • 建立连接时,若未收到 MSS 选项:
    • IPv4 默认 MSS = 536(576 - 40)
    • IPv6 默认 MSS = 1220(1280 - 60)
  • 实际发送段的最大大小(有效 MSS):

Eff.snd.MSS = min(SendMSS+20, MMS_S) - TCPhdrsize - IP选项大小

其中:

  • SendMSS:对端通告的 MSS(若无则取默认值)
  • MMS_S:本端可发送的最大消息大小
  • TCP_hdr:TCP 首部长度(含选项)
  • IP_opt:IP 选项或扩展首部长度

RFC 6691 对 MSS 的计算和选项影响有更详细说明。

3.7.2 路径 MTU 发现(PMTUD/PLPMTUD)

  • TCP 通常只知道直连链路的 MTU,而非整个路径的 MTU。
  • IPv4 默认最小 MTU = 576,IPv6 = 1280。
  • 固定值会限制性能,因此推荐使用:
    • PMTUD:依赖 DF 标志和 ICMP 错误消息
    • PLPMTUD:改进版,减少对 ICMP 的依赖,提升健壮性
  • MSS 选项的值会影响 PMTUD 的结果。

3.7.3 可变 MTU 接口

  • 某些链路(如 ROHC 压缩)导致有效 MTU 动态变化。
  • TCP 应该基于 最小有效 MTU 来计算并通告 MSS(SHLD-6),避免重传干扰压缩同步。

3.7.4 Nagle 算法

  • 由 RFC 896 提出,用于减少小包过多的问题。
  • 规则:若存在未确认数据,则缓冲新数据,直到:
    1. 收到 ACK,或
    2. 可以发送一个满 MSS 的段。
  • TCP 应实现 Nagle 算法(SHLD-7),但必须允许应用禁用它(MUST-17)。
  • 注意:Nagle 与延迟确认可能交互不佳,因此部分实现采用变体。

笔者简单补充一下Nagle 算法

以ssh连接为例,每一次敲击键盘时,如果都会发送一次数据传输,那么会造成相当高的网络传输代价。

Nagle算法的要求:当一个TCP连接中有在传数据(已经发送但还没用确认的数据),那么这些小的报文段(长度小于SMSS)就不能被发送,直到所有的在传数据都收到ACK。并且,在收到ACK后,TCP需要收集这些小数据,将其整合到一个报文段中发送。

这种方法迫使TCP协议遵循停等规则。

该算法的精妙之处在于实现了自时钟控制:ACK返回得越快,数据传输得也越快,也就是说:RTT(往返时间)控制着发包速率。

3.7.5 IPv6 巨型报文(Jumbograms)

  • IPv6 支持超过 64KB 的报文(巨型报文)。
  • RFC 2675 规定:MSS = 65,535 表示“无限大”,实际大小由 PMTUD 决定。
  • 但目前 IPv6 节点通常不要求支持巨型报文。

3.8 数据通信

在连接建立后,数据通过 TCP 段 进行交换。由于网络错误(如校验和失败)或拥塞,段可能丢失;TCP 通过 重传机制 确保可靠交付。由于重传或网络原因,接收端也可能收到重复段。TCP 会根据 序列号和确认号 来验证段的有效性(见第 3.4 节)。

发送端维护以下关键变量:

  • SND.NXT:下一个要发送的序列号
  • SND.UNA:最早未确认的序列号

接收端维护:

  • RCV.NXT:期望接收的下一个序列号

当数据流空闲且所有数据已确认时,三者相等。

  • 发送段时:SND.NXT 前进
  • 接收段时:RCV.NXT 前进并发送 ACK
  • 收到 ACK 时:SND.UNA 前进

这些变量之间的差异反映了通信延迟。
一旦进入 ESTABLISHED 状态,所有段必须携带当前的确认信息。

调用 CLOSE 相当于执行 Push 操作,并伴随发送 FIN 标志。

3.8.1 重传超时(RTO)

这部分是重点内容:

RTT

RTT是数据包的往返时间,但是使用单独一个数据包作为整体数据包的往返时间有太大的偶然性,正确的做法是将一组数据包的估计值作为真实的RTT值,也就是:

RTT = a * sRTT + (1 - a)RTT,

即:真实RTT = 敏感因子 * 历史RTT + (1 - 敏感因子) * 本次RTT。

通常,敏感因子是7/8,当然,我们也可以通过各种技术修改敏感因子,甚至是,可以通过eBPF等技术在Linux内核中自定义RTT估计算法。

RTO

RTO是在RTT基础上设置的最大往返时间限度,这是验证一个数据包是否超时的依据。

  • RTO 必须动态计算(MUST-18),采用 Karn 算法 等方法。

  • 算法演进:RFC 793 → RFC 1122 → RFC 2988 → RFC 6298。

  • RFC 1122 允许在重传时复用相同的 IPv4 Identification 字段(MAY-4),但 TCP 不应依赖该字段来识别重复段。

3.8.2 拥塞控制

  • 拥塞控制是 TCP 的核心机制,防止网络崩溃。
  • 必须实现的算法(MUST-19):
    • 慢启动(Slow Start)
    • 拥塞避免(Congestion Avoidance)
    • 快速重传与快速恢复(Fast Retransmit/Recovery)
    • RTO 指数退避
  • 当前标准:RFC 5681(拥塞控制)、RFC 6298(RTO 退避)。
  • 可选增强:显式拥塞通知(ECN),定义于 RFC 3168(SHLD-8)。

3.8.3 连接失败与过度重传

  • 若同一段被过度重传,说明路径或对端可能故障。
  • 处理流程(MUST-20):
    1. 定义两个阈值 R1R2(基于重传次数或时间)。
    2. 达到 R1 → 向 IP 层报告“死网关”诊断。
    3. 达到 R2 → 关闭连接。
    4. 应用必须能配置 R2(MUST-21)。
    5. 在 R1 与 R2 之间,应通知应用(SHLD-9)。
  • 推荐值:
    • R1 ≥ 3 次重传(SHLD-10)
    • R2 ≥ 100 秒(SHLD-11)
  • 对于 SYN 段,R2 必须 ≥ 3 分钟(MUST-23)。

3.8.4 保活机制(Keep-Alive)

  • Keep-Alive 是可选机制(MAY-5),默认关闭(MUST-25)。
  • 应用必须能启用/禁用(MUST-24)。
  • 发送条件:
    • 连接空闲(无数据待发/待收)
    • 间隔 ≥ 2 小时(默认,MUST-28,可配置 MUST-27)
  • Keep-Alive 段通常使用 SEG.SEQ = SND.NXT-1,可能带一个填充字节。
  • 响应失败不能直接视为连接死亡(MUST-29)。

3.8.5 紧急数据(Urgent Data)

  • 新应用不应使用(SHLD-13),但 TCP 必须支持(MUST-30)。
  • 机制:
    • URG 标志 + 紧急指针 指示紧急数据结束位置。
    • 接收端进入“紧急模式”,直到 RCV.NXT 追上紧急指针。
  • 要求:
    • 支持任意长度的紧急数据(MUST-31)
    • 紧急指针必须指向紧急数据后的字节(MUST-62)
    • 必须异步通知应用层(MUST-32, MUST-33)

3.8.6 窗口管理

  • 每个段中的 窗口字段 表示接收端可接受的序列范围。
  • 大窗口 → 提高吞吐;小窗口 → 增加延迟。
  • 缩小窗口(Shrinking Window):强烈不推荐(SHLD-14),但发送端必须具备鲁棒性(MUST-34)。
  • 若窗口为零,必须使用 零窗口探测(ZWP)(MUST-36)。

3.8.6.1 零窗口探测

  • 发送端必须定期发送探测段,即使窗口为零。
  • 允许接收端无限期通告零窗口(MAY-8),只要继续响应探测(MUST-37)。
  • 探测间隔应指数退避(SHLD-30)。

3.8.6.2 避免愚蠢窗口综合症(SWS)

  • 发送端算法(MUST-38):
    • 仅在满足以下条件之一时发送:
      1. 可发送 ≥ MSS 的数据
      2. PUSH 且所有数据可立即发送
      3. 可发送 ≥ 窗口的一半
      4. 超时触发
  • 接收端算法(MUST-39):
    • 仅在可用空间 ≥ min(½ 缓冲区, MSS) 时才更新窗口。

3.8.6.3 延迟确认(Delayed ACK)

  • 目的:减少 ACK 数量,提高效率。
  • 要求:
    • 应实现延迟 ACK(SHLD-18)
    • 延迟 < 0.5 秒(MUST-40)
    • 至少每秒发送一个 ACK,或每 2*RMSS 字节发送一个 ACK(SHLD-19)
  • 特殊情况:乱序段应立即确认,加速丢包恢复。

3.9 接口

TCP 涉及两个主要接口:

  1. 用户/TCP 接口 —— 提供给应用程序使用
  2. TCP/低层接口 —— 与下层协议(如 IP)交互

其中,用户/TCP 接口相对复杂,而 TCP/低层接口依赖于具体下层协议的规范(例如 IP),因此此处不做详细说明。

3.9.1 用户/TCP 接口

以下描述的是 TCP 提供给用户的基本功能接口。

  • 不同操作系统的实现方式可能不同,但所有 TCP 实现必须提供一组最小功能,以保证协议一致性。
  • 这些接口通常以类似函数调用的形式出现,但具体形式由实现决定。
  • TCP 不仅要接受用户命令,还必须向用户返回信息,例如:
    • 连接的一般状态(中断、远程关闭等)
    • 用户命令的成功或失败反馈

3.9.1.1 打开连接(OPEN)

格式:

1
OPEN(本地端口, 远程套接字, 主动/被动 [, 超时] [, Diffserv] [, 安全/隔间] [, 本地IP] [, 选项]) -> 本地连接名
  • 被动 OPEN:进入 LISTEN 状态,等待连接。可指定远程套接字,也可接受任意远程请求。
  • 主动 OPEN:立即发起连接建立。
  • 本地 IP 地址:必须支持(MUST-43),用于多宿主环境。
  • 错误处理:无效地址(如广播/组播)必须拒绝(MUST-46)。
  • 多连接支持:必须允许同一端口同时存在 LISTEN 与 SYN-SENT/SYN-RECEIVED(MUST-42)。

3.9.1.2 发送数据(SEND)

格式:

1
SEND(本地连接名, 缓冲区地址, 字节数, 紧急标志 [, PUSH] [, 超时])
  • 将缓冲区数据发送到指定连接。
  • PUSH 标志:
    • 表示应用希望立即传输数据,最后一个段设置 PSH 位。
    • 若未设置,TCP 可合并多次 SEND 提高效率。
  • URGENT 标志:新应用不推荐使用(SHLD-13),但 TCP 必须支持。
  • 超时:可为连接设置新的超时值。
  • 实现方式:
    • 简单实现:阻塞直到完成或超时。
    • 高级实现:立即返回,允许并发 SEND,内部排队。

3.9.1.3 接收数据(RECEIVE)

格式:

1
RECEIVE(本地连接名, 缓冲区地址, 字节数) -> 实际字节数, URGENT 标志 [, PUSH 标志]
  • 从连接接收数据填充缓冲区。
  • PUSH 标志:提示应用数据边界,但不是必须。
  • URGENT 标志:指示是否存在紧急数据。
  • 实现方式:
    • 简单实现:阻塞直到缓冲区填满或出错。
    • 高级实现:允许多个未完成的 RECEIVE,提高吞吐量。

3.9.1.4 关闭连接(CLOSE)

格式:

1
CLOSE(本地连接名)
  • 表示“我没有更多数据要发送”。
  • TCP 会确保所有已发送数据可靠交付。
  • 用户仍需继续接收,直到远端也关闭。
  • 若未能在超时前完成,CLOSE 转为 ABORT。

3.9.1.5 查询状态(STATUS)

格式:

1
STATUS(本地连接名) -> 状态数据

返回与连接相关的信息,例如:

  • 本地/远程套接字
  • 窗口大小
  • 连接状态
  • 等待确认/接收的缓冲区数量
  • 紧急状态
  • Diffserv 值、安全属性
  • 超时设置

3.9.1.6 中止连接(ABORT)

格式:

1
ABORT(本地连接名)
  • 立即终止连接,丢弃所有未完成的 SEND/RECEIVE。
  • 删除 TCB,并向对端发送 RST。

3.9.1.7 刷新(FLUSH)

  • 某些实现提供 FLUSH 调用,用于立即发送发送队列中所有可发送的数据。
  • 可选功能(MAY-14)。

3.9.1.8 异步报告(ERROR_REPORT)

格式:

1
ERROR_REPORT(本地连接名, 原因, 子原因)
  • TCP 必须支持异步错误报告(MUST-47)。
  • 报告内容包括:
    • ICMP 错误消息
    • 过度重传
    • 紧急指针前进
  • 应用可选择禁用(SHLD-20)。

3.9.1.9 设置差异化服务字段(DSCP)

  • 应用必须能设置差异化服务字段(MUST-48)。
  • 字段值影响传出段的 DSCP,但可能在网络中被修改。
  • 应用应能在连接存续期间修改该值(SHLD-21)。
  • TCP 应将当前 DSCP 值传递给 IP 层(SHLD-22)。

3.9.2 TCP/低级接口

TCP 端点通过调用下层协议模块来完成数据的实际发送与接收。目前,TCP 下层的标准互联网协议包括 IPv4IPv6

基本要求

  • Diffserv 字段:由用户提供,用于设置 IP 首部中的差异化服务代码点(DSCP)。
  • 生存时间(TTL):用于发送 TCP 段的 TTL 必须可配置(MUST-49)。
    • RFC 793 曾规定 TTL 固定为 60 秒,以确保报文在 1 分钟内未送达即被丢弃。
    • RFC 1122 更新了该要求,规定 TTL 必须可配置。
  • Diffserv 字段修改:RFC 1122 允许在连接期间修改,但应用层通常无法精确控制单个段,因此仅能粗粒度调整(SHLD-23)。
  • 下层协议必须提供:源地址、目标地址、协议号,以及 TCP 校验和所需的“TCP 长度”。
  • IP 选项处理:当 IP 向 TCP 传递选项时,TCP 必须忽略无法识别的选项(MUST-50)。
  • 可选支持:TCP 可以支持时间戳(MAY-10)和记录路由(MAY-11)选项。

3.9.2.1 源路由

  • 如果下层协议支持源路由,TCP 接口必须允许传递路由信息。
  • 目的:确保 TCP 校验和使用的是 原始源地址和最终目标地址,并能正确处理返回路由。
  • 要求:
    • 应用在 主动打开连接 时必须能指定源路由(MUST-51),并且优先于数据报中接收的源路由(MUST-52)。
    • 被动打开 时,如果收到带有返回路由的 SYN,TCP 必须保存该路由并用于后续分段(MUST-53)。
    • 如果后续段携带新的源路由,应覆盖旧的(SHLD-24)。

3.9.2.2 ICMP 消息

TCP 必须处理来自 IP 层的 ICMP 错误消息,并将其定向到相关连接(MUST-54)。这适用于 IPv4 ICMP 和 ICMPv6。

分类与处理

  • 源抑制(Source Quench):必须静默丢弃(MUST-55)。
  • 软错误(Soft Errors):
    • IPv4:目标不可达(代码 0、1、5)、超时(代码 0、1)、参数问题。
    • IPv6:目标不可达(代码 0、3)、超时(代码 0、1)、参数问题(代码 0、1、2)。
    • 处理:TCP 不得中止连接(MUST-56),但应将信息报告给应用(SHLD-25)。
  • 硬错误(Hard Errors):
    • IPv4:目标不可达(代码 2–4)。
    • 处理:TCP 应中止连接(SHLD-26)。
  • 注意:在连接建立阶段,许多实现会将软错误视为硬错误(见 [35])。

3.9.2.3 源地址验证

  • 无效源地址的 SYN:必须被 TCP 或 IP 层静默丢弃(MUST-63)。
  • 发送到广播或组播地址的 SYN:必须静默丢弃(MUST-57)。
  • 目的:防止错误生成连接状态或回复。
  • 注意:该规则适用于所有传入段,而不仅仅是 SYN(RFC 1122 特别指出)。

3.10 事件处理

本节描述的处理流程是一个可能的实现示例。不同的 TCP 实现可能在处理顺序上略有差异,但在本质逻辑上应保持一致。

TCP 端点的行为可以抽象为对 事件 的响应。事件分为三类:

  1. 用户调用:OPEN、SEND、RECEIVE、CLOSE、ABORT、STATUS
  2. 段到达:接收到来自网络的 TCP 段
  3. 超时:用户超时、重传超时、TIME-WAIT 超时

在 TCP/用户接口的模型中,用户命令通常立即返回,后续结果可能通过事件或延迟响应(伪中断)通知用户。

错误响应以字符串形式返回,例如:

  • 用户引用不存在的连接时,返回 “错误:连接未打开”

所有序列号、确认号和窗口的算术运算均在 模 2³² 的空间内进行。

3.10.1 OPEN 调用

  • CLOSED 状态(无 TCB)
    • 创建新的 TCB,填充本地/远程套接字、Diffserv、安全属性、超时等信息。
    • 被动 OPEN → 进入 LISTEN 状态。
    • 主动 OPEN → 若远程套接字未指定,返回错误;若指定,则发送 SYN 段,选择 ISS,设置 SND.UNA=ISSSND.NXT=ISS+1,进入 SYN-SENT。
    • 权限不足 → 返回“错误:非法连接”。
    • 资源不足 → 返回“错误:资源不足”。
  • LISTEN 状态
    • 若指定远程套接字 → 转为主动,发送 SYN,进入 SYN-SENT。
    • 若未指定远程套接字 → 返回“错误:remote socket 未指定”。
  • 其他状态
    • 返回“错误:连接已存在”。

3.10.2 SEND 调用

  • CLOSED 状态
    • 权限不足 → “错误:非法连接”。
    • 否则 → “错误:连接不存在”。
  • LISTEN 状态
    • 若指定远程套接字 → 转为主动,发送 SYN,进入 SYN-SENT。数据可与 SYN 一起发送或排队等待 ESTABLISHED。
    • 若未指定远程套接字 → “错误:remote socket 未指定”。
  • SYN-RECEIVED / ESTABLISHED / CLOSE-WAIT
    • 将数据分段并发送,ACK=RCV.NXT。
    • 若资源不足 → 返回“错误:资源不足”。
    • 若设置 URGENT → 更新 SND.UP
  • FIN-WAIT-1 / FIN-WAIT-2 / CLOSING / LAST-ACK / TIME-WAIT
    • 返回“错误:连接关闭”。

3.10.3 RECEIVE 调用

  • CLOSED 状态
    • 权限不足 → “错误:非法连接”。
    • 否则 → “错误:连接不存在”。
  • SYN-RECEIVED
    • 请求排队,待进入 ESTABLISHED 后处理。
  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2
    • 若数据不足 → 请求排队。
    • 若有数据 → 重组并交付,标记 PUSH(如适用)。
    • 若存在紧急数据 → 通知用户。
  • CLOSE-WAIT
    • 若有剩余数据 → 交付。
    • 否则 → “错误:连接关闭”。
  • 其他状态
    • 返回“错误:连接关闭”。

3.10.4 CLOSE 调用

  • CLOSED 状态
    • 权限不足 → “错误:非法连接”。
    • 否则 → “错误:连接不存在”。
  • LISTEN 状态
    • 删除 TCB,进入 CLOSED。
  • SYN-SENT
    • 删除 TCB,返回所有排队的 SEND/RECEIVE。
  • SYN-RECEIVED / ESTABLISHED
    • 若无待发送数据 → 立即发送 FIN,进入 FIN-WAIT-1。
    • 若有待发送数据 → 排队,待数据发送后再发 FIN。
  • FIN-WAIT-1 / FIN-WAIT-2
    • 严格来说应返回“错误:连接关闭”。
    • 也可接受“OK”,但不得重复发送 FIN。
  • CLOSE-WAIT
    • 待所有 SEND 完成后,发送 FIN,进入 LAST-ACK。
  • 其他状态
    • 返回“错误:连接关闭”。

3.10.5 ABORT 调用

  • CLOSED 状态(TCB 不存在)

    • 若用户无权访问 → 返回“错误:非法连接”。
    • 否则 → 返回“错误:连接不存在”。
  • LISTEN 状态

    • 所有未完成的 RECEIVE 返回“连接重置”。
    • 删除 TCB,进入 CLOSED。
  • SYN-SENT 状态

    • 所有排队的 SEND/RECEIVE 返回“连接重置”。
    • 删除 TCB,进入 CLOSED。
  • SYN-RECEIVED / ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2 / CLOSE-WAIT

    • 发送 RST 段:

      1
      <SEQ=SND.NXT><CTL=RST>
    • 所有排队的 SEND/RECEIVE 返回“连接重置”。

    • 清空重传队列,删除 TCB,进入 CLOSED。

  • CLOSING / LAST-ACK / TIME-WAIT

    • 返回“ok”,删除 TCB,进入 CLOSED。

3.10.6 STATUS 调用

  • CLOSED 状态
    • 若用户无权访问 → “错误:非法连接”。
    • 否则 → “错误:连接不存在”。
  • 其他状态
    • 返回当前状态(如 LISTEN、SYN-SENT、ESTABLISHED 等)及 TCB 指针。

3.10.7. 段到达(SEGMENT ARRIVES)

3.10.7.1. CLOSED 状态

如果状态为 CLOSED(即 TCB 不存在),则:

  • 丢弃传入段中的所有数据。

  • 如果传入段包含 RST,则丢弃该段。

  • 如果传入段 不包含 RST,则发送一个 RST 作为响应。

    • 确认号和序列号字段的值应选择为,使得该复位序列对发送该错误段的 TCP 端点是可接受的。
  • 如果 ACK 位关闭,则使用序列号零:

    1
    <SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
  • 如果 ACK 位开启:

    1
    <SEQ=SEG.ACK><CTL=RST>

然后返回。

3.10.7.2. LISTEN 状态

如果状态为 LISTEN,则:

第一步,检查 RST:

  • 传入的 RST 段不可能有效,因为它不可能是对该连接实例所发送内容的响应。
  • 因此,传入的 RST 应被忽略。返回。

第二步,检查 ACK:

  • 在 LISTEN 状态下,任何到达的确认都是错误的。

  • 对于任何带有 ACK 的传入段,应形成一个复位段作为响应,其格式为:

    1
    <SEQ=SEG.ACK><CTL=RST>

然后返回。

第三步,检查 SYN:

  • 如果 SYN 位被设置,则检查安全属性。

  • 如果传入段中的 security/compartment 与 TCB 中的不完全匹配,则发送复位并返回:

    1
    <SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
  • 否则:

    • 设置 RCV.NXT = SEG.SEQ + 1IRS = SEG.SEQ

    • 其他控制或数据应排队,稍后处理。

    • 选择一个 ISS,并发送如下格式的 SYN 段:

      1
      <SEQ=ISS><ACK=RCV.NXT><CTL=SYN,ACK>
    • 设置 SND.NXT = ISS + 1SND.UNA = ISS

    • 将连接状态改为 SYN-RECEIVED

    • 注意:任何与 SYN 一同到达的控制或数据将在 SYN-RECEIVED 状态下处理,但 SYN 和 ACK 的处理不应重复。

    • 如果 LISTEN 未完全指定(即远程套接字未完全指定),则现在应填写未指定的字段。

第四步,其他数据或控制:

  • 不应到达此处。丢弃该段并返回。
  • 任何其他不包含 SYN 的控制或数据段必须带有 ACK,因此会在第二步的 ACK 检查中被丢弃,除非它已在第一步的 RST 检查中被丢弃。

3.10.7.3. SYN-SENT 状态

如果当前状态为 SYN-SENT,则:

第一步,检查 ACK 位:

  • 如果 ACK 位被设置:

    • 如果 SEG.ACK <= ISSSEG.ACK > SND.NXT,则发送一个复位(除非 RST 位已设置,如果是,则丢弃该段并返回):

      1
      <SEQ=SEG.ACK><CTL=RST>

      然后丢弃该段并返回。

    • 如果 SND.UNA < SEG.ACK <= SND.NXT,则该 ACK 是可接受的。

      注意:一些已部署的 TCP 代码使用了 SEG.ACK == SND.NXT(使用“==”而不是“<=”)的检查,但当协议栈支持在 SYN 上携带数据时,这种做法是不合适的,因为对端可能不会接受并确认 SYN 上的所有数据。

第二步,检查 RST 位:

  • 如果 RST 位被设置:
    • RFC 5961 [9] 描述了一种潜在的 盲复位攻击。该文档中提出的缓解措施有特定适用性,但不能替代加密保护(例如 IPsec 或 TCP-AO)。
    • 支持 RFC 5961 缓解措施的 TCP 实现 应当 在执行下一步之前,首先检查序列号是否与 RCV.NXT 完全匹配。
    • 如果 ACK 是可接受的,则向用户发出 “错误:连接重置” 的信号,丢弃该段,进入 CLOSED 状态,删除 TCB,然后返回。
    • 否则(没有 ACK),丢弃该段并返回。

第三步,检查安全性:

  • 如果段中的 security/compartment 与 TCB 中的不完全匹配,则发送复位:

    • 如果有 ACK:

      1
      <SEQ=SEG.ACK><CTL=RST>
    • 否则:

      1
      <SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
  • 如果发送了复位,则丢弃该段并返回。

第四步,检查 SYN 位:

  • 仅当 ACK 合法,或没有 ACK,且该段不包含 RST 时,才会执行此步骤。

  • 如果 SYN 位被设置,且安全/隔间检查通过:

    • 设置 RCV.NXT = SEG.SEQ + 1IRS = SEG.SEQ
    • 如果有 ACK,则将 SND.UNA 推进到等于 SEG.ACK,并删除重传队列中已被确认的段。
  • 如果 SND.UNA > ISS(即我们的 SYN 已被确认):

    • 将连接状态改为 ESTABLISHED,形成一个 ACK 段:

      1
      <SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>
    • 并发送它。已排队的数据或控制信息 可以 一并发送。

    • 一些 TCP 实现会在接收段已包含数据时抑制发送该 ACK,因为后续处理步骤无论如何都会生成 ACK,从而避免额外的 SYN 确认。

    • 如果该段中还有其他控制或数据,则继续执行 3.10.7.4 第六步(检查 URG 位);否则返回。

  • 否则,进入 SYN-RECEIVED 状态,形成一个 SYN+ACK 段:

    1
    <SEQ=ISS><ACK=RCV.NXT><CTL=SYN,ACK>

    并发送它。设置以下变量:

    1
    2
    3
    SND.WND <- SEG.WND
    SND.WL1 <- SEG.SEQ
    SND.WL2 <- SEG.ACK

    如果该段中还有其他控制或数据,则将其排队,待进入 ESTABLISHED 状态后再处理,然后返回。

注意:在 SYN 段上发送和接收应用数据是合法的(即上文提到的“段中的文本”)。历史上对此存在大量误解和错误信息。一些防火墙和安全设备会将其视为可疑。然而,该能力曾用于 **T/TCP [21]**,并被 TCP Fast Open (TFO) [48] 使用,因此实现和网络设备必须允许。

第五步,如果 SYN 和 RST 位均未设置,则丢弃该段并返回。

3.10.7.4. 其他状态

否则,

第一步,检查序列号:

适用状态:

  • SYN-RECEIVED
  • ESTABLISHED
  • FIN-WAIT-1
  • FIN-WAIT-2
  • CLOSE-WAIT
  • CLOSING
  • LAST-ACK
  • TIME-WAIT

段按序处理。到达时的初始检查用于丢弃旧的重复段,但进一步处理是按照 SEG.SEQ 顺序进行的。
如果一个段的内容跨越了“旧数据”和“新数据”的边界,则只处理其中的新部分。

通常,接收段的处理 必须 尽可能聚合 ACK 段(MUST-58)。例如,当 TCP 端点正在处理一系列排队的段时,它 必须 在发送任何 ACK 段之前先处理完所有这些段(MUST-59)。

段可接受性测试有四种情况:

表 6:段可接受性测试

段长度 接收窗口 测试条件
0 0 SEG.SEQ = RCV.NXT
0 >0 RCV.NXT <= SEG.SEQ < RCV.NXT+RCV.WND
>0 0 不可接受
>0 >0 RCV.NXT <= SEG.SEQ < RCV.NXT+RCV.WND RCV.NXT <= SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND

实现序列号验证时,请注意附录 A.2。

如果 RCV.WND=0,则没有段是可接受的,但应作特殊处理以接受合法的 ACK、URG 和 RST。

如果传入段不可接受,应发送一个确认作为回复(除非该段带有 RST 位,此时应丢弃并返回):

1
<SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>

发送确认后,丢弃该不可接受段并返回。

注意:对于 TIME-WAIT 状态,有一种改进算法(见 [40]),使用时间戳来处理传入的 SYN 段,而不是依赖这里描述的序列号检查。如果实现了该改进算法,则上述逻辑不适用于在 TIME-WAIT 状态下接收到带时间戳选项的 SYN 段。

在以下描述中,假设段是理想化的,即它从 RCV.NXT 开始且不超过窗口。实际段可以通过裁剪窗口外的部分(包括 SYN 和 FIN)来满足这一假设,并且只有当段从 RCV.NXT 开始时才继续处理。序列号更高的段 应当 保留以便稍后处理 (SHLD-31)。

第二步,检查 RST 位:

RFC 5961 第 3 节描述了一种潜在的盲重置攻击及可选的缓解方法。这不是加密保护(如 IPsec 或 TCP-AO),但在 RFC 5961 描述的场景中可适用。对于实现了 RFC 5961 所述保护的协议栈,以下三条检查适用;否则,处理逻辑见后续描述。

  1. 如果 RST 位被设置,且序列号在当前接收窗口之外 → 静默丢弃该段。

  2. 如果 RST 位被设置,且序列号 正好等于 下一个期望的序列号 (RCV.NXT) → TCP 端点 必须 按照下述方式根据连接状态复位连接。

  3. 如果 RST 位被设置,且序列号在当前接收窗口内,但 不等于 RCV.NXT → TCP 端点 必须 发送一个确认(质询 ACK):

    1
    <SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>

    发送质询 ACK 后,TCP 端点 必须 丢弃该不可接受段,并停止进一步处理该报文。注意:RFC 5961 和勘误 ID 4772 [99] 对 ACK 限速有额外考虑。

各状态下的 RST 处理

SYN-RECEIVED 状态

  • 如果 RST 位被设置:
    • 如果连接是由 被动打开(来自 LISTEN 状态)发起的 → 返回 LISTEN 状态并返回,用户无需通知。
    • 如果连接是由 主动打开(来自 SYN-SENT 状态)发起的 → 连接被拒绝;通知用户“连接被拒绝”。
    • 在两种情况下,都应清空重传队列。
    • 在主动打开的情况下,还应进入 CLOSED 状态并删除 TCB,然后返回。

ESTABLISHED、FIN-WAIT-1、FIN-WAIT-2、CLOSE-WAIT 状态

  • 如果 RST 位被设置:
    • 所有未完成的接收和发送操作应收到“复位”响应。
    • 所有段队列应清空。
    • 用户应收到一个非请求的通用“连接复位”信号。
    • 进入 CLOSED 状态,删除 TCB,并返回。

CLOSING、LAST-ACK、TIME-WAIT 状态

  • 如果 RST 位被设置:
    • 进入 CLOSED 状态,删除 TCB,并返回。

第三步,检查安全性:

  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2 / CLOSE-WAIT / CLOSING / LAST-ACK / TIME-WAIT 状态

    如果段中的

    security/compartment

    与 TCB 中的不完全匹配,则发送复位;所有未完成的 RECEIVE 和 SEND 应收到“reset”响应。所有段队列应被清空。用户还应收到一个未经请求的一般性“connection reset”信号。进入

    CLOSED

    状态,删除 TCB,然后返回。

    注意:此检查放在序列号检查之后,以防止来自旧连接(相同端口号但不同安全属性)的段导致当前连接被错误中止。

第四步,检查 SYN 位:

  • SYN-RECEIVED 状态
    如果连接是通过被动 OPEN 建立的,则将该连接返回到 LISTEN 状态并返回。否则,按下文同步状态的规则处理。

  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2 / CLOSE-WAIT / CLOSING / LAST-ACK / TIME-WAIT 状态
    如果在这些同步状态下 SYN 位被设置,它可能表示:

    • 一个合法的新连接尝试(例如 TIME-WAIT 状态下),

    • 一个应当复位的错误,

    • 或一次攻击尝试(RFC 5961 [9] 所述)。

    • 对于 TIME-WAIT 状态,如果使用了时间戳选项并符合预期(见 [40]),则可以接受新连接。

    • 对于其他情况,RFC 5961 建议:在这些同步状态下,如果 SYN 位被设置,不论序列号如何,TCP 端点 必须 向对端发送一个“质询 ACK”:

      1
      <SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>

      发送确认后,TCP 实现必须丢弃该段并停止进一步处理。

      注意:RFC 5961 和勘误 ID 4772 [99] 对 ACK 限速有额外说明。

    • 如果未实现 RFC 5961,则遵循 RFC 793 的原始行为:

      • 如果 SYN 在窗口内,这是错误:发送复位,所有未完成的 RECEIVE 和 SEND 返回“reset”,清空队列,用户收到“connection reset”,进入 CLOSED,删除 TCB,返回。
      • 如果 SYN 不在窗口内,则不会到达此步骤,而会在第一步(序列号检查)中发送 ACK。

第五步,检查 ACK 字段:

  • 如果 ACK 位关闭 → 丢弃该段并返回。

  • 如果 ACK 位开启:

    • RFC 5961 [9] 第 5 节描述了潜在的 盲数据注入攻击,并给出可选缓解措施(MAY-12)。

    • 实现 RFC 5961 的 TCP 栈必须检查 ACK 值是否在以下范围内:

      1
      (SND.UNA - MAX.SND.WND) <= SEG.ACK <= SND.NXT

      不满足条件的段必须被丢弃,并发送一个 ACK。

      • MAX.SND.WND 定义为本端曾从对端接收的最大窗口(考虑窗口缩放),或可硬编码为允许的最大窗口值。
    • 当 ACK 值可接受时,按状态处理:

      • SYN-RECEIVED

        • 如果

          1
          SND.UNA < SEG.ACK <= SND.NXT

          则进入ESTABLISHED,并设置:

          1
          2
          3
          SND.WND <- SEG.WND
          SND.WL1 <- SEG.SEQ
          SND.WL2 <- SEG.ACK
        • 如果 ACK 不可接受 → 发送复位:

          1
          <SEQ=SEG.ACK><CTL=RST>
      • ESTABLISHED

        • 如果 SND.UNA < SEG.ACK <= SND.NXT,则更新 SND.UNA <- SEG.ACK,并删除重传队列中已确认的段。用户应收到对已发送并完全确认缓冲区的“ok”响应。

        • 如果 ACK 是重复的(SEG.ACK <= SND.UNA),忽略。

        • 如果 ACK 确认了尚未发送的数据(SEG.ACK > SND.NXT),则发送 ACK,丢弃该段并返回。

        • 如果

          1
          SND.UNA <= SEG.ACK <= SND.NXT

          则更新发送窗口:

          • 1
            (SND.WL1 < SEG.SEQ)

            1
            (SND.WL1 = SEG.SEQ 且 SND.WL2 <= SEG.ACK)

            则:

            1
            2
            3
            SND.WND <- SEG.WND
            SND.WL1 <- SEG.SEQ
            SND.WL2 <- SEG.ACK
          • 注意:SND.WND 是相对于 SND.UNA 的偏移量;SND.WL1 记录上次更新窗口的段序列号;SND.WL2 记录上次更新窗口的确认号。此检查防止旧段更新窗口。

      • FIN-WAIT-1

        • 除了 ESTABLISHED 的处理外,如果 FIN 已被确认 → 进入 FIN-WAIT-2
      • FIN-WAIT-2

        • 除了 ESTABLISHED 的处理外,如果重传队列为空 → 用户的 CLOSE 可被确认“ok”,但不删除 TCB。
      • CLOSE-WAIT

        • 与 ESTABLISHED 相同。
      • CLOSING

        • 除了 ESTABLISHED 的处理外,如果 ACK 确认了我们的 FIN → 进入 TIME-WAIT;否则忽略该段。
      • LAST-ACK

        • 唯一可能到达的是对我们 FIN 的确认。如果 FIN 被确认 → 删除 TCB,进入 CLOSED。
      • TIME-WAIT

        • 唯一可能到达的是远端 FIN 的重传。确认它,并重启 2MSL 定时器。

第六步,检查 URG 位:

  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2
    • 如果 URG 位被设置:
      • RCV.UP <- max(RCV.UP, SEG.UP)
      • 如果紧急指针在已消费数据之前,则通知用户远端有紧急数据。
      • 如果用户已被通知(或仍处于“紧急模式”),则不再重复通知。
  • CLOSE-WAIT / CLOSING / LAST-ACK / TIME-WAIT
    • 不应出现(因为远端已发送 FIN),忽略 URG。

第七步,处理段数据:

  • ESTABLISHED / FIN-WAIT-1 / FIN-WAIT-2

    • 可以将段数据交付到用户接收缓冲区,直到缓冲区满或段耗尽。

    • 如果段耗尽且带有 PUSH 标志 → 当缓冲区返回时通知用户“PUSH 已收到”。

    • 一旦 TCP 端点负责交付数据 → 必须确认接收。

    • RCV.NXT 前进,RCV.WND 根据缓冲区可用性调整。RCV.NXT + RCV.WND 总和不得减少。

    • TCP 实现 可以 在段位于窗口内但不在左边界时发送 ACK(MAY-13)。

    • 确认格式:

      1
      <SEQ=SND.NXT><ACK=RCV.NXT><CTL=ACK>

      应尽可能搭载在传输段上,避免额外延迟。

  • CLOSE-WAIT / CLOSING / LAST-ACK / TIME-WAIT

    • 不应出现(远端已发送 FIN),忽略数据。

第八步,检查 FIN 位:

  • SYN-RECEIVED / ESTABLISHED
    • 如果收到 FIN,则进入 CLOSE-WAIT 状态。
  • FIN-WAIT-1
    • 如果我们的 FIN 已被确认(可能在此段中),则进入 TIME-WAIT 状态,启动 time-wait 定时器,并关闭其他定时器;
    • 否则,进入 CLOSING 状态。
  • FIN-WAIT-2
    • 进入 TIME-WAIT 状态,启动 time-wait 定时器,并关闭其他定时器。
  • CLOSE-WAIT
    • 保持在 CLOSE-WAIT 状态。
  • CLOSING
    • 保持在 CLOSING 状态。
  • LAST-ACK
    • 保持在 LAST-ACK 状态。
  • TIME-WAIT
    • 保持在 TIME-WAIT 状态,并重新启动 2MSL 的 time-wait 超时。

然后返回。

3.10.8 超时

  • 用户超时
    • 刷新队列,通知“错误:用户超时中止连接”。
    • 删除 TCB,进入 CLOSED。
  • 重传超时
    • 重传队列中的段,重置定时器。
  • TIME-WAIT 超时
    • 删除 TCB,进入 CLOSED。

4. 术语表(Glossary)

ACK
确认(Acknowledge)控制位,不占用序列号空间。表示本段的确认号字段有效,指示发送方期望接收的下一个序列号,从而确认了之前所有字节的接收。

connection(连接)
由一对套接字(socket)标识的逻辑通信路径。

datagram(数据报)
在分组交换网络中发送的消息。

Destination Address(目标地址)
网络层中,接收端点的地址。

FIN
结束(Finish)控制位,占用一个序列号。表示发送方不再发送数据或占用序列空间的控制信息。

flush(刷新)
清空缓冲区或队列中的所有内容(数据或段)。

fragment(分片)
数据的一个片段。特别地,互联网分片是互联网数据报的一部分。

header(首部)
位于消息、段、分片、数据包或数据块开头的控制信息。

host(主机)
计算机。在通信网络中,作为消息的源或目的地。

Identification(标识字段)
IP 首部中的字段,用于帮助接收方重组分片数据报。

internet address(互联网地址)
网络层地址。

internet datagram(互联网数据报)
在互联网主机之间交换的数据单元,包含数据和互联网首部。

internet fragment(互联网分片)
互联网数据报的一部分,带有互联网首部。

IP
互联网协议(Internet Protocol)。

IRS
初始接收序列号(Initial Receive Sequence number)。

ISN
初始序列号(Initial Sequence Number),连接建立时选取,需唯一且不可预测。

ISS
初始发送序列号(Initial Send Sequence number)。

left sequence(左边界序列号)
接收端下一个需要确认的序列号,或发送端最早未确认的序列号。

module(模块)
协议或过程的软件实现。

MSL
最大报文段生存期(Maximum Segment Lifetime),定义为 2 分钟。

octet(八位字节)
8 位字节。

Options(选项)
TCP 首部中的可选字段,可包含多个选项,每个选项长度为若干字节。

packet(分组/包)
带有首部的数据单元,可能是逻辑或物理上的封装。

port(端口)
连接标识的一部分,用于在端点进行多路复用。

process(进程)
正在执行的程序,从 TCP 端点的角度看,是数据的源或目的地。

PUSH
推送控制位,不占用序列号空间。表示本段数据必须立即交付给接收应用。

RCV.NXT
下一个期望接收的序列号。

RCV.UP
接收紧急指针。

RCV.WND
接收窗口,表示接收端愿意接收的序列号范围。

RST
复位控制位,不占用序列号空间。表示接收方应立即删除连接。

SEG.ACK
段的确认号。

SEG.LEN
段的长度(占用的序列号空间,包括数据和控制位)。

SEG.SEQ
段的序列号。

SEG.UP
段的紧急指针字段。

SEG.WND
段的窗口字段。

segment(段)
TCP 数据传输的基本单元。

send sequence(发送序列号)
发送端下一个将使用的序列号。

send window(发送窗口)
接收端通告的可接收序列号范围。

SND.NXT
下一个要发送的序列号。

SND.UNA
最早未确认的序列号。

SND.UP
发送紧急指针。

SND.WL1
上次窗口更新时的段序列号。

SND.WL2
上次窗口更新时的段确认号。

SND.WND
发送窗口大小。

socket(套接字)
由 IP 地址和 TCP 端口号组成的标识符。

Source Address(源地址)
发送端的网络层地址。

SYN
同步控制位,占用一个序列号。用于连接建立,指示序列号的起始位置。

TCB
传输控制块(Transmission Control Block),记录连接状态的数据结构。

TCP
传输控制协议(Transmission Control Protocol),一种面向连接的、可靠的传输层协议。

TOS
服务类型(Type of Service),IPv4 中已废弃的字段。现由差异化服务字段(DSCP + ECN)取代。

URG
紧急控制位,不占用序列号空间。表示接收方应立即处理紧急数据。

urgent pointer(紧急指针)
仅在 URG 位有效时有意义,指示紧急数据结束位置。

附录 A. 其他实现说明

本节包含一些关于 TCP 实现的补充说明和参考资料。这些内容目前不属于 RFC 系列的正式部分,也未纳入 TCP 标准,但实现者在设计时可以加以考虑。

A.1. IP 安全区间与优先级

早期 IPv4 规范 [1] 使用 服务类型(TOS)字段,后来被 Diffserv [4] 取代。

  • RFC 793 要求检查传入 TCP 段的 IP 安全区间和优先级,以确保连接一致性。
  • 这些机制已过时,未在 RFC 793 中更新。
  • 优先级问题在 RFC 2873 [25] 中被修正,因此当前 TCP 规范已包含这些更改。
  • 多级安全(MLS)系统可能仍使用 IP 安全选项,但这属于特殊场景。

注意:若因 IP 安全区间或 Diffserv 值不匹配而重置连接,可能成为攻击向量 [63]。因此有讨论建议修改 TCP 规范以避免因这些字段不一致而中止连接。

A.1.1 优先级

  • 在 Diffserv 中,旧的“优先级”值被视为类选择器代码点。
  • RFC 793/1122 中的逻辑假设连接两端使用相同优先级,但 Diffserv 是非对称架构。
  • 解决方案:忽略 IP 优先级字段(RFC 2873)。
  • 实际实现中,Diffserv 字段值在 TCP 与网络层之间传递,每个方向可独立设置。

A.1.2 MLS 系统

  • IP 安全选项(IPSO)最早定义于 RFC 791,后在 RFC 1038、RFC 1108 中扩展,但已废弃。
  • 商业 IP 安全选项(CIPSO)曾在 FIPS-188 中定义,但已撤销。
  • IPv6 定义了类似的 CALIPSO [36]。
  • 对于非 MLS 系统,TCP 实现可忽略这些字段。
  • 在 MLS 网络中,RFC 5570 提供了使用 IPSO/CIPSO/CALIPSO 的指导。

A.2. 序列号验证

  • 在某些情况下,严格的序列号验证可能阻止 ACK 字段处理,导致连接问题(如同时打开、自连接、同时关闭、零窗口探测等)[64]。
  • 这些情况在实际互联网中很少发生。
  • 不同操作系统采用了不同的缓解措施,但尚未形成标准。

A.3. Nagle 算法修改

  • 常见操作系统默认启用 Nagle 算法延迟确认
  • 在请求-响应型应用中,这种组合可能导致性能下降。
  • 一些系统实现了 Nagle 的修改版本 [68],改善了性能。
  • 该修改不影响互操作性,但未纳入标准。
  • 许多应用直接通过套接字选项禁用 Nagle。

A.4. 低水位设置

  • 一些操作系统提供套接字选项:
    • SO_SNDLOWAT:发送缓冲区低水位
    • SO_RCVLOWAT:接收缓冲区低水位
  • 另有 TCP_NOTSENT_LOWAT,用于限制写队列中未发送数据的字节数。
  • 这些机制有助于避免过多缓冲和延迟,尤其适用于多路复用场景(如交互式与批量数据混合)。

附录 B. TCP 要求摘要

本节改编自 RFC 1122,总结了 TCP 的实现要求。

(ps: 换而言之,如果你想实现一个TCP协议栈,需要参考以下表格)

  • 注意:此列表未包含 PLPMTUD 的要求,但推荐实现。
Feature ReqID MUST SHOULD MAY SHOULD NOT MUST NOT
PUSH flag (PUSH 标志)
Aggregate or queue un-pushed data MAY-16 X
Sender collapse successive PSH bits SHLD-27 X
SEND call can specify PUSH MAY-15 X
If cannot: sender buffer indefinitely MUST-60 X
If cannot: PSH last segment MUST-61 X
Notify receiving ALP1 of PSH MAY-17 X
Send max size segment when possible SHLD-28 X
Window (窗口)
Treat as unsigned number MUST-1 X
Handle as 32-bit number REC-1 X
Shrink window from right SHLD-14 X
Send new data when window shrinks SHLD-15 X
Retransmit old unacked data within window SHLD-16 X
Time out conn for data past right edge SHLD-17 X
Robust against shrinking window MUST-34 X
Receiver’s window closed indefinitely MAY-8 X
Use standard probing logic MUST-35 X
Sender probe zero window MUST-36 X
First probe after RTO SHLD-29 X
Exponential backoff SHLD-30 X
Allow window stay zero indefinitely MUST-37 X
Retransmit old data beyond SND.UNA+SND.WND MAY-7 X
Process RST and URG even with zero window MUST-66 X
Urgent Data 紧急数据
Include support for urgent pointer MUST-30 X
Pointer indicates first non-urgent octet MUST-62 X
Arbitrary length urgent data sequence MUST-31 X
Inform ALP1 asynchronously of urgent data MUST-32 X
ALP1 can learn if/how much urgent data Q’d MUST-33 X
ALP employ the urgent mechanism SHLD-13 X
TCP Options TCP 选项
Support the mandatory option set MUST-4 X
Receive TCP Option in any segment MUST-5 X
Ignore unsupported options MUST-6 X
Include length for all options except EOL+NOP MUST-68 X
Cope with illegal option length MUST-7 X
Process options regardless of word alignment MUST-64 X
Implement sending & receiving MSS Option MUST-14 X
IPv4 Send MSS Option unless 536 SHLD-5 X
IPv6 Send MSS Option unless 1220 SHLD-5 X
Send MSS Option always MAY-3 X
IPv4 Send-MSS default is 536 MUST-15 X
IPv6 Send-MSS default is 1220 MUST-15 X
Calculate effective send seg size MUST-16 X
MSS accounts for varying MTU SHLD-6 X
MSS not sent on non-SYN segments MUST-65 X
MSS value based on MMS_R MUST-67 X
Pad with zero MUST-69 X
TCP Checksums TCP 校验和
Sender compute checksum MUST-2 X
Receiver check checksum MUST-3 X
ISN Selection ISN 选择
Include a clock-driven ISN generator component MUST-8 X
Secure ISN generator with a PRF component SHLD-1 X
PRF computable from outside the host MUST-9 X
Opening Connections 打开连接
Support simultaneous open attempts MUST-10 X
SYN-RECEIVED remembers last state MUST-11 X
Passive OPEN call interfere with others MUST-41 X
Function: simultaneously LISTENs for same port MUST-42 X
Ask IP for src address for SYN if necessary MUST-44 X
Otherwise, use local addr of connection MUST-45 X
OPEN to broadcast/multicast IP address MUST-46 X
Silently discard seg to bcast/mcast addr MUST-57 X
Closing Connections 关闭连接
RST can contain data SHLD-2 X
Inform application of aborted conn MUST-12 X
Half-duplex close connections MAY-1 X
Send RST to indicate data lost SHLD-3 X
In TIME-WAIT state for 2MSL seconds MUST-13 X
Accept SYN from TIME-WAIT state MAY-2 X
Use Timestamps to reduce TIME-WAIT SHLD-4 X
Retransmissions 重传
Implement exponential backoff, slow start, and congestion avoidance MUST-19 X
Retransmit with same IP identity MAY-4 X
Karn’s algorithm MUST-18 X
Generating ACKs 生成 ACK
Aggregate whenever possible MUST-58 X
Queue out-of-order segments SHLD-31 X
Process all Q’d before send ACK MUST-59 X
Send ACK for out-of-order segment MAY-13 X
Delayed ACKs SHLD-18 X
Delay < 0.5 seconds MUST-40 X
Every 2nd full-sized segment or 2*RMSS ACK’d SHLD-19 X
Receiver SWS-Avoidance Algorithm MUST-39 X
Sending Data 发送数据
Configurable TTL MUST-49 X
Sender SWS-Avoidance Algorithm MUST-38 X
Nagle algorithm SHLD-7 X
Application can disable Nagle algorithm MUST-17 X
Connection Failures 连接失败
Negative advice to IP on R1 retransmissions MUST-20 X
Close connection on R2 retransmissions MUST-20 X
ALP1 can set R2 MUST-21 X
Inform ALP of R1<=retxs<R2 SHLD-9 X
Recommended value for R1 SHLD-10 X
Recommended value for R2 SHLD-11 X
Same mechanism for SYNs MUST-22 X
R2 at least 3 minutes for SYN MUST-23 X
Send Keep-alive Packets 发送保持活动数据包
Send Keep-alive Packets: MAY-5 X
Application can request MUST-24 X
Default is “off” MUST-25 X
Only send if idle for interval MUST-26 X
Interval configurable MUST-27 X
Default at least 2 hrs. MUST-28 X
Tolerant of lost ACKs MUST-29 X
Send with no data SHLD-12 X
Configurable to send garbage octet MAY-6 X
IP Options IP 选项
Ignore options TCP doesn’t understand MUST-50 X
Timestamp support MAY-10 X
Record Route support MAY-11 X
Source Route:
ALP1 can specify MUST-51 X
Overrides src route in datagram MUST-52 X
Build return route from src route MUST-53 X
Later src route overrides SHLD-24 X
Receiving ICMP Messages from IP 从 IP 接收 ICMP 消息
Receiving ICMP messages from IP MUST-54 X
Dest Unreach (0,1,5) => inform ALP SHLD-25 X
Abort on Dest Unreach (0,1,5) MUST-56 X
Dest Unreach (2-4) => abort conn SHLD-26 X
Source Quench => silent discard MUST-55 X
Abort on Time Exceeded MUST-56 X
Abort on Param Problem MUST-56 X
Address Validation 地址验证
Reject OPEN call to invalid IP address MUST-46 X
Reject SYN from invalid IP address MUST-63 X
Silently discard SYN to bcast/mcast addr MUST-57 X
TCP/ALP Interface Services TCP/ALP 接口服务
Error Report mechanism MUST-47 X
ALP can disable Error Report Routine SHLD-20 X
ALP can specify Diffserv field for sending MUST-48 X
Passed unchanged to IP SHLD-22 X
ALP can change Diffserv field during connection SHLD-21 X
ALP generally changing Diffserv during conn. SHLD-23 X
Pass received Diffserv field up to ALP MAY-9 X
FLUSH call MAY-14 X
Optional local IP addr param in OPEN MUST-43 X
RFC 5961 Support RFC 5961 支持
Implement data injection protection MAY-12 X
Explicit Congestion Notification 显式拥塞通知
Support ECN SHLD-8 X
Alternative Congestion Control 替代拥堵控制
Implement alternative conformant algorithm(s) MAY-18 X

RFC及TCP输入伪代码

重新整理一下3.7,这里我们关心的是TCP的输入:

3.10.7 段到达

3.10.7.1 CLOSED 状态

  • 丢弃所有数据段。

  • 若段含 RST → 丢弃。

  • 若段不含 RST → 回复 RST:

    • 若无 ACK:

      1
      <SEQ=0><ACK=SEG.SEQ+SEG.LEN><CTL=RST,ACK>
    • 若有 ACK:

      1
      <SEQ=SEG.ACK><CTL=RST>

3.10.7.2 LISTEN 状态

  • RST:无效,丢弃。

  • ACK:非法,回复 RST。

  • SYN:

    • 校验安全/隔间,不匹配则回复 RST。

    • 否则:

      • RCV.NXT = SEG.SEQ+1IRS = SEG.SEQ

      • 选择 ISS,发送:

        1
        <SEQ=ISS><ACK=RCV.NXT><CTL=SYN,ACK>
      • SND.NXT=ISS+1SND.UNA=ISS

      • 状态 → SYN-RECEIVED

  • 其他数据/控制:丢弃。

3.10.7.3 SYN-SENT 状态

  • ACK 检查:
    • SEG.ACK <= ISSSEG.ACK > SND.NXT → 回复 RST。
    • SND.UNA < SEG.ACK <= SND.NXT → ACK 可接受。
  • RST 检查:
    • 若 ACK 可接受 → 通知“连接重置”,删除 TCB,进入 CLOSED。
  • 安全检查:不匹配 → 回复 RST。
  • SYN 检查:
    • 若 SYN 有效 → 更新 RCV.NXTIRS,并根据 ACK 情况进入 ESTABLISHED 或 SYN-RECEIVED。
  • 其他情况:丢弃。

3.10.7.4 其他状态(SYN-RECEIVED, ESTABLISHED, FIN-WAIT, CLOSE-WAIT, CLOSING, LAST-ACK, TIME-WAIT)

  • 序列号检查:丢弃不可接受段,必要时回复 ACK。
  • RST 检查:
    • 若在窗口外 → 丢弃。
    • 若匹配 RCV.NXT → 重置连接。
    • 若在窗口内但不匹配 → 回复质询 ACK。
  • 安全检查:不匹配 → 回复 RST,关闭连接。
  • SYN 检查:
    • TIME-WAIT → 可用时间戳算法处理。
    • 其他状态 → 回复质询 ACK 或 RST。
  • ACK 检查:
    • 更新 SND.UNA、SND.WND 等变量。
    • 若 ACK 确认 FIN → 进入下一状态(如 FIN-WAIT-2、TIME-WAIT、CLOSED)。
  • URG 检查:更新紧急指针,通知用户。
  • 数据处理:交付至接收缓冲区,更新 RCV.NXT,发送 ACK。
  • FIN 检查:
    • 通知用户“连接关闭”,更新状态(如 ESTABLISHED → CLOSE-WAIT,FIN-WAIT → CLOSING/TIME-WAIT)。

伪代码

参考4.4FreeBSD代码,TCP输入实际处理如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
void tcp_input()
{
检查校验和
寻找对应的PCB控制块
重置idle time为0,设置保活keepalive时间为2小时,这是协议规范,可以参考上面的文档
这里可以使用首部预测算法简化TCP处理流程,由于是FreeBSD操作系统实现的机制,在后面的源码讲解环节再详细展开
switch(tp->state) {
case TCPS_LISTEN:
如果SYN没有设置,接收新的连接请求
可以跳到后面的更新窗口信息的代码处了
case TCPS_SYN_SENT:
如果对方确认了我们发出的 SYN,那么连接就建立完成了
可以跳到后面的更新窗口信息的代码处了
}
遵循RFC 1323的处理(后面再补充)
如果数据部分超出了接收端当前通告的接收窗口(RCV.NXT 到 RCV.NXT + RCV.WND - 1 之间的范围),那么接收端必须丢弃(裁剪掉, trim)那些不在窗口范围内的数据字节,只保留窗口范围内的数据进行处理
if (RST flag set) {
处理依赖于状态
丢弃该包
}
if (ACK flag set) {
if (是SYN_RCVD状态)
双方几乎同时主动发起连接请求(SYN)并且完成握手
if (重复 ACK) {
启用快速恢复算法
我们知道如果是三次重复ACK会触发重传,但是在FreeBSD遵守的RFC 793中并没有这一条,这是在后面的RFC文档才规定的
(但是,最新进展(RACK, RFC 8985,RACK(Recent ACKnowledgment)算法基于时间和确认信息来判断丢包,也就是说, 三次重复ACK会触发重传这一条已经不适用了)
}
如果某个报文段(segment)是被用来测量往返时间 (RTT) 的,就用它的 ACK 来更新 RTT 估计器
根据收到的 ACK 更新拥塞窗口 (cwnd),具体增长方式取决于算法:慢启动:cwnd指数增长,拥塞避免:cwnd线性增长
把已经被确认的数据从发送缓冲区中移除
如果当前连接状态是 FIN-WAIT-1、CLOSING 或 LAST-ACK,就根据 ACK 的含义改变状态
}
更新窗口信息
处理URG标志。但是,现代应用几乎不用URG,因为它的语义模糊、实现差异大,很多防火墙甚至把带URG的段当作可疑流量
处理段中的数据,当一个 TCP 段到达时,如果它携带了应用层数据(payload),接收端需要检查它的序列号范围是否在接收窗口内。如果合 法,就把这部分数据交给 TCP 的接收逻辑,但不能马上交付给应用层,因为 TCP 必须保证 按序交付。
if (FIN flag is set) {
process depending on state
}
/*如果套接字开启了 SO_DEBUG 选项,就调用 tcp_trace() 记录调试信息*/
if (SO_DEBUG socket option) {
tcp_trace(TA_INPUT)
}
/*如果需要立即发送输出(例如确认 ACK、窗口更新、重传等),就调用 tcp_output() 发送*/
if (need output || ACK now) {
tcp_output()
}
return;
dropafterack:
先发一个 ACK(通常是对非法段的回应),然后调用 tcp_output() 生成一个 RST,再返回。
tcp_output() to generate RST
return;

dropwithreset:
直接调用 tcp_respond() 生成一个 RST 段作为回应,然后返回
tcp_respond() to generate RST
return;

drop:
if (SO_DEBUG socket option)
tcp_trace(TA_DROP);
return;
}


RFC 3.10.7 与 内核代码实现对比

场景 RFC 逻辑描述 内核代码常见处理路径
CLOSED 状态 收到非 RST 段 必须回复一个 RST(根据 ACK 位不同选择格式) dropwithreset:tcp_respond() 生成 RST
CLOSED 状态 收到 RST 段 丢弃 drop: → 静默丢弃
LISTEN 状态 收到 RST 无效,丢弃 drop:
LISTEN 状态 收到 ACK 非法,必须回复 RST dropwithreset:
LISTEN 状态 收到 SYN 合法,进入 SYN-RECEIVED,发送 SYN+ACK 正常路径:更新变量,调用 tcp_output()
LISTEN 状态 收到其他控制/数据 不应出现,丢弃 drop:
SYN-SENT 状态 收到不可接受的 ACK 回复 RST dropwithreset:
SYN-SENT 状态 收到可接受的 ACK+SYN 进入 ESTABLISHED 或 SYN-RECEIVED 正常路径:状态转移,tcp_output()
其他状态 收到不可接受的段 丢弃并可能回复 ACK dropafterack:tcp_output() 发送 ACK,再丢弃
其他状态 收到 RST(窗口外) 丢弃 drop:
其他状态 收到 RST(匹配 RCV.NXT) 复位连接,进入 CLOSED dropwithreset: 或直接状态转移
其他状态 收到 SYN(异常) RFC 793:错误,复位连接;RFC 5961:质询 ACK dropafterack:dropwithreset:
其他状态 收到数据/FIN/URG 按 RFC 状态机更新 RCV.NXT、RCV.UP、进入 CLOSE-WAIT 等 正常路径:process depending on state,必要时 tcp_output()

其实这里还非常简约,我们还是需要阅读源码才能明白网络协议栈真正的实现。