《TCP/IP 详解 卷1:协议》第13章:TCP连接管理
引言
TCP(传输控制协议)是面向连接的单播协议,数据传输前需先建立连接。需要管理多种连接状态(如建立、关闭、重启等)。
连接建立与终止是 TCP 的重要组成部分,也是其与 UDP 最大的区别之一。
TCP连接的建立与终止
一个TCP连接由一个四元组唯一标识:(源IP地址、源端口号、目标IP地址、目标端口号)。连接的每一端可看作一个“套接字(socket)”。
连接的建立
-
客户端 → 服务器
- 发送
SYN=1
,携带客户端初始序列号ISN(c)
- 表示“请求建立连接”
- 报文段1
- 发送
-
服务器 → 客户端
- 回复
SYN=1
和ACK=ISN(c)+1
- 同时发送服务器的初始序列号
ISN(s)
- 报文段2
- 回复
-
客户端 → 服务器
- 回复
ACK=ISN(s)+1
- 报文段3
- 回复
📌 三次握手的作用:
- 双方确认连接已建立
- 交换初始序列号(ISN)
- 协商连接选项(如窗口大小、时间戳等)
连接的关闭
任何一方都可以主动关闭连接:
-
主动关闭方 → 被动关闭方
- 发送
FIN=1
和ACK
,序列号为K
,确认对方数据序号为L
- 表示“我已发送完毕”
- 报文段1
- 发送
-
被动关闭方 → 主动关闭方
- 回复
ACK=K+1
- 表示“你的关闭请求我已收到”
- 通知应用层
- 报文段2
- 回复
-
被动关闭方 → 主动关闭方
- 发送自己的
FIN=1
和序列号=L
- 表示“我也发送完毕了”
- 报文段3
- 发送自己的
-
主动关闭方 → 被动关闭方
- 回复
ACK=L+1
- 表示“你的关闭请求我也收到了”
- 连接彻底关闭
- 报文段4
- 回复
✅ 一个连接的完整关闭需要 4 个报文段。
半关闭(Half-close)
TCP允许连接的双向传输独立关闭:
- 一方调用
close()
发送FIN
,表示不再发送,但仍可接收。 - 另一方仍可继续发送数据,直到也调用
close()
,最终完全关闭连接。
🔎 半关闭是一种少见但重要的机制,可用于某些单向传输场景。
初始序列号
TCP 是一个面向连接的协议,报文段可能乱序或延迟送达。为了解决这些问题,必须为每个连接引入独特的初始序列号(ISN):
- 防止旧连接残留数据干扰新连接(防 replay)
- 实现可靠的排序与确认机制
ISN 是一个 32 位的无符号整数,以时间为基础:每4微秒增加1,可视为系统中随时间递增的全局计数器。现代操作系统已广泛采用半随机或加密随机算法生成ISN。
Linux 示例:
- ISN = 随时间增加的计数器 + 加密偏移
- 加密偏移基于连接4元组生成的哈希
- 哈希使用加密散列函数 + 每隔5分钟刷新一次输入种子
- ISN的高8位可能用于保密或其他目的
Windows 示例:
- 使用类似 RC4 的伪随机生成器生成 ISN
TCP选项
TCP头部包含了多个选项。选项列表结束(End of Option List,EOL)、无操作(NoOperation,NOP)以及最大段大小(MaximumSegmentSize,MSS)是定义于原始TCP规范中的选项。以下是符合RFC
标准化描述的选项:
最大段大小选项(MSS:Maximum Segment Size)
MSS 是 TCP 协议中用于指定通信一方“能够接收的最大数据字段大小”(单位:字节),不包括 TCP 和 IP 头部, 目的是避免发送的数据报在路径上被分片。
MSS 选项只能在 TCP 的 SYN 报文段中使用,每一方在建立连接时通过 SYN 报文告诉对方自己的 MSS 限制, 该选项为 16 位整数(最大值 65,535)。
选择确认选项(SACK:Selective Acknowledgment)
TCP 默认采用累计确认(ACK)机制,不能反映乱序数据的接收情况,可能导致不必要的重传。选择确认(SACK)机制能解决这一问题。
在连接建立阶段通过 SYN 或 SYN+ACK 报文中的 SACK-Permitted 选项启用。一旦启用,接收方可以在收到乱序段时,发送 SACK 选项,告知对方哪些数据块已收到。
- 每个 SACK 块包含一对 32 位的序列号,表示已接收数据的起始与终止范围。
- 一个 SACK 选项长度为 (8n + 2) 字节,其中 n 是 SACK 块的数量,最大通常为 3 个(假设使用了时间戳选项)。
- SACK 选项只能在连接建立时协商是否启用,但之后可出现在任意报文中。
窗口缩放选项(Window Scale)
根据 [RFC1323],窗口缩放选项(WSCALE 或 WSOPT)用于将 TCP 的窗口大小从 16 位扩展到最多 30 位,以适应大带宽高延迟网络。
- TCP 头部中的窗口字段仍为 16 位。
- 窗口缩放通过一个单字节选项表示,值范围为 0~14。
- 实际窗口大小 = 通告窗口 × 2^R,其中 R 是对方通告的缩放因子。
窗口缩放选项只能出现在 SYN 报文中,连接建立后不能更改。双方需在各自的 SYN 报文中发送此选项,方可在对应方向启用。若主动方发送了窗口缩放,但未收到响应方的对应选项,则双方默认比例为 0(即不缩放)。每个方向可以有不同的比例因子。
时间戳选项与防回绕序列号(TSopt & PAWS)
每个 TCP 报文段中添加两个 4 字节字段:
TSval
:当前时间戳值(发送方生成)TSecr
:回显时间戳值(对方上次发送的 TSval)
用于估算TCP 往返时间(RTT),帮助设置重传超时(RTO)。报文头部增加 10 字节(2 字节类型/长度 + 8 字节数据)。不要求主机间时钟同步,只需保证 TSval 单调递增。
- 精准 RTT 估算(配合 RTO 算法,如 RFC6298)
- 更频繁、准确地采样 RTT,提高传输效率
- 搭配重传机制更智能地判断丢包
TCP 序列号为 32 位,可能在高速传输时“回绕”。时间戳用于检测并丢弃旧报文段。
规则: 接收方仅接受时间戳 ≥ 上次接收的有效时间戳的报文段。
时间戳选项不仅增强 RTT 估算能力,还提供了 防止旧报文干扰新连接的机制(PAWS),是 TCP 高速传输场景中重要的可靠性选项。
用户超时选项(User Timeout Option, UTO)
用户超时(USER_TIMEOUT)定义为 TCP 发送方愿意等待未确认数据的最长时间。UTO 选项允许通信双方在 TCP 报文中显式告知对方自己的超时设置,目的是增强容错能力,例如容忍临时的连接中断。
USER_TIMEOUT = min(U_LIMIT, max(ADV_UTO, REMOTE_UTO, L_LIMIT))
- ADV_UTO: 本地希望的超时
- REMOTE_UTO: 远端希望的超时
- U_LIMIT: 本地允许的最大值
- L_LIMIT: 本地允许的最小值(建议 ≥ 100 秒)
报文携带 UTO 的时机:
- 连接建立时的 SYN 报文
- 第一个非 SYN 报文段
- USER_TIMEOUT 数值发生变化时
认证选项(TCP Authentication Option, TCP-AO)
TCP-AO 是 TCP 协议的一个安全扩展选项,用于认证每个 TCP 报文段的真实性。它是为增强和替代早期的 TCP-MD5 机制([RFC2385])而设计的。
- 共享密钥机制:通信双方必须预先协商出一组共享的密钥。
- 加密散列算法(见第18章):对每个 TCP 报文段进行认证值计算。
- 带内信令:用于指示认证参数(如密钥)是否发生变化。
发送方:使用共享密钥生成通信密钥,使用该通信密钥对报文段进行加密散列计算,将认证值附加到报文段中。
接收方:使用相同密钥进行散列验证,检查报文段是否被篡改。
TCP 的路径最大传输单元发现(PMTUD)
路径最大传输单元(Path MTU, PMTU):两主机间路径上所有链路的最小 MTU 值。
通过路径最大传输单元发现(PMTUD),TCP 能避免 IP 分片,提高传输效率。
TCP 中的 PMTUD 过程
-
连接建立时
- TCP 选择初始的发送方最大段大小(SMSS):
- 取本地接口 MTU 与对方声明的 MSS 中的较小值;
- 若对方未声明 MSS,则默认为 536 字节(较少见)。
- 设置 IPv4 报文中的 DF(Don’t Fragment)位,避免分片。
- TCP 选择初始的发送方最大段大小(SMSS):
-
处理 PTB(Packet Too Big)消息
- 若收到 PTB 或 ICMP “需要分片但 DF 已设置” 消息:
- 减小段大小并重新发送。
- 若包含推荐的下一跳 MTU,则将其减去 IP + TCP 头部长度作为新 MSS。
- 若无推荐值,可采用二分法尝试。
- 若收到 PTB 或 ICMP “需要分片但 DF 已设置” 消息:
-
路径两端方向可能不同
- 每个方向的 PMTU 应独立处理。
黑洞问题
原因:中间设备(如防火墙/NAT)阻止 ICMP 消息转发;
表现:连接建立正常,但大数据包始终重传失败;
解决方式:
- TCP 实现尝试使用较小报文段;
- 启用黑洞探测机制(重传失败后自动降低 MSS);
- 建议部署者配置设备允许 ICMP PTB 消息通过。
TCP状态转换
在一个连接的不同阶段需要发送各种类型的报文段。这些决定TCP应该做什么的规则其实是由TCP所属的状态决定的。当前的状态会在各种触发条件下发生改变,例如传输或接收到的报文段、计时器超时、应用程序的读写操作,以及来自其他层的信息。这些规则可以概括为TCP的状态转换图。
TCP状态转换图
TCP 从 CLOSED
状态启动。
- 根据连接的发起方式:
- 主动打开:转为
SYN_SENT
- 被动打开:转为
LISTEN
- 主动打开:转为
主动打开方(客户端): CLOSED
→ SYN_SENT
→ ESTABLISHED
被动打开方(服务器):CLOSED
→ LISTEN
→ SYN_RCVD
→ ESTABLISHED
主动关闭方: ESTABLISHED
→ FIN_WAIT_1
→ FIN_WAIT_2
→ TIME_WAIT
→ CLOSED
被动关闭方: ESTABLISHED
→ CLOSE_WAIT
→ LAST_ACK
→ CLOSED
CLOSING
:当通信双方同时发送FIN
时出现。SYN_RCVD
→LISTEN
:仅当该状态来自LISTEN
且收到RST
报文时才会回退。LISTEN
→SYN_SENT
:合法但很少使用,不被 Berkeley 套接字支持。
主动关闭状态集合:
FIN_WAIT_1
FIN_WAIT_2
TIME_WAIT
被动关闭状态集合:
CLOSE_WAIT
LAST_ACK
TIME_WAIT 状态
TIME_WAIT
状态也称为 2MSL 等待状态。在该状态中,TCP 将等待两倍于最大段生存期(MSL, Maximum Segment Lifetime)的时间,也称加倍等待。
每个实现必须指定一个 MSL 值,它表示报文段在网络中被允许存在的最长时间。TIME_WAIT 的用途:
- 保证最终 ACK 能被对方接收:
当 TCP 执行主动关闭并发送最后一个 ACK
后,需要等待 2MSL,以便对方未收到 ACK
时重发 FIN
,我们可以重新发送 ACK
。
注意:
ACK
本身不消耗序列号,不会被 TCP 重传。但对方的FIN
消耗序列号,会被重传,因此必须准备再次发送ACK
。
- 避免连接混淆:
处于 TIME_WAIT
的连接不能立即被复用,防止旧连接的延迟报文被误判为新连接的内容。其定义了一条不可重用的四元组:(客户端 IP、端口、服务端 IP、端口)
如果新连接使用了相同四元组,必须满足以下至少一个条件才能避免混淆:
- 已过
2MSL
等待时间; - 新连接初始序列号明显高于旧连接;
客户端主动关闭后通常进入 TIME_WAIT
。如果立刻重启客户端,它无法使用相同本地端口。一般不会有异常情况,因为客户端常用临时端口,并不关心具体数值,且这些端口通常由操作系统自动分配,一般不会重复。
静默时间的概念
在本地与远端的 IP 地址与端口号完全相同的情况下,2MSL
状态可以防止新的连接误将前一连接的延迟报文段当作自己的数据。
然而,这种保护机制依赖于参与 TIME_WAIT
状态的主机在此期间未崩溃或重启。如果某台主机在 MSL
时间内崩溃并重启,并继续使用相同的 IP 与端口号,则可能出现以下风险:
- 重启后的新连接可能接收到旧连接中的延迟报文段;
- TCP 将这些旧数据错误地当作新连接的一部分;
- 即使新连接使用了不同的初始序列号,这些报文也可能通过校验。
为了解决这个问题,[RFC0793] 要求:
在主机崩溃或重启之后,应等待 至少一个
MSL
的时间 再创建新连接,该等待时间称为“静默时间(quiet time)”。
尽管有此要求,但实际中:
- 大多数系统在崩溃重启后需要的时间远超过
MSL
,所以该问题 较少发生; - 现代应用程序普遍使用 校验和、加密 等机制进行数据校验,能够检测出错误数据;
因此,静默时间机制在理论上仍然重要,但在实际中其必要性已被系统恢复延迟与上层校验机制部分取代。
FIN_WAIT_2 状态
当 TCP 连接的一端已经发送 FIN
并收到了对方的确认,此时该端会进入 FIN_WAIT_2
状态。此时的含义是:
- 本端已经完成发送数据;
- 等待对方发送自己的
FIN
,以完成连接的关闭。
如果出现半关闭(即本端关闭了发送方向,但仍期望接收数据),则连接会持续处于 FIN_WAIT_2
状态,直到:
- 对方的应用程序完成关闭;
- 对方发送
FIN
报文段; - 本端收到该
FIN
,并进入TIME_WAIT
状态。
如果对方迟迟不关闭(即未发送 FIN
),那么连接可能永远停留在 FIN_WAIT_2
,从而造成资源泄漏。对应地,对方可能永远处于 CLOSE_WAIT
状态,直到应用层决定关闭。为避免无限等待,大多数实现提供超时机制。
重置报文段
TCP头部有RST位字段。将该字段置位的报文段称为“重置报文段”或简称“重置”。当TCP发现一个到达的报文段对于相关连接(由该报文段的TCP和IP头部四元组指定的连接)是不正确的,通常会发送一个重置报文段。
重置报文段一般会导致TCP连接的快速拆卸。下面通过具体场景说明重置报文段的作用。
针对不存在端口的连接请求
当连接请求到达本地但目的端口没有相关进程侦听时,会产生重置报文段。这与之前“连接被拒绝”的错误消息对应。UDP协议对此类情况采用ICMP目的地不可达(端口不可达)消息,而TCP协议则使用重置报文段完成类似功能。
重点分析重置报文段(第2个报文段)中的序列号和ACK号。因为到达的SYN报文段未设置ACK位,重置报文段的序列号为0,而ACK号为收到的初始序列号加上该报文段中的数据长度。虽然报文中无数据,SYN位逻辑上占用一个序列号空间,所以ACK号等于初始序列号 + 0 + 1。
终止一条连接
TCP连接的正常终止方法是通信一方发送一个FIN报文段,这种方式称为有序释放,保证之前所有排队数据都已发送,避免数据丢失。
也可以通过发送重置(RST)报文段来替代FIN终止连接,这种方式称为终止释放。
使用重置报文段终止连接具有两个特性:
- 任何待发送的数据都会被立即丢弃,连接终止后发送RST报文;
- 接收RST的一方能明确知道连接是被非正常终止而非正常关闭。为了支持这种终止方式,套接字API提供了通过将“逗留于关闭”(SO_LINGER)选项设置为0实现的功能,意味着不会在关闭时等待数据确认到达对端。
总结来看,重置报文段可作为一种快速断开TCP连接的机制,常用于异常终止场景、
半开连接
当一端在未通知另一端的情况下关闭或崩溃,TCP连接进入半开状态。此时,正常工作的那一端无法检测到对端已失效,只要不发送数据,连接仍维持ESTABLISHED状态。
常见原因包括主机突然断电而非正常关机。例如,远程登录客户端关闭电源后,服务器仍认为连接存在,直到新会话启动,导致服务器存在大量半开连接。TCP的keepalive机制(第17章介绍)可用于检测此类失效。
时间等待错误
TIME_WAIT状态设计用于确保关闭连接后,任何滞留的数据报文都能被丢弃。此期间,TCP通常不执行操作,只需维持状态直到2MSL计时结束。
然而,如果TIME_WAIT期间收到来自该连接的报文,尤其是RST报文,可能导致TIME_WAIT状态被破坏,连接提前关闭,这称为时间等待错误(TIME_WAIT Assassination)
客户端(主动关闭方)完成关闭后进入TIME_WAIT,服务器(被动关闭方)连接关闭并清理状态。若客户端接收到一个序号和ACK号都很旧的数据报文,客户端会回复带最新序号和ACK号的ACK报文。服务器此时因无连接状态,会返回RST报文,客户端接收后错误地提前关闭连接。
解决方案:多数系统在TIME_WAIT状态时忽略RST报文,防止连接被误破坏,确保可靠关闭过程。