TCP 协议分析

TCP 全称 Transmission Control Protocol,即传输控制协议,它为调用它的应用程序提供了一种可靠的、面向连接(connection-oriented)的服务。

TCP 连接提供的是全双工服务(full-duplex service),TCP 连接也总是点对点(point-to-point)的。

相比 UDP,TCP 主要多了 2 种服务:可靠数据传输(reliable data transfer)和拥塞控制(congestion control)。

可靠数据传输

TCP 提供的可靠数据传输具体包含以下 3 点:

  • 分组中的比特不会受损(由 0 变为 1,或者相反),即不存在比特差错
  • 分组中的比特不会丢失
  • 所有数据都是按照其发送顺序进行交付

受损

我们先假设分组中的比特不会丢失且顺序不会改变,在此来讨论如何保证比特不会受损:

在计算机网络环境中,基于肯定确认(positive acknowledgment,ACK)(“OK”)与否定确认(negative acknowledgment,NAK)(“请重复一遍”)这样的重传机制的可靠数据传输协议称为自动重传请求(Automatic Repeat reQuest,ARQ)协议

ARQ 协议中还需要另外三种协议功能来处理存在比特差错的情况:

  • 差错检测:需要一种机制以使接收方检测到何时出现了比特差错。类似于 UDP 的做法,通过检验和字段可以实现此功能。
  • 接收方反馈:发送方要了解接收方情况(此时为分组是否被正确接收)的唯一途径就是让接收方提供明确的反馈信息给发送方。
  • 重传:接收方收到有差错的分组时,发送方将重传该分组。

当发送方处于等待 ACK 或 NAK 的状态时,它不能从上层获得更多的数据,因此,发送方将不会发送一块新数据,除非发送方确信接收方已正确接收当前分组。这样的协议被称为停等(stop-and-wait)协议。

除此之外,还需要考虑到 ACK 或 NAK 分组受损的可能性:当发送方收到含糊不清的 ACK 或 NAK 分组时,一个简单的解决方法是重传当前数据分组。然而,这种方法在发送方到接收方的信道中引入了冗余分组(duplicate packet)。冗余分组的根本困难在于接收方不知道它上次所发送的 ACK 或 NAK 是否被发送方正确地收到。因此它无法 事先 知道接收到的分组是新的还是一次重传!

解决这个新问题的一个简单方法是在数据分组中添加一新字段,让发送方对其数据分组编号,即将发送数据分组的序号(sequence number)放在该字段。于是,接收方只需要检查序号即可确定收到的分组是否是一次重传。

至此,我们介绍了保证比特不会受损用到的所有技术,由于引入了序号,还有一个可以优化的地方:如果不发送 NAK,而是对上次正确接收的分组发送一个 ACK,我们也能实现与 NAK 一样的效果。发送方接收到对同一个分组的两个 ACK(即接收冗余 ACK(duplicate ACK))后,就知道接收方没有正确接收到跟在被确认两次的分组后面的分组。

丢失

仍然假设顺序不会改变,在保证比特不会受损之后,我们来讨论如何保证比特不会丢失:

要做到这一点,我们具体要解决两个问题:怎样检测丢包以及发生丢包后该做些什么,对于第二个问题,使用上文所介绍的所有技术即可解决,为解决第一个问题,还需增加一种新的协议机制。

发送方每次发送一个分组(包括第一次分组和重传分组)时,便启动一个倒计数定时器(countdown timer);如果在定时器结束后仍未收到 ACK,则重传该分组。注意到如果一个分组经历了一个特别大的时延,发送方可能会重传该分组,即使该数据分组及其 ACK 都没有丢失。这就在发送方到接收方的信道中引入了冗余数据分组(duplicate data packet)的可能性。幸运的是,已经有足够的功能(即序号)来处理冗余分组情况。

性能

如果我们定义发送方(或信道)的利用率(utilization)为:发送方实际忙于将发送比特送进信道的那部分时间与发送时间之比,那么上文所述的停等协议有着非常低的发送方利用率,要解决此问题,就要允许发送方发送多个分组而无须等待确认。因为许多从 发送方接收方 输送的分组可以被看成是填充到一条流水线中,故这种技术被称为流水线(pipelining)。

流水线技术对可靠数据传输协议可带来如下影响:

  • 必须增加序号范围,因为每个输送中的分组(不计算重传的)必须有一个唯一的序号,而且也许有多个在输送中的未确认报文。
  • 协议的发送方和接收方两端也许不得不缓存多个分组。发送方最低限度应当能缓冲那些已发送但没有确认的分组。接收方或许也需要缓存那些已正确接收的分组。

具体而言,所需序号范围和对缓冲的要求取决于数据传输协议如何处理丢失、损坏及延时过大的分组。解决流水线的差错恢复有两种基本方法是:

  • 回退 N 步(Go-Back-N,GBN)
  • 选择重传(Selective Repeat,SR)

回退 N 步

回退 N 步(GBN)协议中,允许发送方发送多个分组(当有多个分组可用时)而不需等待确认,但它也受限于在流水线中未确认的分组数不能超过某个最大允许数 N。

下图显示了发送方看到的 GBN 协议的序号范围。如果我们将基序号(base)定义为最早未确认分组的序号,将下一个序号(nextseqnum)定义为最小的未使用序号(即下一个待发的序号),则可将序号范围分割成 4 段:

  • [0, base-1] 段内的序号对应于已经发送并被确认的分组;
  • [base, nextseqnum-1] 段内对应已经发送但未被确认的分组;
  • [nextseqnum, base+N-1] 段内的序号能用于那些要被立即发送的分组,如果有数据来自上层的话;
  • 大于或等于 base+N 的序号是不能使用的,直到当前流水线中未被确认的分组(特别是序号为 base 的分组)已得到确认为止。

gbn sender seqnum

如上图所示,那些已被发送但还未被确认的分组的许可序号范围可以被看成是一个在序号范围内长度为 N 的窗口。随着协议的运行,该窗口在序号空间向前滑动。因此,N 常被称为窗口长度(window size),GBN 协议也常被称为滑动窗口协议(sliding-window protocol)。

之所以要限制这些被发送的、未被确认的分组的数目为 N,是因为要实现后续介绍的流量控制。

GBN 发送方必须响应三种类型的事件:

  • 上层的调用。 当上层调用时,发送方首先检查发送窗口是否已满,即是否有 N 个已发送但未被确认的分组。如果窗口未满,则产生一个分组并将其发送,并相应地更新变量。如果窗口已满,发送方只需将数据返回给上层,隐式地指示上层该窗口已满。然后上层可能会过一会儿再试。在实际实现中,发送方更可能缓存(并不立刻发送)这些数据,或者使用同步机制(如一个信号量或标志)允许上层在仅当窗口不满时才调用。
  • 收到一个 ACK。 在 GBN 协议中,对序号为 n 的分组的确认采用累计确认(cumulative acknowledgment)的方式,表示接收方已正确接收到序号为 n 的以前且包括 n 在内的所有分组。
  • 超时事件。 协议的名字“回退 N 步”来源于出现丢失和时延过长分组时发送方的行为。如果出现超时,发送方重传所有已发送但还未被确认过的分组。

对 GBN 接收方而言,如果一个序号为 n 的分组被正确接收到,并且按序(即上次交付给上层的数据是序号为 n-1 的分组),则接收方为分组 n 发送一个 ACK,并将该分组中的数据部分交付到上层。在所有其他情况下,接收方丢弃该分组,并为最近按序接收的分组重新发送 ACK。

这种方法的优点是接收缓存简单,即接收方不需要缓存任何失序分组。因此,虽然发送方必须维护窗口的上下边界及 nextseqnum 在该窗口中的位置,但是接收方需要维护的唯一信息就是下一个按序接收的分组的序号。该值保存在 expectedseqnum 变量中。

选择重传

当窗口长度和带宽时延积都很大时,GBN 会存在着一些性能问题,在流水线中会有很多分组更是如此。单个分组的差错就能够引起 GBN 重传大量分组,许多分组根本没有必要重传。随着信道差错率的增加,流水线可能会被这些不必要重传的分组所充斥。

顾名思义,选择重传(SR)协议通过让发送方仅重传那些它怀疑在接收方出错(即丢失或受损)的分组而避免了不必要的重传。这种个别的、按需的重传要求接收方 逐个 地确认正确接收的分组。再次用窗口长度 N 来限制流水线中未完成、未被确认的分组数。然而,与 GBN 不同的是,发送方已经收到了对窗口中某些分组的 ACK。下图显示了 SR 发送方和接收方看到的序号范围:

sr sender receiver seqnum

SR 接收方将确认一个正确的分组而不管其是否按序。失序的分组将被缓存直到所有丢失分组(即序号更小的分组)皆被收到为止,这时才可以将一批分组按序交付给上层。

对 SR 发送方而言:

  1. 从上层收到数据。 当从上层接收到数据后,SR 发送方检查下一个可用于该分组的序号。如果序号位于发送方的窗口内,则将数据打包并发送;否则就像在 GBN 中一样,要么将数据缓存,要么将其返回给上层以便以后传输。
  2. 超时。 定时器再次被用来防止丢失分组。然而,现在每个分组必须拥有其自己的逻辑定时器,因为超时发生后只能发送一个分组。
  3. 收到 ACK。 如果收到 ACK,倘若该分组序号在窗口内,则 SR 发送方将那个被确认的分组标记为已接收。如果该分组的序号等于 send_base,则窗口基序号向前移动到具有最小序号的未确认分组处。如果窗口移动了并且有序号落在窗口内的未发送分组,则发送这些分组。

对 SR 接收方而言:

  1. 序号在 [rcv_base, rcv_base+N-1] 内的分组被正确接收。在此情况下,收到的分组落在接收方的窗口内,一个选择 ACK 被回送给发送方。如果该分组以前没收到过,则缓存该分组。如果该分组的序号等于接收窗口的基序号(rcv_base),则该分组以及以前缓存的序号连续的(起始于 rcv_base 的)分组交付给上层。然后,接收窗口按向前移动分组的编号向上交付这些分组。
  2. 序号在 [rcv_base-N, rcv_base-1] 内的分组被正确收到。在此情况下,必须产生一个 ACK,即使该分组是接收方以前已确认过的分组。
  3. 其他情况。 忽略该分组。

注意:当窗口长度比序号空间小 1 时协议无法工作。对于 SR 协议而言,窗口长度必须小于或等于序号空间大小的一半。

总结上文,为了实现可靠数据传输,我们需要使用以下技术:

  • 检验和
  • 肯定确认,即 ACK 分组
  • 重传
  • 序号
  • 定时器
  • 窗口、流水线

拥塞控制

在最为宽泛的级别上,可根据网络层是否为运输层拥塞控制提供了显式帮助,来将拥塞控制方法分为两类:

  • 端到端拥塞控制:网络层 没有 为运输层拥塞控制提供 显式支持。即使网络中存在拥塞,端系统也必须通过对网络行为的观察(如分组丢失与时延)来推断之。
  • 网络辅助的拥塞控制:路由器向发送方提供关于网络中拥塞状态的显式反馈信息。这种反馈可以简单地用一个比特来指示链路中的拥塞情况。

TCP 采用端到端的方法解决拥塞控制,因为 IP 层不会向端系统提供有关网络拥塞的反馈信息。

对于网络辅助的拥塞控制,拥塞信息从网络反馈到发送方通常有两种方式:

  • 直接网络反馈:可以由网络路由器发给发送方。这种方式的通知通常采用了一种阻塞分组(choke packet)的形式。
  • 经由接收方的网络反馈:路由器标记或更新从发送方流向接收方的分组中的某个字段来指示拥塞的产生。一旦收到一个标记的分组后,接收方就会向发送方通知该网络拥塞指示。注意到这种形式的通知至少要经过一个完整的往返时间。

TCP 所采用的方法是让每一个发送方根据所感知的网络拥塞程度来限制其能向连接发送流量的速率。如果一个 TCP 发送方感知从它到目的地之间的路径上没什么拥塞,则 TCP 发送方增加其发送速率;如果发送方感知沿着该路径有拥塞,则发送方就会降低其发送速率。这就会涉及到 3 个方面:

  • 如何限制速率
  • 如何感知拥塞
  • 采用何种拥塞控制算法

限制速率

运行在发送方的 TCP 拥塞控制机制跟踪一个额外的变量,即拥塞窗口(congestion window)。拥塞窗口表示为 cwnd,它对一个 TCP 发送方能向网络中发送流量的速率进行了限制。

感知拥塞

当出现过度的拥塞时,在沿着这条路径上的一台(或多台)路由器的缓存会溢出,引起一个数据报(包含一个 TCP 报文段)被丢弃。丢弃的数据报接着会引起发送方的丢包事件(要么超时或收到 3 个冗余 ACK),发送方就认为在发送方到接收方的路径上出现了拥塞的指示。

拥塞控制算法

迄今为止,TCP 有各种各样的拥塞控制算法(congestion control algorithm),我们以广为人熟知的 Reno 算法介绍,其包括 3 个主要部分:

  • 慢启动
  • 拥塞避免
  • 快速恢复

慢启动和拥塞避免是 TCP 的强制部分,两者的差异在于对收到的 ACK 做出反映时增加 cwnd 长度的方式,慢启动比拥塞避免能更快地增加 cwnd 的长度。快速恢复是推荐部分,对 TCP 发送方并非是必需的。

慢启动

慢启动(slow-start)状态,cwnd 的值以 1 个 MSS 开始并且每当传输的报文段首次被确认就增加 1 个 MSS。

例如,TCP 向网络发送第一个报文段并等待一个确认;当该确认到达时,TCP 发送方将拥塞窗口增加 1 个 MSS,并发送出两个最大长度的报文段;这两个报文段被确认,则发送方对每个确认报文段将拥塞窗口增加 1 个 MSS,使得拥塞窗口变为 4 个 MSS,并这样下去。这一过程每过一个 RTT,发送速度就翻番。因此,TCP 发送速率起始慢,但在慢启动阶段以指数增长。

这种指数增长不是无限的,慢启动在遇到以下 3 种情况之一时将结束:

  • 若存在一个由超时指示的丢包事件(即拥塞),TCP 发送方将另一个状态变量 ssthresh(“慢启动阈值”的速记)设为 cwnd/2,再将 cwnd 设为 1 并重新开始慢启动过程。
  • 当 cwnd 的值等于 ssthresh 时,结束慢启动并且 TCP 转移到拥塞避免模式。
  • 若检测到 3 个冗余 ACK,这时 TCP 执行一种快速重传并进入快速恢复状态。

拥塞避免

一旦进入拥塞避免状态,cwnd 的值大约是上次遇到拥塞时的值的一半,即距离拥塞可能并不遥远!因此,TCP 无法每过一个 RTT 再将 cwnd 的值翻番,而是采用了一种较为保守的方法,每个 RTT 只将 cwnd 的值增加一个 MSS。

快速恢复

在快速恢复中,对于引起 TCP 进入快速恢复状态的缺失报文段,对收到的每个冗余的 ACK,cwnd 的值增加一个 MSS。

TCP 报文段结构

TCP 报文段大致分为首部字段数据字段两部分,数据字段包含一块应用数据,首部字段一般是 20 字节,数据字段最大长度为 MSS。下图显示了 TCP 报文段结构:

TCP 报文段结构

同 UDP 一样,首部包括源端口号目的端口号以及检验和字段(checksum field)。除此之外,还包含以下字段:

  • 32 比特的序号字段(sequence number field)和 32 比特的确认号字段(acknowledgment number field):这些字段被 TCP 发送方和接收方用来实现可靠数据传输服务。
  • 16 比特的接收窗口字段(receive window field):该字段用于流量控制,用于指示接收方愿意接受的字节数量。
  • 4 比特的首部长度字段(header length field):该字段指示了以 32 比特的字为单位的 TCP 首部长度。由于 TCP 选项字段的原因,TCP 首部的长度是可变的。通常,选项字段为空,所以 TCP 首部的典型长度是 20 字节。
  • 可选与变长的选项字段(options field):该字段用于发送方与接收方协商最大报文段长度(MSS)时,或在高速网络环境下用作窗口调节因子时使用。首部字段中还定义了一个时间戳选项。
  • 6 比特的标志字段(flag field):ACK 比特用于指示确认字段中的值是有效的,即该报文段包括一个对已被成功接收报文段的确认。RSTSYNFIN 比特用于连接建立和拆除。在明确拥塞通告中使用了 CWRECE 比特。当 PSH 比特被置位时,就指示接收方应立即将数据交给上层。最后,URG 比特用来指示报文段里存在着被发送端的上层实体置为“紧急”的数据。紧急数据的最后一个字节由 16 比特的紧急数据指针字段(urgent data pointer field)指出。

MSS 全称 Maximum Segment Size,即最大报文段长度,指 TCP 可从缓存中取出并放入报文段中的数据的最大数量。MSS 通常根据最初确定的由本地发送主机发送的最大链路层帧长度(即所谓的最大传输单元(Maximum Transmission Unit,MTU))来设置。设置该 MSS 要保证一个 TCP 报文段(当封装在一个 IP 数据报中)加上 TCP/IP 首部长度(通常 40 字节)将适合单个链路层帧。以太网和 PPP 链路层协议都具有 1500 字节的 MTU,因此 MSS 的典型值为 1460 字节。

TCP 连接与状态

创建

客户端 TCP 会用以下方式与服务器端 TCP 建立一条 TCP 连接:

  1. 客户端 TCP 首先向服务器端 TCP 发送一个 SYN 报文段
    该报文段不包含应用层数据,但是报文段的首部中的 SYN 标志位被置为 1,序号字段为一个随机的初始序号(不妨设为 client_isn)。
    客户端 TCP 初始为 CLOSED(关闭)状态,发送此 SYN 报文段后,将进入 SYN_SENT 状态。
  2. 服务器端 TCP 收到 SYN 报文段后,会为该 TCP 连接分配 TCP 缓存和变量,并向客户端 TCP 发送一个 SYNACK 报文段
    该报文段也不包含应用层数据,但是报文段的首部中的 SYN 标志位被置为 1,确认号字段为 client_isn + 1,序号字段为一个随机的初始序号(不妨设为 server_isn)。
    服务器端 TCP 初始为 CLOSED 状态,当服务器应用程序创建一个监听套接字时,将进入 LISTEN 状态,发送此 SYNACK 报文段后,将进入 SYN_RCVD 状态。
  3. 客户端 TCP 收到 SYNACK 报文段后,也会为该 TCP 连接分配 TCP 缓存和变量,并向服务器端 TCP 发送一个 ACK 报文段
    该报文段的首部中的 SYN 标志位被置为 0,确认号字段为 server_isn + 1,可以携带应用层数据。
    发送此 ACK 报文段后,客户端 TCP 将进入 ESTABLISHED(已建立)状态,服务器端 TCP 收到 ACK 报文段后也将进入 ESTABLISHED 状态。

一旦完成以上 3 个步骤,客户和服务器主机就可以相互发送包括数据的报文段了。在以后每一个报文段中,SYN 比特都将被置为 0。为了创建该连接,在两台主机之间发送了 3 个分组,由于这个原因,这种连接创建过程通常被称为 3 次握手(three-way handshake)。

关闭

参与一条 TCP 连接的两个进程中的任何一个都能终止该连接。当连接结束后,主机中的“资源”(即缓存和变量)将被释放。假设客户端打算关闭连接,它将遵循以下步骤:

  1. 客户端 TCP 向服务器端 TCP 发送一个 FIN 报文段
    该报文段的首部中的 FIN 标志位被置为 1。
    发送此 FIN 报文段后,客户端 TCP 将从 ESTABLISHED 状态进入到 FIN_WAIT_1 状态。
  2. 服务器端 TCP 收到 FIN 报文段后,会向客户端 TCP 发送一个 ACK 报文段
    发送此 ACK 报文段后,服务器端 TCP 将从 ESTABLISHED 状态进入到 CLOSE_WAIT 状态,客户端 TCP 收到 ACK 报文段后将进入 FIN_WAIT_2 状态。
  3. 服务器端 TCP 向客户端 TCP 发送一个 FIN 报文段
    发送此 FIN 报文段后,服务器端 TCP 将进入 LAST_ACK 状态。
  4. 客户端 TCP 收到 FIN 报文段后,会向服务器端 TCP 发送一个 ACK 报文段
    发送此 ACK 报文段后,客户端 TCP 将进入 TIME_WAIT 状态,服务器端 TCP 收到 ACK 报文段后将进入 CLOSED 状态。
    假定此 ACK 丢失,TIME_WAIT 状态使 TCP 客户重传最后的确认报文。
    在 TIME_WAIT 状态中所消耗的时间是与具体实现有关的,而典型的值是 30 秒、1 分钟或 2 分钟。
    经过等待后,连接就正式关闭,客户端所有资源(包括端口号)将被释放。

为了关闭该连接,在两台主机之间发送了 4 个分组,由于这个原因,这种连接关闭过程通常被称为 4 次挥手

总结以上,TCP 连接的组成包括:一台主机上的缓存(发送缓存和接收缓存)、变量和与进程连接的套接字,以及另一台主机上的另一组缓存、变量和与进程连接的套接字。

流量控制

TCP 为它的应用程序提供了流量控制服务(flow-control service)以消除发送方使接收方缓存溢出的可能性。流量控制因此是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配。即使流量控制和拥塞控制采取的动作非常相似(对发送方的遏制),但是它们显然是针对完全不同的原因而采取的措施。

TCP 通过让 发送方 维护一个称为接收窗口(receive window)的变量来提供流量控制。通俗地说,接收窗口用于给发送方一个提示 —— 该接收方还有多少可用的缓存空间。因为 TCP 是全双工通信,在连接两端的发送方都各自维护一个接收窗口。

假定主机 A 通过一条 TCP 连接向主机 B 发送一个大文件,定义以下变量:

  • RcvBuffer:主机 B 的接收缓存大小
  • LastByteRead:主机 B 上的应用进程从缓存读出的数据流的最后一个字节的编号
  • LastByteRcvd:从网络中到达的并且已放入主机 B 接收缓存中的数据流的最后一个字节的编号

由于 TCP 不允许已分配的缓存溢出,下式必须成立:

$$LastByteRcvd-LastByteRead\leq RcvBuffer$$

接收窗口用 rwnd 表示,根据缓存可用空间的数量来设置:

$$rwnd = RcvBuffer-(LastByteRcvd-LastByteRead)$$

由于该空间是随着时间变化的,所以 rwnd 是动态的。

主机 B 通过把当前的 rwnd 值放入它发给主机 A 的报文段接收窗口字段中,通知主机 A 它在该连接的缓存中还有多少可用空间。开始时,主机 B 设定 $rwnd = RcvBuffer$。注意到为了实现这一点,主机 B 必须跟踪几个与连接有关的变量。

主机 A 轮流跟踪两个变量,LastByteSent 和 LastByteAcked,注意到这两个变量之间的差 $LastByteSent-LastByteAcked$,就是主机 A 发送到连接中但未被确认的数据量。通过将未被确认的数据量控制在值 rwnd 以内,就可以保证主机 A 不会使主机 B 的接收缓存溢出。因此,主机 A 在该连接的整个生命周期须保证:

$$LastByteSent-LastByteAcked\leq rwnd$$

特别的,TCP 规范中要求:当主机 B 的接收窗口为 0 时,主机 A 继续发送只有一个字节数据的报文段。

参考

  1. James F. Kurose,Keith W. Ross.计算机网络:自顶向下方法[M].北京:机械工业出版社,2018:134-185.