TCP 协议算法解析 | RTT / 滑动窗口 / 拥塞控制

news2025/4/1 19:36:33

注:本文为 “TCP 协议算法解析” 相关文章合辑

略作重排,未去重。

如有内容异常,请看原文。


TCP 的那些事儿(上)

2014 年 05 月 28 日 陈皓

TCP 是一个极为复杂的协议,因为它需要解决众多问题,而这些问题又衍生出诸多子问题和复杂情况。因此,学习 TCP 是一个较为痛苦的过程,但学习过程本身能带来诸多收获。关于 TCP 协议的细节,推荐您阅读 W.Richard Stevens 的《TCP/IP 详解 卷 1:协议》(您也可以阅读 RFC793 以及后续众多 RFC)。此外,本文将使用英文术语,以便您通过这些关键词查找相关技术文档。

撰写本文的目的有以下三个:

  • 锻炼自己能否用简短篇幅清晰描述复杂的 TCP 协议的能力。
  • 许多程序员习惯快餐文化,不愿认真阅读书籍。希望这篇快餐式文章能让您了解 TCP 这一经典技术,并体会到软件设计中的种种难点,进而从中获得软件设计方面的启发。
  • 最重要的是,希望这些基础知识能帮助您澄清一些似是而非的概念,并让您意识到基础的重要性。

因此,本文不会面面俱到,仅对 TCP 协议、算法和原理进行科普。

最初只想写一篇短文,但 TCP 实在太过复杂,远超 C++ 的复杂度。30 多年来,TCP 经历了各种优化、变种、争论和修改。写着写着发现篇幅过长,只能将其拆分为上下两篇:

  • 上篇主要介绍 TCP 协议的定义以及丢包时的重传机制。
  • 下篇将重点介绍 TCP 的流量控制和拥塞处理机制。

言归正传,首先需要明确 TCP 在网络 OSI 七层模型中的位置:TCP 位于第四层——传输层(Transport Layer),IP 位于第三层——网络层(Network Layer),ARP 位于第二层——数据链路层(Data Link Layer)。在第二层上的数据称为帧(Frame),在第三层上的数据称为数据包(Packet),在第四层的数据称为数据段(Segment)。

程序中的数据首先会被封装到 TCP 的数据段(Segment)中,随后 TCP 数据段会被封装到 IP 的数据包(Packet)中,再进一步封装到以太网(Ethernet)的帧(Frame)中。数据传输到对端后,各层会解析对应的协议,并将数据交给上层协议处理。

TCP 头格式

接下来,我们来看 TCP 头的格式:

需要注意以下几点:

  • TCP 数据包中不包含 IP 地址,这是 IP 层的任务。但 TCP 包包含源端口和目标端口。
  • 一个 TCP 连接需要四个元组来唯一标识:(源 IP,源端口,目标 IP,目标端口)。严格来说,应是五元组,再加上协议类型。但本文仅讨论 TCP 协议,因此仅提及四元组。
  • 注意上图中的四个重要字段:
    • Sequence Number(序号):用于解决网络包乱序(reordering)问题。
    • Acknowledgement Number(确认号):即 ACK,用于确认收到数据,以解决数据丢失问题。
    • Window(窗口):也称为 Advertised-Window,即著名的滑动窗口(Sliding Window),用于解决流量控制问题。
    • TCP Flag(标志位):用于控制 TCP 的状态机。

关于其他内容,可参考下图:

TCP 的状态机

实际上,网络传输是无连接的,TCP 亦是如此。TCP 所谓的“连接”,只是在通信双方维护一个“连接状态”,使其看起来像是有连接一样。因此,TCP 的状态转换非常重要。

以下是“TCP 协议的状态机”.以及“TCP 建立连接”、“TCP 断开连接”和“数据传输”的对照图。将两个图并排放在一起,方便您对照查看。这两个图非常重要,您需要牢记。(吐槽一下:看到如此复杂的状态机,就能感受到协议的复杂性。复杂的东西往往容易出问题,TCP 协议也确实存在诸多复杂情况)


许多人会问,为什么建立连接需要三次握手,而断开连接需要四次挥手?

  • 对于建立连接的三次握手,主要是为了初始化 Sequence Number 的初始值。通信双方需要互相告知对方自己的初始序号(Initial Sequence Number,简称 ISN)。这就是 SYN 的含义,全称为 Synchronize Sequence Numbers。上图中的 $ x $ 和 $ y $ 即为双方的 ISN。这个序号将作为后续数据通信的序号,以保证应用层接收到的数据不会因网络传输问题而乱序(TCP 会利用这个序号拼接数据)。

  • 对于四次挥手,实际上可以看作是两次挥手。因为 TCP 是全双工的,所以发送方和接收方都需要发送 FIN 和 ACK。只不过其中一方是被动的,因此看起来像是四次挥手。如果双方同时断开连接,则会进入 CLOSING 状态,最终到达 TIME_WAIT 状态。下图是双方同时断开连接的示意图(您也可以对照 TCP 状态机查看):

此外,还有几个需要注意的事项:

  • 关于建立连接时 SYN 超时:假设服务器端接收到客户端发送的 SYN 后,回复了 SYN-ACK,但客户端掉线,服务器端未收到客户端返回的 ACK。此时,连接处于中间状态,既未成功,也未失败。因此,服务器端会在一定时间内未收到 ACK 时重发 SYN-ACK。在 Linux 系统下,默认重试次数为 5 次,重试间隔时间从 1 秒开始,每次翻倍。5 次重试的间隔时间分别为 1 秒、2 秒、4 秒、8 秒、16 秒,总共 31 秒。第 5 次发送后还需等待 32 秒,直到确认第 5 次也超时,因此总共需要 1 + 2 + 4 + 8 + 16 + 32 = 2 6 − 1 = 63 1 + 2 + 4 + 8 + 16 + 32 = 2^6 - 1 = 63 1+2+4+8+16+32=261=63 秒,TCP 才会断开这个连接。

  • 关于 SYN Flood 攻击:恶意攻击者会利用这一机制,向服务器发送 SYN 请求后立即下线。服务器需要等待 63 秒才会断开连接,攻击者借此耗尽服务器的 SYN 连接队列,导致正常连接请求无法处理。为此,Linux 提供了一个名为 tcp_syncookies 的参数来应对这种情况。当 SYN 队列满时,TCP 会根据源地址、目标地址、端口号和时间戳生成一个特殊的 Sequence Number(称为 cookie)并返回。如果是攻击者,则不会响应;如果是正常连接,则会将这个 SYN Cookie 返回,服务器可以通过 cookie 建立连接(即使该连接不在 SYN 队列中)。请注意,不要随意使用 tcp_syncookies 来处理正常高负载的连接情况,因为 syncookies 是一种妥协的 TCP 实现,并不严谨。对于正常请求,您可以调整以下三个 TCP 参数:

    • tcp_synack_retries:减少重试次数。
    • tcp_max_syn_backlog:增加 SYN 连接数。
    • tcp_abort_on_overflow:在无法处理时直接拒绝连接。
  • 关于 ISN 的初始化:ISN 不能硬编码,否则会导致问题。例如,如果连接建立后始终使用 1 作为 ISN,客户端发送了 30 个数据段后,网络中断,客户端重新连接时仍使用 1 作为 ISN,而之前连接的数据包到达后,就会被误认为是新连接的数据包。此时,客户端的 Sequence Number 可能是 3,而服务器端认为客户端的序号是 30,导致数据混乱。RFC793 规定,ISN 会与一个虚拟时钟绑定,该时钟每 4 微秒对 ISN 进行加 1 操作,直到超过 2 32 2^{32} 232,然后重新从 0 开始。因此,一个 ISN 的周期约为 4.55 小时。我们假设 TCP 数据段在网络中的存活时间不会超过 Maximum Segment Lifetime(MSL,Wikipedia 词条)。只要 MSL 的值小于 4.55 小时,就不会重复使用 ISN。

  • 关于 MSL 和 TIME_WAIT:通过上述对 ISN 的描述,您应该已经了解了 MSL 的由来。在 TCP 状态图中,从 TIME_WAIT 状态到 CLOSED 状态有一个超时设置,该超时时间为 2 × \times × MSL(RFC793 定义 MSL 为 2 分钟,Linux 设置为 30 秒)。为什么需要 TIME_WAIT?为什么不直接转为 CLOSED 状态呢?主要有两个原因:

    1. TIME_WAIT 确保对端有足够时间收到 ACK。如果被动关闭的一方未收到 ACK,则会触发被动端重发 FIN,一来一回正好是 2 个 MSL。
    2. 有足够时间避免该连接与后续连接混淆(要知道,有些路由器会缓存 IP 数据包。如果连接被重用,那么延迟到达的数据包可能会与新连接混淆)。您可以参考这篇文章:TIME_WAIT and its design implications for protocols and scalable client server systems。
  • 关于 TIME_WAIT 数量过多:从上述描述可知,TIME_WAIT 是一个重要的状态,但在高并发短连接场景下,TIME_WAIT 状态的连接过多会消耗大量系统资源。搜索相关资料时,您会发现大多数处理方式都建议设置两个参数:tcp_tw_reusetcp_tw_recycle。这两个参数的默认值均为关闭状态,其中 tcp_tw_recycletcp_tw_reuse 更为激进,而 tcp_tw_reuse 相对较为温和。此外,如果使用 tcp_tw_reuse,必须设置 tcp_timestamps=1,否则无效。请注意,开启这两个参数可能会导致 TCP 连接出现一些诡异问题(因为如果不等待超时就重用连接,新的连接可能会建立失败。正如 官方文档 所说:“It should not be changed without advice/request of technical experts”)。

  • 关于 tcp_tw_reuse:官方文档指出,tcp_tw_reuse 结合 tcp_timestamps(又称为 PAWS,即 Protection Against Wrapped Sequence Numbers)可以在协议层面保证安全性,但这需要 tcp_timestamps 在通信双方均被开启(您可以阅读 tcp_twsk_unique 的源码)。我个人认为在某些场景下仍可能存在潜在问题。

  • 关于 tcp_tw_recycle:如果开启了 tcp_tw_recycle,会假设对端也开启了 tcp_timestamps,然后通过比较时间戳来决定是否重用连接。如果时间戳增大,则可以重用。然而,如果对端处于 NAT 网络环境中(例如,一个公司仅使用一个 IP 地址连接公网),或者对端的 IP 地址被另一台设备重用,情况就会变得复杂。建立连接的 SYN 请求可能会被直接丢弃(您可能会看到“connection timeout”错误)。如果您想查看 Linux 内核代码,可以参考 tcp_timewait_state_process。

  • 关于 tcp_max_tw_buckets:该参数用于控制并发的 TIME_WAIT 状态连接数量,默认值为 180000。如果超过该限制,系统会销毁多余的连接,并在日志中记录警告(例如:“time wait bucket table overflow”)。官方文档指出,该参数用于抵御 DDoS 攻击,并且默认值 180000 已经足够大。不过,这个参数仍然需要根据实际情况进行调整。

再次强调,使用 tcp_tw_reuse 和 tcp_tw_recycle 来解决 TIME_WAIT 问题是非常危险的,因为这两个参数违反了 TCP 协议(RFC 1122)。

其实,TIME_WAIT 状态表示主动断开连接。因此,所谓的“不作死就不会死”。试想,如果让对端断开连接,那么这个问题就会由对方来处理。此外,如果您的服务器是 HTTP 服务器,那么设置 HTTP 的 KeepAlive 就显得尤为重要(浏览器会重用一个 TCP 连接来处理多个 HTTP 请求),并让客户端断开连接(请注意,浏览器可能会非常贪婪,不到万不得已不会主动断开连接)。

数据传输中的 Sequence Number

下图是我从 Wireshark 中截取的访问 coolshell.cn 时的数据传输图,展示了 SeqNum 的变化情况(使用 Wireshark 菜单中的 Statistics -> Flow Graph…):

可以看到,SeqNum 的增加与传输的字节数相关。在上图中,三次握手后,来了两个长度为 1440 字节的数据包,第二个包的 SeqNum 变为 1441。随后,第一个 ACK 回应的是 1441,表示第一个 1440 字节的数据已经收到。

注意:如果您使用 Wireshark 抓包查看三次握手过程,会发现 SeqNum 总是为 0,实际上并非如此。Wireshark 为了显示友好,使用了 Relative SeqNum(相对序号)。您可以在右键菜单中的 protocol preference 中取消该选项,以查看“Absolute SeqNum”。

TCP 重传机制

TCP 需要确保所有数据包都能到达目的地,因此必须具备重传机制。

需要注意的是,接收端给发送端的 ACK 确认只会确认最后一个连续收到的数据包。例如,发送端发送了 1、2、3、4、5 共五个数据包,接收端收到了 1 和 2,于是回传 ACK 3。随后,接收端收到了 4(注意此时 3 未收到),此时 TCP 会如何处理?我们需要明确,正如前面所述,SeqNum 和 ACK 是以字节数为单位的,因此 ACK 不能跳着确认,只能确认最大的连续收到的数据包,否则发送端会误以为之前的包都已收到。

超时重传机制

一种情况是接收端不回传 ACK,发送端等待 3 的 ACK 超时后,会重传 3。一旦接收端收到 3 后,会回传 ACK 4,表示 3 和 4 都已收到。

然而,这种方式存在严重问题:由于发送端等待 3 的 ACK,导致 4 和 5 即便已经到达,发送端也完全不知情。由于未收到 ACK,发送端可能会悲观地认为这些包也丢失了,从而导致 4 和 5 的重传。

对此有两种选择:

  • 仅重传超时的数据包,即第 3 个数据包。
  • 重传超时后的所有数据,即第 3、4、5 三个数据包。

这两种方式各有优劣。第一种方式节省带宽,但速度较慢;第二种方式速度较快,但会浪费带宽,也可能做无用功。总体而言,这两种方式都不理想,因为它们都在等待超时,而超时时间可能较长(在下篇中会讨论 TCP 如何动态计算超时时间)。

快速重传机制

因此,TCP 引入了一种称为 Fast Retransmit 的算法,不依赖时间驱动,而是以数据驱动重传。也就是说,如果数据包未连续到达,就回传最后一个可能丢失的数据包的 ACK。如果发送端连续收到 3 次相同的 ACK,则立即重传。Fast Retransmit 的优点是无需等待超时即可重传。

例如:如果发送端发送了 1、2、3、4、5 五个数据包,第一个数据包先到达,于是回传 ACK 2。结果 2 因某些原因未收到,3 到达后,仍然回传 ACK 2。后面的 4 和 5 都到达了,但仍然回传 ACK 2,因为 2 仍未收到。此时,发送端收到了三个 ACK=2 的确认,意识到 2 未到达,于是立即重传 2。随后,接收端收到 2,此时 3、4、5 都已收到,于是回传 ACK 6。示意图如下:

Fast Retransmit 只解决了超时问题,但它仍然面临一个艰难的选择:是仅重传丢失的一个数据包,还是重传所有数据包。对于上述示例,是重传第 2 个数据包,还是重传第 2、3、4、5 个数据包?因为发送端不清楚这连续的 3 个 ACK(2)是由哪些数据包传回的。也许发送端发送了 20 个数据包,是第 6、10、20 个数据包传来的。这样,发送端可能会重传从 2 到 20 的所有数据(这是一些 TCP 实现的实际做法)。可见,这是一个双刃剑。

SACK 方法

另一种更好的方式称为 Selective Acknowledgment (SACK)(参见 RFC 2018)。这种方式需要在 TCP 头中添加一个 SACK 字段,ACK 仍然与 Fast Retransmit 的 ACK 一致,而 SACK 则用于报告收到的数据片段。参见下图:

这样,发送端可以根据回传的 SACK 信息了解哪些数据已经到达,哪些尚未到达,从而优化 Fast Retransmit 算法。当然,这个协议需要双方都支持。在 Linux 系统下,可以通过 tcp_sack 参数启用该功能(Linux 2.4 及以后版本默认启用)。

这里还需要注意一个问题——接收方 Reneging。所谓 Reneging,是指接收方有权丢弃已向发送端报告的 SACK 中的数据。这种行为是不被鼓励的,因为这会使问题复杂化。然而,在某些极端情况下,接收方可能会这么做,例如需要将内存分配给更重要的任务。因此,发送方不能完全依赖 SACK,仍需依赖 ACK,并维护超时机制。如果后续的 ACK 没有增长,那么仍需重传 SACK 中的数据。此外,接收方永远不能将 SACK 的数据包标记为 ACK

需要注意的是,SACK 会消耗发送方的资源。试想,如果攻击者向数据发送方发送大量 SACK 选项,这将导致发送方开始重传甚至遍历已发送的数据,从而消耗大量发送端资源。详细内容可参考《TCP SACK 的性能权衡》。

Duplicate SACK – 重复收到数据的问题

Duplicate SACK,又称 D-SACK,主要利用 SACK 告知发送方哪些数据被重复接收。RFC-2883 中有详细描述和示例。以下是一些示例(来源于 RFC-2883):

D-SACK 使用 SACK 的第一个段作为标志:

  • 如果 SACK 的第一个段的范围被 ACK 覆盖,则为 D-SACK。
  • 如果 SACK 的第一个段的范围被 SACK 的第二个段覆盖,则为 D-SACK。

示例一:ACK 丢失

在以下示例中,丢失了两个 ACK,因此发送端重传了第一个数据包(3000-3499)。接收端发现重复收到该数据包后,回传了一个 SACK=3000-3500。由于 ACK 已到达 4000,表示已收到 4000 之前的所有数据,因此这个 SACK 是 D-SACK,旨在告知发送端已收到重复数据,并且发送端还知道数据包并未丢失,丢失的是 ACK 包。

TransmittedReceivedACK Sent
3000-34993000-34993500 (ACK lost)
3500-39993500-39994000 (ACK lost)
3000-34993000-34994000, SACK=3000-3500

示例二:网络延迟

在以下示例中,网络包(1000-1499)被网络延迟,导致发送方未收到 ACK,而后续到达的三个包触发了“Fast Retransmit 算法”,因此进行了重传。然而,重传时,被延迟的包又到达了,于是回传了一个 SACK=1000-1500。由于 ACK 已到达 3000,因此这个 SACK 是 D-SACK,表明收到了重复的数据包。

TransmittedReceivedACK Sent
500-999500-9991000
1000-1499(delayed)
1500-19991500-19991000, SACK=1500-2000
2000-24992000-24991000, SACK=1500-2500
2500-29992500-29991000, SACK=1500-3000
1000-14991000-14993000
1000-14993000, SACK=1000-1500

引入 D-SACK 有以下好处:

  1. 让发送方知道是发送的数据包丢失了,还是返回的 ACK 包丢失了。
  2. 判断是否是自己的超时时间设置过小,导致了重传。
  3. 识别网络中是否出现了先发送的数据包后到达的情况(即 reordering)。
  4. 判断网络是否对数据包进行了复制。

了解这些信息可以帮助 TCP 更好地了解网络状况,从而更好地进行网络流量控制。

在 Linux 系统下,tcp_dsack 参数用于启用该功能(Linux 2.4 及以后版本默认启用)。

上篇内容到此结束。如果您觉得本文内容较为浅显易懂,欢迎继续阅读下篇:TCP 的那些事儿(下)。


TCP 的那些事儿(下)

2014 年 05 月 28 日 陈皓

这篇文章是下篇,如果您对 TCP 不熟悉,建议您先阅读上篇:TCP 的那些事儿(上)。在上篇中,我们介绍了 TCP 的协议头、状态机以及数据重传机制。然而,TCP 还需要解决一个关键问题,即根据不同的网络状况动态调整发包速度。这不仅能让单个连接更稳定,还能使整个网络更稳定。在阅读下篇之前,请您做好准备,本文涉及诸多算法和策略,可能会引发您的深入思考,需要您的大脑分配大量内存和计算资源,因此不适合在厕所中阅读。

TCP 的 RTT 算法

从前面的 TCP 重传机制中我们知道,超时时间(Timeout)的设置对于重传至关重要。

  • 如果设置过长,重传速度会变慢,导致数据丢失后很久才进行重传,效率低下,性能差。
  • 如果设置过短,可能会在数据并未真正丢失时就进行重传,从而增加网络拥塞,导致更多的超时和更多的重传。

此外,不同网络环境下的超时时间无法设置为一个固定的值,只能动态调整。为此,TCP 引入了 RTT(Round Trip Time,往返时间),即一个数据包从发送出去到返回的时间。这样,发送端可以大致了解所需时间,从而方便地设置超时时间——RTO(Retransmission Timeout,重传超时时间),使重传机制更加高效。听起来似乎很简单,似乎只需要在发送端发送数据包时记录时间 t 0 t_0 t0,然后在接收端返回 ACK 时再记录时间 t 1 t_1 t1,于是 RTT = t 1 − t 0 \text{RTT} = t_1 - t_0 RTT=t1t0。然而,这只是一个采样,不能代表普遍情况。

经典算法

RFC793 中定义的经典算法如下:

  1. 首先,采样 RTT,记录最近几次的 RTT 值。

  2. 然后计算平滑 RTT(Smoothed RTT,SRTT)。公式为(其中 α \alpha α 的取值在 0.8 到 0.9 之间,该算法称为 Exponential Weighted Moving Average,即加权移动平均):

    SRTT = ( α × SRTT ) + ( ( 1 − α ) × RTT ) \text{SRTT} = (\alpha \times \text{SRTT}) + ((1 - \alpha) \times \text{RTT}) SRTT=(α×SRTT)+((1α)×RTT)

  3. 开始计算 RTO。公式如下:

    RTO = min ⁡ [ UBOUND , max ⁡ [ LBOUND , ( β × SRTT ) ] ] \text{RTO} = \min \left[ \text{UBOUND}, \max \left[ \text{LBOUND}, (\beta \times \text{SRTT}) \right] \right] RTO=min[UBOUND,max[LBOUND,(β×SRTT)]]

    其中:

    • UBOUND 是超时时间的最大值
    • LBOUND 是超时时间的最小值
    • β \beta β 的值通常在 1.3 到 2.0 之间

Karn / Partridge 算法

然而,上述算法在重传时会面临一个终极问题:是使用第一次发送数据的时间和 ACK 返回的时间作为 RTT 样本值,还是使用重传的时间和 ACK 返回的时间作为样本值?

无论选择哪种方式,都会出现问题。如下图所示:

  • 情况(a):ACK 未返回,因此进行重传。如果使用第一次发送和 ACK 返回的时间计算 RTT,则会高估 RTT。
  • 情况(b):ACK 返回较慢,导致重传,但重传后不久,之前的 ACK 就返回了。如果使用重传时间和 ACK 返回的时间差计算 RTT,则会低估 RTT。

因此,1987 年提出了 Karn / Partridge Algorithm,该算法的最大特点是——忽略重传,不将重传的 RTT 作为采样值(这样就无需解决不存在的问题)。

然而,这样一来,又会引发一个大问题:如果在某一时刻,网络出现短暂波动,导致延迟增加,这可能使所有数据包都需要重传(因为之前的 RTO 很小),而由于重传的 RTT 不被采样,因此 RTO 不会被更新,这是一个灾难。于是,Karn 算法采用了一个取巧的方式——只要发生重传,就将现有的 RTO 值翻倍(这就是所谓的 Exponential Backoff)。显然,这种固定规则对于需要准确估计 RTT 的场景并不靠谱。

Jacobson / Karels 算法

前面两种算法使用的都是“加权移动平均”,这种方法最大的问题是如果 RTT 出现大幅波动,很难被检测到,因为波动被平滑掉了。因此,1988 年提出了一种新的算法,即 Jacobson / Karels Algorithm(参见 RFC 6298)。该算法引入了最新 RTT 采样值与平滑过的 SRTT 之间的差距作为因子进行计算。公式如下(其中 DevRTT 表示 RTT 的偏差):

SRTT = SRTT + α ( RTT − SRTT ) \text{SRTT} = \text{SRTT} + \alpha (\text{RTT} - \text{SRTT}) SRTT=SRTT+α(RTTSRTT)

DevRTT = ( 1 − β ) × DevRTT + β × ∣ RTT − SRTT ∣ \text{DevRTT} = (1 - \beta) \times \text{DevRTT} + \beta \times |\text{RTT} - \text{SRTT}| DevRTT=(1β)×DevRTT+β×RTTSRTT

RTO = μ × SRTT + ∂ × DevRTT \text{RTO} = \mu \times \text{SRTT} + \partial \times \text{DevRTT} RTO=μ×SRTT+×DevRTT

(在 Linux 系统中, α = 0.125 \alpha = 0.125 α=0.125 β = 0.25 \beta = 0.25 β=0.25 μ = 1 \mu = 1 μ=1 ∂ = 4 \partial = 4 =4。这些参数被称为“调得一手好参数”,虽然没有人知道为什么,但它确实有效。)最终,这个算法被应用于今天的 TCP 协议中(Linux 源代码位于:tcp_rtt_estimator)。

TCP 滑动窗口

需要说明的是,如果您不了解 TCP 的滑动窗口机制,那么您就无法真正理解 TCP 协议。我们知道,TCP 必须解决可靠传输以及数据包乱序(reordering)的问题,因此,TCP 必须了解网络的实际数据处理带宽或数据处理速度,以避免引发网络拥塞导致数据丢失。

因此,TCP 引入了一些技术和设计来实现网络流量控制,滑动窗口(Sliding Window)是其中之一。前面提到,TCP 头中有一个字段称为 Window,也称为 Advertised-Window,该字段是接收端告知发送端其缓冲区还能接收多少数据。于是,发送端可以根据接收端的处理能力发送数据,从而避免接收端处理不过来。为了说明滑动窗口,我们需要先了解 TCP 缓冲区的一些数据结构:

在上图中,我们可以看到:

  • 接收端的 LastByteRead 指向 TCP 缓冲区中已读取的位置,NextByteExpected 指向收到的连续数据包的最后一个位置,LastByteRcvd 指向已接收数据包的最后一个位置。可以看到中间有些数据尚未到达,因此存在数据空白区。
  • 发送端的 LastByteAcked 指向已被接收端确认的数据位置(表示成功发送确认),LastByteSent 表示已发送但尚未收到确认 ACK 的数据,LastByteWritten 指向上层应用正在写入的位置。

因此:

  • 接收端在回传 ACK 时会报告自己的 AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1。
  • 发送端则根据该窗口控制发送数据的大小,以确保接收端能够处理。

接下来,我们来看发送端的滑动窗口示意图:

上图分为四个部分(其中黑色部分为滑动窗口):

  • #1 已收到 ACK 确认的数据。
  • #2 已发送但尚未收到 ACK 的数据。
  • #3 在窗口内尚未发送的数据(接收端仍有空间)。
  • #4 窗口外的数据(接收端无空间)。

以下是滑动后的示意图(收到 36 的 ACK,并发送了 46-51 字节的数据):

接下来,我们来看一个接收端控制发送端的示意图:

Zero Window

在上图中,我们可以看到一个处理缓慢的服务器(接收端)是如何将客户端(发送端)的 TCP 滑动窗口降至 0 的。此时,您可能会问,如果窗口变为 0,TCP 会怎样?发送端是否会停止发送数据?答案是肯定的,发送端会停止发送数据,可以想象为“窗口关闭”。那么,您可能会问,如果发送端停止发送数据,接收端的窗口大小可用时,如何通知发送端呢?

为了解决这个问题,TCP 使用了 Zero Window Probe 技术,简称为 ZWP。也就是说,当窗口变为 0 后,发送端会向接收端发送 ZWP 数据包,以请求接收端确认其窗口大小。通常,这个值会设置为 3 次,每次大约 30-60 秒(不同的实现可能会有所不同)。如果 3 次之后窗口大小仍为 0,某些 TCP 实现可能会发送 RST 数据包以断开连接。

注意:只要有等待的地方,就可能出现 DDoS 攻击,Zero Window 也不例外。一些攻击者会在与 HTTP 建立连接并发送 GET 请求后,将窗口大小设置为 0,然后服务器端只能等待进行 ZWP。于是,攻击者会并发大量这样的请求,耗尽服务器端的资源。(关于这方面的攻击,您可以参考 Wikipedia 的 SockStress 词条)

此外,在 Wireshark 中,您可以使用 tcp.analysis.zero_window 来过滤数据包,然后使用右键菜单中的 follow TCP stream,查看 ZeroWindowProbe 及 ZeroWindowProbeAck 数据包。

Silly Window Syndrome

Silly Window Syndrome 翻译为中文就是“糊涂窗口综合症”。正如您前面所见,如果接收方太忙,来不及取走 Receive Window 中的数据,那么会导致发送方的窗口大小越来越小。最终,如果接收方腾出几个字节并告知发送方当前的窗口大小,发送方会毫不犹豫地发送这几个字节的数据。

要知道,TCP + IP 头部有 40 个字节,为了这几个字节的数据,却要承担如此大的开销,这显然非常不经济。

此外,您需要了解网络中的 MTU(Maximum Transmission Unit,最大传输单元)。对于以太网来说,MTU 是 1500 字节。去掉 TCP + IP 头部的 40 个字节后,实际可用于数据传输的字节数为 1460,这就是所谓的 MSS(Maximum Segment Size,最大报文段长度)。需要注意的是,TCP 的 RFC 规定 MSS 的默认值为 536,因为 RFC 791 规定任何一个 IP 设备至少需要接收 576 字节大小的数据(实际上 576 是拨号网络的 MTU,576 减去 IP 头部的 20 个字节就是 536)。

如果您的网络包能够填满 MTU,那么您可以充分利用整个带宽。如果不能,那么您就会浪费带宽。(大于 MTU 的数据包有两种结局:一种是直接被丢弃,另一种是会被重新分块打包发送。)您可以将其想象为一个 MTU 就相当于一架飞机的最大载客量。如果飞机满载,带宽利用率最高;如果飞机只运一个人,成本无疑会大幅增加。

因此,Silly Window Syndrome 这种现象就像是您本可以乘坐载客量为 200 人的飞机,却只坐了 1 或 2 个人。解决这个问题并不难,即避免对小窗口大小做出响应,直到窗口大小足够大时再响应。这种思路可以在发送端和接收端同时实现。

  • 如果该问题是由接收端引起的,则可以采用 David D. Clark 的方案。在接收端,如果收到的数据导致窗口大小小于某个值,可以直接回传 ACK(0) 给发送端,从而关闭窗口,阻止发送端继续发送数据。等到接收端处理了一些数据后,窗口大小大于等于 MSS,或者接收端缓冲区有一半为空时,再打开窗口,允许发送端发送数据。

  • 如果该问题是由发送端引起的,则可以采用著名的 Nagle’s Algorithm。该算法的思路也是延迟处理,它有两个主要条件:

    1. 等待窗口大小 ≥ \geq MSS 或数据大小 ≥ \geq MSS。
    2. 收到之前发送数据的 ACK 回包后,才会发送数据,否则会积累数据。

此外,Nagle 算法默认是启用的。因此,对于一些需要小数据包的场景——例如像 telnet 或 ssh 这样的交互性较强的程序,您需要关闭该算法。您可以在 Socket 设置中使用 TCP_NODELAY 选项来关闭该算法(关闭 Nagle 算法没有全局参数,需要根据每个应用的特点进行关闭)。

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

此外,网上有些文章称 TCP_CORK 的 socket option 也可以关闭 Nagle 算法,这是不正确的。TCP_CORK 实际上是一种更激进的 Nagle 算法,完全禁止小数据包的发送,而 Nagle 算法并未禁止小数据包的发送,只是禁止了大量小数据包的发送。最好不要同时设置这两个选项。

TCP 的拥塞处理 – Congestion Handling

前面我们了解到,TCP 通过滑动窗口(Sliding Window)实现流量控制(Flow Control),但 TCP 认为这还不够,因为滑动窗口依赖于连接的发送端和接收端,它并不了解网络中间发生了什么。TCP 的设计者认为,一个伟大而优秀的协议不能仅仅实现流量控制,因为流量控制只是网络模型第四层以上的事情,TCP 还应该更聪明地了解整个网络的情况。

具体来说,我们知道 TCP 通过定时器采样 RTT 并计算 RTO,但如果网络延迟突然增加,TCP 对此的应对措施只有重传数据,然而重传会导致网络负担加重,从而导致更大的延迟以及更多的丢包,这种情况会不断恶性循环并被放大。试想一下,如果一个网络中有成千上万的 TCP 连接都这样行事,那么很快就会形成“网络风暴”,TCP 协议可能会拖垮整个网络。这将是一场灾难。

因此,TCP 不能忽视网络中发生的事情,而盲目地不断重传数据,对网络造成更大的伤害。对此,TCP 的设计理念是:TCP 不是一个自私的协议,在发生拥塞时,应该做出自我牺牲。就像交通阻塞时,每辆车都应该让出道路,而不是去抢路

关于拥塞控制的论文,您可以参考《Congestion Avoidance and Control》(PDF)。

拥塞控制主要涉及四个算法:1)慢启动2)拥塞避免3)拥塞发生时的处理4)快速恢复。这四个算法并非一蹴而就,其发展历程经历了很长时间,至今仍在不断优化。备注如下:

  • 1988 年,TCP-Tahoe 提出了 1)慢启动,2)拥塞避免,3)拥塞发生时的快速重传。
  • 1990 年,TCP Reno 在 Tahoe 的基础上增加了 4)快速恢复。

慢启动算法 – Slow Start

首先,我们来看 TCP 的慢启动算法。慢启动的意思是,刚刚加入网络的连接应该逐步提速,不要一开始就霸道地占满整个带宽。新加入的连接应该像初上高速的车辆一样,缓慢行驶,以免扰乱已经在高速公路上正常行驶的车辆秩序。

慢启动算法如下(cwnd 表示拥塞窗口,全称为 Congestion Window):

  1. 连接建立后,初始化 cwnd = 1,表示可以传输一个 MSS(Maximum Segment Size,最大报文段长度)大小的数据。
  2. 每收到一个 ACK,cwnd 增加 1,呈线性增长。
  3. 每经过一个 RTT,cwnd = cwnd × \times × 2,呈指数增长。
  4. 还有一个 ssthresh(慢启动阈值,Slow Start Threshold),是一个上限值。当 cwnd ≥ \geq ssthresh 时,将进入“拥塞避免算法”(后面会详细介绍该算法)。

因此,我们可以看到,如果网速很快,ACK 也会快速返回,RTT 会很短,那么慢启动过程实际上并不会很慢。下图说明了这个过程:

这里需要提及一篇 Google 的论文《An Argument for Increasing TCP’s Initial Congestion Window》。Linux 3.0 及以后版本采用了该论文的建议,将 cwnd 初始化为 10 个 MSS。而在 Linux 3.0 之前的版本,例如 2.6,Linux 采用了 RFC 3390 的规定:如果 MSS < 1095 < 1095 <1095,则 cwnd = 4;如果 MSS > 2190 > 2190 >2190,则 cwnd = 2;在其他情况下,cwnd = 3。

拥塞避免算法 – Congestion Avoidance

前面提到,还有一个 ssthresh(慢启动阈值),是一个上限值。当 cwnd ≥ \geq ssthresh 时,将进入“拥塞避免算法”。一般来说,ssthresh 的值为 65535 字节。当 cwnd 达到该值后,算法如下:

  1. 每收到一个 ACK,cwnd = cwnd + 1/cwnd
  2. 每经过一个 RTT,cwnd = cwnd + 1

此处可以理解为左边为 t + 1 t+1 t+1,右边为 t t t

这样可以避免因增长过快而导致网络拥塞,逐渐调整到网络的最佳值。显然,这是一个线性增长的算法。

拥塞发生时的算法

前面我们提到,当发生丢包时,会有以下两种情况:

  1. 等待 RTO 超时后重传数据包。TCP 认为这种情况非常糟糕,因此反应非常强烈。

    • ssthresh = cwnd / 2
    • cwnd 重置为 1
    • 进入慢启动过程
  2. Fast Retransmit 算法,即在收到 3 个重复的 ACK 时就开始重传,而无需等待 RTO 超时。

    • TCP Tahoe 的实现与 RTO 超时相同。
    • TCP Reno 的实现如下:
      • cwnd = cwnd / 2
      • ssthresh = cwnd
      • 进入快速恢复算法——Fast Recovery

从上面的描述可以看出,RTO 超时后,ssthresh 会变为 cwnd 的一半。这意味着,如果在 cwnd ≤ \leq ssthresh 时发生丢包,那么 TCP 的 ssthresh 就会减半。然后,当 cwnd 又迅速以指数级增长到这个值时,就会转变为缓慢的线性增长。我们可以看到,TCP 是如何通过这种强烈的振荡快速而谨慎地找到网络流量的平衡点的。

快速恢复算法 – Fast Recovery

TCP Reno

该算法定义在 RFC 5681 中。快速重传(Fast Retransmit)和快速恢复(Fast Recovery)算法通常同时使用。快速恢复算法认为,收到 3 个重复的 ACK 说明网络状况并不太糟糕,因此无需像 RTO 超时那样做出强烈反应。需要注意的是,正如前面所述,在进入快速恢复算法之前,cwnd 和 ssthresh 已经被更新如下:

  • cwnd = cwnd / 2
  • ssthresh = cwnd

然后,真正的快速恢复算法如下:

  • cwnd = ssthresh + 3 × \times × MSS(3 表示确认有 3 个数据包已被收到)
  • 重传重复 ACK 指示的数据包
  • 如果再收到重复的 ACK,那么 cwnd = cwnd + 1
  • 如果收到新的 ACK,那么 cwnd = ssthresh,然后进入拥塞避免算法

如果您仔细思考上述算法,就会发现它存在一个问题,即——它依赖于 3 个重复的 ACK。需要注意的是,3 个重复的 ACK 并不代表只丢失了一个数据包,很可能丢失了多个数据包。但该算法只会重传一个数据包,而其他丢失的数据包只能等待 RTO 超时,从而进入噩梦模式——每次超时都会使 TCP 的传输速度减半,多个超时会导致 TCP 的传输速度呈指数级下降,并且无法再触发快速恢复算法。

通常来说,正如我们前面所述,SACK 或 D-SACK 方法可以使快速恢复算法或发送端在决策时更加智能,但并非所有 TCP 实现都支持 SACK(SACK 需要双方都支持),因此需要一个不依赖 SACK 的解决方案。通过 SACK 进行拥塞控制的算法是 FACK(后面会讲到)。

TCP New Reno

于是,1995 年,TCP New Reno(参见 RFC 6582)算法被提出,主要是在没有 SACK 支持的情况下改进快速恢复算法。

  • 当发送端收到 3 个重复的 ACK 时,进入快速重传模式,开始重传重复 ACK 指示的数据包。如果只有这一个数据包丢失,那么重传该数据包后返回的 ACK 会确认所有已发送的数据。如果没有,说明丢失了多个数据包。我们称这个 ACK 为 Partial ACK。

  • 一旦发送端检测到 Partial ACK 出现,就可以推断出有多个数据包丢失,于是继续重传滑动窗口中未被确认的第一个数据包。直到再也收不到 Partial ACK,才真正结束快速恢复过程。

可以看到,这个“快速恢复的变更”是一种非常激进的策略,它同时延长了快速重传和快速恢复的过程。

算法示意图

接下来,我们来看一个简单的示意图,以同时展示上述各种算法:

FACK 算法

FACK 的全称是 Forward Acknowledgment 算法,论文地址为(PDF):Forward Acknowledgement: Refining TCP Congestion Control。该算法基于 SACK,前面我们提到 SACK 使用 TCP 扩展字段来确认哪些数据已收到,哪些数据未收到。与 Fast Retransmit 的 3 个重复 ACK 相比,SACK 的优势在于,前者只知道有数据包丢失,但不知道是一个还是多个,而 SACK 可以准确地知道哪些数据包丢失了。因此,SACK 可以让发送端在重传过程中,只重传那些丢失的数据包,而不是一个一个地重传。然而,这样一来,如果重传的数据包较多,又会使本来就很繁忙的网络更加繁忙。因此,FACK 用于在重传过程中进行拥塞控制。

  • 该算法将 SACK 中最大的序列号保存在 snd.fack 变量中,snd.fack 的更新由 ACK 带来。如果网络一切正常,则 snd.fack 与 snd.una 相同(snd.una 是尚未收到 ACK 的位置,即滑动窗口中 category #2 的第一个位置)。

  • 然后定义一个 awnd = snd.nxt – snd.fack(snd.nxt 指向发送端滑动窗口中即将发送的位置——即滑动窗口图中的 category #3 的第一个位置),因此 awnd 表示网络中尚未确认的数据量(所谓 awnd 即 actual quantity of data outstanding in the network)。

  • 如果需要重传数据,那么 awnd = snd.nxt – snd.fack + retran_data,即 awnd 是已发送的数据量加上重传的数据量。

  • 触发快速恢复的条件是:$ ( \text{snd.fack} - \text{snd.una} ) > (3 \times \text{MSS} ) $ 或 $ \text{dupacks} == 3 $。这样一来,就不需要等到 3 个重复的 ACK 才进行重传,而是只要 SACK 中最大的数据与 ACK 的数据差距较大(3 个 MSS),就会触发重传。在整个重传过程中,cwnd 保持不变。直到第一次丢失的数据包的 snd.nxt ≤ \leq snd.una(即重传的数据都被确认),然后进入拥塞避免机制——cwnd 线性增长。

可以看出,如果没有 FACK,那么在丢失较多数据包的情况下,原来的保守算法会低估所需的窗口大小,需要几个 RTT 的时间才能完成恢复,而 FACK 会更加激进地处理这种情况。然而,FACK 在网络数据包可能会发生重排序的场景中可能会出现问题。

其他拥塞控制算法简介

TCP Vegas 拥塞控制算法

该算法于 1994 年被提出,主要对 TCP Reno 进行了一些修改。该算法通过对 RTT 的严格监控来计算一个基准 RTT,然后通过该基准 RTT 来估计当前网络的实际带宽。如果实际带宽比期望的带宽小或大,那么就开始线性地减少或增加 cwnd 的大小。如果计算出的 RTT 大于超时时间,那么在 ACK 超时之前直接进行重传。(Vegas 的核心思想是利用 RTT 的值来影响拥塞窗口,而不是通过丢包来实现。)该算法的论文是《TCP Vegas: End to End Congestion Avoidance on a Global Internet》。这篇论文还对比了 Vegas 和 New Reno:

关于该算法的实现,您可以参考 Linux 源码:tcp_vegas.h 和 tcp_vegas.c。

HSTCP (High Speed TCP) 算法

该算法来自 RFC 3649(Wikipedia 词条)。它对基础算法进行了修改,使得拥塞窗口(Congestion Window)增长得更快,减少得更慢。具体如下:

  • 拥塞避免时的窗口增长方式:cwnd = cwnd + α ( c w n d ) \alpha(cwnd) α(cwnd) / cwnd
  • 丢包后窗口减少方式:cwnd = (1 - β ( c w n d ) \beta(cwnd) β(cwnd)) × \times × cwnd

注: α ( c w n d ) \alpha(cwnd) α(cwnd) β ( c w n d ) \beta(cwnd) β(cwnd) 都是函数。如果您希望它们与标准 TCP 一致,那么可以设置 α ( c w n d ) = 1 \alpha(cwnd) = 1 α(cwnd)=1 β ( c w n d ) = 0.5 \beta(cwnd) = 0.5 β(cwnd)=0.5。对于 α ( c w n d ) \alpha(cwnd) α(cwnd) β ( c w n d ) \beta(cwnd) β(cwnd) 的值,它们是动态变化的。关于该算法的实现,您可以参考 Linux 源码:tcp_highspeed.c。

TCP BIC 算法

2004 年,BIC 算法被提出。您还可以查阅相关新闻《Google:[美科学家研发 BIC-TCP 协议 速度是 DSL 六千倍](https://www.google.com/search?lr=lang_zh-CN|lang_zh-TW&newwindow=1&biw=1366&bih=597&tbs=lr%3Alang_1zh-CN|lang_1zh-TW&q=美科学家研发 BIC-TCP 协议 + 速度是 DSL 六千倍&oq=美科学家研发 BIC-TCP 协议 + 速度是 DSL 六千倍)》。BIC 全称 Binary Increase Congestion control,在 Linux 2.6.8 中是默认的拥塞控制算法。BIC 的发明者们意识到,众多的拥塞控制算法都在努力寻找一个合适的 cwnd(拥塞窗口),而 BIC-TCP 的提出者们看穿了事情的本质——这其实是一个搜索过程。因此,BIC 算法主要使用二分查找(Binary Search)来实现。关于该算法的实现,您可以参考 Linux 源码:tcp_bic.c。

TCP WestWood 算法

WestWood 采用与 Reno 相同的慢启动算法和拥塞避免算法。WestWood 的主要改进在于:在发送端进行带宽估计,当检测到丢包时,根据带宽值设置拥塞窗口和慢启动阈值。那么,该算法是如何测量带宽的呢?每个 RTT 时间内,会测量一次带宽,测量公式非常简单,即在该 RTT 内成功被 ACK 的字节数。因为,这个带宽值和用 RTT 计算 RTO 一样,也是需要从每个样本通过加权移动平均公式平滑到一个值的。此外,我们知道,如果一个网络的带宽是每秒可以发送 X X X 个字节,而 RTT 是一个数据包发送出去并得到确认所需的时间,那么 X × RTT X \times \text{RTT} X×RTT 应该是我们的缓冲区大小。因此,在这个算法中,ssthresh 的值为 est_BD × min ⁡ -RTT \text{est\_BD} \times \min\text{-RTT} est_BD×min-RTT(最小 RTT 值)。如果丢包是由重复 ACK 引起的,那么当 cwnd > ssthresh 时,cwnd = ssthresh;如果是 RTO 引起的,那么 cwnd = 1,并进入慢启动阶段。

关于该算法的实现,您可以参考 Linux 源码:tcp_westwood.c。

其他拥塞控制算法

更多算法可以在 Wikipedia 的 TCP Congestion Avoidance Algorithm 页面中找到相关信息。

后记

至此,本文可以告一段落了。TCP 的内容繁多,不同的人可能有不同的理解,TCP 发展至今,其内容之丰富足以写成多本书籍。本文的主要目的,是带领您走进这些经典的基础技术和知识领域,希望本文能让您对 TCP 有更深入的了解,更希望它能激发您学习这些基础或底层知识的兴趣和信心。


TCP 拥塞控制算法(Reno、HSTCP、BIC、Vegas、Westwood)

BIG_GENERAL_DD 于 2018-01-10 13:49:19 发布

一、TCP 拥塞控制的研究框架

TCP协议拥塞控制算法(Reno、HSTCP、BIC、Vegas、Westwood)

注:

  • 基于丢包反馈:通过 ACK 所带回来的丢包信息来调整源端的拥塞窗口。Reno 等是针对 ACK 返回的丢包信息改进传统 TCP 协议。近年来,随着网络带宽的提高、传输延时的增大,针对提高 TCP 带宽利用率,出现了 HSTCP、BIC-TCP、STCP 协议。
  • 基于路径延时反馈:RTT 相对于丢包信息反应更灵敏,更能及时反映出一般网络的拥塞情况。适用于小缓存的中间节点,效率较理想。但对于路由器经常缓存数据促使 RTT 延长调节拥塞窗口,实际上并未发生拥塞情况。
  • 基于显示拥塞反馈:典型的 ECN 利用中间节点自身检测其拥塞状态(如路由器的反馈状态),直接反馈给 TCP 源端,以此调节源端的窗口值和发送速率。

二、重点 TCP 拓展算法

1. 基于丢包反馈的 TCP 协议(Tahoe、Reno、New Reno、SACK)

1988 年,V. Jacobson 提出了慢启动和拥塞避免的算法。后期对 TCP 传输协议算法不断优化改进。目前使用最广泛的 TCP Reno 拥塞控制主要分为 4 个阶段:

  1. 慢启动阶段 cwnd \text{cwnd} cwnd 呈现指数增长趋势。
  2. 拥塞避免阶段 cwnd > ssthresh \text{cwnd} > \text{ssthresh} cwnd>ssthresh 呈现线性增长趋势。
  3. 快重传阶段:发送方只要一连接收到三个重复确认就应该立即重传对方尚未的报文段,而不必等到重传计时器超时后发送。由 3 个重复应答判断有包丢失,重新发送丢包的信息。
  4. 快速恢复阶段:主要取决于收到的重复应答数据的初始门限值(一般为 3)。

与慢启动不同,Reno 的发送方用额外到达的应答为后续包定时。

发送方窗口的上限值 = min ⁡ ( 接收方窗口,拥塞窗口 ) \min \left( \text{接收方窗口},\text{拥塞窗口} \right) min(接收方窗口拥塞窗口)

整个 Reno 过程见下图:

TCP协议拥塞控制算法(Reno、HSTCP、BIC、Vegas、Westwood)

2. 基于延时反馈的 TCP 协议(Vegas、Westwood)

经典的 Vegas 算法的基本思路:RTT 增加,拥塞窗口减小;RTT 减少,拥塞窗口变大。

  1. 重传机制:Vegas 采用更精确的 RTT 估计值在以下两种情形下决定是否重发:

    • 当接收到重复 ACK 时,Vegas 检查目前时间和记录的时间标签之差是否比超时值大。如果是,则立刻重发数据包,不必等第三个重复 ACK。当接收到重传数据包应答后,Vegas 以 3 4 \frac{3}{4} 43 而不是 1 2 \frac{1}{2} 21 因子降低拥塞窗口。

    • 当接收到非重复的 ACK 时,如果它是重发之后的第一个或是第二个确认,Vegas 将再次检测数据发送时间间隔是否超过超时值。如果是,则重发。

  2. 拥塞避免机制:Vegas 通过比较实际吞吐量和期望吞吐量来调节拥塞窗口的大小。

    • 期望吞吐量: Expected = cwnd BaseRTT \text{Expected} = \frac{\text{cwnd}}{\text{BaseRTT}} Expected=BaseRTTcwnd
    • 实际吞吐量: Actual = cwnd RTT \text{Actual} = \frac{\text{cwnd}}{\text{RTT}} Actual=RTTcwnd
    • 计算差值: diff = ( Expected − Actual ) × BaseRTT \text{diff} = (\text{Expected} - \text{Actual}) \times \text{BaseRTT} diff=(ExpectedActual)×BaseRTT

    其中, BaseRTT \text{BaseRTT} BaseRTT 是所有观测来回响应时间的最小值,一般是建立连接后所发的第一个数据包的 RTT; cwnd \text{cwnd} cwnd 是当前的拥塞窗口的大小。

    定义阈值 a a a b b b,当 diff < a \text{diff} < a diff<a,拥塞窗口增大 + 1 +1 +1;当 diff > b \text{diff} > b diff>b,拥塞窗口缩小 − 1 -1 1;当 a ≤ diff ≤ b a \leq \text{diff} \leq b adiffb,拥塞窗口不变。通常 a = 1 a = 1 a=1 b = 3 b = 3 b=3,意味着该连接至少保留一个包在队列中。

  3. 慢启动机制

    由于一开始慢启动没有相关的传输数据和带宽速度等参数,需要设定慢启动门限。Vegas 每经过两个 RTT 使 cwnd \text{cwnd} cwnd 拥塞窗口增加 1 倍。然后计算 diff \text{diff} diff,当 diff > a \text{diff} > a diff>a,则结束慢启动,转入拥塞避免。

    在 Vegas 慢启动中,每经过两个 RTT 使 cwnd \text{cwnd} cwnd 增加 1 倍。其中前一个 RTT 内是与 TCP Reno 相同的指数增长,即每收到一个 ACK 包就将 cwnd \text{cwnd} cwnd 加 1,同时发送出两个数据包,可称为增长期;后一个 RTT 内保持不变以观测 RTT 的变化,可称为观测期。Vegas 的慢启动过程就是由一个增长期和一个观测期周期往复,在每个观测期结束时,计算新的 RTT 和 diff \text{diff} diff 的值,以此决定是继续下一个周期还是结束慢启动。

    这种慢启动事实上严重降低了传输速率。有些学者提出了自适应慢启动算法,或是慢启动阶段采用 Reno 方法。

    Vegas-A:主要思路是 a a a b b b 可以随着网络情况自动调节,初始值分别是 1 和 3。

无线传输中的 Westwood 算法

基本思路:发送端利用检测到的 ACK 的到达率来估测可使用的带宽。

  1. 快速恢复机制:Westwood 算法重点放在出现报文丢失时如何使用带宽估计值设定拥塞窗口的 cwnd \text{cwnd} cwnd ssthresh \text{ssthresh} ssthresh。在收到三个重复的 ACK 或是超时时,具体算法如下:

    BWE = PacketSize RTT min ⁡ \text{BWE} = \frac{\text{PacketSize}}{\text{RTT}_{\min}} BWE=RTTminPacketSize

    其中, PacketSize \text{PacketSize} PacketSize 指分组大小, BWE \text{BWE} BWE 为带宽估计值(单位时间分组数目 × \times × 分组大小), RTT min ⁡ \text{RTT}_{\min} RTTmin 是测得的最小 RTT 值,理想情况下等于中间队列长度为零时测得的值。TCP Westwood 在 Reno 的基础上,通过测量估算出网络的可用带宽,对拥塞窗口 cwnd \text{cwnd} cwnd 进行适当调整,实现更快速的恢复。这种机制尤其在无线网中非常有效,与 Reno 相比吞吐量成倍提高。

  2. 慢启动阶段

    慢启动和加性递增阶段仍然采用 Reno 的增加机制。慢启动处于带宽探测阶段, cwnd \text{cwnd} cwnd 呈指数增长。如果 ssthresh \text{ssthresh} ssthresh 定得过高,容易导致较多的分组重传;而如果 ssthresh \text{ssthresh} ssthresh 定得过低,则会导致过早进入线性递增,降低带宽利用率。

    由于 Westwood 无法区分网络拥塞丢包和无线随机错误丢包,尤其是无线网络时延抖动和随机错误丢包会被 Westwood 误认为是网络拥塞,大大降低网络带宽利用。

    Westwood-v 算法:在慢启动阶段,改进算法对慢启动阶段的网络状态根据队列中的报文长度 N N N 进行区分,并把窗口调整与估计带宽紧密结合起来,根据网络状况,采用不同的窗口增加策略,避免了原有 TCP Westwood 中拥塞窗口的盲目增加导致的分组拥塞丢失。此外,对于分段,不是采用固定值 ssthresh / 2 \text{ssthresh}/2 ssthresh/2,而是根据当前的拥塞窗口与预测到的下一时刻的可用带宽的关系来分段,具有动态自适应性。简而言之,慢启动时,采用 Vegas 的方法计算 diff \text{diff} diff b b b 比较,如果说明带宽还未充分利用,保持 Reno 的指数和线性增加。当 diff > b \text{diff} > b diff>b 意味着带宽充分利用,但接近拥塞状态,如果还处于慢启动阶段,说明 ssthresh \text{ssthresh} ssthresh 偏大,需要更改,使 TCP 进入拥塞避免阶段。如果处于拥塞避免阶段,则降低窗口增加速率。

3. 基于丢包反馈的高速带宽算法(HSTCP、STCP、BIC-TCP、CUBIC)

HSTCP 与 STCP 的基本思想:当拥塞窗口 > > > 阈值时,窗口增加因子 a ( w ) a(w) a(w) 与减少因子 b ( w ) b(w) b(w) 成为窗口调节大小 w w w 的函数。

1. HSTCP - High Speed TCP 高速传输协议

适用于高速度、大时延网络,窗口快速增长,乘性缩小。

该算法的根本思想是修改标准 TCP 协议的反应函数,受到窗口增长和丢包下降函数综合影响。重点介绍 HSTCP 的拥塞避免阶段窗口的调节算法。在拥塞避免阶段,接收一个 ACK 后,增长方式为:

cwnd = cwnd + a ( w ) cwnd \text{cwnd} = \text{cwnd} + \frac{a(w)}{\text{cwnd}} cwnd=cwnd+cwnda(w)

发生一次拥塞,拥塞窗口减少方式为:

cwnd = ( 1 − b ( w ) ) × cwnd \text{cwnd} = (1 - b(w)) \times \text{cwnd} cwnd=(1b(w))×cwnd

a ( w ) a(w) a(w) b ( w ) b(w) b(w) 的公式依次为:

w > W L w > W_L w>WL 时,

TCP协议拥塞控制算法(Reno、HSTCP、BIC、Vegas、Westwood)

w ≤ W L w \leq W_L wWL 时, a ( w ) = 1 a(w) = 1 a(w)=1 b ( w ) = 0.5 b(w) = 0.5 b(w)=0.5

2. STCP - Scalable TCP

拥塞避免阶段接收 ACK 时:

cwnd = cwnd + a ( w ) \text{cwnd} = \text{cwnd} + a(w) cwnd=cwnd+a(w)

拥塞发生时:

cwnd = ( 1 − b ( w ) ) × cwnd \text{cwnd} = (1 - b(w)) \times \text{cwnd} cwnd=(1b(w))×cwnd

其中, a = 0.01 a = 0.01 a=0.01 b = 0.125 b = 0.125 b=0.125,参数固定不变。

3. BIC-TCP

BIC 将拥塞控制视为一个搜索问题,具有良好的拓展性、友好性和公平性。但当丢失率 < 1 × 1 0 − 8 < 1 \times 10^{-8} <1×108 时,其发送速率的增长不如 HSTCP、STCP 快。

BIC 包括两部分:二分搜索增加(Binary Search Increase)+ 加性增加(Additive Increase)

  • 二分搜索:TCP 主动而非被动搜索一个处于丢包触发阈值的分组发送速率。

    W min ⁡ W_{\min} Wmin:快速恢复结束后的拥塞窗口大小值

    W max ⁡ W_{\max} Wmax:快速恢复结束前的拥塞窗口大小值

    首先设置目标窗口的大小为 W min ⁡ W_{\min} Wmin W max ⁡ W_{\max} Wmax 的中间值。如果没有丢包,则当前窗口的值设为 W min ⁡ W_{\min} Wmin,拥塞窗口增加现在与 W min ⁡ W_{\min} Wmin 差值的一半;如果丢包,当前窗口值为 W max ⁡ W_{\max} Wmax

  • 加性增长:如果当前值与目标值差值太大,将拥塞窗口直接设置为目标值会带来很大压力。此时设置一个 S max ⁡ S_{\max} Smax,按照 S max ⁡ S_{\max} Smax 步长增长。

4. CUBIC

对 BIC 算法的简化,用三次项支配窗口扩充算法,新窗口的尺寸 w w w 为:

TCP协议拥塞控制算法(Reno、HSTCP、BIC、Vegas、Westwood)


TCP 拥塞控制算法 优缺点 适用环境 性能分析

zhangskd 于 2011-08-24 17:56:07 发布

摘要:对多种 TCP 拥塞控制算法进行简要说明,指出它们的优缺点以及它们的适用环境。

关键字:TCP 拥塞控制算法、优点、缺点、适用环境、公平性

公平性

公平性是指在发生拥塞时,各源端(或同一源端建立的不同 TCP 连接或 UDP 数据报)能公平地共享同一网络资源(如带宽、缓存等)。处于相同级别的源端应该得到相同数量的网络资源。产生公平性的根本原因在于拥塞发生必然导致数据包丢失,而数据包丢失会导致各数据流之间为争抢有限的网络资源发生竞争,争抢能力弱的数据流将受到更多损害。因此,没有拥塞,也就没有公平性问题。

TCP 层上的公平性问题表现在以下两方面:

  1. 面向连接的 TCP 和无连接的 UDP 在拥塞发生时对拥塞指示的不同反应和处理,导致对网络资源的不公平使用问题。在拥塞发生时,有拥塞控制反应机制的 TCP 数据流会按拥塞控制步骤进入拥塞避免阶段,从而主动减小发送入网络的数据量。但对于无连接的数据报 UDP,由于没有端到端的拥塞控制机制,即使网络发出了拥塞指示(如数据包丢失、收到重复 ACK 等),UDP 也不会像 TCP 那样减少向网络发送的数据量。结果是,遵守拥塞控制的 TCP 数据流得到的网络资源越来越少,而没有拥塞控制的 UDP 则会得到越来越多的网络资源,这就导致了网络资源在各源端分配的严重不公平。
  2. 一些 TCP 连接之间也存在公平性问题。产生问题的原因在于一些 TCP 在拥塞前使用了大窗口尺寸,或者它们的 RTT 较小,或者数据包比其他 TCP 大,这样它们也会多占带宽。

RTT 不公平性

AIMD 拥塞窗口更新策略也存在一些缺陷。加性增加策略使发送方发送数据流的拥塞窗口在一个往返时延(RTT)内增加了一个数据包的大小。因此,当不同的数据流对网络瓶颈带宽进行竞争时,具有较小 RTT 的 TCP 数据流的拥塞窗口增加速率将会快于具有大 RTT 的 TCP 数据流,从而将会占有更多的网络带宽资源。

附加说明

中美之间的线路质量不是很好,RTT 较长且时常丢包。TCP 协议“成也丢包,败也丢包”;TCP 的设计目的是解决不可靠线路上可靠传输的问题,即为了解决丢包,但丢包却使 TCP 传输速度大幅下降。HTTP 协议在传输层使用的是 TCP 协议,所以网页下载的速度就取决于 TCP 单线程下载的速度(因为网页就是单线程下载的)。丢包使得 TCP 传输速度大幅下降的主要原因是丢包重传机制,而控制这一机制的就是 TCP 拥塞控制算法。Linux 内核中提供了若干套 TCP 拥塞控制算法,已加载进内核的可以通过内核参数 net.ipv4.tcp_available_congestion_control 查看。

具体算法分析

1. Vegas

1994 年,Brakmo 提出了一种新的拥塞控制机制 TCP Vegas,从另一个角度来进行拥塞控制。TCP 的拥塞控制通常是基于丢包的,一旦出现丢包,于是调整拥塞窗口。然而,由于丢包不一定是由于网络进入了拥塞,而 RTT 值与网络运行情况有比较密切的关系,于是 TCP Vegas 利用 RTT 值的改变来判断网络是否拥塞,从而调整拥塞控制窗口。如果发现 RTT 在增大,Vegas 就认为网络正在发生拥塞,于是开始减小拥塞窗口;如果 RTT 变小,Vegas 认为网络拥塞正在逐步解除,于是再次增加拥塞窗口。由于 Vegas 不是利用丢包来判断网络可用带宽,而是利用 RTT 变化来判断,因而可以更精确地探测网络的可用带宽,从而效率更好。然而 Vegas 有一个缺陷,并且可以说是致命的,最终影响 TCP Vegas 并没有在互联网上大规模使用。这个问题是采用 TCP Vegas 的流的带宽竞争力不及未使用 TCP Vegas 的流。这是因为网络中路由器只要缓冲了数据,就会造成 RTT 的变大,如果缓冲区没有溢出的话,并不会发生拥塞,但处理时延会导致 RTT 变大,特别是在带宽比较小的网络上,只要一开始传输数据,RTT 就会急剧增大,无线网络上这种情况尤为明显。在这种情况下,TCP Vegas 会降低自己的拥塞窗口,但只要没有丢包,标准的 TCP 不会降低自己的窗口,于是两者开始不公平。如果所有 TCP 都采用 Vegas 拥塞控制方式的话,流之间的公平性会更好,但竞争能力并不是 Vegas 算法本身的问题。

适用环境:很难在互联网上大规模适用(带宽竞争力低)。

2. Reno

Reno 是目前应用最广泛且较为成熟的算法。该算法所包含的慢启动、拥塞避免和快速重传、快速恢复机制,是现有众多算法的基础。从 Reno 运行机制中很容易看出,为了维持一个动态平衡,必须周期性地产生一定量的丢失。再加上 AIMD 机制——减少快,增长慢,尤其是在大窗口环境下,由于一个数据报的丢失所带来的窗口缩小要花费很长的时间来恢复,这样,带宽利用率不可能很高且随着网络的链路带宽不断提升,这种弊端将越来越明显。公平性方面,根据统计数据,Reno 的公平性还是得到了相当的肯定,它能够在较大的网络范围内理想地维持公平性原则。

Reno 算法以其简单、有效和鲁棒性成为主流,被广泛采用。但它不能有效处理多个分组从同一个数据窗口丢失的情况。这一问题在 New Reno 算法中得到解决。

基于丢包反馈的协议

近年来,随着高带宽延时网络(High Bandwidth-Delay product network)的普及,针对提高 TCP 带宽利用率,涌现出许多新的基于丢包反馈的 TCP 协议改进,这其中包括 HSTCP、STCP、BIC-TCP、CUBIC 和 H-TCP。

总的来说,基于丢包反馈的协议是一种被动式的拥塞控制机制,其依据网络中的丢包事件来做网络拥塞判断。即便网络中的负载很高时,只要没有产生拥塞丢包,协议就不会主动降低自己的发送速度。这种协议可以最大程度地利用网络剩余带宽,提高吞吐量。然而,由于基于丢包反馈协议在网络近饱和状态下所表现出来的侵略性,一方面大大提高了网络的带宽利用率;但另一方面,对于基于丢包反馈的拥塞控制协议来说,大大提高网络利用率同时意味着下一次拥塞丢包事件为期不远了,所以这些协议在提高网络带宽利用率的同时也间接加大了网络的丢包率,造成整个网络的抖动性加剧。

友好性:BIC-TCP、HSTCP、STCP 等基于丢包反馈的协议在大大提高了自身吞吐率的同时,也严重影响了 Reno 流的吞吐率。基于丢包反馈的协议产生如此低劣的 TCP 友好性的主要原因在于这些协议算法本身的侵略性拥塞窗口管理机制,这些协议通常认为网络只要没有产生丢包就一定存在多余的带宽,从而不断提高自己的发送速率。其发送速率从时间的宏观角度上来看呈现出一种凹形的发展趋势,越接近网络带宽的峰值发送速率增长得越快。这不仅带来了大量拥塞丢包,同时也恶意吞并了网络中其他共存流的带宽资源,造成整个网络的公平性下降。

3. HSTCP(High Speed TCP)

HSTCP(高速传输控制协议)是高速网络中基于 AIMD(加性增长和乘性减少)的一种新的拥塞控制算法,它能在高速度和大时延的网络中更有效地提高网络的吞吐率。它通过对标准 TCP 拥塞避免算法的增加和减少参数进行修改,从而实现了窗口的快速增长和慢速减少,使得窗口保持在一个足够大的范围,以充分利用带宽。它在高速网络中能够获得比 TCP Reno 高得多的带宽,但存在很严重的 RTT 不公平性。公平性指共享同一网络瓶颈的多个流之间占有的网络资源相等。

TCP 发送端通过网络所期望的丢包率来动态调整 HSTCP 拥塞窗口的增量函数。

  • 拥塞避免时的窗口增长方式:
    cwnd = cwnd + a ( cwnd ) cwnd \text{cwnd} = \text{cwnd} + \frac{a(\text{cwnd})}{\text{cwnd}} cwnd=cwnd+cwnda(cwnd)
  • 丢包后窗口下降方式:
    cwnd = ( 1 − b ( cwnd ) ) × cwnd \text{cwnd} = (1 - b(\text{cwnd})) \times \text{cwnd} cwnd=(1b(cwnd))×cwnd

其中, a ( cwnd ) a(\text{cwnd}) a(cwnd) b ( cwnd ) b(\text{cwnd}) b(cwnd) 为两个函数。在标准 TCP 中, a ( cwnd ) = 1 a(\text{cwnd}) = 1 a(cwnd)=1 b ( cwnd ) = 0.5 b(\text{cwnd}) = 0.5 b(cwnd)=0.5。为了达到 TCP 的友好性,在窗口较低的情况下(非 BDP 网络环境下),HSTCP 采用与标准 TCP 相同的 a a a b b b 来保证两者之间的友好性。当窗口较大时(临界值 LowWindow = 38 \text{LowWindow} = 38 LowWindow=38),采取新的 a a a b b b 来达到高吞吐的要求。具体可以参考 RFC3649 文档。

4. Westwood

在无线网络中,大量研究发现 TCP Westwood 是一种较理想的算法。其主要思想是通过在发送端持续不断地检测 ACK 的到达速率来进行带宽估计,当拥塞发生时用带宽估计值来调整拥塞窗口和慢启动阈值,采用 AIAD(加性增加和自适应减少)拥塞控制机制。它不仅提高了无线网络的吞吐量,而且具有良好的公平性和与现行网络的互操作性。存在的问题是不能很好地区分传输过程中的拥塞丢包和无线丢包,导致拥塞机制频繁调用。

5. H-TCP

在高性能网络中,综合表现比较优秀的算法是 H-TCP,但它存在 RTT 不公平性和低带宽不友好性等问题。

6. BIC-TCP

BIC-TCP 的缺点如下:

  1. 抢占性较强:在小链路带宽和时延较短的情况下,BIC-TCP 的增长函数比标准 TCP 更具抢占性。它在探测阶段相当于重新启动一个慢启动算法,而 TCP 在稳定后窗口是线性增长的,不会再次执行慢启动过程。
  2. 算法实现复杂:BIC-TCP 的窗口控制阶段分为二分搜索增加、最大探测等,还有 S max ⁡ S_{\max} Smax S min ⁡ S_{\min} Smin 的区分,这些增加了算法实现的难度,同时也对协议性能的分析模型增加了复杂度。
  3. 在低 RTT 网络和低速环境中,BIC 可能会过于“积极”,因此人们对 BIC 进行了进一步改进,即 CUBIC。BIC 是 Linux 在采用 CUBIC 之前的默认算法。

7. CUBIC

CUBIC 在设计上简化了 BIC-TCP 的窗口调整算法。在 BIC-TCP 的窗口调整中会出现一个凹和凸的增长曲线,CUBIC 使用了一个三次函数(即一个立方函数),该曲线形状和 BIC-TCP 的曲线图十分相似,于是该部分取代了 BIC-TCP 的增长曲线。CUBIC 最关键的点在于它的窗口增长函数仅仅取决于连续的两次拥塞事件的时间间隔值,从而窗口增长完全独立于网络的时延 RTT。之前提到的 HSTCP 存在严重的 RTT 不公平性,而 CUBIC 的 RTT 独立性质使得 CUBIC 能够在多条共享瓶颈链路的 TCP 连接之间保持良好的 RTT 公平性。

CUBIC 是一种用于 TCP 的拥塞控制协议,也是当前 Linux 中默认的 TCP 算法。该协议修改了现有 TCP 标准中的线性窗口增长函数,采用三次函数来提高 TCP 在快速和长距离网络中的可扩展性。它还通过使窗口增长独立于 RTT,实现了在不同 RTT 的流之间更公平的带宽分配——即这些流以相同的速率增长其拥塞窗口。在稳态时,CUBIC 在窗口远离饱和点时积极增加窗口大小,而在窗口接近饱和点时缓慢增加。这一特性使得 CUBIC 在网络的带宽和延迟乘积较大时具有很高的可扩展性,同时保持高度稳定,并且对标准 TCP 流公平。

8. STCP(Scalable TCP)

STCP 算法由 Tom Kelly 于 2003 年提出,通过修改 TCP 的窗口增加和减少参数来调整发送窗口大小,以适应高速网络的环境。该算法具有很高的链路利用率和稳定性,但该机制窗口增加和 RTT 成反比,在一定程度上存在 RTT 不公平现象。而且,与传统 TCP 流共存时,它会过分占用带宽,其 TCP 友好性也较差。


TCP 拥塞控制

Wed, Jan 6, 2021

通过窗口管理,发送端与接收端能够协调彼此的发送和接收速率,避免出现超出接收端能力的丢包。但网络并非只有这一个 TCP 连接在使用,当数据报在发送端和接收端之间传输时,需要流转多个网络设备,这些网络设备的能力和使用情况也不同。可以将网络设备视为城市中的道路,数据报文就是其中的车辆。当车辆超过某条道路的处理能力时,由于数据报文不具备像车辆那样提前选择其他道路的能力,电信号只能被丢弃,于是这条道路直接将车辆“扔进外太空”,这种现象称为拥塞。但数据报文并不具备观察拥塞并自动避让的能力。如何避免发生拥塞,以及在发生堵塞时如何合理避让,这就是 TCP 的拥塞控制(Congestion Control)。

拥塞控制需要关注的几个问题如下:

  1. 拥塞控制是如何工作的?
  2. 当网络出现问题时,如何判断是否是拥塞问题?
  3. 高速网络如何避免因拥塞控制导致的传输效率降低?

拥塞征兆:如何知道发生了拥塞?

在内核算法层面,丢包、收到重复的 ACK、收到 SACK,都可以认为是网络中发生了拥塞。而 ECN 算法通过一个扩展位协商,允许一个 TCP 发送端向网络报告拥塞状况,而无需检测丢包,也可以用作拥塞检测的方法。

ECN 在 Linux 下可以通过 net.ipv4.tcp_ecnnet.ipv4.tcp_ecn_fallback 两个值配置。作为一个扩展,并非网络中所有的设备都支持该功能,因此并不能保证起作用。当 net.ipv4.tcp_ecn = 1 时,TCP 连接默认开启 ECN;当其为 2 时,Linux 将只对 accept 且开启了 ECN 的 TCP 连接启用 ECN。net.ipv4.tcp_ecn_fallback 置为 1,可以使 TCP 在尝试 ECN 失败时尝试不使用 ECN。

大部分拥塞控制算法都是基于以上方法,如 Reno、Cubic 等。也有一些算法尝试通过检测 RTT 的增大来预判拥塞的发生,例如 Vegas、Westwood 等。

在日常生活中,经常会听到“网络不好”的说法,可以使用 iperf 测试 TCP/UDP 的网络状况:

iperf

图中的 TCP Server 可以用 Go 语言通过一段简单代码运行,但随便选一个公网地址是无法允许随意写入的。

我们还可以通过 tc 来模拟网络延迟和丢包。例如,对 lo 网卡增加 100ms 延迟,发现传输变慢了:

delay

tc 对 lo 网卡造成的负面影响可以通过 sudo tc qdisc delete dev lo root 消除。

更多 tc 的使用可以参考:

  • Linux 下使用 tc 模拟网络延迟和丢包_tc 注入时延 ipv6-CSDN 博客
    https://blog.csdn.net/weiweicao0429/article/details/17578011

拥塞窗口(Congestion Window)

在 Linux 上使用命令 ip tcp_metrics,可以获得一个列表,代表与其它主机的 TCP 连接相关数据。其中的 rtt 以及 rttvar 在超时重传中已经介绍过,这里出现了一个 cwnd 参数,即拥塞窗口(Congestion Window)。它表示向该 IP 发送 TCP 数据报时,没有收到 ACK 回复的数据量不能大于 10。

在 TCP 流量控制中,还有一个通知窗口 awnd,取:

W = min ⁡ ( cwnd , awnd ) W = \min(\text{cwnd}, \text{awnd}) W=min(cwnd,awnd)

这样,对外发送但没有收到 ACK 的数据量不能多于 W W W。已经发出但还没确认的数据量大小,也叫做“在外数据值”(flight size),总是小于等于 W W W

W W W 不能过大或过小。一般希望能够接近“带宽延迟积”(Bandwidth-Delay Product, BDP),也被称作最佳窗口大小,因为此时发送数据能够最大化利用带宽。

BDP = Bandwidth × Delay \text{BDP} = \text{Bandwidth} \times \text{Delay} BDP=Bandwidth×Delay

慢启动

传输初始阶段,由于不清楚网络传输能力,需要缓慢探测可用传输资源,防止短时间内注入大量数据导致拥塞。这个缓慢探测的过程就是慢启动,其主要目的是预防拥塞。

SMSS = min ⁡ ( 接收方 MSS , MTU ) \text{SMSS} = \min(\text{接收方 MSS}, \text{MTU}) SMSS=min(接收方 MSS,MTU),初始窗口(Initial Window, IW)设置约为 1 SMSS,然后每次发送上次数据报的两倍,等到接收到 ACK 之后,再发上次的两倍,直到出现丢包停止增长。上面提到,拥塞窗口取 cwnd \text{cwnd} cwnd awnd \text{awnd} awnd 中的较小值,当接收方的 awnd \text{awnd} awnd 足够大时, cwnd \text{cwnd} cwnd 就是决定因素。如果采用延时确认,TCP 会直到慢启动结束才返回 ACK,这会导致 cwnd \text{cwnd} cwnd 增长较慢。

慢启动的主要目的是帮助确定一个慢启动阈值( ssthresh \text{ssthresh} ssthresh),当达到这个阈值时,就要开始进行拥塞避免。

拥塞避免

当慢启动达到阈值时,可能有更多的传输资源,但我们不能再像慢启动时的指数增长那样一下子占用很多资源,导致其他连接出现拥塞丢包。这时,每接收到一个 ACK,按如下方式更新 cwnd \text{cwnd} cwnd

cwnd t + 1 = cwnd t + SMSS × SMSS cwnd t \text{cwnd}_{t+1} = \text{cwnd}_t + \frac{\text{SMSS} \times \text{SMSS}}{\text{cwnd}_t} cwndt+1=cwndt+cwndtSMSS×SMSS

通常认为,慢启动阶段 cwnd \text{cwnd} cwnd 呈指数增长,而拥塞避免阶段 cwnd \text{cwnd} cwnd 呈线性增长。

慢启动和拥塞避免的选择

一般情况下,慢启动和拥塞避免在同一时刻只会使用一个。当 cwnd < ssthresh \text{cwnd} < \text{ssthresh} cwnd<ssthresh 时,使用慢启动算法;当 cwnd > ssthresh \text{cwnd} > \text{ssthresh} cwnd>ssthresh 时,使用拥塞避免算法;当 cwnd = ssthresh \text{cwnd} = \text{ssthresh} cwnd=ssthresh 时,任一算法都可使用。慢启动阈值 ssthresh \text{ssthresh} ssthresh 并不是固定值,而是记录了上次没有丢包情况下“最好的”操作窗口估计值。

拥塞窗口校验(CWV)

拥塞窗口校验(Congestion Window Validation, CWV)的主要目的是防止 TCP 连接由于长期空闲,之前已经较高的 cwnd \text{cwnd} cwnd 无法准确反映网络状况,导致网络拥塞。因此,CWV 机制在 TCP 停止发送一段时间后,开始衰减 cwnd \text{cwnd} cwnd。但 CWV 在尝试衰减 cwnd \text{cwnd} cwnd 时,需要弄清楚 TCP 连接是空闲(idle,暂时没有数据需要发送)还是应用受限(application-limited,因 CPU 竞争等原因,有数据需要发送但暂时没轮上)。在这两种情况下, cwnd \text{cwnd} cwnd 都有必要适度衰减。当 TCP 是空闲时,空闲一段时间之后,网络可能已经进入了严重拥堵状态,因此需要比较快的衰减。当应用受限时,虽然资源竞争对 TCP 的影响力不太可能有闲置那么久,但仍有必要对其做相对较慢的衰减。

Linux 内核中的拥塞控制算法

拥塞控制算法的配置和选择

在较新的 Linux 内核中,可以在 net/ipv4/Kconfig 文件中看到以下内容:

config DEFAULT_TCP_CONG
string
default "bic" if DEFAULT_BIC
default "cubic" if DEFAULT_CUBIC
default "htcp" if DEFAULT_HTCP
default "hybla" if DEFAULT_HYBLA
default "vegas" if DEFAULT_VEGAS
default "westwood" if DEFAULT_WESTWOOD
default "veno" if DEFAULT_VENO
default "reno" if DEFAULT_RENO
default "dctcp" if DEFAULT_DCTCP
default "cdg" if DEFAULT_CDG
default "bbr" if DEFAULT_BBR
default "cubic"

在如今的 Linux 内核参数中(从 2.6.13 开始),有三个参数可以控制拥塞控制算法:

net.ipv4.tcp_allowed_congestion_control = reno cubic
net.ipv4.tcp_available_congestion_control = reno cubic
net.ipv4.tcp_congestion_control = cubic

这表明目前系统设定可用的拥塞控制算法有 Reno 和 Cubic 两种,默认为 Cubic。那么如何选择它们呢?这就需要了解这些算法的特点,根据自己的使用场景进行设置了。

可以通过 modprobe 命令动态加载内核模块,从而增加可选的拥塞控制算法。例如,使用 modprobe tcp_highspeed

tcp_highspeed

在编写代码时,可以通过 setsockopt 手动设置 socket 使用的拥塞控制算法。对于监听连接,会从之前继承拥塞控制算法。

Linux 内核中拥塞控制算法的主要组成部分

struct tcp_congestion_ops tcp_reno = {
    .flags = TCP_CONG_NON_RESTRICTED,
    .name = "reno",
    .owner = THIS_MODULE,
    .ssthresh = tcp_reno_ssthresh,
    .cong_avoid = tcp_reno_cong_avoid,
    .undo_cwnd = tcp_reno_undo_cwnd,
};

对于每个拥塞控制算法,都有类似上面的代码,它有点像 C++ 的抽象类,每个算法都实现对应的部分。Reno 算法的拥塞参数代表了该拥塞控制算法的主要组成部分:

  • name 字段表明算法名字。
  • ssthresh 用于确定慢启动阈值。
  • cong_avoid 用于拥塞避免。
  • undo_cwnd 用于取消 cwnd \text{cwnd} cwnd 增长或衰退。
  • 其他算法中还可能看到 initpkts_acked 等,用于处理初始化、收到 ACK 包等(因为有些算法是通过 ACK 来进行拥塞避免等操作的)。

Tahoe、Reno 与快速恢复

Tahoe 算法是早期 BSD 采用的算法。连接一开始处于慢启动状态,当出现丢包时,重新将 cwnd \text{cwnd} cwnd 设定为初始值,重新开始慢启动。这个算法的问题在于在 BDP \text{BDP} BDP 较大的链路中,会使 TCP 发送方经常重新慢启动,不能有效利用带宽。根据包守恒原理(数据包从发送端到接收端,总会以某种形式存在于某个地方,当发送方和接收方达成平衡,并将链路塞满时,就能达到理想的传输状态),只要能够收到 ACK(包括重传 ACK),就有可能传输新的数据包。因此,Reno 算法中加入了快速恢复机制,在恢复阶段,每收到一个 ACK, cwnd \text{cwnd} cwnd 就能临时增长 1 SMSS,因此拥塞窗口会在一段时间内急速增长,直到接收一个好的 ACK。不重复的 ACK(好的)表明 TCP 结束恢复阶段,拥塞已经减少到之前状态。

Reno 算法在 Linux 内核中总是一个可用的选择,也是作为一个“TCP 标准”实现的。Reno 的特点是能够比较公平地分配带宽,也能够较好地利用带宽。

标准 TCP

在 RFC5681 中描述了一个基础的方法,被许多基础的 TCP 实现所使用。

  • 慢启动阶段, ssthresh \text{ssthresh} ssthresh 取较大值( ≥ awnd \geq \text{awnd} awnd),当收到一个好的 ACK 时,执行以下更新:
    cwnd + = SMSS ( 若 cwnd < ssthresh ,  执行慢启动 ) \text{cwnd} += \text{SMSS} \quad (\text{若} \ \text{cwnd} < \text{ssthresh}, \ \text{执行慢启动}) cwnd+=SMSS( cwnd<ssthresh, 执行慢启动)
    cwnd + = SMSS × SMSS cwnd ( 若 cwnd > ssthresh, 执行拥塞避免 ) \text{cwnd} += \frac{\text{SMSS} \times \text{SMSS}}{\text{cwnd}} \quad (\text{若} \ \text{cwnd} > \text{ssthresh},\ \text{执行拥塞避免}) cwnd+=cwndSMSS×SMSS( cwnd>ssthresh 执行拥塞避免)

  • 当收到三次重复 ACK,或其他表明需要重传的信号时:

    1. ssthresh \text{ssthresh} ssthresh 更新为大于以下等式的值:
      ssthresh = max ⁡ ( flightSize 2 , 2 × SMSS ) \text{ssthresh} = \max\left(\frac{\text{flightSize}}{2}, 2 \times \text{SMSS}\right) ssthresh=max(2flightSize,2×SMSS)
    2. 启用快速重传算法,设置 cwnd = ( ssthresh + 3 × SMSS ) \text{cwnd} = (\text{ssthresh} + 3 \times \text{SMSS}) cwnd=(ssthresh+3×SMSS)
    3. 每接收一个重复 ACK, cwnd + = 1 × SMSS \text{cwnd} += 1 \times \text{SMSS} cwnd+=1×SMSS
    4. 当接收到一个好的 ACK 时, cwnd = ssthresh \text{cwnd} = \text{ssthresh} cwnd=ssthresh

标准 TCP 每接收到一个好的 ACK, cwnd \text{cwnd} cwnd 增加 1 cwnd \frac{1}{\text{cwnd}} cwnd1,每出现一次丢包, cwnd \text{cwnd} cwnd 减半,这被称为“和式增加/积式减少”(Additive Increase/Multiplicative Decrease, AIMD)拥塞控制。通过将 1 cwnd \frac{1}{\text{cwnd}} cwnd1 替换为 a a a 1 2 \frac{1}{2} 21 替换为 b b b,得到 AIMD 一般化等式:

cwnd t + 1 = cwnd t + a cwnd t \text{cwnd}_{t+1} = \text{cwnd}_t + \frac{a}{\text{cwnd}_t} cwndt+1=cwndt+cwndta

cwnd t + 1 = cwnd t − b × cwnd t \text{cwnd}_{t+1} = \text{cwnd}_t - b \times \text{cwnd}_t cwndt+1=cwndtb×cwndt

p p p 为丢包率 [ 0 , 0.1 ] [0, 0.1] [0,0.1],故每个 RTT 内发包个数为:

T = a ( 2 − b ) 2 b p T = \frac{\sqrt{\frac{a(2 - b)}{2b}}}{\sqrt{p}} T=p 2ba(2b)

对于标准 TCP, a = 1 a = 1 a=1 b = 0.5 b = 0.5 b=0.5,故而可以简化为 T = 1.2 p T = \frac{1.2}{\sqrt{p}} T=p 1.2

高速网络环境下的 HSTCP(Highspeed TCP)

在高速网络环境下,传统的 TCP 使用 AIMD 拥塞控制,面临以下问题:

  1. 在线性增加阶段,高速网络下 cwnd \text{cwnd} cwnd 很容易变得很大,导致到达临界点时,下一次增长立即导致积式减少,不能探知理想的边界。
  2. 在积式减少阶段,高速网络下的 cwnd \text{cwnd} cwnd 马上衰减成一个较小的值,严重降低带宽利用率。

为了解决上述问题,RFC3649 定义了一种高速环境下能够充分利用带宽,低速环境下与标准 TCP 一致的算法。

HSTCP 修改了慢启动阶段,称为“受限的慢启动”,引入新的参数 max ssthresh \text{max ssthresh} max ssthresh。当 cwnd ≤ max ssthresh \text{cwnd} \leq \text{max ssthresh} cwndmax ssthresh 时,增长方式与传统 TCP 相同。当超过这个阈值时, cwnd \text{cwnd} cwnd 每次只能增长 max ssthresh 2 \frac{\text{max ssthresh}}{2} 2max ssthresh 个 SMSS,规避以单斜率线性增长导致无法探知正确边界的问题。

另外,通过之前的每隔 RTT 发包个数计算公式,在不同的丢包率下,通过调整 a a a b b b 的值,可以有效地增大发包效率。HSTCP 通过设定不同的比率,优化了其发送效率。在丢包率较大时或窗口较低时,HSTCP 与传统 TCP 发送效率基本一致。

Linux 内核中,HSTCP 的配置算法名为 highspeed,代码实现在 net/ipv4/tcp_highspeed.c 中。

HSTCP 在拥有大 BDP 的网络中,能够让低 RTT 的 TCP 连接得到更高的带宽。当有多个不同 RTT 的连接竞争时,高 RTT 的连接将会遭到不公平的对待。

二进制增长拥塞控制:BIC 和 CUBIC

上面提到了 HSTCP 的不公平性,而 BIC 和 CUBIC 则尝试在有效利用高速网络的同时兼顾公平性。最开始是 BIC,在此基础上,Linux 内核现在默认使用的是 CUBIC。

BIC

BIC 的核心思想是在和式增加上增加了 BI,即二分搜索增加(Binary Increase)。思路是取无丢包的窗口为 A A A,取有丢包的窗口为 B B B,然后找到中间值看是否丢包。如果发生丢包,则取之为 B B B,反之则为 A A A,以此类推,直到两者之间的差值低于临界值 S min ⁡ S_{\min} Smin 时停止,就能在对数时间内找到合适的窗口。这种方式越接近临界值,增长越慢,容易找到饱和点。但是,在一个 RTT 内突然将 cwnd \text{cwnd} cwnd 提升到中值,可能会导致严重的拥塞。因此,当最大值与最小值之间的差值大于临界值 S max ⁡ S_{\max} Smax 时,就不再使用二分搜索直接取中值,而是采用和式增加,每次增加不超过 S max ⁡ S_{\max} Smax

CUBIC

BIC 使用固定的临界值来决定何时使用 BI,何时使用 AI,不能很好地自动适应不同的网络环境,另外也存在某些情况下增长过快的问题。在 BIC 基础上,CUBIC 通过一个高阶多项式函数来控制窗口的增大:

W ( t ) = C ( t − K ) 3 + W max ⁡ W(t) = C(t - K)^3 + W_{\max} W(t)=C(tK)3+Wmax

在表达式中, t t t 是距离最近的一次窗口减小所经过的时间,以秒为单位; W ( t ) W(t) W(t) 代表时刻 t t t 的窗口大小; C C C 是一个常量(默认为 0.4); W max ⁡ W_{\max} Wmax 是最后一次调整前的窗口大小; K K K 是在没有丢包的情况下窗口从 W W W 增长到 W max ⁡ W_{\max} Wmax 所用的时间。

CUBIC 还增加了“TCP 友好性”功能,当发现 CUBIC 的窗口增长不如标准 TCP 时,可以采用标准 TCP 的,这样就实现了在低速网络环境下的性能优化。

基于延迟的拥塞控制算法

之前的拥塞控制算法都是利用丢包、SACK、重复 ACK 等探测拥塞,从而启动拥塞避免过程。当拥塞即将发生时,RTT 也会逐渐增大,因此有一类拥塞算法,利用 RTT 增长趋势来检测拥塞并生效。

Vegas 算法

在拥塞避免阶段,Vegas 算法测量每个 RTT 所传输的数据量,并将该数据除以网络中观察到的最小延迟时间,得到 v = T RTT min ⁡ v = \frac{T}{\text{RTT}_{\min}} v=RTTminT。算法维护两个阈值 α \alpha α β \beta β,其中 α < β \alpha < \beta α<β。当 v < α v < \alpha v<α 时,增大拥塞窗口;当 v > β v > \beta v>β 时,减小拥塞窗口。若 α ≤ v ≤ β \alpha \leq v \leq \beta αvβ,窗口保持不变。其中拥塞窗口的改变是现行的,即“和式增加/和式减少”(Additive Increase/Additive Decrease, AIAD)的拥塞控制策略。

由于 Vegas 是通过 RTT 来评估是否需要拥塞避免,当发送端与接收端往返的链路不同,而 ACK 的链路发生拥塞时,Vegas 会误判认为需要减少发送端的数据发送,过度降低发送窗口。

Vegas 与其他的 Vegas TCP 链接共用链路时,是平等的。但是与标准 TCP 共用链路时,由于标准 TCP 的目标是占满等待队列,而 Vegas 则是尝试通过预判来使队列相对空闲,这时 Vegas 将会自动降低发送速率,导致在竞争中处于劣势。

Vegas 目前在 Linux 内核中设置 α = 2 \alpha = 2 α=2 β = 4 \beta = 4 β=4 且不可动态调整,需要手动加载模块 tcp_vegas 后,修改 net.ipv4.tcp_congestion_control 来启用它。

Westwood 算法

Westwood 是基于延迟在 New Reno 算法基础上进行了改进,从而实现对大带宽延迟积链路的处理。当发生丢包时,Westwood 不会直接将 cwnd \text{cwnd} cwnd 减半,而是根据 RTT min ⁡ \text{RTT}_{\min} RTTmin 计算出一个估算 BDP,然后将 ssthresh \text{ssthresh} ssthresh 值更新成该值。在慢启动阶段,它是呈指数增加的。

Westwood 也可以通过类似 Vegas 的方式在 Linux 中启用。


via:

  • TCP 的那些事儿(上)_火龙果软件 发布于 2014-07-04
    http://www.uml.org.cn/safe/201407041.asp?artid=2483

  • TCP 的那些事儿(下)_火龙果软件 发布于 2014-07-07
    http://www.uml.org.cn/safe/201407075.asp?artid=2863

  • TCP协议拥塞控制算法(Reno、HSTCP、BIC、Vegas、Westwood)_王霉霉_新浪博客
    https://blog.sina.com.cn/s/blog_73b05f070101ipaw.html

  • TCP拥塞控制算法 优缺点 适用环境 性能分析_实际系统中流量和拥塞控制算法,举例并说明优缺点-CSDN博客
    https://blog.csdn.net/zhangskd/article/details/6715751

  • TCP拥塞控制 · E.T.
    https://tangyanhan.github.io/posts/tcp-congestion-control/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2325380.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

卷积神经网络 - ResNet(残差网络)

残差网络(Residual Network&#xff0c;ResNet)通过给非线性的卷积层增加直连边 (Shortcut Connection)(也称为残差连接(Residual Connection))的方式来提高信息的传播效率。 这是一种特殊的深度神经网络结构&#xff0c;由 Kaiming He 等人在 2015 年提出&#xff0c;目的是解…

GreenPlum学习

简介 Greenplum是一个面向数据仓库应用的关系型数据库&#xff0c;因为有良好的体系结构&#xff0c;所以在数据存储、高并发、高可用、线性扩展、反应速度、易用性和性价比等方面有非常明显的优势。Greenplum是一种基于PostgreSQL的分布式数据库&#xff0c;其采用sharednothi…

传统神经网络、CNN与RNN

在网络上找了很多关于深度学习的资料&#xff0c;也总结了一点小心得&#xff0c;于是就有了下面这篇文章。这里内容较为简单&#xff0c;适合初学者查看&#xff0c;所以大佬看到这里就可以走了。 话不多说&#xff0c;上图 #mermaid-svg-Z3k5YhiQ2o5AnvZE {font-family:&quo…

无人机,雷达定点飞行时,位置发散,位置很飘,原因分析

参考&#xff1a; 无人车传感器 IMU与GPS数据融合进行定位机制_gps imu 组合定位原始数-CSDN博客 我的无人机使用雷达定位&#xff0c;位置模式很飘 雷达的更新频率也是10HZ&#xff0c; 而px飞控的频率是100HZ&#xff0c;没有对两者之间的频率差异做出处理 所以才导致无人…

【Linux探索学习】第二十九弹——线程概念:Linux线程的基本概念与线程控制详解

Linux学习笔记&#xff1a; https://blog.csdn.net/2301_80220607/category_12805278.html?spm1001.2014.3001.5482 前言&#xff1a; 在现代操作系统中&#xff0c;线程是程序执行流的最小单元。与进程相比&#xff0c;线程更加轻量级&#xff0c;创建和销毁的开销更小&…

深入探索 iOS 卡顿优化

认识卡顿 一些概念 FPS&#xff1a;Frames Per Second&#xff0c;表示每秒渲染的帧数&#xff0c;通过用于衡量画面的流畅度&#xff0c;数值越高则表示画面越流畅。CPU&#xff1a;负责对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码…

# 基于 OpenCV 的选择题自动批改系统实现

在教育领域&#xff0c;选择题的批改工作通常较为繁琐且重复性高。为了提高批改效率&#xff0c;我们可以利用计算机视觉技术&#xff0c;通过 OpenCV 实现选择题的自动批改。本文将详细介绍如何使用 Python 和 OpenCV 实现一个简单的选择题自动批改系统。 1. 项目背景 选择题…

身份验证:区块链如何让用户掌控一切

在网上证明你自称的身份变得越来越复杂。由于日常生活的很多方面现在都在网上进行&#xff0c;保护你的数字身份比以往任何时候都更加重要。 我们可能都接受过安全培训&#xff0c;这些培训鼓励我们选择安全的密码、启用双因素身份验证或回答安全问题&#xff0c;例如“你祖母…

嵌入式硬件: GPIO与二极管基础知识详解

1. 前言 在嵌入式系统和硬件开发中&#xff0c;GPIO&#xff08;通用输入输出&#xff09;是至关重要的控制方式&#xff0c;而二极管作为基础电子元件&#xff0c;广泛应用于信号整流、保护电路等。本文将从基础原理出发&#xff0c;深入解析GPIO的输入输出模式&#xff0c;包…

游戏引擎学习第194天

为当天的活动做铺垫 正在进行游戏开发中的调试和视图功能开发。目标是增加一些新功能&#xff0c;使得在开发过程中能够有效地检查游戏行为。今天的重点是推进用户界面&#xff08;UI&#xff09;的开发&#xff0c;并且尝试在调试变量的管理上找到一个折中的解决方案。计划探…

js文字两端对齐

目录 一、问题 二、原因及解决方法 三、总结 一、问题 1.text-align: justify; 不就可以了吗&#xff1f;但是实际测试无效 二、原因及解决方法 1.原因&#xff1a;text-align只对非最后一行文字有效。只有一行文字时&#xff0c;text-align无效&#xff0c;要用text-alig…

HarmonyOS 介绍

HarmonyOS简介 随着万物互联时代的开启&#xff0c;应用的设备底座将从几十亿手机扩展到数百亿IoT设备。全新的全场景设备体验&#xff0c;正深入改变消费者的使用习惯。 同时应用开发者也面临设备底座从手机单设备到全场景多设备的转变&#xff0c;全场景多设备的全新底座&am…

每天一篇目标检测文献(六)——Part One

今天看的是《Object Detection with Deep Learning: A Review》 目录 一、摘要 1.1 原文 1.2 翻译 二、介绍 2.1 信息区域选择 2.2 特征提取 2.3 分类 三、深度学习的简要回顾 3.1 历史、诞生、衰落和繁荣 3.2 CNN架构和优势 一、摘要 1.1 原文 Due to object dete…

ESXI 安装及封装第三方驱动和在ESXI系统下安装驱动

ESXI 安装及封装第三方驱动和在ESXI系统下安装驱动 准备工作在线安装 Windows PowerShell离线安装 Windows PowerShell更新在线更新离线更新 下载 ESXi-Customizer-PS-v2.6.0.ps1安装Python安装pip安装相关插件 下载离线捆绑包下载对应的网卡驱动&#xff08;如果纯净版可以进去…

【12】Ajax的原理和解析

一、前言 二、什么是Ajax 三、Ajax的基本原理 3.1 发送请求 3.2 解析内容 3.3 渲染网页 3.4 总结 四、Ajax 分析 五、过滤请求-筛选所有Ajax请求 一、前言 当我们在用 requests 抓取页面的时候&#xff0c;得到的结果可能会和在浏览器中看到的不一样&a…

双塔模型2之如何选择正确的正负样本

双塔模型&#xff1a;正负样本 选对正负样本的作用 > 改进模型的结构 正样本 什么是正样本&#xff1f;答&#xff1a;曝光且有点击的 “用户-物品” 二元组 存在的问题&#xff1a;存在28法则&#xff0c;即少部分物品&#xff08;比如热门物品&#xff09;占大部分点击…

《八大排序算法》

相关概念 排序&#xff1a;使一串记录&#xff0c;按照其中某个或某些关键字的大小&#xff0c;递增或递减的排列起来。稳定性&#xff1a;它描述了在排序过程中&#xff0c;相等元素的相对顺序是否保持不变。假设在待排序的序列中&#xff0c;有两个元素a和b&#xff0c;它们…

零基础使用AI从0到1开发一个微信小程序

零基础使用AI从&#xff10;到&#xff11;开发一个微信小程序 准备操作记录 准备 想多尝试一些新的交互方式&#xff0c;但我没有相关的开发经验&#xff0c;html&#xff0c;JavaScript 等都不了解&#xff0c;看了一些使用AI做微信小程序的视频教程&#xff0c;觉得自己也行…

基于Spring Boot的社区互助平台的设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

【Elasticsearch入门到落地】10、初始化RestClient

接上篇《9、hotel数据结构分析》 上一篇我们讲解了导入的宾馆数据库tb_hotel表结构的具体含义&#xff0c;并分析如何建立其索引库。本篇我们来正式进入链接Elasticsearch的Java代码的编写阶段&#xff0c;先进行RestClient的初始化。 RestClient的初始化分为三步&#xff0c;…