个人对TCP流量控制与拥塞控制的理解

news2024/9/20 10:25:22

TCP 协议和 UDP 协议

TCP 作为最常用的两大传输层协议之一,无疑是久经生产环境检验的。传输层有两个我们广泛使用的协议:UDP 协议、TCP 协议,我们一般会说前者是面向无连接的,后者是面向连接的。

这里的 “连接” 具体是什么意思呢?

简单来说,UDP 协议是一种没有状态的协议,节点之间如果采用 UDP 协议通信,两个节点能做的就是在 UDP 协议上发送一个个数据包,协议本身不会关心这些包之间的关系,所以在复杂的网络下,包的顺序和可达性都是没有保证的,应用层需要自己处理这些包的丢失和乱序问题。

当然,UDP 协议也因此可以设计的非常轻量,所以 在网络传输本来就比较稳定的内网环境下,或者对丢包可以容忍但对时延要求较高的场景下,UDP 协议有许多应用

而 TCP 很不一样,它在节点之间建立了真正的连接的概念。相信你肯定听过 TCP 的三次握手吧,UDP 就没有这个过程,三次握手完成时,TCP 连接就建立了;结束通信的时候,通信双方往往也需要主动断开连接。

TCP 的传输基于字节流,也引入了状态,明确记录着每个包是否发送、是否被接受到、包本身的序列号等状态;所以, TCP 为我们提供了可靠有序的传输能力,也被设计的相当复杂。TCP 协议不止考虑了包的可靠传输,同时也兼顾了效率,更提供了对流量控制和拥塞避免的能力,滑动窗口和拥塞窗口就是为了这两个目的而设计的。

那 TCP 的具体是如何做到让网络中的包传输可靠且效率高的呢?

TCP 中包的发送

一个包在网络中发送出去,其实就像古时候你给别人用信鸽寄了一封信一样,在外界环境非常复杂的情况下,你完全没有把握这封信能不能真的送达收信人那边,除非有一天你收到了收信人的回信。

类似的,在复杂网络环境下,TCP 为了能保证每个包真的送达了,并且接收端收到包的顺序和发送端是一致的,我们每发出一个包,自然也需要一个类似回信的机制。

这个回信也就是 ACK 包, 每个包发送的时候会有一个序列号,接收端回 ACK 包的时候会把序列号 + 1 发送回来,发送端如果没有收到某个包的 ACK 包,会在一段时间之后尝试重新发送,直到收到 ACK 为止。这其实也是在网络和各种分布式系统中能确保消息可达的唯一方式。

那问题来了,我们为了确保消息顺序可达,难道每次发送一个新的包,都等待上一个包的 ACK 回来之后才能发送吗?这样一来一回的效率显然是很低的,也就是每经过一个 RTT 的时间,我们只能发送一个包,假设一个 RTT 是 100ms,那在一秒中我们甚至只能发送 10 个包,这完全是不可接受的。

其实我们 在等待 ACK 的时候没有必要停止后续包的发送,因为网络传输虽然不稳定,但大部分包往往还是可达的,这样我们就可以获得数倍的传输效率提升。如果真的不幸遇到了丢包,接收端 ACK 姗姗来迟的时候,也就告诉了我们某个序列号之前的所有包全部收到,我们再根据一定的策略,尝试重新发送对应丢失的包就可以了。

所以自然而然的,发送方需要缓存已发出但尚未收到 ACK 的包,接收方收到包但没有被用户进程消费之前也得把收到的包留着。

但是,缓存是有大小限制的,程序消费数据和链路传输数据的能力也是有限的,发送端和接受端都需要一种机制来限制可发送或者可接收数据的最大范围。

于是,滑动窗口和拥塞窗口应运而生。

这两个算法核心都是为了防止像网络中发送的包太多。不同的是两者的目的, 滑动窗口机制,可以用来控制流量,防止接收方处理不过来消息;同样基于窗口机制的拥塞控制算法,则用来处理网络上数据包太多的情况,以避免网络中出现拥塞

流量控制

我们先来看看如何用滑动窗口控制流量。

这里说流量控制,主要就是为了防止接收方处理数据的速度跟不上发送方,避免随着时间推移,数据自然溢出接收方的缓冲区。

虽然协议可以保证发送方没有收到 ACK,最终会尝试重新发送,但如果需要大量反复发送冗余的数据,所占用的网络资源就被白白浪费了,在网络资源很紧缺的时候,这也会造成网络环境的恶化。

TCP 控制流量的方式也很简单,就是滑动窗口机制。

接收端会建立一个滑动窗口,由接收方向发送方通告,TCP 首部里的 window 字段就是用来表示窗口大小的,窗口表示的就是接收方目前能接收的缓冲区的剩余大小。

但是发送方也会根据这个通告窗口的大小建立自己的滑动窗口。为了兼顾效率和可靠性,在发送方,所有未收到 ACK 的消息虽然可以发送,但是在收到 ACK 之前是一定要在缓冲区中保存的。

我们一起来看一下。

发送端的窗口

发送窗口根据三个标准来划分:是否发送、是否收到 ACK、是否在接收方通告处理范围内,分成了四个部分。

  • 第一部分就是已经发送且收到 ACK 的部分,这一块我们知道已经成功发送,所以不需要在缓冲区保留了。
  • 第二部分是已发送但尚未收到 ACK 的部分。
  • 第三部分是还没有发送,但是还在接收方通告窗口也就是处理范围内的数据,这块我们也可以称为可用窗口;第二、第三部分一起构成了我们的整个发送窗口。
  • 最后一部分则是我们 需要发送,但已经超过接收方通告窗口范围的部分,这一部分在没有收到新的 ACK 之前,发送方是不会发送这些数据的。通过这个限制,发送的数据就一定不会超过接收方的缓冲区了。

但如果发送方一直没有收到 ACK,随着数据不断被发送,很快可用窗口就会被耗尽。在这种情况下,发送方也就不会继续发送数据了,这种发送端可用窗口为零的情况我们也称为 “零窗口”。

正常来说,等接收端处理了一部分数据,又有了新的可用窗口之后,就会再次发送 ACK 报文通告发送端自己有新的可用窗口(因为发送端的可用窗口是受接收端控制的)。

但是,万一要是 ACK 消息在网络传输中正好丢包了,那发送端还能感知到接收端窗口的变化吗?其实是不会的,在这个情况下,接收端就会一直等着发送端发送数据,而发送端也还会以为接收端仍然处于零窗口的状态,这样一直互相等待,就好像进入了死锁状态。

解决办法也很简单,我们可以再引入一个零窗口定时器,如果发送端陷入零窗口的状态,就会启动这个定时器,去定时地询问接收端窗口是否可用了,这也是在分布式系统中常见的处理丢包的方式之一。

接收端的窗口

相对发送端来说,接收端要简单的多,主要就分为已经接收并确认的数据和未收到但可以接收的数据,这一部分也就是接收窗口;剩下的就是缓冲区放不下的区域,也就是不可接收的区域。

如果进程读取缓冲区速度有所变化,接收端可能也会改变接收窗口的大小,每次通告给发送端,就可以控制发送端的发送速度了。这就是所谓的滑动窗口,也就是流量控制机制。

而之所以是滑动窗口,也很好理解,随着 ACK 或者进程读取数据,窗口也会顺次往后移动。比如在发送端的窗口中,如果我们在某次通信中收到了一条 ACK 消息,表示 36 之前的消息都已经被收到了,那么整个可用的窗口就会顺次往右移动。

总的来说,滑动窗口(流量控制机制)解决了发送端消息可能淹没接收端,导致处理跟不上的情况。

流量拥塞

那 TCP 协议如何解决流量拥塞的情况呢?也就是网络中由于大量包传输,导致吞吐量下降甚至为 0 的情况。这和我们的道路交通很像,当车流越来越大的时候,整体的行车速度可能会不断下降,导致拥堵,最后吞吐量反而不如车少的时候。

在实际网络中,因为大量的包传输,可能导致中间某些节点的缓冲区满载,从而多余的包被丢弃,需要重新发送,情况越发恶化,最差的时候,网络上的包都是重传的包并且反复地丢弃;整个网络传输能力甚至可以降低为 0。

这当然是一个很严重的问题,TCP 协议同样提出了另外一个叫拥塞窗口的机制,很好地解决了这个问题。具体是怎么做的呢?我们一起来看一下。

拥塞控制

网络中每个节点不会有全局的网络通信情况,唯一能发现的就是自己的部分包丢了,这种时候它就有理由怀疑网络环境劣化,可能产生了拥塞。

TCP 是一个比较无私的协议,在这种情况下,会选择减少自己发送的包。当网络上大部分通信协议传输层都采用的是 TCP 协议时,在出现拥塞的情况下,大部分节点都会不约而同地减少自己传输的包,这样网络拥塞情况就会得到极大的缓解,一直处于比较好的网络状态。

所以我们就需要 在发送端定义一个窗口 CWND(congestion window),也就是拥塞窗口;发送端能发送的最多没有收到 ACK 的包,也不会超过拥塞窗口的范围。

引入拥塞控制机制的 TCP 协议,发送端最大的发送范围是拥塞窗口和滑动窗口中小的一个。拥塞窗口会动态地随着网络情况的变化而进行调整,大体上的策略是如果没有出现拥塞,我们扩大窗口大小,否则就减少窗口大小。

具体是如何实现的呢?经典拥塞控制算法主要包括四个部分:

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

我们一个个来看。首先是慢启动,在不确定拥塞是否会发生的时候,我们不会一上来就发送大量的包,而是会采用倍增的方式缓慢增加窗口的大小,窗口大小从 1 开始尝试,然后尝试 2、4、8、16 等越来越大的窗口。

整个慢启动的过程看起来就像图中这样,指数型的增加拥塞窗口的大小

这样,倍增的方式窗口就会很快扩大;我们会在窗口大到一定程度时,减慢增加的速度,转成线性扩大窗口的方式,也就是每次收到新的 ACK 没有丢包的话只比上次窗口增大 1。整个过程看起来就像这样:

这两个慢启动阶段和拥塞避免阶段的分界点,我们就叫 “慢启动门限(ssthresh)”

随着窗口进一步缓慢增加,终于有一天,网络还是遇到了丢包的情况,我们就会假定这是拥塞造成的。

这个时候我们一方面会进行超时重传或者快速重传,另一方面也会把窗口调整到更小的范围。

  • 超时重传,往往意味着拥塞情况更严重,我们的策略也会更激进一些,会直接将 ssthresh 设置为重传发生时拥塞窗口大小的一半,而拥塞窗口大小直接重置为 0,再进入慢启动阶段。像这样:
  • 快速重传,如果我们连续 3 次收到同样序号的 ACK,包还能回传,说明这个时候可能只是碰到了部分丢包,网络阻塞还没有很严重,我们就会采用柔和一点的策略,也就是快速恢复策略。

我们会先把拥塞窗口变成原来的一半,ssthresh 也就设置成当前的窗口大小,然后开始执行拥塞避免算法。有些实现也会把拥塞窗口直接设置为 ssthresh+3,本质上区别不大。

总结

总体而言,TCP 就是通过滑动窗口、拥塞窗口这两个简单的窗口实现了流量控制和拥塞控制。

滑动窗口由接收端控制,向发送端通告,这样就可以保证发送端发出的包数量上限是明确的,也就不会存在淹没接收端导致来不及处理的情况。

拥塞窗口由发送端控制,它会根据网络中的情况动态的调整,通过慢启动、拥塞避免、拥塞发生、快速恢复四个算法,就可以很好地调整窗口的大小。和滑动窗口一起限制了发送端最大的发送范围,从而保证了拥塞在网络上不会发生。

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

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

相关文章

Prometheus-v2.45.0 + 钉钉告警

目录 1. 创建一个钉钉内部群 2. 添加自定义机器人 3. 配置钉钉发送告警服务 4. 配置alertmanager,接入dingding 5. 配置告警消息发送模板 6. 修改配置规则文件 续接上篇:https://blog.csdn.net/Lzcsfg/article/details/140851688 相关软件包链接…

sqli-labs环境搭建以及部分sql注入

sqli-labs靶场搭建: 安装phpstudy并创建网页 初始化靶场 实现sql注入: less-1(根据数据库的命令在超链接后输入?id1) less-2(如法炮制?id2) less-3(闭合单引号和括号) less-9&am…

新增AI Copilot,DataEase开源数据可视化分析工具v2.9.0发布

2024年8月5日,人人可用的开源数据可视化分析工具DataEase正式发布v2.9.0版本。 这一版本的功能变动包括:导航栏新增Copilot入口,借助AI技术,通过自然语言交互实现即问即答,让数据分析更加直观和便捷;图表方…

vxe-pulldown下拉容器的使用,覆盖样式z-index来解决在Modal中初次显示在Modal的下层的问题。

1、官网的例子&#xff1a; 可以说这个vxe-pulldown用起来还是挺方便的。 2、代码&#xff1a; <template><div><vxe-pulldown ref"pulldownRef" popup-class-name"my-dropdown4" transfer><template #header><div class&quo…

FFMPEG 序列帧图片合成视频

需求&#xff1a; 将多张.png图片合成为视频 注意: 1需要Windows电脑 2将图片重命名 下载EXE 官网 https://ffmpeg.org/download.html#build-windows 解压后长这样 将图片和exe放在同一目录下 文件中找个空白地&#xff0c;Shift右键 进入PowerShell 输入命令: ./ffm…

Chapter 27 封装

欢迎大家订阅【Python从入门到精通】专栏&#xff0c;一起探索Python的无限可能&#xff01; 文章目录 前言一、基本概念二、私有成员 前言 在面向对象编程&#xff08;OOP&#xff09;中&#xff0c;封装是一个重要的概念&#xff0c;旨在保护数据并限制对对象内部状态的直接…

48天笔试训练错题——day41

目录 选择题 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 编程题 1. 五子棋 2. Emacs计算器 选择题 1. 当发生拥塞时&#xff0c;TCP 有个机制&#xff0c;会从慢开始进行发送。 1. 把慢开始的门限值设为当前窗口的一半&#xff0c;即 ssthresh 1/2 * 8 4 KB 2. 把拥塞窗…

【TS】declare 全局声明方式

declare关键字 declare是描述TS文件之外信息的一种机制&#xff0c;它的作用是告诉TS某个类型或变量已经存在&#xff0c;我们可以使用它声明全局变量、函数、类、接口、类型别名、类的属性或方法以及后面会介绍的模块与命名空间。 declare关键字用来告诉编译器&#xff0c;某…

【C语言】关于字符串函数的使用及模拟实现(2)

一、字符串的复制 1.1 库函数strcpy的使用 在函数strcpy中&#xff0c;函数的返回类型为char* &#xff0c;参数部分首先是指向目标地址的指针&#xff0c;其次是指向源地址的指针(由于源地址中内容不受影响&#xff0c;则可以使用const修饰)&#xff0c;函数所需的头文件为st…

51单片机—串口

一、 串口基本认知 串行接口简称串口&#xff0c;也称串行通信接口或串行通讯接口&#xff08;通常指COM接口&#xff09;&#xff0c;是采用串行通信方 式的扩展接口。串行接口&#xff08;Serial Interface&#xff09;是指数据一位一位地顺序传送。其特点是通信线路简 单&a…

#70结构体案例1(导师,学生,成绩)

效果&#xff1a; 代码&#xff1a; #include <iostream> #include <string> using namespace std;#include "random"int get_random_num(int min,int max) {random_device rd;mt19937 gen(rd());uniform_int_distribution<> dis(min,max);int ra…

C语言实现常见O(N^2)的排序算法

目录 1.排序的概念及常见排序算法 1.1排序的概念 1.2常见的排序算法 2.常见O(N^2)排序算法的实现 2.1插入排序 2.1.1基本思想 2.1.2直接插入排序 2.1.2.1直接插入排序的特性 2.1.2.2直接插入排序算法实现 2.2选择排序 2.2.1基本思想 2.2.2直接选择排…

【电脑基础硬件】磁盘阵列

磁盘阵列 一、磁盘阵列概述作用&#xff1a;RAID级别&#xff1a; 二、Raid0Raid1 一、磁盘阵列概述 磁盘阵列&#xff08;Disk Array&#xff09;是一种将多个独立的磁盘驱动器组合起来形成一个逻辑单元的技术&#xff0c;目的是为了提高存储系统的性能、可靠性和可用性。磁盘…

关于儿童编程语言

青少年通常会通过Scratch或Python开始学习编程。在这两种语言中&#xff0c;代码的编写&#xff08;或者在Scratch中是构建&#xff09;方式类似于英语&#xff0c;这使得初学者更容易学习。Scratch的一个重要卖点是对视觉和运动感知学习者非常友好。这些代码块按颜色编码&…

亚信安全获国家信息安全服务(风险评估和安全工程类)二级资质

近日&#xff0c;亚信安全荣获由中国信息安全测评中心颁发的《国家信息安全测评信息安全服务资质证书—风险评估二级》和《国家信息安全测评信息安全服务资质证书—安全工程类二级》资质。亚信安全凭借综合实力和优秀的技术能力&#xff0c;成为为数不多的获得国家信息安全服务…

PXE无人值守

PXE介绍 预启动执行环境&#xff08;Preboot eXecution Environment&#xff0c;PXE&#xff09;也被称为预执行环境&#xff0c;提供了一种使用网络接口&#xff08;Network Interface&#xff09;启动计算机的机制。这种机制让计算机的启动可以不依赖本地数据存储设备&#…

支持跨设备操作,自带横向霍尔感应滚轮,雷柏MT760系列鼠标上手

日常工作中&#xff0c;确实很需要一只专为商务办公设计的鼠标&#xff0c;这类鼠标通常会有更低调的外观&#xff0c;同时具备静音按键之类的设计&#xff0c;能够为工作带来更多的方便。我最近上手了雷柏MT760和MT760 Mini。这两款鼠标虽然尺寸不同&#xff0c;但共享了相同的…

12.upload-labs靶场通关详解(11~17)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 1、Pass11&#xff08;白名单&#xff1a;get00截断&#xff09;2、Pass12&#xff08;白名单&#xff1a;post00截断&#xff09;3、Pass13&#xff08;图片马绕过…

封装组件之使用vue3封装input框并显示联想数据功能

新建Input.vue 定义input框及相关事件 <inputtype"text"v-model"query"input"onInput"blur"hideSuggestions"focus"onInput"/> input输入事件、blur失去焦点、focus获取焦点 //当输入内容时才显示ul内容 const onI…

优化if-else的11种方案

优雅永不过时&#xff01; 1. 使用早返回&#xff08;Early Return&#xff09;&#xff1a;尽可能早地返回&#xff0c;避免嵌套的if-else。 优化前&#xff1a; public class NoEarlyReturnExample {public boolean hasPositiveNumber(int[] numbers) {boolean foundPositi…