可靠传输

1. 字节编号机制

TCP 数据段会以字节为单位对数据段中的数据部分进行一一编号,这样可以确保每个字节的数据都可以有序地传送和接收。

当接收端收到 TCP 数据段后,根据其中的序号以及去掉 TCP 报头后剩余部分的数据字节数就可以得出要向对端发送 ACK 确认数据段时的确认号。如果一个数据段在传输过程中被丢失了或者只正确接收了其中部分的数据,则接收端在向发送端发送 ACK 确认数据段时就不会包含整个该数据段的字节数。

比如:一个数据段的序号为 101,该数据段中数据部分为 100 个字节,则本数据段数据部分的第一个字节的序号是 101,最后一个字节的序号是 200,这样如果接收端收到这个数据段要进行确认的话,则在 ACK 确认数据段中的确认号就是 201(即 200+1)。

2. 正确接收确认机制

TCP 数据段发送完后需要等到对方发回一个包括该数据段确认信息的 ACK 数据段后才会确认对方已正确接收了该数据段,才会从自己的缓存中清除原来缓存的这部分数据,这就是 TCP 协议的“正确接收确认机制”。它包括以下两个方面的具体措施:
image.png
1)通过 “确认号” 字段确定对方已正确接收的数据段
TCP 协议头部的确认号字段值表明了接收端已正确接收的连续数据段序号。对接收端已确认正确接收的数据从缓存表中清除,接收端没确认的数据在超时后从该数据段开始后面的所有已发数据段进行重发,不管数据段是否已正确接收。

2)通过 “数据偏移” 字段实现无误数据段重组
如果某个 TCP 数据段在发送时因超过 IP 协议对所接收的数据段大小的限制时就要进行分段了,这时每个 TCP 分段都会带上一个 TCP 协议头,但 TCP 协议头因为有可选项字段,所以每个协议头长度可能不一样。那在接收端如何确定每个 TCP 数据分段要去掉的 TCP 协议头长度,实现各数据分段中数据部分的重组呢?

那就是在每个数据段头部都有一个数据偏移字段,这个字段是指数据段中数据部分在分段开始处的偏移量,也就是指定了每个 TCP 数据段或数据分段头部的大小,这样就可以非常准确地去掉 TCP 协议头部了,然后再根据每个数据段中序号由小到大把这些纯数据部分进行组装即可。

3. 超时重传机制

在 TCP 中有一个超时重传定时器(RTT),在发送一个数据段的同时也启动了该定时器。如果在定时器过期之前该数据段还没有被对方确认,则发送端启动重传机制,重新发送对应序号的数据段,直到发送成功为止。

注意并不是 RTT 定时器一到就会立即重传数据,毕竟从“发送窗口”缓存中找到对应的数据段,然后安排重新发送都是需要时间的,所以实际上超时重发的时间间隔(RTO)要大于 RTT 值。

4. 选择性确认机制

在 TCP 重传机制中,如果在重传定时器超时后仍没收到一个数据段的确认,则可能会重传对应序号后面的所有数据段,因为后面的这些数据段均暂时不会被确认,这明显大大降低 TCP 数据传输的性能。但通过选择性确认机制(SACK)可以避免这种现象的出现。

在 TCP 头部的“可选项”字段中添加了一个代表支持 SACK 的选项。发送端通过识别接收端返回在 ACK 确认数据段中的 SACK 扩展选项就可以得知接收端已收到了哪些不连续序号的数据段,这样发送端就可以不再发送这些数据段,而只发送已丢失(发送端已发送,且在重传定时器超时后仍看到接收端没有收到)的数据段。

比如:接收端已收到 1、101、201、401、501 这 5 个序号的数据段,在发送“确认号”为 301 的确认数据段时,在 SACK 扩展选项中标记 401 和 501 这两个不连续的数据段。这时发送端就知道不需要再发送 401 和 501 这两个数据段,只需发送 301 号数据段即可。这大大节省了网络资源,也提高了数据传输效率。

5. Keep-Alive

TCP 有一个保持活跃的机制叫做 Keep-Alive,这个机制的原理是这样的:

定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。

上述的可定义变量,分别被称为保活时间、保活时间间隔和保活探测次数。在 Linux 系统中,这些变量分别对应变量 net.ipv4.tcp_keepalive_time、net.ipv4.tcp_keepalive_intvl、net.ipv4.tcp_keepalve_probes,默认设置是 7200 秒(2 小时)、75 秒和 9 次探测。
image.png
如果开启了 TCP 保活,需要考虑以下几种情况:

  • 第一种,对端程序是正常工作的。当 TCP 保活的探测报文发送给对端, 对端会正常响应,这样 TCP 保活时间会被重置,等待下一个 TCP 保活时间的到来。

  • 第二种,对端程序崩溃并重启。当 TCP 保活的探测报文发送给对端后,对端是可以响应的,但由于没有该连接的有效信息,会产生一个 RST 报文,这样很快就会发现 TCP 连接已经被重置。

  • 第三种,是对端程序崩溃,或对端由于其他原因导致报文不可达。当 TCP 保活的探测报文发送给对端后,石沉大海,没有响应,连续几次,达到保活探测次数后,TCP 会报告该 TCP 连接已经死亡。

TCP 保活机制默认是关闭的,当我们选择打开时,可以分别在连接的两个方向上开启,也可以单独在一个方向上开启。

TCP 流量控制

在数据传输的过程中,如果发送端一次发送的数据太大,接收端来不及处理,同时缓冲区又放不下时,那么这部分数据只能在接收端被丢弃。这种情况大大影响了数据传输的可靠性和传输效率,因为这么大的数据被丢弃后又需要发送端重传,浪费大量资源,最后导致网络崩溃。

为了解决这个问题,在 TCP 协议的头部有一个名为窗口大小的字段,用来告诉对方本端下次可接收数据的最大大小,这样对方在向本端发送数据时就会自动把要发送的数据大小调整在窗口大小范围内,确保发送的数据不会在接收端被丢弃。但是注意,实际发送的数据大小虽然一定不能大于对方发来数据段中的窗口大小的值,但并不一定就等于这个窗口大小的值,因为这还要受本端的发送窗口大小影响,而且是在不断变化的。这就是我们所说的“滑动窗口”的含义。

发送窗口的大小与缓存中所缓存的数据大小有关,所缓存的数据包括两个部分:一是来自对方但本地设备还来不及处理的数据,二是自己向对方发送但还没有收到对方对这些数据段的确认的数据。也就是说,缓存中可能会同时包含来自对方的数据以及自己原来发送的数据。

1. 滑动窗口流量控制机制

image.png
假设现在发送端收到了接收端的一个确认数据段 ack=301,可发送的窗口大小为 500,表示发送端可以连续发送 5 个数据段(起始序号为 301 )。左边那个虚线“发送窗口”代表的就是要发送的 5 个数据段,此时已达到接收段的窗口大小值,因此不能继续发送了,需要停下来等待对端的确认。

如果发送数据前又收到了来自接收端的一个确认数据段 ack=501,可发送的窗口大小为 400(起到了流量控制的作用),表示接收端已正确接收了 301 和 401,即从发送窗口中删除这两个数据段,窗口向前滑动两个数据段的大小,移到图中细实线“发送窗口”位置。此时 501、601、701 是原来已发送但没收到确认的缓存数据段。801、901 才是真正要等待发送的。

如果此后又收到了一个确认数据段 ack=801,可发送的窗口大小为 500(流量控制),则发送端知道接收端已收到了 701 及以前所有的数据段了,于是从发送窗口中删除这些缓存的数据段,此时缓存的数据段仅为 801 号数据段(因为上一次发送了 801),并且由于原来的 901 号数据段已在发送窗口中等待发送了,所以此时发送窗口只需再继续向前移动三个数据段大小,如图中的粗实线“发送窗口”,继续发送 901、1001、1101、1201 数据段。

2. 数据丢失情况下的流量控制

如果某一时刻,对端发送的数据段显示窗口大小的值为 0,这时发送端自然不能再发送数据了,只好等待对方发来一个窗口大小不为 0 的数据段。可是对端发来的这个数据段在传输过程中丢失了,此时发送端就一直在等待来自接收端窗口大小值非 0 的数据段,而接收端又在等待发送端发来新的数据,因为它自己不知道所发送的数据段在途中丢失了。

为了解决这个问题,TCP 引入了一个称为“持续计时器”的定时器。它是在 TCP 连接的一端收到对端的一个窗口大小值为 0 的数据段时就启动该定时器。在这个定时器到期后,收到窗口大小值为 0 的一端会向对端发送一个探测数据段,这时对端在收到这个探测数据段后会返回一个确认数据段。如果在确认字段中的窗口大小值仍为 0 则重启上面的“持续定时器”,否则结合确认数据段中窗口大小值和当前可用的发送窗口大小发送相应字节的数据,解除以上这种双方持续等待的局面。

TCP 拥塞控制

TCP 的流量控制只是在考虑单个连接的数据传递,但 TCP 数据包是需要经过网卡、交换机、核心路由器等一系列的网络设备的,网络设备本身的能力也是有限的,当多个连接的数据包同时在网络上传送时,势必会发生带宽争抢、数据丢失等,这样 TCP 就必须考虑多个连接共享在有限的带宽上,兼顾效率和公平性的控制,这就是拥塞控制的本质。

网络拥塞发生的原因可能是多方面的,比如 TCP 连接的整个链路中,节点设备的缓存空间太小,数据转发能力太低或某段链路带宽太小等都可能引起网络拥塞,而且往往是多个因素同时存在,所以要处理拥塞控制问题必须从全局角度来寻找解决方案,否则可能不仅不解决拥塞现象,还会形成新的瓶颈。

在实际的网络传输过程中,刚开始时网络系统的吞吐量随输入负荷的增加呈同步提高态势,但当输入负荷到达或接近最大吞吐量时,如果继续提高输入负荷,此时网络系统的实际吞吐量会呈现下降趋势。如果此时输入负荷继续增加,最终可能会导致网络系统的吞吐量下降到 0,出现完全死锁状态。

举个形象点的例子,有一个货车行驶在半夜三点的大路上,这样的场景是断然不需要拥塞控制的。我们可以把网络设备形成的网络信息高速公路和生活中实际的高速公路做个对比。正是因为有多个 TCP 连接,形成了高速公路上的多队运送货车,高速公路上开始变得熙熙攘攘,这个时候就需要拥塞控制的接入了。

在 TCP 协议中,拥塞控制是通过拥塞窗口来完成的,拥塞窗口的大小会随着网络状况实时调整。拥塞控制常用的算法有慢启动,它通过一定的规则,慢慢地将网络发送数据的速率增加到一个阈值。超过这个阈值后,慢启动就结束了,另一个叫做拥塞避免的算法登场。在这个阶段,TCP 会不断地探测网络状况,并随之不断调整拥塞窗口的大小。

1. 慢启动

慢启动是为了避免出现网络拥塞而采取的一种 TCP 拥塞初期预防方案。其基本思想就是在 TCP 连接正式数据传输时每次可发送的数据大小(拥塞窗口)是逐渐增大的,也就是先发送一些小字节数的试探性数据,在收到这些数据段的确认后,再慢慢增大发送的数据量,直到到达了某个原先设定的极限值(慢启动阀值)。

在任何一个时刻,TCP 发送缓冲区的数据是否能真正发送出去,至少取决于两个因素,一个是当前的发送窗口大小,另一个是拥塞窗口大小,而 TCP 协议中总是取两者中最小值作为判断依据。比如当前发送的字节为 100,发送窗口的大小是 200,拥塞窗口的大小是 80,那么取 200 和 80 中的最小值,就是 80,当前发送的字节数显然是大于拥塞窗口的,结论就是不能发送出去。

慢启动实现机制:

  • 在一个 TCP 传输连接建立时,发送端将“拥塞窗口”初始化为该连接上当前使用的最大数据段(MSS )大小,即拥塞窗口初始大小 为 1MSS,然后发送一个大小为 MSS 的数据段。

  • 如果在定时器过期前发送端收到了该数据段的确认,则发送端将“拥塞窗口”大小再增加一个 MSS,此时拥塞窗口大小也就是 2MSS,然后发送 2MSS 大小的数据。

  • 如果这次发送的 2MSS 数据段也都被确认了,则“拥塞窗口”大小再增加 2MSS(一共 4MSS),依此类推。也就是每一次发送数据时的拥塞窗口大小和所发送的数据段大小都是前一次发送的两倍。

拥塞窗口不会无限制地继续增大,拥塞窗口大小就是一个临界点,于是引入了“慢启动”方案的另一个重要参数——慢启动阈值,当发生一次数据丢失时,阈值就会变为当前拥塞窗口的一半,然后拥塞窗口又重新被置为 1MSS。然后继续使用“慢启动”方案来解决网络拥塞问题。当拥塞窗口再次增长到阈值(此时仅为原来窗口的一半) 时便停止使用“慢启动”方案,需要采用“拥塞避免”的解决方案。

2. 拥塞避免

当本次发送数据时的拥塞窗口再次达到慢启动阈值时就启动“拥塞避免”的解决方案。它的基本思想是:在拥塞窗口值第二次到达阈值时,让拥塞窗口每经过一次传输,值仅加 1MSS(不是原来的翻倍),使其以线性方式慢慢增大,而不是继续像“慢启动”方案中那样以指数方式快速增大。

当再次发生数据丢失时,又会把阈值减为当前拥塞窗口的一半 ,然后把拥塞窗口重置为 1MSS,重新进入“慢启动”数据发送过程,依此类推。

3. 快速重传、快速恢复

我们知道,接收端需要对每个接收到的 TCP 分组进行确认,也就是发送 ACK 报文,但是 ACK 报文本身是不带数据的分段,如果一直这样发送大量的 ACK 报文,就会消耗大量的带宽。

为此 TCP 在接收端进行了优化,这个优化的算法叫做延时 ACK。延时 ACK 在收到数据后并不马上回复,而是累计需要发送的 ACK 报文,等到有数据需要发送给对端时,将累计的 ACK 捎带一并发送出去。当然该机制不会无限延时下去,否则发送端误认为数据包没有发送成功,引起重传,反而会占用额外的网络带宽。

快速重传的基本思想是:当接收端收到一个不是按序到达的数据段时,TCP 实体会迅速发送一个重复 ACK 数据段,而不用等到有数据需要发送时顺带发出确认;在重复收到 3 个重复 ACK 数据段后就认为对应 ACK 的数据段已经丢失,此时 TCP 不等重传定时器超时就会重传这个看来已经丢失的数据段。
image.png
在快速重传算法发送了看来已经丢失的数据段后,快速恢复算法同时开始作用。快速恢复算法的基本思想是:在收到第 3 个重复 ACK 时,把当前拥塞窗口设为当前慢启动阈值的一半,减轻网络负荷负担,然后执行拥塞避免算法,使拥塞窗口值慢慢增大,以避免再次出现网络拥塞。