TCP详解之流量控制

2023-09-17 22:41:41

TCP详解之流量控制

发送方不能无脑的发数据给接收方,要考虑接收方处理能力。

如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。

为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制

下面举个栗子,为了简单起见,假设以下场景:

  • 客户端是接收方,服务端是发送方
  • 假设接收窗口和发送窗口相同,都为 200
  • 假设两个设备在整个传输过程中都保持相同的窗口大小,不受外界影响

流量控制

根据上图的流量控制,说明下每个过程:

  1. 客户端向服务端发送请求数据报文。这里要说明下,本次例子是把服务端作为发送方,所以没有画出服务端的接收窗口。
  2. 服务端收到请求报文后,发送确认报文和 80 字节的数据,于是可用窗口 Usable 减少为 120 字节,同时 SND.NXT 指针也向右偏移 80 字节后,指向 321,这意味着下次发送数据的时候,序列号是 321
  3. 客户端收到 80 字节数据后,于是接收窗口往右移动 80 字节,RCV.NXT 也就指向 321,这意味着客户端期望的下一个报文的序列号是 321,接着发送确认报文给服务端。
  4. 服务端再次发送了 120 字节数据,于是可用窗口耗尽为 0,服务端无法再继续发送数据。
  5. 客户端收到 120 字节的数据后,于是接收窗口往右移动 120 字节,RCV.NXT 也就指向 441,接着发送确认报文给服务端。
  6. 服务端收到对 80 字节数据的确认报文后,SND.UNA 指针往右偏移后指向 321,于是可用窗口 Usable 增大到 80。
  7. 服务端收到对 120 字节数据的确认报文后,SND.UNA 指针往右偏移后指向 441,于是可用窗口 Usable 增大到 200。
  8. 服务端可以继续发送了,于是发送了 160 字节的数据后,SND.NXT 指向 601,于是可用窗口 Usable 减少到 40。
  9. 客户端收到 160 字节后,接收窗口往右移动了 160 字节,RCV.NXT 也就是指向了 601,接着发送确认报文给服务端。
  10. 服务端收到对 160 字节数据的确认报文后,发送窗口往右移动了 160 字节,于是 SND.UNA 指针偏移了 160 后指向 601,可用窗口 Usable 也就增大至了 200。

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

前面的流量控制例子,我们假定了发送窗口和接收窗口是不变的,但是实际上,发送窗口和接收窗口中所存放的字节数,都是放在操作系统内存缓冲区中的,而操作系统的缓冲区,会被操作系统调整

当应用进程没办法及时读取缓冲区的内容时,也会对我们的缓冲区造成影响。

那操作系统的缓冲区,是如何影响发送窗口和接收窗口的呢?

我们先来看看第一个例子。

当应用程序没有及时读取缓存时,发送窗口和接收窗口的变化。

考虑以下场景:

  • 客户端作为发送方,服务端作为接收方,发送窗口和接收窗口初始大小为 360
  • 服务端非常的繁忙,当收到客户端的数据时,应用层不能及时读取数据。

缓冲区的影响

根据上图的流量控制,说明下每个过程:

  1. 客户端发送 140 字节数据后,可用窗口变为 220 (360 - 140)。
  2. 服务端收到 140 字节数据,但是服务端非常繁忙,应用进程只读取了 40 个字节,还有 100 字节占用着缓冲区,于是接收窗口收缩到了 260 (360 - 100),最后发送确认信息时,将窗口大小通告给客户端。
  3. 客户端收到确认和窗口通告报文后,发送窗口减少为 260。
  4. 客户端发送 180 字节数据,此时可用窗口减少到 80。
  5. 服务端收到 180 字节数据,但是应用程序没有读取任何数据,这 180 字节直接就留在了缓冲区,于是接收窗口收缩到了 80 (260 - 180),并在发送确认信息时,通过窗口大小给客户端。
  6. 客户端收到确认和窗口通告报文后,发送窗口减少为 80。
  7. 客户端发送 80 字节数据后,可用窗口耗尽。
  8. 服务端收到 80 字节数据,但是应用程序依然没有读取任何数据,这 80 字节留在了缓冲区,于是接收窗口收缩到了 0,并在发送确认信息时,通过窗口大小给客户端。
  9. 客户端收到确认和窗口通告报文后,发送窗口减少为 0。

可见最后窗口都收缩为 0 了,也就是发生了窗口关闭。当发送方可用窗口变为 0 时,发送方实际上会定时发送窗口探测报文,以便知道接收方的窗口是否发生了改变,这个内容后面会说,这里先简单提一下。

我们先来看看第二个例子。

当服务端系统资源非常紧张的时候,操作系统可能会直接减少了接收缓冲区大小,这时应用程序又无法及时读取缓存数据,那么这时候就有严重的事情发生了,会出现数据包丢失的现象。

数据包丢失

说明下每个过程:

  1. 客户端发送 140 字节的数据,于是可用窗口减少到了 220。
  2. 服务端因为现在非常的繁忙,操作系统于是就把接收缓存减少了 120 字节,当收到 140 字节数据后,又因为应用程序没有读取任何数据,所以 140 字节留在了缓冲区中,于是接收窗口大小从 360 收缩成了 100,最后发送确认信息时,通告窗口大小给对方。
  3. 此时客户端因为还没有收到服务端的通告窗口报文,所以不知道此时接收窗口收缩成了 100,客户端只会看自己的可用窗口还有 220,所以客户端就发送了 180 字节数据,于是可用窗口减少到 40。
  4. 服务端收到了 180 字节数据时,发现数据大小超过了接收窗口的大小,于是就把数据包丢失了
  5. 客户端收到第 2 步时,服务端发送的确认报文和通告窗口报文,尝试减少发送窗口到 100,把窗口的右端向左收缩了 80,此时可用窗口的大小就会出现诡异的负值。

所以,如果发生了先减少缓存,再收缩窗口,就会出现丢包的现象。

为了防止这种情况发生,TCP 规定是不允许同时减少缓存又收缩窗口的,而是采用先收缩窗口,过段时间再减少缓存,这样就可以避免了丢包情况

窗口关闭

在前面我们都看到了,TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。

如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭

窗口关闭潜在的危险

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

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

窗口关闭潜在的危险

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

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

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

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

窗口探测

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

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

糊涂窗口综合症

如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。

到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症

要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要搭上这么大的开销,这太不经济了。

就好像一个可以承载 50 人的大巴车,每次来了一两个人,就直接发车。除非家里有矿的大巴司机,才敢这样玩,不然迟早破产。要解决这个问题也不难,大巴司机等乘客数量超过了 25 个,才认定可以发车。

现举个糊涂窗口综合症的栗子,考虑以下场景:

接收方的窗口大小是 360 字节,但接收方由于某些原因陷入困境,假设接收方的应用层读取的能力如下:

  • 接收方每接收 3 个字节,应用程序就只能从缓冲区中读取 1 个字节的数据;
  • 在下一个发送方的 TCP 段到达之前,应用程序还从缓冲区中读取了 40 个额外的字节;

糊涂窗口综合症

每个过程的窗口大小的变化,在图中都描述的很清楚了,可以发现窗口不断减少了,并且发送的数据都是比较小的了。

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

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

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

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

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

接收方通常的策略如下:

当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。

等到接收方处理了一些数据后,窗口大小 >= MSS,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。

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

发送方通常的策略如下:

使用 Nagle 算法,该算法的思路是延时处理,只有满足下面两个条件中的任意一个条件,才可以发送数据:

  • 条件一:要等到窗口大小 >= MSS 并且 数据大小 >= MSS
  • 条件二:收到之前发送数据的 ack 回包;

只要上面两个条件都不满足,发送方一直在囤积数据,直到满足上面的发送条件。

Nagle 伪代码如下:

if 有数据要发送 {
    if 可用窗口大小 >= MSS and 可发送的数据 >= MSS {
    	立刻发送MSS大小的数据
    } else {
        if 有未确认的数据 {
            将数据放入缓存等待接收ACK
        } else {
            立刻发送数据
        }
    }
}

注意,如果接收方不能满足「不通告小窗口给发送方」,那么即使开了 Nagle 算法,也无法避免糊涂窗口综合症,因为如果对端 ACK 回复很快的话(达到 Nagle 算法的条件二),Nagle 算法就不会拼接太多的数据包,这种情况下依然会有小数据包的传输,网络总体的利用率依然很低。

所以,接收方得满足「不通告小窗口给发送方」+ 发送方开启 Nagle 算法,才能避免糊涂窗口综合症

另外,Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。

可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法(关闭 Nagle 算法没有全局参数,需要根据每个应用自己的特点来关闭)

setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)&value, sizeof(int));
更多推荐

【程序员接口干货】热门免费的api接口大全

企业基本信息API:通过公司名称/公司ID/注册号或社会统一信用代码获取企业基本信息,企业基本信息包括公司名称或ID、类型、成立日期、经营状态、注册资本、法人、工商注册号、统一社会信用代码、组织机构代码、纳税人识别号等字段信息。企业联系方式查询API:通过公司名称、公司ID、注册号或社统一信用代码获取企业联系方式信息,

STM32WB55开发(4)----配置串口打印Debug调试信息

STM32WB55开发----4.配置串口打印Debug调试信息概述硬件准备视频教学样品申请源码下载选择芯片型号配置时钟源配置时钟树RTC时钟配置查看开启STM32_WPAN条件配置HSEM配置IPCC配置RTC启动RF开启蓝牙开启串口调试配置蓝牙参数设置工程信息工程文件设置Keil工程配置代码配置结果演示概述在STM

STM32WB55开发(2)----修改蓝牙地址

STM32WB55开发----2.修改蓝牙地址概述硬件准备视频教学样品申请完整代码下载选择芯片型号配置时钟源配置时钟树RTC时钟配置查看开启STM32_WPAN条件配置HSEM配置IPCC配置RTC启动RF开启蓝牙设置工程信息工程文件设置修改置BLE设备公共地址Ble_Hci_Gap_Gatt_Init结果演示概述在嵌

【STM32】驱动库的选择:CMSIS Driver、SPL、HAL、LL | 在ARM MDK、STM32Cube中如何选择?

阅读本专栏其他文章,有助于理解本文。👆文章目录一、开发库选择1.1概述1.2CMSISpack1.3SPL库1.4HAL库1.5LL库1.6寄存器开发二、代码对比2.1使用寄存器2.2使用CMSIS库2.3使用SPL库2.4使用HAL库2.5使用LL库2.6使用RTOS三、如何在软件中选择不同的库3.1ARMMDK3

Spring Boot自动装配原理

简介SpringBoot是一个开源的Java框架,旨在简化Spring应用程序的搭建和开发。它通过自动装配的机制,大大减少了繁琐的配置工作,提高了开发效率。本文将深入探讨SpringBoot的自动装配原理。自动装配的概述在传统的Spring框架中,我们需要手动配置各种组件和依赖关系。而SpringBoot则通过自动扫描

基于微信小程序的高校就业招聘系统设计与实现(源码+lw+部署文档+讲解等)

前言💗博主介绍:✌全网粉丝10W+,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战✌💗👇🏻精彩专栏推荐订阅👇🏻2023-2024年最值得选的微信小程序毕业设计选题大全:100个热门选

c++Flood Fill算法之池塘计数,城堡问题,山峰与山谷(acwing)

FloodFill算法有很多应用场景,以下是一些常见的应用场景:1.图像处理:在图像处理中,我们通常需要对图像的某一个区域进行涂色、填充、描边等操作,FloodFill算法就可以用来实现这些操作。2.游戏开发:在游戏中,FloodFill算法可以用来实现区域填充、地图探索、迷宫求解等功能。3.自动化绘制:FloodFi

如何用ate自动测试设备对DC-DC电源模块负载调整率进行测试?

电源模块负载调整率测试是功能测试之一,是电源模块非常重要的一项指标,它的大小直接影响着电源模块的整体质量。因此使用ate自动测试设备对DC-DC电源模块负载调整率进行测试是制造生产过程中至关重要的一环。电源模块负载调整率计算公式:负载调整率=(满载输出电压-空载输出电压)/额定输出电压*100%测量电源模块的负载调整率

解决高并发问题

在处理项目中的高并发问题时,可以采取以下几种方法:后端处理:大部分的高并发处理是在后端进行的。可以通过优化数据库查询、增加缓存机制(如集成Redis)、使用分布式技术(如分布式缓存、分布式锁)、使用消息队列等方式来提高系统的并发处理能力。此外,还可以通过水平扩展(增加服务器数量)或垂直扩展(增加服务器的硬件配置)来提高

Kafka【命令行操作】

Kafka命令行操作Kafka主要包括三大部分:生产者、主题分区节点、消费者。1、Topic命令行操作也就是我们kafka下的脚本kafka-topics.sh的相关操作。常用命令行操作参数描述--bootstrap-server<String:servertoconnectto>连接的KafkaBroker主机名称和

【Android Framework系列】第15章 Fragment+ViewPager与Viewpager2相关原理

1前言上一章节【AndroidFramework系列】第14章Fragment核心原理(AndroidX版本)我们学习了Fragment的核心原理,本章节学习常用的Fragment+ViewPager以及Fragment+ViewPager2的相关使用和一些基本的源码分析。2Fragment+ViewPager我们常用

热文推荐