TCP 设计
可靠传输单个数据包
数据包从发送方传输到接收方所需的时间称为单程延迟。数据包从发送方到接收方的传输时间,加上回复数据包从接收方到发送方的传输时间,称为往返时间(RTT)。

让我们通过设计一个简化的协议来直观理解如何可靠地发送单个数据包。
发送方尝试发送一个数据包。发送方如何知道数据包是否被成功接收呢?

接收方可以发送一个确认(ack) 消息,确认已收到数据包。
如果数据包丢失了会发生什么?
如果数据包丢失,我们可以重新发送它。但我们如何知道何时重新发送呢?

发送方可以维持一个计时器。当计时器到期时,我们就可以重新发送数据包。
当发送方收到确认后,就可以取消计时器,无需再重新发送数据包。
如果确认消息丢失了会发生什么?

该协议无需修改仍能正常工作。发送方会因未收到确认而超时,然后重新发送数据包,直到确认消息成功发送为止。在这种情况下,接收方会收到同一个数据包的两个副本,但这没关系。接收方可以识别出重复的数据包并将其丢弃。
计时器应该如何设置?如果计时器设置得太长,数据包可能需要比必要的时间更长才能被发送。如果计时器设置得太短,数据包可能会在不需要重新发送的时候被重发。计时器设置不当会影响传输效率。
一个合适的计时器时长应该是往返时间。这是发送方期望收到确认的时间,所以如果到那时还没收到确认,发送方就应该重新发送数据包。
在实际中,估计往返时间并不容易。往返时间会因数据包在网络中所走的路径不同而变化,即使是同一条路径,往返时间也会受到该路径上的负载和拥塞情况的影响。
估计往返时间的一种方法是测量发送数据包到收到该数据包的确认之间的时间。我们可以从每个发送的数据包中获得一个往返时间的测量值,并应用某种算法(例如指数移动平均)将这些测量值合并为一个往返时间估计值。我们的算法还必须考虑到数据包重传的情况(测量值的方差)。
在实际操作中,操作人员通常倾向于将计时器设置得稍长一些。如果计时器太短,超时现象不断发生,那么连接的性能可能会很差(不断地重新发送数据包)。
如果数据包的比特发生损坏会怎样?

我们可以在传输层头部添加一个校验和(与 IP 层的校验和不同)。当接收方发现数据包损坏时,它可以采取两种做法:一种是接收方明确发送一个否认确认(nack),告诉发送方重新发送数据包。
另一种是接收方丢弃损坏的数据包,不做任何处理(不发送确认或否认确认)。然后,发送方会超时并重新发送数据包。

这两种方法(否认确认或等待超时)都是可行的,但 TCP 采用了后者(等待超时),没有实现否认确认。
如果数据包被延迟了会怎样?

无需进行任何修改。如果延迟很长,发送方可能会在确认到达之前超时。发送方会重新发送数据包(因此接收方可能会收到两个重复的数据包),发送方也可能会收到两个确认,但这都没关系。
如果发送方发送了一个数据包,但该数据包在网络中被复制,接收方收到了两个副本会怎样?

无需进行任何修改。接收方会发送两个确认,但发送方和接收方都能安全地处理重复的确认。
注意:从这个简化的协议中,我们可以看到接收方有时会收到两个数据包副本。如果某个链路正在执行可靠性协议,该链路的接收端可能会收到两个副本。通常,重复的数据包会被丢弃,只有一个数据包会被转发到目的地。但是,如果在两个副本到达之间路由器崩溃并重启,路由器可能会将两个副本都转发到目的地。
总之,单个数据包的可靠性协议如下:
如果你是发送方:发送数据包,并设置一个计时器。如果在计时器到期前没有收到确认,重新发送数据包并重置计时器。当收到确认后,停止并取消计时器。
如果你是接收方:如果收到未损坏的数据包,发送一个确认。(如果多次收到该数据包,可能会发送多个确认。)
这个例子中的核心思想也适用于后续的协议:校验和(用于检测损坏)、确认、重新发送数据包以及超时。
请注意,这个协议保证至少一次交付,因为可能存在重复。
可靠传输多个数据包
如何将这个协议扩展到传输多个数据包呢?

我们可以对每个数据包遵循相同的传输规则(超时则重发)。为了区分不同的数据包,我们可以给每个数据包附加一个唯一的序列号。每个确认都与特定的数据包相关联。序列号还可以帮助我们对到达顺序错乱的数据包进行重新排序。
发送方何时发送每个数据包呢?最简单的方法是停等协议,即发送方等待第 i 个数据包的确认后再发送第 i+1 个数据包。这种方法确实能提供可靠性,但速度非常慢。每个数据包至少需要一个往返时间才能发送(如果数据包丢失或损坏,所需时间会更长)。

这种协议在对效率要求不高的小场景中可能适用,但对于互联网来说太慢了。我们如何提高速度呢?

我们可以并行发送数据包。更具体地说,我们可以在等待确认到达的同时发送更多的数据包。当一个数据包被发送但尚未收到相应的确认时,我们称该数据包处于在途状态。
最简单的方法是立即发送所有数据包,但这可能会使网络不堪重负(例如,连接到计算机的链路可能带宽有限)。
基于窗口的算法
一次发送一个数据包太慢,但一次发送所有数据包又会使网络过载。为了解决这个问题,我们会设置一个限制值 W,规定任何时候在途的数据包数量不能超过 W。这就是基于窗口的协议的核心思想,其中 W 是窗口大小。
如果 W 是最大在途数据包数量,那么发送方可以开始时发送 W 个数据包。当收到一个确认后,就发送下一个数据包。

应该如何选择 W 呢?
我们希望充分利用可用的网络容量(“填满管道”)。如果 W 太小,我们就无法充分利用可用的带宽。
然而,我们不希望使链路过载,因为其他人可能也在使用该链路(拥塞控制)。我们也不希望使接收方过载,接收方需要接收和处理来自发送方的所有数据包(流量控制)。
窗口大小:填满管道
让我们关注第一个往返时间,即从发送第一个数据包到收到第一个确认的时间。假设这段时间是 5 秒(这不是一个实际的数字,仅用于举例)。另外,假设 outgoing 链路允许发送方每秒发送 10 个数据包(这也不是一个实际的数字)。那么,在第一个往返时间内,发送方总共应该能够发送 50 个数据包。因此,50 是一个合理的窗口大小,这样发送方就能一直发送数据包,不会处于空闲状态。
如果我们将 W 设置为小于 50,那么发送方会在收到第一个确认之前就发送完所有初始数据包。然后,发送方将被迫在等待确认到达时处于空闲状态,部分网络带宽就会被浪费。更一般地说,我们希望发送方在整个往返时间内都在发送数据包。

在这个例子中,W 是 4。但是,发送完 4 个数据包后,发送方在等待第一个确认到达时处于空闲状态,浪费了带宽。

在这个例子中,W 被增大,使得发送方能够持续发送数据包。当第一个确认到达时,发送方刚好快要达到 W 个在途数据包的限制,并且能够在更多确认到达时立即继续发送数据包。
到目的地的路径可能有多个链路,每个链路的容量不同。设 B 为路径上最小(瓶颈)链路的带宽。我们发送数据包的速度不应超过 B,以避免链路过载。我们也不希望发送速度低于 B(即我们希望始终以 B 的速率发送)。
另外,假设 R 是发送方和接收方之间的往返时间。我们可以将 R 乘以 B,得到往返时间内可以发送的数据包总数。(我们每秒可以发送 B 个数据包,持续 R 秒。)这就告诉我们窗口大小(以数据包为单位)。
实际上,B 的单位是比特每秒,而不是数据包每秒。当我们将 R 乘以 B 时,得到的是往返时间内可以发送的比特数。(B 比特每秒,持续 R 秒。)这告诉我们窗口大小(以字节为单位)。总的来说,我们可以写成:
W × 数据包大小 = R × B
等式左边表示窗口期间发送的字节数(W 个数据包,乘以每个数据包的字节数),等式右边表示往返时间内可以发送的字节数。
举一个具体的例子,假设往返时间 = 1 秒,B=8 兆比特 / 秒。那么,R×B 是 8 兆比特,即 1 兆字节,也就是 1,000,000 字节。
如果我们的数据包大小是 100 字节,那么我们希望 W=10,000 个数据包,这样我们就能充分利用带宽,在往返时间内发送 1,000,000 字节。


我们也可以从链路本身的角度来描述窗口大小。在这张图中,我们展示了特定链路的 outgoing 和 incoming 方向。当发送方以最大容量通过链路发送数据包时,第一个确认会在第 6 个数据包发送后立即到达。因此,我们的窗口大小应该是 6。
注意,窗口大小不是 3。当发送第 6 个数据包时,有 3 个数据包正在发送中,但还有 3 个数据包的确认尚未到达,所以总共有 6 个在途数据包。
如果我们将窗口大小设置为 3,那么在 1、2、3 号数据包的确认在途期间,outgoing 管道就会处于未使用状态。

注意,确认不会填满整个 incoming 管道,因为确认数据包除了确认收到某个数据包外,不包含任何实际数据。
窗口大小:流量控制
考虑接收方操作系统中的传输层协议。接收方可能会收到乱序的数据包,但字节流抽象要求数据包按顺序交付。这意味着传输层实现必须通过缓冲(将它们保存在内存中)来保留乱序的数据包,直到轮到它们被交付。
例如,假设接收方已经接收并处理了 1 号和 2 号数据包。然后,接收方收到了 4 号和 5 号数据包。传输层实现不能立即将 4 号和 5 号数据包交付给应用程序。相反,我们必须等待 3 号数据包到达,同时,我们必须将 4 号和 5 号数据包存储在传输层实现的内存中。

然而,内存不是无限的,接收方用于存储乱序数据包的缓冲区大小是有限的。接收方必须将每个乱序数据包存储在内存中,直到中间缺失的数据包到达。如果连接存在大量的数据包丢失和重排序,接收方可能会耗尽内存。
流量控制确保接收方的缓冲区不会耗尽内存。为了实现这一点,接收方会告诉发送方缓冲区中还剩多少空间。接收方缓冲区中剩余的空间量称为通告窗口。在确认中,接收方会说 “我已经收到了这些数据包,我的缓冲区还有 X 字节的空间来容纳数据包。”

当发送方了解到通告窗口后,会相应地调整其窗口。具体来说,在途数据包的数量不能超过接收方的通告窗口。如果接收方说 “我的缓冲区有足够的空间容纳 5 个数据包”,那么发送方必须将窗口设置为最多 5 个数据包(即使带宽可能允许更多的在途数据包)。
窗口大小:拥塞控制
回想一下,为了充分利用带宽,发送方会将窗口大小设置为完全占用瓶颈链路的带宽。例如,如果瓶颈链路的带宽是 1Gbps,我们会设置窗口大小,使得发送方在整个往返时间内持续以 1Gbps 的速率发送数据(没有空闲时间)。
实际上,1Gbps 的链路不太可能只被一个连接使用。其他连接也可能在使用该链路的容量。发送方不应占用该链路的全部带宽,而只应占用其应得的那部分带宽容量。

但是,每个连接应分得多少带宽呢?
假设我们有两个连接,分别使用 400MBps 和 250MBps。如果另一个连接也尝试使用同一条链路,那么这个新连接的发送方可能分得剩余的 350MBps。但另一种观点是,这种带宽分配不公平,所以或许每个连接都应该调整为使用 333MBps。
确定和计算每个连接应得的带宽量是拥塞控制的目标。拥塞控制算法是一个独立的完整主题(将在下一节介绍)。目前,我们先不深入讨论拥塞控制,只说作为传输层的一部分,发送方会执行拥塞控制算法,其作用是动态计算连接在瓶颈链路上的发送方份额。
该算法的结果是发送方的拥塞窗口(cwnd)。目前,你只需要知道该算法会输出这个数值,它代表在不使链路过载、同时与其他连接公平共享带宽的情况下,实现性能最大化的带宽。
现在我们知道了如何设置窗口以实现前面的三个目标。为了充分利用网络容量,我们会根据往返时间和瓶颈链路带宽来设置窗口大小。
为了避免使接收方过载,我们会根据接收方的通告窗口来限制窗口大小。为了避免使链路过载,我们会根据发送方的拥塞窗口(发送方执行拥塞控制算法输出的某个数值)来限制窗口大小。
为了满足所有三个目标,我们会将窗口大小设置为这三个值中的最小值。实际上,请注意拥塞窗口(第三个目标)总是小于或等于充分利用带宽时的窗口大小(第一个目标)。如果没有拥塞,我们会充分利用所有瓶颈带宽,所以这两个数值会相等。在大多数情况下,拥塞会迫使我们使用小于瓶颈带宽的容量,所以第三个数值会小于第一个数值。不存在拥塞窗口带宽大于瓶颈带宽的情况。
此外,在实际中,很难发现瓶颈带宽。发送方必须以某种方式遍历网络拓扑,了解每个链路的带宽。由于第一个数值难以获取,且总是大于或等于第三个数值,我们可以将窗口大小设置为后两个数值中的最小值(忽略第一个数值)。窗口大小是发送方的拥塞窗口和接收方的通告窗口中的最小值。
更智能的确认
到目前为止,每个确认数据包都对应一个单独的数据包。我们能比逐个确认做得更好吗?逐个确认存在哪些问题呢?

在这个例子中,其中一个确认丢失了,尽管接收方成功接收了所有 4 个数据包。这将迫使发送方重新发送 2 号数据包,即使这次重发是不必要的。
我们可以在每次发送确认时,列出所有已收到的数据包,而不是只为特定的一个数据包发送确认。这称为全信息确认。

在这个例子中,确认信息现在是:“我收到了 1 号数据包”、“我收到了 1 号和 2 号数据包”、“我收到了 1 号、2 号和 3 号数据包”、“我收到了 1 号、2 号、3 号和 4 号数据包”。
即使第二个确认丢失了,第三个和第四个确认也能帮助发送方确认 2 号数据包已被接收,因此无需再重新发送 2 号数据包。
随着发送的数据包越来越多,已接收数据包的列表会变得很长。全信息确认可以通过以下方式简化这一信息:“我已收到所有编号 up to 12 的数据包。此外,我还收到了 14 号和 15 号数据包。” 形式上,我们给出最高的累积确认(所有小于或等于该编号的数据包都已收到),再加上任何其他已收到的数据包列表。
即使有了这种简化,全信息确认也可能会很长。例如,如果所有偶数编号的数据包都丢失了,那么最高的累积确认将始终是 1(我们只能说 up to 1 的所有数据包都已收到,因为 2 号数据包丢失了)。其余已收到的数据包必须列在列表中,如 [1, 3, 5, 7, 9, …],这可能会变得很长。

在逐个确认(每个确认丢失都会迫使重发)和全信息确认(确认可能很长)之间的一种折中方案是累积确认,即我们只提供最高的累积确认,而舍弃额外的列表。形式上,确认信息会编码一个最高序列号,该序列号及其之前的所有数据包都已被接收。

在这个偶数编号数据包丢失的例子中,每个累积确认都会说:“我已收到 up to 并包括 1 号的所有数据包。” 尽管 3 号和 5 号数据包已被接收,但累积确认不会编码这一信息,因为它只确认从 1 号开始的连续数据包的接收情况。
累积确认不再有扩展性问题(我们总是发送一个数字,而不是一个数字列表)。然而,它们可能会更模糊,如上面的情况。发送方看到三个确认都在说 “我已收到 up to 并包括 1 号的所有数据包”,可以推断出收到了 3 个数据包(1 号数据包和另外两个数据包),但无法推断出这另外两个数据包是什么。
早期检测丢失
除了等待超时,我们能利用收到的其他信息来更早地检测丢失并更快地重新发送数据包吗?例如,在我们的逐个确认模型中,如果我们收到了 1 号、3 号、4 号、5 号、6 号数据包的确认,我们可能会推断 2 号数据包丢失了,并在 2 号数据包的计时器到期之前重新发送它。
更正式地说,我们可以设置一个值 K(与窗口无关),并规定如果在丢失的数据包之后有 K 个后续数据包被确认,我们就认为该数据包丢失了(即使计时器尚未到期)。例如,如果 K=3,我们正在等待 5 号数据包的确认,并且收到了 6 号、7 号和 8 号数据包的确认,那么我们可以认为 5 号数据包丢失了。

实际上,通过后续确认检测丢失比等待超时快得多。如果我们的超时时间是根据往返时间计算的,可能会达到几秒量级。另一方面,现代的带宽可以使确认每隔几微秒到达一次。
这种检测丢失的策略会因我们发送确认的策略不同而有所差异。上面的例子假设我们在发送逐个确认,但对于其他两种确认模型呢?
如果我们使用全信息确认,策略会非常相似,而且确认会更清楚地显示丢失的数据包。

如果 5 号数据包丢失,确认可能会说 “up to 4 号”,然后是 “up to 4 号,加上 6 号”,接着是 “up to 4 号,加上 6 号、7 号”,再然后是 “up to 4 号,加上 6 号、7 号、8 号”。此时,如果 K=3,那么在 5 号数据包之后有 K 个数据包被确认,所以我们可以宣布 5 号数据包丢失。
如果我们使用累积确认,这种策略可能会更模糊。如果 5 号数据包丢失,那么确认可能会说 “up to 4 号”(确认 4 号)、“up to 4 号”(确认 6 号)、“up to 4 号”(确认 7 号)、“up to 4 号”(确认 8 号)。由于连续数据包存在间隙,发送方会看到重复确认。如果 K=3,那么在收到 3 个重复数据包(对应于间隙之后有 3 个更多的数据包被确认)后,总共收到 4 个重复数据包,我们就可以宣布 5 号数据包丢失。

当我们使用单独的和全信息的确认(acks)时,我们可以清楚地知道需要重传哪个数据包。当时有一个数据包的确认丢失了(随后有 K 个确认到达)。然而,在使用累积确认时,决定重传哪个数据包就变得更加模糊,尤其是当多个数据包丢失时。
举个例子,假设发送方的窗口大小 W=6,K=3。到目前为止,数据包 1 和 2 已经被确认,数据包 3-8 正在传输中(在途)。假设数据包 3 和 5 已经丢失。我们先来看一下使用单独确认的情况。
数据包 4 到达,接收方发送对 4 的确认。发送方现在可以发送 9。
数据包 6 到达,接收方发送对 6 的确认。发送方现在可以发送 10。
数据包 7 到达,接收方发送对 7 的确认。发送方现在可以发送 11。
此时,发送方注意到数据包 3 之后的 K=3 个数据包(即 4、6 和 7)已经被确认。发送方可以判定 3 丢失,并也重传 3。
请注意,即使发送方重传了 3,并且作为对 7 的确认的响应发送了 11,这次重传后仍然总共有 6 个在途数据包,因此没有违反窗口规则。这是因为 3 在重传时已经是在途数据包之一。
数据包 8 到达,接收方发送对 8 的确认。发送方现在可以发送 12。
此外,发送方注意到数据包 5 之后的 K=3 个数据包(即 6、7 和 8)已经被确认,因此发送方也可以重传 5。
数据包 9 到达,接收方发送对 9 的确认。发送方现在可以发送 13。
现在,我们用累积确认来重新看这个例子。
数据包 4 到达,接收方发送对 4 的确认,但这个确认的内容是 “确认所有到 2 的数据包”。此时,发送方知道一定有一个数据包到达了,但不知道是 4。不过,发送方接下来可以发送 9。需要注意的是,这没有违反窗口规则,因为尽管发送方看起来有 7 个未被确认的数据包,但其中一个已经被重复的 “确认所有到 2 的数据包” 所确认,所以实际上只有 6 个在途数据包。
数据包 6 到达,接收方发送对 6 的确认,内容仍然是 “确认所有到 2 的数据包”。发送方由此推断又有一个数据包到达,可以接下来发送 10。
数据包 7 到达,接收方发送对 7 的确认,内容还是 “确认所有到 2 的数据包”。发送方推断又有一个数据包到达,可以发送 11。
此时,发送方注意到 “确认所有到 2 的数据包” 已经到达了 3 次重复确认(除了最初对 2 的确认之外)。下一个未被确认的数据包是 3,所以发送方会重传 3。
这时候问题就变得模糊了。当 8、9、10 到达接收方时,发送方会收到另外三个 “确认所有到 2 的数据包” 的副本(我们假设接收方还没有收到 3,因为 3 是在 9 和 10 之后才重传的)。
发送方现在可以发送 12、13 和 14,因为又有三个确认到达了,但接下来应该重传哪个数据包呢?是重传 3、4、5,还是其他的?
这个例子表明,累积确认并不总能准确指示哪些数据包已被接收。不过,确认的数量(可能包括重复确认)可以用来确定已接收的数据包数量(无需知道具体是哪些数据包),这让我们能够根据窗口大小继续发送数据包。然而,当收到过多重复确认时,我们无法判断应该重传哪个数据包,就会出现模糊性。