计算机网络:传输层

TCP 的主要特点

  1. TCP 是面向连接的传输层协议,应用程序在使用 TCP 协议之前必须先建立 TCP 连接,数据传输完成后,必须释放已建立的连接。

  2. TCP 连接只能是点对点的。

  3. TCP 提供可靠交付的服务,通过 TCP 连接传送的数据,无差错、不丢失、不重复,并且按序到达。

  4. TCP 提供全双工通信,允许通信双方的进程在任意时间发送数据,这是因为 TCP 连接的两端均设有发送缓存和接收缓存,用来存放临时通信数据,在发送时,应用程序只需要把待发送的数据放在发送缓存中就可以做其它的事,TCP 会在合适的时候把数据发送出去,接收数据时,TCP 会把收到的数据放入缓存,应用程序在合适的时间读取缓存即可。

  5. 面向字节流,TCP 只将上层交付的数据当做连续无结构的字节流处理,并不对关心其中的含义。

停止等待协议

无差错情况

TCP 提供可靠的交付服务,所以一定要等到收到接收方的确认回复后才可以继续发送后续的包。

传输过程中出现差错

如果 B 接收 M1 时检测到了差错就丢弃 M1,其余什么都不做。也有可能是 M1 在传输过程中直接丢失了,B 什么都不知道,这两种情况下 B 都不会回复任何消息。

A 只要超过了一段时间仍然没收到确认消息,就认为刚才的分组丢失了,因而重传这个分组,这种机制叫超时重传。发送方每发送完一个分组就会设置一个超时计时器,如果在到期之前收到了确认消息,就撤销该定时器。这里需要注意三点:

  1. 发送方发送完一个分组以后,必须暂时保存已发送分组的副本(超时重传时需要使用),只要收到确认消息后才能清除副本。

  2. 分组和确认分组都必须进行编号,这样才能确定是哪一个分组收到了确认。

  3. 超时重传的时间应当比数据在分组传输的平均往返时间更长一些,否则接收方在发送确认分组时,发送方已经到期会导致误传。超时时间设定的过长会导致通信效率变低,设定的过短会导致不必要的重传。超时时间的设定是 TCP 协议中非常棘手的一个问题,具体内容将会再后续讲到。[TODO]

确认丢失

如果 B 发送对 M1 的确认的报文丢失了,A 在超时重传的时间内没有收到确认,A 并不知道是是自己发出去的分组出错、丢失还是 B 的确认丢失,因此 A 在超时后必须重传 M1

当 B 再次收到重传的 M1 后,要做两件事:

  • 丢弃重复分组 M1,不向上层协议交付。

  • 再次向 A 发送确认,不能因为之前发送过 M1 的确认就不在发送,因为 A 之所以重传 M1,必然是因为没有收到确认。

确认迟到

如果 B 发送确认分组没有按时到达 A,也会触发 A 的重传机制。A 收到迟到的确认什么都不做。

信道利用率

停止等待协议的优点是实现简单,但缺点是信道的利用率太低。

假定 A 发送分组需要的时间是 TD。显然,TD 等于分组长度除以数据率。再假定分组正常到达 B 后,B 处理分组的时间也可以忽略不计,同时立即发送确认。假定 B 发送确认分组需要时间 TA。如果 A 处理确认分组的时间可以可以忽略不计,那么 A 在经过时间 (TD + RTT + TA) 后就可以再发送下一个分组,这里的 RTT 是往返时间。因为仅仅是在时间 TD 内采用来传送有用的数据,因此新到的额利用率 U 可用下式计算:

往返时间 RTT 取决于所使用的信道,例如 RTT = 20ms,分组长度是 1200 bit,发送速率是 1 Mbit/s。若忽略处理时间和 TA,可算出信道的利用率 U = 5.66%。若把发送速率提高到 10 Mbit/s 则 U = 5.96 x 10-4,信道在绝大多数时间内都是空闲的。

为了提高传输效率,发送方可以不使用低效率的停止等待协议,而是采用流水线传输。流水线传输就是发送方可以发送多个分组,不必每发完一个分组就等待确认。这样可使信道上一直有数据不间断的在传送,从而提高信道利用率。当使用流水线传输时,就需要使用到连续 ARQ 协议滑动窗口协议

连续 ARQ 协议

滑动窗口协议比较复杂,这里先暂且给出连续 ARQ 协议最基本的概念。

下图表示发送方维持的发送窗口,它表示发送窗口内的 5 个分组可以连续发出去,而不用等待对方确认,分组发送是按照分组序号从小到大发送的。

连续 ARQ 协议规定,发送方每收到一个确认,就把发送窗口向前滑动一个分组的位置,如下图表示发送方收到了第一个分组的确认,于是把发送窗口向前移动一个分组的位置,如果原来已发送了前 5 个分组,那么现在就可以发送窗口内的第 6 个分组了。

接收方一般采用累积确认的方式,接收方不必对收到的每个分组都发送确认,而是在收到几个分组后,对按序到达的最后一个分组发送确认,表示包括这个分组和这个分组以前的所有分组都正确收到了。

累积确认的优点是逻辑简单易实现,即使确认丢失也不必重传,缺点是不能向发送方反映出接收方已经正确收到的所有分组的信息。

例如,如果发送方发送了前五个分组,第三个分组丢失了。这时接收方只能对前两个分组发出确认,发送方无法知道后面三个分组的下落,只好把后面三个分组都重传一次,这称为 Go-back-N,表示需要再退回来重传已发送过的 N 个分组。

在学习滑动窗口时,必须先了解 TCP 报文段的首部格式。

TCP 报文段的首部格式

TCP 报文段首部的前 20 个字节是固定的,后面有 4n 字节是根据需要而增加的选项,因此 TCP 首部的最小长度是 20 字节。

源端口号和目的端口号:各占两字节,与 UDP 的分用相似。

序号:占 4 字节,序号范围是 [0, 232 - 1],共 232 个序号。序号使用 mod 232 运算保证序号值达到上限后重新回到 0。TCP 是面向字节流的,在一个 TCP 连接中传送的字节流中的每一个字节都要按顺序编号,字节流的起始序号必须在连接建立时设置。首部中的序号字段值则指的是本报文段锁发送的数据的第一个字节的序号。

确认号:占 4 字节,是期望收到对方下一个报文段的第一个数据字节的序号。

数据偏移:占 4 位,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。这个字段实际上是指出 TCP 报文段的首部长度。由于首部中还有长度不确定的选项字段,因此数据偏移字段是必要的。

保留:占 6 位,保留为今后使用,但目前应置为 0。

紧急 URG:当 URG = 1 时,表名紧急指针字段有效,它告诉系统此报文段中有紧急数据,应尽快传送,而不要按原来的排队顺序来传送。

确认 ACK:仅当 ACK = 1 时确认号字段才有效。TCP 规定在连接建立后所有传送的报文段都必须把 ACK 置 1。

推送 PSH:发送方如果希望接收方尽快的处理发送的数据,可以将 PSH 置为 1,接收方 收到 PSH = 1 的报文段后会尽快的交付上层应用进程,而不再等到整个缓存都填满了再向上交付。

复位 RST:当 RST = 1 时,表明 TCP 连接中出现严重差错,必须释放连接重新建立。RST 置 1 还用来拒绝一个非法的报文段或拒绝打开一个连接,RST 也被称为重建位或重置位。

同步 SYN:在连接建立时用来同步序号,当 SYN = 1 而 ACK = 0 时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使用 SYN = 1 和 ACK = 1。因此,SYN 置为 1 就表示这是一个连接请求或连接接收报文。

终止 FIN:当 FIN = 1 时,表名此报文段的发送方的数据已经发送完毕,并要求释放运输连接。

窗口:占 2 字节,窗口值是 [0, 216 - 1] 之间的整数。窗口指的是发送本报文段的一方的接收窗口,而不是自己的发送窗口。窗口值告诉对方,从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量(以字节为单位)。之所以有这个限制,是因为接收方的数据缓存空间是优先的。总之,窗口值是接收方让发送方设置其发送窗口的依据。

检验和:占 2 字节,检验和字段检验的范围包括首部和数据两部分。

紧急指针:占 2 字节,紧急指针尽在 URG = 1 时才有意义,它指出本报文段中的紧急数据的字节数

选项:长度可变,最长可达 40 字节,当没有使用选项时,TCP 的首部长度是 20 字节。

滑动窗口协议

TCP 的滑动窗口是以字节为单位的,假定现在 A 收到了 B 发来的确认报文段,其中报文头中窗口的值为 20,确认号为 31(这表明 B 期望收到的下一个序号是 31,31 号以前的数据都收到了),根据这两个数据,A 可以构造出自己的发送窗口:

发送窗口表示,即使尚未收到 B 的确认,A 仍可以把窗口内的数据发送出去。凡是已经发送的数据,在收到确认报文以前,都必须保留,以便在超时重传时使用。

发送窗口的位置由前沿和后沿共同决定,后沿只有在接收到确认报文后才会向前移动,前沿通常是在不断向前移动的,也有两种可能不动:没有收到新的确认,对方通知的窗口大小也不变;收到了新的确认,但同时也通知了窗口缩小,二者正好抵消。

进程和 TCP 协议之间其实是“解耦”的,因为在进程和 TCP 之间还有一层缓存。发送数据时,进程将字节流写入发送缓存,接收数据时,进程从接收缓存中读取字节流,缓存空间则是被重复利用的,

发送缓存用来暂时存放

  1. 进程准备发送的数据。
  2. TCP 已发送但尚未收到确认的数据。

发送窗口通常只是发送缓存的一部分,已被确认的数据应当从发送缓存中删除。

接收缓存用来暂时存放:

  1. 按序到达、但尚未被进程读取的数据。
  2. 未按序到达的数据。

如果收到的分组被检测出有差错,则丢弃。如果进程没有及时读取收到的数据,接受缓存耗尽,将会使接收窗口为 0。反之,如果进程及时读取缓存中的数据,接收窗口则可以增大,但最大不超过接收缓存的大小。

虽然 A 的发送窗口是根据 B 的接收窗口设置的,但在同一时刻,A 的发送窗口并不总是和 B 的接收窗口一样大。一方面是因为网络时延的原因导致一定的时间滞后,另一方面 A 可能还会根据网络的拥塞情况[TODO]适当减小自己的发送窗口数值。

对于不按序到达的数据 TCP 标准并未规定如何处理,通常会将其暂存在接收窗口中,等到字节流所缺失的字节收到后,再按序交付给上层。

TCP 要求接收方必须有累积确认的功能,以减小传输开销。接收方可以在合适的时间发送确认,也可以在自己有数据要发送的时候携带确认。但有两点需要注意:

  1. 接收方不应该过分推迟发送确认,否则会导致发送方不必要的重传。标准规定不应超过 0.5 秒。若接收到一连串具有最大长度的报文,则必须每隔一个报文段就发送一个确认。
  2. 捎带确认其实并不常见,因为大多数程序不同时在两个方向上发送数据。

超时重传的选择

TCP 在一定的时间内没有收到确认报文将会进行重传,但是这个时间的选择确是 TCP 最复杂的问题之一。一个 TCP 报文段可能只是经过一个简答的局域网,也可能是经过多个低速率的互联网,每个 IP 数据报选择的路由可能还不相同。如果把超时重传时间设置的太短,可能会导致不必要的重传,增加网络负载。设置的太长,又降低了传出效率。

TCP 采用了一种自适应的算法,它记录了报文段的往返时间 RTT,保留了 RTT 的一个加权平均往返时间 RTTS(也称为平滑的往返时间)。

每当第一次测量到 RTT 样本时,RTTS 的值就取为测量到的 RTT 的样本值,但以后每测量到一个新的 RTT 样本,就按下式重新计算:

新的 RTTS = (1 - α) * (旧的 RTTS) + α * (新的 RTT 样本)

显然 α 越接近 0,表示新的 RTTS 和旧的相比变化不大,而对新的 RTT 样本影响不大(RTT 值更新的较慢)。若 α 越接近 1,则表示对新的 RTT 样本影响较大(RTT 值更新的较快)。推荐的 α 为 0.125。

显然,超时重传时间 RTO 应略大于 RTTS,推荐使用下式:

RTO = RTTS + 4 * RTTD

RTTD 是 RTT 的偏差的加权平均值,它与 RTTS 和新的 RTT 样本之差有关。建议这样计算:第一次测量时 RTTD 为 RTT 样本值的一半,之后的测量中使用下式:

新的 RTTD = (1 - β) * (旧的 RTTD) + β * |RTTS - 新的 RTT 样本|

这里的 β 是小于 1 的系数,推荐值为 0.25。

假设发出一个报文,在设定的重传时间内没有收到确认,于是重新发送报文,过了一段时间,收到了确认报文,**那如何判定此确认报文段是对先发送报文段的确认,还是对后来重传的报文段的确认?**由于重传的报文段和原来的报文段完全一样,从而无法正确判断,而正确判断对确定加权平均 RTTS 的值影响很大。

如果收到的是第一次发送报文的确认,却被当成重传报文的确认,则会导致 RTT 样本值减小,计算出的 RTTS 和 RTO 就会偏大,累积几次这样的情况后,RTO 会越来越长。相反这会导致 RTO 越来越小,重传越来越频繁。

因此,在计算 RTTS 时,只要报文段发生了重传,就不采用其 RTT 样本,这样得出的 RTTS 和 RTO 就较为准确。这还不算完,如果网络时延突然增大,在原来得出的重传时间内不会收到确认,触发重传,但重传的 RTT 样本又不会被计算到 RTTS 中,导致 RTO 无法更新。所以报文段每重传一次,就把 RTO 增大一些,典型的做法的变为原来的两倍,当不再重传时,才重新计算 RTO。

拥塞控制

发送方维持一个叫 cwnd (拥塞窗口) 的状态变量,它的大小取决于当前网络的拥塞程度,并且动态地在变化。当拥塞窗口数值发生变化时,发送窗口的大小也随之变化,一般情况下发送窗口的大小等于拥塞窗口,但是考虑到接收房的接收能力,发送窗口还有可能小于 拥塞窗口。

发送方控制拥塞窗口的原则是:只要网络没有发生拥塞,拥塞窗口就增大一些,以便更多的分组发送出去。反之,则减少拥塞窗口,以减少注入到网络中的分组数。发送方只要没有按时收到应当到达的确认报文,就可以猜想网络可能出现了拥塞。

慢开始和拥塞避免

当主机开始发送数据时,如果立即把大量数据注入网络有可能会引起网络拥塞,因为现在还不知道网络的负荷情况。较好的办法就是先探测一下,由小到大的增大拥塞窗口数值(相当于增大发送窗口)。通常在开始时,先把拥塞窗口 cwnd 设置为一个最大报文段 MSS 的数值,每收到一个对新的报文段的确认后(不是收到一个 ACK,因为收到一个 ACK 可能是对多个报文段的确认),把拥塞窗口增加至多一个 MSS 的数值。

假设发送方 cwnd = 1,发送第一个报文段 M1,收到确认后,cwnd 由 1 增大到 2,所以可以接着发送 M2 和 M3 两个报文,收到累积确认后,表示 M2 和 M3 两个数据段都被确认,所以 cmnd 从 2 增加到 4。因此使用慢开始算法后,**每经过一个传播轮次,拥塞窗口 cwnd 就加倍。**此处的传播轮次是指把拥塞窗口允许的报文段连续发送出去后,并收到了对已发送最后一个字节的确认。

慢开始的“慢”并不是指 cwnd 的增长速率慢,而是指数据开始传输时 cwnd 设置为 1,导致一开始只能发送一个报文段,这样比一下把大量数据注入到网络中要“慢得多”。

为了防止拥塞窗口 cwnd 增长过大引起网络拥塞,还要设置一个慢开始门限 ssthresh 状态变量:

  • cwnd < ssthresh 时,使用慢开始算法。
  • cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
  • cwnd = ssthresh 时,既可以使用慢开始算法,也可以使用拥塞避免算法。

拥塞避免算法的思路是让拥塞窗口 cwnd 缓慢地增大,即每经过一个往返时间 RTT 就把发送方的拥塞窗口 cwnd 加 1,而不是加倍,这样 cwnd 的按线性规律增长,要比慢开始拥塞窗口的增长速率慢得多。

无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(没有按时收到确认),就要把慢开始门限 ssthresh 设置为出现拥塞时的发送窗口数值的一半(但不能小于 2)。然后把拥塞窗口 cwnd 重新设置为 1,执行慢开始算法。这样可以迅速减少网络中的分组数,使得发生拥塞的路由器有足够的时间把队列中积压的分组处理完毕。

快重传和快恢复

快重传算法要求接收方每收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有达到对方)而不要等待自己发送数据时才进行捎带确认。

如下图,接收方收到 M4 的时候判断发生了乱序,按照快重传算法的要求,立即发送重复确认 M2 的报文(因为当前只确认到 M2),同理在后面收到 M5 和 M6 的时候也进行重复确认。当发送方连续收到三个重复确认后,会立即重传丢失的 M3 报文,而不必等待为 M3 设置的重传计时器到期。

与快重传算法配合使用的还有快恢复算法:

  1. 当接收方连续收到三个重复确认时,会将慢开始门限 ssthresh 减半。这是为了预防网络发生拥塞,但是接下来并不执行慢开始算法。

  2. 因为连续收到了确认报文,所以发送方认为网络很可能没有发生拥塞。因此无需执行慢开始算法(即拥塞窗口 cwnd 不设置为 1),而是把 cwnd 设置为慢开始门限 ssthresh 减半后的数值,然后开始执行拥塞避免算法。

TCP 连接管理

TCP 是面向连接的协议,分为三个阶段:建立连接、传输数据、释放连接

连接的建立

在 TCP 的连接过程中需要解决以下三个问题:

  1. 要使双方都能确知对方的存在。
  2. 允许双方协商一些参数(如最大窗口值、是否使用窗口扩大选项和时间戳选项以及服务质量等)。
  3. 能够对运输实体资源(如缓存大小、连接表中的项目等)进行分配。

如下图,左右两侧方块表示各个阶段的状态,中间则为报文的交互内容:

上面给出的连接建立过程即为三次握手,最后 Client 为什么还要向 Server 发送一条 ACK 报文呢?主要是为了防止已经失效的连接请求的报文段突然有传送到了 Server 端。假定出现这种情况,Client 发送了第一个 SYN 报文,却因为某些原因在网络节点中滞留了,并且一直滞留到两端连接已经释放后才到达 Server。而 Server 则会认为这是 Client 新发起的一次连接,从而回复 ACK 报文。如果没有三次握手的最后一次报文,只要 Server 回复了 ACK 就意味着连接已经建立成功了,Server 端会一直保持着这条 TCP 连接,而 Client 端并没有感知到这条连接,也就不会传输数据或者主动断开。这样 Server 端的连接迟迟无法释放,资源被白白浪费。

连接的释放

释放连接的过程如下图所示:

重点注意,Client 端进入 CLOSED 状态要等待 2 * MSL 的时间,MSL 称为最长报文段寿命,通常建议 MSL 为 2 分钟,也就是说 Client 端发送完最后一个报文后,要等待 4 分钟后才会进入 CLOSED 状态。原因主要有两个:

  1. 保证 Client 发送的最后一个 ACK 报文可以到达 Server 端。因为最后一个报文可能会丢失,Server 端在约定时间内没有收到 ACK,会重新发送 FIN + ACK 报文,同时 Client 端重新启动 2MSL 定时器。如果 Client 没有 TIME_WAIT 直接 CLOSED 了,那就永远不会回复 ACK,Server 端的连接也就永远无法释放。

  2. 防止已失效的报文。考虑下面这种情况:

    1. 假设第一条连接没有 TIME_WAIT 阶段,Client 端直接进入 CLOSED 状态释放连接。
    2. 但最后一次 ACK 报文丢失。
    3. Client 重新请求和 Server 建立了第二条连接,恰好复用了第一条连接使用的端口号。
    4. Server 端迟迟没有收到第一条连接最后的 ACK,约定时间过后重传 FIN + ACK。
    5. 但此刻 Client 中第一条连接已经彻底释放了,重传的 FIN + ACK 被认为是第二条连接发来的,于是第二条连接被关闭了。

以上过程就是 TCP 的四次挥手

除了 TIME_WAIT 阶段的时间清代计时器外,TCP 还设有一个保活计时器(keepalive timer)。假设连接建立后,某一端突然掉线,对端将不会再接收到任何报文,所以还需要一种机制可以让对端发现连接异常并主动释放连接,这里就要使用到保活计时器。对端每收到一次数据,就重新设置保活计时器(通常是 2 小时),如果 2 个小时后没有收到数据,则会发送一个探测报文,之后每隔 75 分钟发送一次,一连发送 10 个探测报文后都没有收到响应,则可以直接关闭这个链接。

TCP 的有限状态机