网站开发好还是app好,wordpress 禁用修订,闭站保护期间网站能够打开吗,网站备案准备资料TCP 是一个可靠传输的协议#xff0c;那它是如何保证可靠的呢#xff1f;
为了实现可靠性传输#xff0c;需要考虑很多事情#xff0c;例如数据的破坏、丢包、重复以及分片顺序混乱等问题。如不能解决这些问题#xff0c;也就无从谈起可靠传输。
那么#xff0c;TCP 是…TCP 是一个可靠传输的协议那它是如何保证可靠的呢
为了实现可靠性传输需要考虑很多事情例如数据的破坏、丢包、重复以及分片顺序混乱等问题。如不能解决这些问题也就无从谈起可靠传输。
那么TCP 是通过序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输的。
今天将重点介绍 TCP 的重传机制、滑动窗口、流量控制、拥塞控制。 重传机制
TCP 实现可靠传输的方式之一是通过序列号与确认应答。
在 TCP 中当发送端的数据到达接收主机时接收端主机会返回一个确认应答消息表示已收到消息。 但在错综复杂的网络并不一定能如上图那么顺利能正常的数据传输万一数据在传输过程中丢失了呢
所以 TCP 针对数据包丢失的情况会用重传机制解决。
接下来说说常见的重传机制 超时重传 快速重传 SACK D-SACK
超时重传
重传机制的其中一个方式就是在发送数据时设定一个定时器当超过指定的时间后没有收到对方的 ACK 确认应答报文就会重发该数据也就是我们常说的超时重传。
TCP 会在以下两种情况发生超时重传 数据包丢失 确认应答丢失 超时时间应该设置为多少呢 我们先来了解一下什么是 RTTRound-Trip Time 往返时延从下图我们就可以知道 RTT 指的是数据发送时刻到接收到确认的时刻的差值也就是包的往返时间。
超时重传时间是以 RTO Retransmission Timeout 超时重传时间表示。
假设在重传的情况下超时时间 RTO 「较长或较短」时会发生什么事情呢 上图中有两种超时时间不同的情况 当超时时间 RTO 较大时重发就慢丢了老半天才重发没有效率性能差 当超时时间 RTO 较小时会导致可能并没有丢就重发于是重发的就快会增加网络拥塞导致更多的超时更多的超时导致更多的重发。
精确的测量超时时间 RTO 的值是非常重要的这可让我们的重传机制更高效。
根据上述的两种情况我们可以得知超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。 至此可能大家觉得超时重传时间 RTO 的值计算也不是很复杂嘛。
好像就是在发送端发包时记下 t0 然后接收端再把这个 ack 回来时再记一个 t1于是 RTT t1 – t0。没那么简单这只是一个采样不能代表普遍情况。
实际上「报文往返 RTT 的值」是经常变化的因为我们的网络也是时常变化的。也就因为「报文往返 RTT 的值」 是经常波动变化的所以「超时重传时间 RTO 的值」应该是一个动态变化的值。
我们来看看 Linux 是如何计算 RTO 的呢
估计往返时间通常需要采样以下两个 需要 TCP 通过采样 RTT 的时间然后进行加权平均算出一个平滑 RTT 的值而且这个值还是要不断变化的因为网络状况不断地变化。 除了采样 RTT还要采样 RTT 的波动范围这样就避免如果 RTT 有一个大的波动的话很难被发现的情况。
RFC6289 建议使用以下的公式计算 RTO 其中 SRTT 是计算平滑的RTT DevRTR 是计算平滑的RTT 与 最新 RTT 的差距。
在 Linux 下α 0.125β 0.25 μ 1∂ 4。别问怎么来的问就是大量实验中调出来的。
如果超时重发的数据再次超时的时候又需要重传的时候TCP 的策略是超时间隔加倍。
也就是每当遇到一次超时重传的时候都会将下一次超时时间间隔设为先前值的两倍。两次超时就说明网络环境差不宜频繁反复发送。
超时触发重传存在的问题是超时周期可能相对较长。那是不是可以有更快的方式呢
于是就可以用「快速重传」机制来解决超时重发的时间等待。
快速重传
TCP 还有另外一种快速重传Fast Retransmit机制它不以时间为驱动而是以数据驱动重传。
快速重传机制是如何工作的呢其实很简单一图胜千言。 在上图发送方发出了 12345 份数据 第一份 Seq1 先送到了于是就 Ack 回 2 结果 Seq2 因为某些原因没收到Seq3 到达了于是还是 Ack 回 2 后面的 Seq4 和 Seq5 都到了但还是 Ack 回 2因为 Seq2 还是没有收到 发送端收到了三个 Ack 2 的确认知道了 Seq2 还没有收到就会在定时器过期之前重传丢失的 Seq2。 最后收到了 Seq2此时因为 Seq3Seq4Seq5 都收到了于是 Ack 回 6 。
所以快速重传的工作方式是当收到三个相同的 ACK 报文时会在定时器过期之前重传丢失的报文段。
快速重传机制只解决了一个问题就是超时时间的问题但是它依然面临着另外一个问题。就是重传的时候是重传之前的一个还是重传所有的问题。
比如对于上面的例子是重传 Seq2 呢还是重传 Seq2、Seq3、Seq4、Seq5 呢因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。
根据 TCP 不同的实现以上两种情况都是有可能的。可见这是一把双刃剑。
为了解决不知道该重传哪些 TCP 报文于是就有 SACK 方法。
SACK 方法
还有一种实现重传机制的方式叫SACK Selective Acknowledgment 选择性确认。
这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西它可以将缓存的地图发送给发送方这样发送方就可以知道哪些数据收到了哪些数据没收到知道了这些信息就可以只重传丢失的数据。
如下图发送方收到了三次同样的 ACK 确认报文于是就会触发快速重发机制通过 SACK 信息发现只有 200~299 这段数据丢失则重发时就只选择了这个 TCP 段进行重复。 如果要支持 SACK必须双方都要支持。在 Linux 下可以通过 net.ipv4.tcp_sack 参数打开这个功能Linux 2.4 后默认打开。
Duplicate SACK
Duplicate SACK 又称 D-SACK其主要使用了 SACK 来告诉「发送方」有哪些数据被重复接收了。
下面举例两个栗子来说明 D-SACK 的作用。
栗子一号ACK 丢包 「接收方」发给「发送方」的两个 ACK 确认应答都丢失了所以发送方超时后重传第一个数据包3000 ~ 3499 于是「接收方」发现数据是重复收到的于是回了一个 SACK 3000~3500告诉「发送方」 3000~3500 的数据早已被接收了因为 ACK 都到了 4000 了已经意味着 4000 之前的所有数据都已收到所以这个 SACK 就代表着 D-SACK。 这样「发送方」就知道了数据没有丢是「接收方」的 ACK 确认报文丢了。
栗子二号网络延时 数据包1000~1499 被网络延迟了导致「发送方」没有收到 Ack 1500 的确认报文。 而后面报文到达的三个相同的 ACK 确认报文就触发了快速重传机制但是在重传后被延迟的数据包1000~1499又到了「接收方」 所以「接收方」回了一个 SACK1000~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也就是窗口大小。
这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据而不会导致接收端处理不过来。
所以通常窗口的大小是由接收方的窗口大小来决定的。
发送方发送的数据大小不能超过接收方的窗口大小否则接收方就无法正常接收到数据。 发送方的滑动窗口 我们先来看看发送方的窗口下图就是发送方缓存的数据根据处理的情况分成四个部分其中深蓝色方框是发送窗口紫色方框是可用窗口 #1 是已发送并收到 ACK确认的数据1~31 字节 #2 是已发送但未收到 ACK确认的数据32~45 字节 #3 是未发送但总大小在接收方处理范围内接收方还有空间46~51字节 #4 是未发送但总大小超过接收方处理范围接收方没有空间52字节以后
在下图当发送方把数据「全部」都一下发送出去后可用窗口的大小就为 0 了表明可用窗口耗尽在没收到 ACK 确认之前是无法继续发送数据了。 在下图当收到之前发送的数据 32~36 字节的 ACK 确认应答后如果发送窗口的大小没有变化则滑动窗口往右边移动 5 个字节因为有 5 个字节的数据被应答确认接下来 52~56 字节又变成了可用窗口那么后续也就可以发送 52~56 这 5 个字节的数据了。 程序是如何表示发送方的四个部分的呢 TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针指特定的序列号一个是相对指针需要做偏移。 SND.WND表示发送窗口的大小大小是由接收方指定的 SND.UNA是一个绝对指针它指向的是已发送但未收到确认的第一个字节的序列号也就是 #2 的第一个字节。 SND.NXT也是一个绝对指针它指向未发送但可发送范围的第一个字节的序列号也就是 #3 的第一个字节。 指向 #4 的第一个字节是个相对指针它需要 SND.UNA 指针加上 SND.WND 大小的偏移量就可以指向 #4 的第一个字节了。
那么可用窗口大小的计算就可以是
可用窗口大 SND.WND -SND.NXT - SND.UNA 接收方的滑动窗口 接下来我们看看接收方的窗口接收窗口相对简单一些根据处理的情况划分成三个部分 #1 #2 是已成功接收并确认的数据等待应用进程读取 #3 是未收到数据但可以接收的数据 #4 未收到数据并不可以接收的数据 其中三个接收部分使用两个指针进行划分: RCV.WND表示接收窗口的大小它会通告给发送方。 RCV.NXT是一个指针它指向期望从发送方发送来的下一个数据字节的序列号也就是 #3 的第一个字节。 指向 #4 的第一个字节是个相对指针它需要 RCV.NXT 指针加上 RCV.WND 大小的偏移量就可以指向 #4 的第一个字节了。 接收窗口和发送窗口的大小是相等的吗 并不是完全相等接收窗口的大小是约等于发送窗口的大小的。
因为滑动窗口并不是一成不变的。比如当接收方的应用进程读取数据的速度非常快的话这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的所以接收窗口和发送窗口是约等于的关系。 流量控制
发送方不能无脑的发数据给接收方要考虑接收方处理能力。
如果一直无脑的发数据给对方但对方处理不过来那么就会导致触发重发机制从而导致网络流量的无端的浪费。
为了解决这种现象发生TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量这就是所谓的流量控制。
下面举个栗子为了简单起见假设以下场景 客户端是接收方服务端是发送方 假设接收窗口和发送窗口相同都为 200 假设两个设备在整个传输过程中都保持相同的窗口大小不受外界影响 根据上图的流量控制说明下每个过程 客户端向服务端发送请求数据报文。这里要说明下本次例子是把服务端作为发送方所以没有画出服务端的接收窗口。 服务端收到请求报文后发送确认报文和 80 字节的数据于是可用窗口 Usable 减少为 120 字节同时 SND.NXT 指针也向右偏移 80 字节后指向 321这意味着下次发送数据的时候序列号是 321。 客户端收到 80 字节数据后于是接收窗口往右移动 80 字节RCV.NXT 也就指向 321这意味着客户端期望的下一个报文的序列号是 321接着发送确认报文给服务端。 服务端再次发送了 120 字节数据于是可用窗口耗尽为 0服务端无法再继续发送数据。 客户端收到 120 字节的数据后于是接收窗口往右移动 120 字节RCV.NXT 也就指向 441接着发送确认报文给服务端。 服务端收到对 80 字节数据的确认报文后SND.UNA 指针往右偏移后指向 321于是可用窗口 Usable 增大到 80。 服务端收到对 120 字节数据的确认报文后SND.UNA 指针往右偏移后指向 441于是可用窗口 Usable 增大到 200。 服务端可以继续发送了于是发送了 160 字节的数据后SND.NXT 指向 601于是可用窗口 Usable 减少到 40。 客户端收到 160 字节后接收窗口往右移动了 160 字节RCV.NXT 也就是指向了 601接着发送确认报文给服务端。 服务端收到对 160 字节数据的确认报文后发送窗口往右移动了 160 字节于是 SND.UNA 指针偏移了 160 后指向 601可用窗口 Usable 也就增大至了 200。
操作系统缓冲区与滑动窗口的关系
前面的流量控制例子我们假定了发送窗口和接收窗口是不变的但是实际上发送窗口和接收窗口中所存放的字节数都是放在操作系统内存缓冲区中的而操作系统的缓冲区会被操作系统调整。
当应用进程没办法及时读取缓冲区的内容时也会对我们的缓冲区造成影响。 那操心系统的缓冲区是如何影响发送窗口和接收窗口的呢 我们先来看看第一个例子。
当应用程序没有及时读取缓存时发送窗口和接收窗口的变化。
考虑以下场景 客户端作为发送方服务端作为接收方发送窗口和接收窗口初始大小为 360 服务端非常的繁忙当收到客户端的数据时应用层不能及时读取数据。 根据上图的流量控制说明下每个过程 客户端发送 140 字节数据后可用窗口变为 220 360 - 140。 服务端收到 140 字节数据但是服务端非常繁忙应用进程只读取了 40 个字节还有 100 字节占用着缓冲区于是接收窗口收缩到了 260 360 - 100最后发送确认信息时将窗口大小通告给客户端。 客户端收到确认和窗口通告报文后发送窗口减少为 260。 客户端发送 180 字节数据此时可用窗口减少到 80。 服务端收到 180 字节数据但是应用程序没有读取任何数据这 180 字节直接就留在了缓冲区于是接收窗口收缩到了 80 260 - 180并在发送确认信息时通过窗口大小给客户端。 客户端收到确认和窗口通告报文后发送窗口减少为 80。 客户端发送 80 字节数据后可用窗口耗尽。 服务端收到 80 字节数据但是应用程序依然没有读取任何数据这 80 字节留在了缓冲区于是接收窗口收缩到了 0并在发送确认信息时通过窗口大小给客户端。 客户端收到确认和窗口通告报文后发送窗口减少为 0。
可见最后窗口都收缩为 0 了也就是发生了窗口关闭。当发送方可用窗口变为 0 时发送方实际上会定时发送窗口探测报文以便知道接收方的窗口是否发生了改变这个内容后面会说这里先简单提一下。
我们先来看看第二个例子。
当服务端系统资源非常紧张的时候操心系统可能会直接减少了接收缓冲区大小这时应用程序又无法及时读取缓存数据那么这时候就有严重的事情发生了会出现数据包丢失的现象。 说明下每个过程 客户端发送 140 字节的数据于是可用窗口减少到了 220。 服务端因为现在非常的繁忙操作系统于是就把接收缓存减少了 120 字节当收到 140 字节数据后又因为应用程序没有读取任何数据所以 140 字节留在了缓冲区中于是接收窗口大小从 360 收缩成了 100最后发送确认信息时通告窗口大小给对方。 此时客户端因为还没有收到服务端的通告窗口报文所以不知道此时接收窗口收缩成了 100客户端只会看自己的可用窗口还有 220所以客户端就发送了 180 字节数据于是可用窗口减少到 40。 服务端收到了 180 字节数据时发现数据大小超过了接收窗口的大小于是就把数据包丢失了。 客户端收到第 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 算法默认是打开的如果对于一些需要小数据包交互的场景的程序比如telnet 或 ssh 这样的交互性比较强的程序则需要关闭 Nagle 算法。
可以在 Socket 设置 TCP_NODELAY 选项来关闭这个算法关闭 Nagle 算法没有全局参数需要根据每个应用自己的特点来关闭
setsockopt(sock_fd, IPPROTO_TCP, TCP_NODELAY, (char *)value, sizeof(int));拥塞控制 为什么要有拥塞控制呀不是有流量控制了吗 前面的流量控制是避免「发送方」的数据填满「接收方」的缓存但是并不知道网络的中发生了什么。
一般来说计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。
在网络出现拥堵时如果继续发送大量数据包可能会导致数据包时延、丢失等这时 TCP 就会重传数据但是一重传就会导致网络的负担更重于是会导致更大的延迟以及更多的丢包这个情况就会进入恶性循环被不断地放大....
所以TCP 不能忽略网络上发生的事它被设计成一个无私的协议当网络发送拥塞时TCP 会自我牺牲降低发送的数据量。
于是就有了拥塞控制控制的目的就是避免「发送方」的数据填满整个网络。
为了在「发送方」调节所要发送数据的量定义了一个叫做「拥塞窗口」的概念。 什么是拥塞窗口和发送窗口有什么关系呢 拥塞窗口 cwnd是发送方维护的一个的状态变量它会根据网络的拥塞程度动态变化的。
我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系那么由于加入了拥塞窗口的概念后此时发送窗口的值是swnd min(cwnd, rwnd)也就是拥塞窗口和接收窗口中的最小值。
拥塞窗口 cwnd 变化的规则 只要网络中没有出现拥塞cwnd 就会增大 但网络中出现了拥塞cwnd 就减少 那么怎么知道当前网络是否出现了拥塞呢 其实只要「发送方」没有在规定时间内接收到 ACK 应答报文也就是发生了超时重传就会认为网络出现了用拥塞。 拥塞控制有哪些控制算法 拥塞控制主要是四个算法 慢启动 拥塞避免 拥塞发生 快速恢复
慢启动
TCP 在刚建立连接完成后首先是有个慢启动的过程这个慢启动的意思就是一点一点的提高发送数据包的数量如果一上来就发大量的数据这不是给网络添堵吗
慢启动的算法记住一个规则就行当发送方每收到一个 ACK拥塞窗口 cwnd 的大小就会加 1。
这里假定拥塞窗口 cwnd 和发送窗口 swnd 相等下面举个栗子 连接建立完成后一开始初始化 cwnd 1表示可以传一个 MSS 大小的数据。 当收到一个 ACK 确认应答后cwnd 增加 1于是一次能够发送 2 个 当收到 2 个的 ACK 确认应答后 cwnd 增加 2于是就可以比之前多发2 个所以这一次能够发送 4 个 当这 4 个的 ACK 确认到来的时候每个确认 cwnd 增加 1 4 个确认 cwnd 增加 4于是就可以比之前多发 4 个所以这一次能够发送 8 个。 可以看出慢启动算法发包的个数是指数性的增长。 那慢启动涨到什么时候是个头呢 有一个叫慢启动门限 ssthresh slow start threshold状态变量。 当 cwnd ssthresh 时使用慢启动算法。 当 cwnd ssthresh 时就会使用「拥塞避免算法」。
拥塞避免算法
前面说道当拥塞窗口 cwnd 「超过」慢启动门限 ssthresh 就会进入拥塞避免算法。
一般来说 ssthresh 的大小是 65535 字节。
那么进入拥塞避免算法后它的规则是每当收到一个 ACK 时cwnd 增加 1/cwnd。
接上前面的慢启动的栗子现假定 ssthresh 为 8 当 8 个 ACK 应答确认到来时每个确认增加 1/88 个 ACK 确认 cwnd 一共增加 1于是这一次能够发送 9 个 MSS 大小的数据变成了线性增长。 所以我们可以发现拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长还是增长阶段但是增长速度缓慢了一些。
就这么一直增长着后网络就会慢慢进入了拥塞的状况了于是就会出现丢包现象这时就需要对丢失的数据包进行重传。
当触发了重传机制也就进入了「拥塞发生算法」。
拥塞发生
当网络出现拥塞也就是会发生数据包重传重传机制主要有两种 超时重传 快速重传
这两种使用的拥塞发送算法是不同的接下来分别来说说。 发生超时重传的拥塞发生算法 当发生了「超时重传」则就会使用拥塞发生算法。
这个时候ssthresh 和 cwnd 的值会发生变化 ssthresh 设为 cwnd/2 cwnd 重置为 1 接着就重新开始慢启动慢启动是会突然减少数据流的。这真是一旦「超时重传」马上回到解放前。但是这种方式太激进了反应也很强烈会造成网络卡顿。
就好像本来在秋名山高速漂移着突然来个紧急刹车轮胎受得了吗。。。 发生快速重传的拥塞发生算法 还有更好的方式前面我们讲过「快速重传算法」。当接收方发现丢了一个中间包的时候发送三次前一个包的 ACK于是发送端就会快速地重传不必等待超时再重传。
TCP 认为这种情况不严重因为大部分没丢只丢了一小部分则 ssthresh 和 cwnd 变化如下 cwnd cwnd/2 也就是设置为原来的一半; ssthresh cwnd; 进入快速恢复算法
快速恢复
快速重传和快速恢复算法一般同时使用快速恢复算法是认为你还能收到 3 个重复 ACK 说明网络也不那么糟糕所以没有必要像 RTO 超时那么强烈。
正如前面所说进入快速恢复之前cwnd 和 ssthresh 已被更新了 cwnd cwnd/2 也就是设置为原来的一半; ssthresh cwnd;
然后进入快速恢复算法如下 拥塞窗口 cwnd ssthresh 3 3 的意思是确认有 3 个数据包被收到了 重传丢失的数据包 如果再收到重复的 ACK那么 cwnd 增加 1 如果收到新数据的 ACK 后把 cwnd 设置为第一步中的 ssthresh 的值原因是该 ACK 确认了新的数据说明从 duplicated ACK 时的数据都已收到该恢复过程已经结束可以回到恢复之前的状态了也即再次进入拥塞避免状态 也就是没有像「超时重传」一夜回到解放前而是还在比较高的值后续呈线性增长。
拥塞算法示意图
好了以上就是拥塞控制的全部内容了看完后你再来看下面这张图片每个过程我相信你都能明白