破解网络奥秘:一图胜千言,TCP重传、滑动窗口、流量与拥塞控制全解析

引言

TCP 是一个非常复杂且伟大的协议,它通过许多机制来确保传输的可靠性。为了实现这一目标,TCP需要处理各种问题,比如数据损坏、丢包、重复数据以及分片顺序混乱等。如果这些问题得不到解决,可靠传输就无从谈起。

众所周知,TCP 是一个可靠传输的协议,那么它是如何实现这种可靠性的呢?

为了实现可靠传输,需要考虑许多方面的问题,例如数据的破坏、丢包、重复传输以及分片顺序的混乱等。如果这些问题得不到解决,可靠传输就无从谈起。

TCP 是通过以下机制来实现可靠传输的:序列号、确认应答、重传控制、连接管理以及窗口控制。

今天,我们将重点介绍 TCP 的重传机制滑动窗口流量控制拥塞控制

在这里插入图片描述

重传机制

TCP 实现可靠传输的一种方式是通过使用序列号和确认应答。

在 TCP 协议中,当发送端的数据到达接收端时,接收端会发送一个确认应答消息,表示已经成功接收到数据。

在这里插入图片描述

然而,在复杂的网络环境中,数据传输并不总是那么顺利。如果数据在传输过程中丢失了怎么办?

为了应对数据包丢失的情况,TCP 引入了重传机制

接下来说说常见的重传机制:

  • 超时重传
  • 快速重传
  • SACK
  • D-SACK

超时重传

重传机制的一种方式是在发送数据时设置一个定时器。如果在指定时间内没有收到对方的 ACK 确认应答消息,发送端就会重新发送该数据包,这种方法被称为超时重传

TCP 会在以下两种情况下触发超时重传:

  1. 数据包丢失
  2. 确认应答丢失

超时时间应该设置为多少呢?

我们先来了解一下什么是 RTT(Round-Trip Time 往返时延),从下图我们就可以知道:

在这里插入图片描述
RTT 指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。

超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。

假设在重传的情况下,超时时间 RTO 「较长或较短」时,会发生什么事情呢?

  • 当超时时间 RTO 较大时,重发速度慢,数据丢失后需要等待很长时间才会重传,效率低,性能差;
  • 当超时时间 RTO 较小时,可能会导致未丢失的数据也被重传,重传过快会增加网络拥塞,进而引发更多的超时,导致更多的重传。

精确测量超时时间 RTO 的值是非常重要的,这样可以使重传机制更加高效。

从上述情况可以看出,超时重传时间 RTO 的值应略大于数据报文的往返时间 RTT

在这里插入图片描述

到这里,可能大家会觉得计算超时重传时间 RTO 的值似乎并不复杂。

似乎只需要在发送端发送数据包时记下时间 t0,然后在接收端收到 ACK 时再记下时间 t1,那么 RTT = t1 - t0。其实没那么简单,这只是一次采样,不能代表普遍情况

实际上,“报文往返时间 RTT 的值” 经常变化,因为网络状况也是不断变化的。由于 “报文往返时间 RTT 的值” 经常波动,所以 “超时重传时间 RTO 的值” 也应该是一个动态变化的值。这里我不做过多讲解,感兴趣的朋友可以自行查阅相关资料。

尽管超时重传机制能确保丢失的数据最终被重传,但它的局限性在于可能会导致较长的延迟,因为发送端需要等待定时器超时后才能重传数据。为了解决这一问题,TCP 引入了快速重传机制

快速重传

TCP 还引入了另一种称为快速重传(Fast Retransmit)的机制。这种机制不是依靠时间,而是依靠数据驱动来触发重传

那么,快速重传机制是如何工作的呢?其实很简单,一图胜千言。

在这里插入图片描述

在上图中,发送方依次发出了 1,2,3,4,5 份数据:

  1. 第一份 Seq1 先送到了接收方,因此接收方返回 ACK 2
  2. Seq2 因为某些原因没有到达接收方,而 Seq3 到达了接收方,所以接收方仍然返回 ACK 2
  3. 随后,Seq4Seq5 也成功到达了接收方,但接收方还是返回 ACK 2,因为它还在等待 Seq2
  4. 发送方收到三个 ACK 2 后,意识到 Seq2 丢失了,因此在定时器过期之前重传 Seq2
  5. 最后,接收方收到了重传的 Seq2,由于之前的 Seq3Seq4Seq5 都已经收到了,于是接收方返回 ACK 6

因此,快速重传的工作方式是在接收方收到三个相同的 ACK 报文时,发送方会在定时器过期之前重传丢失的报文段。

快速重传机制虽然解决了超时时间的问题,但仍然面临另一个问题:在重传时,应该重传丢失的一个报文段,还是重传所有报文段

例如,在上述例子中,是只重传 Seq2 还是重传 Seq2Seq3Seq4Seq5?因为发送端无法确定这连续的三个 ACK 2 是哪个报文段传回的。

根据 TCP 的不同实现,这两种情况都是可能的。因此,快速重传机制有时会显得不够精确

为了更好地解决这个问题,即明确知道该重传哪些报文段,TCP 引入了 SACK(Selective Acknowledgment,选择性确认) 方法。

SACK

这种方法需要在 TCP 头部的「选项」字段中增加一个 SACK 字段。SACK 能将接收方的缓存状态发送给发送方,使发送方明确哪些数据已经收到,哪些数据还没有收到。通过这些信息,发送方可以只重传丢失的数据

如果要支持 SACK,必须双方都要支持。在 Linux 下,可以通过 net.ipv4.tcp_sack 参数打开这个功能(Linux 2.4 后默认打开)。

D-SACK

Duplicate SACK 又称 D-SACK,其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了

下面举两个例子来说明 D-SACK 的作用。

例子一:ACK 丢包

在这里插入图片描述

  • 「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)。
  • **接收方发现数据重复收到,于是回了一个 SACK = 30003500**,告知「发送方」30003500 的数据早已被接收了。由于 ACK 已经到了 4000,意味着 4000 之前的所有数据都已收到,因此这个 SACK 就代表着 D-SACK
  • 这样「发送方」就知道了,数据并没有丢失,丢失的是「接收方」的 ACK 确认报文。

例子二:网络延迟

在这里插入图片描述

  • 数据包(1000~1499)由于网络延迟,导致「发送方」没有收到 Ack 1500 的确认报文。
  • 后续报文到达时,三个相同的 ACK 确认报文触发了快速重传机制。重传后,被延迟的数据包(1000~1499)又到了「接收方」;
  • 接收方回了一个 SACK=1000~1500,因为 ACK 已经到了 3000,所以这个 SACK 是 D-SACK,表示收到了重复的包
  • 这样,「发送方」就知道快速重传触发的原因不是发出去的包丢失,也不是回应的 ACK 包丢失,而是由于网络延迟。

可见,D-SACK 有以下几个好处:

  • 可以让「发送方」知道是发送出去的包丢了,还是接收方回应的 ACK 包丢了。
  • 可以识别出「发送方」的数据包是否被网络延迟了。
  • 可以检测网络中是否出现了「发送方」的数据包被复制的情况。

在 Linux 下,可以通过 net.ipv4.tcp_dsack 参数来开启或关闭这个功能(在 Linux 2.4 版本之后默认是开启的)。

滑动窗口

我们都知道TCP协议采用了一种基于可靠传输的设计,其中包含了确认应答机制,确保每一个数据包都能得到接收方的确认。这类似于两个人之间的对话,一方发送信息后必须等待另一方的回应,确认信息已被正确接收,然后才能发送下一条信息。

然而,这种一对一的应答机制就像是一场严格交替的对话,其中一个人必须完全专注于对方的回应,才能继续发言。这就好像我们在交流时,你必须暂停直到我对你的话做出反应,你才能继续你的思路,这显然是低效且不符合日常交流习惯的。

在这里插入图片描述

因此,这种传输方式存在一个缺点:数据包的往返时间越长,通信效率就越低

为了解决这个问题,TCP 引入了窗口机制。即使在往返时间较长的情况下,窗口机制也能保持较高的通信效率。

有了窗口机制,可以指定窗口大小,窗口大小指的是无需等待确认应答即可继续发送数据的最大量

窗口的实现实际上是在操作系统中开辟一个缓存空间,发送方主机在等待确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,数据就可以从缓存区清除。

假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。如下图:

在这里插入图片描述

图中的 ACK 600 确认应答报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方收到了 ACK 700 确认应答,就意味着 700 之前的所有数据「接收方」都收到了。这个模式就叫累计确认或者累计应答

窗口大小由哪一方决定?

TCP头中有一个字段称为窗口大小(Window)。

这个字段由接收端用来告知发送端自身剩余的缓冲区容量。根据接收端的处理能力,发送端可以调整发送数据的速度,避免接收端因处理不过来而导致数据丢失。

因此,通常情况下,窗口大小由接收端决定。

发送端发送的数据量不能超过接收端的窗口大小,否则接收端将无法正常接收这些数据。

流量控制

发送方不能盲目地发送数据给接收方,需要考虑接收方的处理能力。

如果发送方不停地发送数据而不考虑接收方的处理能力,会导致接收方无法处理,进而触发重传机制,从而造成网络资源的浪费。

为了解决这种问题,TCP 提供了一种机制,使得发送方可以根据接收方的实际接收能力来控制数据发送量,这就是所谓的流量控制。这主要是通过滑动窗口(Sliding Window) 机制实现的。

想象一下,发送方和接收方之间有一条管道,数据就像水流一样通过这条管道。滑动窗口机制就像是在这条管道的接收端设置了一个可调节的阀门,用来控制水流的速度,确保接收方能够顺利处理所有流进的数据。

操作系统缓冲区与滑动窗口的关系

操作系统缓冲区

操作系统缓冲区是内存中用于暂时存储数据的区域,以便在数据传输过程中平衡生产和消费速率。操作系统在发送和接收数据时,都会用到缓冲区。

  1. 发送缓冲区:存储即将通过网络发送的数据。发送方应用程序将数据写入发送缓冲区,操作系统再根据网络状况和滑动窗口大小将数据发送出去。
  2. 接收缓冲区:存储接收到的数据,等待应用程序读取。接收方从网络接收到的数据先存储在接收缓冲区,然后再由应用程序读取和处理。

TCP滑动窗口

TCP滑动窗口是TCP协议用来控制数据流量和确保可靠传输的机制。它规定了在等待接收方确认(ACK)之前,发送方可以发送的数据量。

  1. 发送窗口:发送方维护一个发送窗口,包含已发送但未确认的数据量。这个窗口限制了发送方在等待ACK之前可以发送的数据量。
  2. 接收窗口:接收方维护一个接收窗口,告诉发送方它可以接收的数据量。接收窗口大小通常由接收缓冲区的可用空间决定。

关系和协作

  1. 同步和协调

    • 发送缓冲区和发送窗口:发送缓冲区存储即将发送的数据,而发送窗口决定了在等待ACK之前可以发送的数据量。如果发送窗口变小(例如,接收方的接收缓冲区满了),发送方将减少发送速率,将更多数据留在发送缓冲区。
    • 接收缓冲区和接收窗口:接收缓冲区存储接收到但尚未被应用程序处理的数据。接收窗口大小通常反映了接收缓冲区的剩余空间。如果接收缓冲区快满了,接收窗口会变小,通知发送方减少发送速率。
  2. 窗口大小调整

    • 接收方根据接收缓冲区的可用空间动态调整接收窗口大小,并通过ACK包通知发送方。发送方则根据接收窗口的大小调整发送数据的速率。
    • 当接收缓冲区有足够空间时,接收方会增大接收窗口,允许发送方发送更多数据;当接收缓冲区空间不足时,接收方会减小接收窗口,限制发送方的数据传输速率。
  3. 数据流管理

    • 操作系统通过缓冲区管理和滑动窗口机制,确保数据流在网络上传输时的平稳性和可靠性。缓冲区用来平衡发送方和接收方的处理速度,而滑动窗口用来控制数据传输的节奏,避免网络拥堵和数据丢失。

窗口关闭

在TCP流量控制中,“窗口关闭”是指接收窗口的大小变为零的情况。这意味着接收方目前没有可用缓冲区空间来接收更多的数据,因此它告诉发送方暂停发送新的数据包,直到缓冲区有空闲空间为止。这种情况可能发生在接收方的应用程序处理数据速度较慢或者系统资源不足时。

  1. 窗口关闭的触发
  • 接收窗口为零:当接收方的缓冲区满了,接收窗口的大小就会变为零,通知发送方不要再发送数据。
  • ACK 包中的窗口大小:接收方在发送ACK包时,会在窗口大小字段中填入0,表示当前无法接收更多的数据。
  1. 发送方的反应
  • 停止发送数据:发送方在接收到窗口大小为零的ACK包后,会停止发送新的数据包。
  • Zero Window Probe:发送方会定期发送零窗口探测包(Zero Window Probe),询问接收方的窗口大小是否恢复。如果接收方的缓冲区有了新的可用空间,会回复一个窗口更新包。
  1. 窗口重新开放
  • 接收方缓冲区释放空间:接收方应用程序处理数据,释放缓冲区空间后,接收窗口会重新开放。
  • 更新窗口大小:接收方在下一次发送ACK包时,会更新窗口大小字段,通知发送方当前可以接收的数据量。
  • 恢复数据传输:发送方收到更新后的窗口大小后,继续发送数据。

窗口关闭潜在的危险

接收方向发送方通告窗口大小时,是通过 ACK 报文来通告的。

那么,当发生窗口关闭时,接收方处理完数据后,会向发送方通告一个窗口非 0 的 ACK 报文,如果这个通告窗口的 ACK 报文在网络中丢失了,那麻烦就大了。

在这里插入图片描述

这会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。

TCP 是如何解决窗口关闭时,潜在的死锁现象呢?

为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器

如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

在这里插入图片描述

  • 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;
  • 如果接收窗口不是 0,那么死锁的局面就可以被打破了。

窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。

糊涂窗口综合症

如果接收方太忙,没空处理数据,那么发送方的发送窗口就会越来越小。

到最后,接收方终于挤出一点时间,腾出了几个字节的空间,并通知发送方:“好了,现在有几个字节可以用了!” 结果,发送方二话不说就立即发送这几个字节的数据。这就是传说中的糊涂窗口综合症

要知道,TCP + IP 的头部就占了 40 个字节,为了传输这几个字节的数据要耗费这么多资源,真是亏大了。

这就好比一辆能载 50 人的大巴车,每次只来了几个人就发车。除非司机是个土豪,不然这么干迟早要破产。其实,解决这个问题也不难。大巴司机只需要等乘客人数超过 25 人再发车,这样就不会浪费资源啦。

要不,司机还可以挂个牌子:“车不满,不发车!”

糊涂窗口综合症的现象是可以发生在发送方和接收方:

  • 接收方可以通告一个小的窗口
  • 而发送方可以发送小数据

于是,要解决糊涂窗口综合症,就解决上面两个问题就可以了

  • 让接收方不通告小窗口给发送方
  • 让发送方避免发送小数据

怎么让接收方不通告小窗口呢?

接收方就像餐厅的后厨,如果后厨的盘子(缓存空间)快放不下了(窗口小于 min( MSS, 缓存空间的一半 )),就会对前台(发送方)说:“别再接单了!” 相当于通告窗口为 0。

然后后厨开始忙着把盘子上的菜端出去(处理数据)。当盘子上有足够的空间(窗口大小 >= MSS 或者缓存空间的一半可以使用),后厨就会对前台说:“可以继续接单了!” 这样前台就可以继续接新的订单(发送数据)。

这样一来,后厨就不会被堆积的订单压垮,也不会因为只处理几个订单就浪费大量资源。所有人都轻松,生意也更好。

怎么让发送方避免发送小数据呢?

Nagle 算法就像一个超级节约的快递公司老板,他有两个发货规则:

  • 如果车(窗口大小)能装满(>= MSS),或者快递包裹够大(>= MSS),就可以发货。
  • 必须等到之前发出的快递收到回执(ACK)才能发下一批。

如果这两个条件都没满足,老板就会一直囤积包裹,等到条件满足后再发货。

然而,对于一些需要快速小包裹来回跑的情况,比如送外卖(telnet 或 ssh),这种策略就不合适了。外卖小哥不能等满车再走,要不然客户早就饿晕了。这时,快递公司就会关闭节约模式(Nagle 算法),直接派人送餐(关闭 TCP_NODELAY 选项),保证外卖快速送达。

所以,不是所有情况下都需要节约模式,有时候迅速响应更重要!

可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法:

setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));

拥塞控制

为什么要有拥塞控制呀,不是有流量控制了吗?

流量控制是为了防止发送方的数据量超过接收方的缓存能力,确保接收方不会被过多数据淹没。然而,这种控制机制无法感知网络层的状况。

在计算机网络中,网络带宽是共享资源,其他主机的通信可能导致网络拥堵。网络拥堵时,如果继续发送大量数据包,会增加数据包的延迟和丢包率。TCP 会尝试重传这些丢失的数据,但这会进一步加重网络负担,从而导致更高的延迟和更多的丢包,形成恶性循环

为了避免这种情况,TCP 引入了拥塞控制机制。拥塞控制的目的是防止发送方的数据填满整个网络。为此,TCP 使用了一个名为拥塞窗口的概念,该窗口用于限制发送方可以发送的数据量,从而降低网络拥塞的风险。拥塞窗口根据网络状况动态调整,确保数据传输既不超出接收方处理能力,也不会导致网络拥堵。

什么是拥塞窗口?和发送窗口有什么关系呢?

拥塞窗口(Congestion Window,CWND) 是 TCP 协议中用于拥塞控制的一个重要概念。它表示发送方在不引发网络拥塞的情况下,能够发送的最大数据量。拥塞窗口的大小动态调整,以反映网络当前的拥塞状况。具体来说,拥塞窗口的大小基于网络的反馈信息(如延迟、丢包等)来增加或减少,从而控制数据的发送速率,避免过度拥塞。

发送窗口(Send Window) 是流量控制的概念,表示发送方能够发送但尚未被接收方确认的数据量。发送窗口的大小由接收方的接收窗口(Receiver Window)和拥塞窗口(CWND)中较小者决定。接收窗口控制着发送方的最大发送数据量,以避免淹没接收方的缓存,而拥塞窗口则控制着网络的总负载,防止网络拥堵。

总的来说,发送窗口和拥塞窗口在数据传输中共同作用:发送窗口确保发送方不会超出接收方的处理能力,而拥塞窗口则防止发送方的数据过多地填满网络造成拥堵。最终,TCP 发送方会根据这两个窗口的最小值来决定实际可以发送的数据量。

那么怎么知道当前网络是否出现了拥塞呢?

其实,如果发送方在规定时间内没有收到 ACK 确认报文,也就是说发生了超时重传,发送方就会认为网络出现了拥塞。这是因为超时通常表明网络传输出现了问题,可能是由于网络拥堵或丢包造成的。因此,发送方会把这种超时情况视作网络拥塞的信号,并调整其发送策略以缓解拥塞。

拥塞控制有哪些控制算法?

拥塞控制主要是四个算法:

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

1. 慢启动(Slow Start):

  • 目的: 快速找出网络的最大容量。
  • 方法: 开始时用很小的数据量发送,接收到确认后,发送量快速增加。就像在探测水深时,从小处开始,然后快速加深。

2. 拥塞避免(Congestion Avoidance):

  • 目的: 平稳增加发送数据量,避免网络拥堵。
  • 方法: 当网络负担较重时,发送数据量的增加变得比较慢。就像在汽车进入拥堵区时,从加速驾驶改为平稳驾驶。

3. 快重传(Fast Retransmit):

  • 目的: 迅速重传丢失的数据包。
  • 方法: 如果发现某个数据包的确认收不到,发送方会快速重传这个数据包,而不是等到超时。就像收到一个丢失的邮件后,立刻重新发送。

4. 快恢复(Fast Recovery):

  • 目的: 在发现丢包后快速恢复数据传输。
  • 方法: 快重传后,拥塞窗口会减少到一半,然后逐渐恢复到正常速度。就像在堵车后,车速会先减慢,再慢慢恢复正常行驶。

拥塞算法示意图

在这里插入图片描述

参考

小林coding——TCP 重传、滑动窗口、流量控制、拥塞控好难?看完图解就不愁了(重制)
在这里插入图片描述

小结

啊哈,让我们以一种轻松愉快的方式总结一下TCP的那些事儿吧!

想象一下,你和你的朋友在玩“扔沙包”游戏,但是你们俩中间隔着一个特别长的隧道。为了让游戏进行得既公平又有趣,你们发明了一套规则:

  1. TCP重传 —— 如果你扔过去的沙包,你的朋友没接到,他会喊一声:“没接到,再扔一次!”于是你就得重新扔过去。这就是TCP重传,确保数据包准确无误地到达目的地。

  2. 滑动窗口 —— 假设你朋友的口袋只能装三个沙包,他告诉你:“一次扔三个,多了我接不住。”这就是滑动窗口,它控制了你一次能扔多少沙包,确保你的朋友能处理得过来。

  3. 流量控制 —— 有时候你的朋友在忙别的事,他就说:“慢点扔,我这忙着呢。”这就是流量控制,它确保你的朋友不会被沙包淹没,也就是防止数据溢出。

  4. 拥塞控制 —— 如果隧道里有很多人在玩这个游戏,大家都扔沙包,那隧道就会堵住。所以你们得聪明点,看看隧道里情况怎么样,适当调整扔沙包的频率,这就是拥塞控制,避免整个网络被数据包堵死。

所以,TCP协议就是你们玩“扔沙包”游戏时制定的一系列规则,确保游戏既好玩又不会让任何人受伤,或者让隧道变成沙包海洋。记住,无论游戏多么复杂,只要规则清晰,每个人都能玩得开心!

这就是TCP协议背后的“扔沙包”哲学,你get到了吗?下次当你在网络的海洋中遨游时,不妨想想你其实是在玩一场高级的“扔沙包”游戏哦!每一次点击、每一次加载,都是一次精准投掷和默契接应的展现!

  • 11
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值