TCP可靠性
-
连接(三次握手、四次挥手)
-
校验和
-
超时重传
-
流量控制
-
拥塞控制
TCP协议如何保证可靠传输?
-
应用数据被分割成 TCP 认为最适合发送的数据块。
-
TCP 给发送的每一个包进行编号,接收方对数据包进行排序,把有序数据传送给应用层。
-
校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。
-
TCP 的接收端会丢弃重复的数据。
-
流量控制: TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议。 (TCP 利用滑动窗口实现流量控制)
-
拥塞控制: 当网络拥塞时,减少数据的发送。
-
ARQ 协议: 也是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认。在收到确认后再发下一个分组。
-
超时重传: 当 TCP 发出一个段后,它启动一个定时器,等待目的端确认收到这个报文段。如果不能及时收到一个确认,将重发这个报文段。
ARQ协议(自动重传请求)
自动重传请求(Automatic Repeat-reQuest,ARQ)是 OSI 模型中数据链路层和传输层的错误纠正协议之一。它通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在发送后一段时间之内没有收到确认帧,它通常会重新发送。ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。
停止等待ARQ协议
停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复 ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组。
在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认。
优缺点:
-
优点: 简单
-
缺点: 信道利用率低,等待时间长
连续ARQ协议
连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累积确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。
优缺点:
-
优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。
-
缺点: 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5 条 消息,中间第三条丢失(3 号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。
滑动窗口和流量控制
TCP利用滑动窗口实现流量控制,流量控制是为了控制发送方发送速率,保证接收方来得及接收。接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为0,则发送方不能发送数据。
拥塞控制
在某一段时间里面,有了大量的请求到达,而提供的资源不足,会导致程序变慢,网络的性能变坏,这种情况叫做拥塞。拥塞控制就是避免这种情况的发生,防止过多的数据请求到达网络中,导致路由器或链路承受不住。
TCP重传、滑动窗口、流量控制、拥塞控制
面向连接的,字节流的,可靠的
TCP是一个可靠传输的协议,但是它是如何保证他的可靠的。因为数据在网络上进行传输的过程中,会出现数据的破坏、丢包、重复等问题,所以我们通过重传机制、拥塞控制、流量控制等来保证TCP的可靠数据传输。
重传机制
TCP实现可靠传输的方式一致,是通过序列号与确认应答俩实现的。在TCP中,当发送端的数据到达接收主机时,发送端会返回一个确认应答报文,表示已收到消息。
TCP 针对数据包丢失的情况,会用重传机制解决。
-
超时重传
-
快速重传
-
SACK
-
D-SACK
超时重传
定义:发送端发送报文后,会启动一个计时器,等待接收端的ACK确认应答报文,如果没有再规定时间接收到ACK,会重发该数据。
会出现以下两种情况:
-
数据包丢失(发送端在发送报文时,发生丢包)
-
确认应答丢失(接收端接收到报文后,响应报文ACK丢失)
但是出现了一个问题,超时时间应该设置为多少?
RTT(Round-Tirp Time 往返时延):数据从一段传送到另一端所需的时间,即包的往返时间。
RTO(超时重传时间)
假设在重传的情况下,超时时间RTO较长或较短时,会发生什么事情?
RTO时间太长的话,会导致传输的效率降低,而RTO较短的话,没有丢包,会导致重复发送报文,增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。
据此,我们可以设置超时重传时间RTO应该略大于RTT。
实际上「报文往返 RTT 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返RTT 的值」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。
如果超时重发的数据,再次超时的时候,有需要重传的时候,TCP的策略是超时间隔加倍。
每当遇到一次超时重传的时候,都会将下一次的超时时间间隔设为先前值的两倍,两次超时,说明网络环境差,不宜频繁发送。但是超时重传存在的问题是,超时周期可能相对来说相对来说是很长的,这是我们需要更快的方式来解决。
快速重传
工作机制:当发送端接收到三个相同的应答报文ACK时候,定时器过期之前,触发快速重传机制,重新发送丢失的报文段。
-
第一份 Seq1 先送到了,于是就 Ack 回 2;
-
结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2;
-
后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
-
发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。
-
最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。
快速重传机制只解决了一个问题,就是超时时间的问题,但是,出现一个问题,在快速重传的过程中,我们不知道应该重传的是哪些数据,是重传丢失之前的一个,还是重传所有的报文。(比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清 楚这连续的三个 Ack 2 是谁传回来的。)
这时我们为了解决这个问题,我们提出了SACK
SACK
这是方式是在TCP的头部加入了SACK,用来将缓存的地图发送给发送端。这样发送端就知道哪些数据收到了,哪些数据没有接收到,然后只重传丢失的数据。这样有效地避免了重传所有报文的情况。
例如:下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
D-SACK
D-SACK:主要使用了SACK来告诉发送端哪些数据被重复接收了。
例如:
ACK丢包
-
「接收方」发给「发送方」的两个 ACK 确认应答都丢失了,所以发送方超时后,重传第一个数据包(3000 ~ 3499)
-
于是「接收方」发现数据是重复收到的,于是回了一个 SACK = 3000~3500,告诉「发送方」3000~3500 的数据早已被接收了,因为 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 包丢了;
-
可以知道是不是「发送方」的数据包被网络延迟了;
-
可以知道网络中是不是把「发送方」的数据包给复制了;
滑动窗口
TCP每发送一个数据,都要进行一次确认应答。当上一次的确认应答报文收到后,在发送下一个。这个模式有点像面对面聊天,一人一句,这样我们这样聊天是没有问题的,但是放在网络上就会出现问题,如果包的往返时间越长,网络的吞吐量会越低。网络的传输性能会非常低。所以我们使用滑动窗口在解决这一个问题,使用滑动窗口来作为缓冲区,来缓存发送端发送的数据,接收端收到确认应答的数据后,再从滑动窗口中移出发送的数据,以此来提高网络的传输效率。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为0,则发送方不能发送数据。
引入窗口的原因
TCP每发送一个数据,都要进行一次确认应答。当上一次的确认应答报文收到后,在发送下一个。
这个模式有点像面对面聊天,一人一句,但是这样的效率太低了,在网络上如果这样做的话,网络的传输性能会非常低。为每个数据包确认应答的缺点:包的往返时间越长,网络的吞吐量会越低。
缺点:数据包的往返时间越长,通信的效率就越低。
为了解决这个问题,TCP引入了窗口这个概念,即在往返时间较长的情况下,它也不会降低网络通信的效率。
窗口大小:无需等待确认应答,就可以继续发送数据的最大值。窗口的实现实际上是操作系统分配了一个缓冲区,在发送端收到确认应答报文之前,在缓冲区中保留已发送的数据,收到确认应答的报文,就从缓冲区中移出。
假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK丢失,可以通过「下一个确认应答进行确认」。如下图:
图中ACK 600丢失,但是下一个可以通过确认应答进行确认,只要发送方收到ACK = 700,就意味着700之前的数据接收方都收到了,就是600的确认应答报文丢失了。这种默认就是累计应答(或累计确认)
但是其中我们发现了一个问题,那就是窗口的大小应该由哪一方决定?
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报文在网络中丢失的话,会发生死锁,接收方和发送方都进入了等待的状态。
TCP是如何解决窗口关闭后,造成的死锁问题呢?
为了解决这个问题,TCP为每个连接都设定了一个持续计时器,只要TCP连接一方收到了对方窗口关闭的通知,就会启动计数器。如果持续计时器超时,就会发送窗口探测报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。如果接收窗口为0,那么收到这个报文的一方就会重新启动持续计时器;否则,就重新启动计时器。
- 如果接收窗口仍然为 0,那么收到这个报文的一方就会重新启动持续计时器;
- 如果接收窗口不是 0,那么死锁的局面就可以被打破了。
窗口探测的次数一般为 3 次,每次大约 30-60 秒(不同的实现可能会不一样)。如果 3 次过后接收窗口还是 0 的话,有的 TCP 实现就会发 RST 报文来中断连接。
糊涂窗口综合症
如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。到最后,如果接收方腾出几个字节并告诉发送方有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。
要知道,我们的TCP + IP头有40个字节,为了传输那几个字节的数据,要使用这么大的开销,这是不太经济的。
现举个糊涂窗口综合症的栗子,考虑以下场景:
接收方的窗口大小是 360 字节,但接收方由于某些原因陷入困境,假设接收方的应用层读取的能力如下:
- 接收方每接收 3 个字节,应用程序就只能从缓冲区中读取 1 个字节的数据;
- 在下一个发送方的 TCP 段到达之前,应用程序还从缓冲区中读取了 40 个额外的字节;
每个窗口的大小的变化,在图中都描述的很清楚了,可以发现窗口不断减少了,并且发送的数据都是比较小的了。所以,糊涂窗口综合症的现象是可以发生在发送方和接收方:
-
接收方可以通告一个小的窗口
-
而发送方可以发送小数据
于是,要解决糊涂窗口综合症,就解决上面两个问题就可以了
-
让接收方不通告小窗口给发送方
-
让发送方避免发送小数据
怎么让接收方不通告小窗口呢?
当窗口大小小于min(MSS , 缓存空间/2)
,也就是小于MSS与1/2缓存大小中的最小值时,就会向发送方通告窗口为0,也就是阻止了发送方再发数据过来。等到接收方处理了一些数据后,窗口大小>= MSS ,或者接收方缓存空间有一半可以使用,就可以把窗口打开让发送方发送数据过来。
怎么让发送方避免发送小数据呢?
发送方通常的策略: 使用 Nagle 算法,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据:
-
要等到窗口大小 >= MSS 或是 数据大小 >= MSS
-
收到之前发送数据的 ack 回包
只要没满足上面条件中的一条,发送方一致在囤积数据,知道满足上面的发送条件。
另外,Nagle
算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet或ssh这样的交互性比较强的程序,则需要关闭Nagle
算法。
可以在Socket设置TCP_NODELAY
选择赖关闭这个算法。
拥塞控制
为什么要有拥塞控制,不是有流量控制了吗?
前面的流量控制,是避免发送端发送数据填满接收方的缓存,但是不知道网络中发生了什么。
一般来说,计算机网络处于共享的环境,因此可能会因为其他主机之间的通信使得网络拥堵。
在网络出现拥堵时,如果继续发送大量的数据包,可能会导致数据包实验、丢失等,这时TCP就会重传数据,但是一重传就会导致网络的负担更重,于是导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地方法。
所以,TCP不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP会自我牺牲,降低发送的数据量。
拥塞控制:目的就是避免发送方的数据填满整个网络。
为了在发送方调节所要发送数据的量,定义一个叫做拥塞窗口的概念。
什么是拥塞窗口?和发送窗口有什么关系?
拥塞窗口 cwnd
是发送方维护的一个的状态变量,他会根据网络的拥塞程度动态变化的。
我们在前面提到过发送窗口 swnd 和接收窗口 rwnd 是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd)
,也就是拥塞窗口和接收窗口中的最小值。
拥塞窗口cwnd
变化的规则:
-
只要网络中没有出现拥塞,
cwnd
就会增大 -
但网络中出现了拥塞,
cwnd
就减少
那么怎么知道当前网络是否出现了拥塞呢?
只要发送方没有再规定的时间段内接收到ACK应答报文,就发生了超时重传,就会认为出现了拥塞
拥塞控制有哪些控制算法?
-
慢启动
-
拥塞避免
-
拥塞发生
-
快速恢复
慢启动
慢启动的算法记住一个规则就行:当发送方每收到一个 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/8,8 个 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 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
也就是没有像「超时重传」一夜回到解放前,而是还在比较高的值,后续呈线性增长。
拥塞算法示意图