Skip to content

3.4. 序列号

TCP 设计的一个基本概念是:经 TCP 连接发送的每个数据八位组都带有序列号。每个八位组都有编号,因而都能得到确认。TCP 的确认机制采用累积确认:确认序列号 X 即表示序号小于 X 的所有八位组均已收到。这一机制使得重传发生时可以直接检测重复数据。报文段内各八位组的编号规则如下:紧随首部的第一个数据八位组序号最小,其后各八位组依次编号。

必须牢记:实际的序列号空间虽然很大,但终归有限,其范围为 02³² - 1。由于空间有限,所有涉及序列号的算术运算都必须按 2³² 取模。采用无符号运算,可以在序列号从 2³² - 1 回绕到 0 时仍保持正确的大小关系。计算机中的模运算存在一些细节问题,编程实现这些比较时务必谨慎。符号 =< 表示“小于或等于”(按 2³² 取模)。

TCP 实现必须执行的典型序列号比较包括:

  1. 判断确认号是否指向已经发送但尚未确认的序列号。
  2. 判断报文段占用的所有序列号是否均已确认(例如据此从重传队列中移除报文段)。
  3. 判断传入报文段是否包含期望的序列号,即该报文段是否与接收窗口“重叠”。

发送数据后,TCP 端点会收到确认。处理确认需要以下比较量:

变量含义
SND.UNA最早尚未确认的序列号。
SND.NXT下一个要发送的序列号。
SEG.ACK接收方 TCP 对等端发来的确认号,即接收方期望的下一个序列号。
SEG.SEQ报文段的第一个序列号。
SEG.LEN报文段数据占用的八位组数,其中计入 SYNFIN
SEG.SEQ+SEG.LEN-1报文段的最后一个序列号。

满足下列不等式的确认称为“可接受确认”(acceptable ack):

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

如果重传队列中某报文段的序列号与长度之和小于或等于传入报文段的确认值,则该报文段即被完全确认。

接收数据时需要以下比较量:

变量含义
RCV.NXT传入报文段中期望的下一个序列号,也是接收窗口的左侧/下边界。
RCV.NXT+RCV.WND-1传入报文段中期望的最后一个序列号,也是接收窗口的右侧/上边界。
SEG.SEQ传入报文段占用的第一个序列号。
SEG.SEQ+SEG.LEN-1传入报文段占用的最后一个序列号。

如果满足下列任一条件,则认为报文段占用了有效接收序列空间的一部分:

text
RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND

或:

text
RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND

第一式检查报文段开头是否落在窗口内,第二式检查报文段末尾是否落在窗口内;只要通过其中一项检查,即说明该报文段在窗口内含有数据。

实际情况还要稍微复杂一些。由于存在零窗口和零长度报文段,传入报文段的可接受性共分四种情况:

报文段长度接收窗口测试
00SEG.SEQ = RCV.NXT
0>0RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND
>00不可接受
>0>0RCV.NXT =< SEG.SEQ < RCV.NXT+RCV.WND,或 RCV.NXT =< SEG.SEQ+SEG.LEN-1 < RCV.NXT+RCV.WND

表 5:报文段可接受性测试。

接收窗口为零时,除 ACK 报文段外不应接受任何报文段。因此,TCP 实现可以在持续发送数据并接收确认的同时,将接收窗口保持为零。即使接收窗口为零,TCP 接收方也必须处理所有传入报文段中的 RSTURG 字段(MUST-66)。

TCP 的编号机制还用于保护某些控制信息:将部分控制标志隐式纳入序列空间,使其能够重传和确认而不致混淆,即同一控制信息只会被处理一次。控制信息并不实际占用报文段的数据空间,因此需要规定如何为其隐式分配序列号。只有 SYNFIN 需要这种保护,且二者仅用于连接的打开和关闭。就序列号而言,SYN 位于其所在报文段的第一个实际数据八位组之前,FIN 位于其所在报文段的最后一个实际数据八位组之后。报文段长度(SEG.LEN)包括数据以及占用序列空间的控制信息。如果报文段带有 SYN,则 SEG.SEQ 即为该 SYN 的序列号。

3.4.1. 初始序列号选择

连接由一对套接字定义。连接可以被复用;连接的新实例称为该连接的一个 incarnation(实例)。由此产生的问题是:“TCP 实现如何识别来自该连接先前实例的重复报文段?”当连接被快速打开又关闭,或者连接因内存丢失而中断、随后重新建立时,这个问题尤为突出。为此,TIME-WAIT 状态限制了连接复用的速率,而下文所述的初始序列号选择机制则进一步避免无法区分传入分组属于哪个连接实例的问题。

为避免混淆,必须确保:在早期实例用过的相同序列号仍可能残留于网络时,不得再使用某个连接实例的报文段;即使 TCP 端点已完全失去对所用序列号的记忆,也应尽力做到这一点。建立新连接时,由初始序列号(ISN)生成器选出一个新的 32 位 ISN。如果带外攻击者能够预测或猜测 ISN,就会带来安全问题(参见 RFC 6528)。

TCP 初始序列号取自一个单调递增直至回绕的数字序列,通常宽泛地称为“时钟”。该时钟是一个 32 位计数器,典型情况下至少约每 4 微秒递增一次;它既不要求是真实时间,也不要求精确,且无需跨重启持久化。时钟分量的作用是保证所生成的 ISN 在最大报文段生存期(MSL)内唯一:该时钟约每 4.55 小时循环一周,远长于 MSL。现代网络速率很高,连接有可能在一个 MSL 之内快速推进序列空间,导致序列号重叠;对于这类网络,建议实现后文第 3.4.3 节所述的时间戳选项。

TCP 实现必须使用上述类型的“时钟”,以时钟驱动方式选择初始序列号(MUST-8),并应使用下式生成初始序列号:

text
ISN = M + F(localip, localport, remoteip, remoteport, secretkey)

其中,M 是 4 微秒计时器,F() 是以连接标识参数(localip, localport, remoteip, remoteport)和秘密密钥(secretkey)为输入的伪随机函数(PRF)(SHLD-1)。F() 必须无法从外部计算(MUST-9),否则攻击者仍可能根据其他连接所用的 ISN 猜测序列号。PRF 可通过对 TCP 连接参数与某些秘密数据的拼接结果执行密码学哈希来实现。关于具体哈希算法的选择及秘密密钥数据管理的讨论,见 RFC 6528 第 3 节

每个连接都有发送序列号和接收序列号。初始发送序列号(ISS)由发送数据的 TCP 对等端选择;初始接收序列号(IRS)在建立连接的过程中获知。

建立或初始化连接时,两个 TCP 对等端必须同步彼此的初始序列号。同步通过交换建连报文段完成,这些报文段携带名为 SYN(synchronize,同步)的控制位以及初始序列号。作为简写,携带 SYN 位的报文段也称为 SYN。因此,这一方案既需要合适的初始序列号选择机制,也需要一套略显复杂的握手来交换 ISN。

同步要求每一方发送自己的初始序列号,并收到远端 TCP 对等端对它的确认;同时,每一方还必须收到远端对等端的初始序列号,并发送确认。

text
1) A --> B  SYN 我的序列号是 X
2) A <-- B  ACK 你的序列号是 X
3) A <-- B  SYN 我的序列号是 Y
4) A --> B  ACK 你的序列号是 Y

由于步骤 2 和步骤 3 可以合并在同一条消息中,这个过程称为三次(或三消息)握手(3WHS)。

之所以需要 3WHS,是因为序列号并未绑定到网络中的全局时钟,而且不同 TCP 实现可能采用不同机制选择 ISN。除非记住了该连接最后使用的序列号(这并不总是可能),否则第一个 SYN 的接收方无法判断该报文段是否为旧的重复报文段,因此它必须要求发送方验证这个 SYN。三次握手以及时钟驱动 ISN 选择的优点,见相关连接管理讨论。

3.4.2. 判断何时保持静默

这里存在一个理论上的问题:主机重启后,如果复用相同的端口号和序列空间,网络中的旧报文段就可能与新报文段混淆,造成数据损坏。下面的“静默时间”概念即用于处理这一问题;本文保留这段讨论,是因为它在某些场景下仍有参考价值,但目前大多数实现并不认为必须执行。这个问题在 TCP 的早期历史中更为重要。在今天的互联网实际使用中,出错条件已足够罕见,可以安全地忽略。原因包括:(a)ISS 和临时端口的随机化降低了重启后复用端口号与序列号的可能性;(b)随着链路提速,互联网的有效 MSL 已经缩短;(c)重启本身通常就耗时超过一个 MSL。

TCP 实现必须避免生成这样的报文段:其序列号可能与网络中残留的旧报文段重复。为此,TCP 端点在启动时,或从已丢失序列号记忆的状态中恢复时,必须先静默一个 MSL,然后才能分配序列号。本文将 MSL 取为 2 分钟。这是一个工程选择,如实践表明确有必要,可以修改。需要注意的是,如果 TCP 端点以某种方式重新初始化,但仍保留着正在使用的序列号记忆,则无需等待;它只需确保使用大于近期用过的序列号即可。

3.4.3. TCP 静默时间概念

主机若因任何原因丢失了各条活动连接(即尚未关闭的连接)最后发送序列号的记忆,则应在发送任何 TCP 报文段之前,至少等待其所在互联网系统约定的 MSL。下面解释这一规定的原因。TCP 实现者可以违反“静默时间”的限制,但须承担相应风险:某些接收方可能把旧数据当作新数据接受,或把新数据误当作旧的重复数据而拒绝。

TCP 端点每形成一个报文段并将其放入源主机的网络输出队列,都会消耗一部分序列号空间。TCP 的重复检测与排序算法依赖于报文段数据与序列空间之间的唯一绑定:在绑定到某些序列号的数据已交付并经接收方确认、且其所有重复副本已从互联网中“排空”之前,序列号不应循环经过全部 2³² 个值。倘若没有这一前提,两个不同的 TCP 报文段就可能被分配相同或重叠的序列号,使接收方无从判断哪些数据是新的、哪些是旧的。每个报文段绑定的连续序列号数量,等于该报文段中数据八位组数量与 SYN/FIN 标志数量之和。

正常情况下,TCP 实现会记录下一个要发送的序列号以及最早等待确认的序列号,从而避免在序列号的首次使用得到确认之前错误地复用它。但这还不足以保证旧的重复数据已从网络中排空,因此序列空间要设计得足够大,以降低游荡的重复报文到达时造成问题的概率。以 2 Mbit/s 的速率耗尽 2³² 个数据八位组的序列空间需要 4.5 小时。由于网络中的最大报文段生存期很可能不超过几十秒,即使数据速率提升到数十 Mbit/s,也可以认为这足以为可预见的网络提供保护。在 100 Mbit/s 下,循环时间为 5.4 分钟,可能略短,但仍在合理范围内。如今还存在更高的数据速率,其影响见本小节最后一段。

然而,如果源 TCP 端点不记得某条连接最后使用的序列号,TCP 的基础重复检测和排序算法就可能失效。例如,假设 TCP 实现让所有连接都从序列号 0 开始;主机重启后,TCP 对等端可能重建早期的连接(可能先经历半开连接处理),并发送这样的分组:其序列号与网络中仍残留的报文段相同或重叠,而这些报文段来自同一连接的早期实例。如果不知道某条连接曾使用过哪些序列号,TCP 规范建议源端在发送该连接的报文段之前先等待 MSL 秒,以便早期连接实例的报文段从系统中排空。

即使主机能够记住当前时间并用它来选择初始序列号,也无法免除这个问题;换言之,即便每个新连接实例都依据当前时间选择 ISN,问题依然存在。

例如,假设某条连接从序列号 S 开始建立。假设该连接很少使用,最终初始序列号函数 ISN(t) 取到了某个值 S1,而 S1 恰好等于该 TCP 端点在这条连接上最后发送的报文段序列号。现在假设主机恰在此刻重启,并建立该连接的新实例,选择的初始序列号为 S1 = ISN(t)——正是旧实例最后使用过的序列号!如果恢复得足够快,网络中携带 S1 附近序列号的旧重复报文可能到达,并被新实例的接收方当作新分组处理。

问题在于,恢复中的主机可能不知道自己在重启期间停机了多久,也不知道系统中是否仍残留更早连接实例的旧重复报文。

解决方法之一,是在重启恢复后有意等待一个 MSL 再发送报文段,这就是“静默时间”规范。希望避免等待、并愿意承担新旧分组可能混淆之风险的主机,可以选择不等待“静默时间”。实现者可以让 TCP 用户按连接选择重启后是否等待,也可以对所有连接统一执行“静默时间”。显然,即使用户选择等待,只要主机已经“运行”了至少 MSL 秒,也无需再等待。

总结来说:每个发出的报文段都会占用序列空间中的一个或多个序列号;在 MSL 秒过去之前,这些序列号都处于“忙”或“使用中”状态。主机重启时,任何仍可能在途的报文段,其数据八位组以及 SYN/FIN 标志都会在时空上占据一块区域。如果新连接启动过早,所用的序列号恰好落在上一连接实例中可能仍在途的报文段所占的时空区域内,序列号就会重叠,从而使接收方产生混淆。

在高性能场景下,序列空间的循环时间会远短于上述以 Mbit/s 量级为基础的基础 TCP 设计所考虑的数值。在 1 Gbit/s 下,循环时间为 34 秒;10 Gbit/s 下仅为 3 秒;100 Gbit/s 下约为三分之一秒。在这类更高性能的场景中,TCP 时间戳选项与回绕序列号保护(PAWS,参见 RFC 7323)提供了检测并丢弃旧重复报文所需的能力。

Licensed under CC BY-NC-SA 4.0.