对方的16位窗口大小和拥塞窗口的值中的最小值
min(对方的16位窗口大小,拥塞窗口的值)
在这里为了方便我们理解滑动窗口我们可以先认为这个滑动窗口的大小等于对方16位窗口大小。
例如我们现在连续发送 1001-2000、2001-3000、3001-4000、4001-5000这四个段的时候,不需要等待任何ACK,可以直接进行发送。
当收到对方响应的确认序号为2001时,说明1001-2000这个数据段已经被对方收到了,此时该数据段应该被纳入发送缓冲区当中的已发送且已经ACK中,我们假设对方的窗口大小一直是4000,因此滑动窗口现在可以向右移动,继续发送5001-6000的数据段,以此类推就可以提高数据的发送效率。
滑动窗口的几个注意点:
- 当发送方发送出去的数据段陆陆续续收到对应的ACK时,就可以将收到ACK的数据段归置到滑动窗口的左侧,并根据当前滑动窗口的大小来决定,是否需要将滑动窗口右侧的数据归置到滑动窗口当中。
- TCP的重传机制要求暂时保存发出但未收到确认的数据,而这部分数据实际就位于滑动窗口当中,只有滑动窗口左侧的数据才是可以被覆盖或删除的,因为这部分数据才是发送并被对方可靠的收到了,所以滑动窗口除了限定可以不收到ACK直接发送的数据之外,滑动窗口也可以支持TCP的重传机制
2、滑动窗口的一些常见问题以及回答
问:1.滑动窗口只能向右滑动吗? 向左可以吗?
答案是:只能向右滑动!滑动窗口的左侧表示已经发送且受到ACK的数据,这部分数据以后是要被覆盖的,滑动窗口向左滑动没有意义!
问:2.滑动窗口能变,能变小吗? 能变0吗? 变0之后,表示什么意思?
答案是:能!滑动窗口的大小是:
min(对方的16位窗口大小,拥塞窗口的值)
为了方便我们理解滑动窗口我们可以先认为这个滑动窗口的大小等于对方16位窗口大小,所以当对方的窗口大小变大了,我们的滑动窗口也会变大,所以当对方的窗口大小变小了,我们的滑动窗口也会变小,当滑动窗口为0时代表对方已经没有接收能力了!滑动窗口也就不能发送数据了。
问:3.能一直滑动吗? 越界怎么办?
答案是:能!TCP的设计者很聪明,实际的发送缓冲区是一个环形缓冲区,所以能够一直向右滑动。
问:4.滑动窗口的大小更新的依据是什么?
答案是:依据响应报头(ACK)中的16位窗口大小和确认序列号
由于TCP是保证可靠性的,所以应答也能按序到达,所以我们就可以对按序到达的响应报文提取出「确认序列号」和「16位窗口大小」。然后按照下面的公式进行更新窗口。
w i n s t a r t = 确认序列号 ; w i n e n d = w i n s t a r t + 16 位窗口大小 ; winstart = 确认序列号; winend = winstart + 16位窗口大小;winstart=确认序列号;winend=winstart+16位窗口大小;
问:5.滑动窗口内部的报文既然可以直接发送多个报文,如果第一个丢失了呢? 中间的丢失了呢? 最后一个丢失呢?
这里分应该两种情况讨论:
情况一: 数据包已经抵达, ACK被丢了
这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认。
比如图中1 ~ 1000,2001 ~ 3000,和30001 ~ 4000数据包对应的ACK丢失了,但只要发送端收到了最后5001 ~ 6000数据包的响应,此时发送端也就知道1 ~ 1000,2001 ~ 3000,和30001 ~ 4000的数据包实际上被接收端收到了,因为如果接收方没有收到1 ~ 1000,2001 ~ 3000,和30001 ~ 4000的数据包是不能够设置确认序号为6001的,确认序号为6001的含义就是序号为1-6000的字节数据我都收到了,你下一次应该从序号为6001的字节数据开始发送。
- 第一个ACK丢失,可能能够通过后续报文的ACK来消除影响,实在不行就「超时重传」。
- 中间ACK的丢失了,说明前面的ACK没有丢失,于是滑动窗口向右进行滑动就转化为第一个ACK丢失的情况。
- 最后一个ACK丢失,说明前面的ACK没有丢失,于是滑动窗口向右进行滑动就转化为第一个ACK丢失的情况。
情况二: 数据包真的丢了
当1001-2000的数据包丢失后,发送端会一直收到确认序号为1001的响应报文,就好像提醒发送端“下一次应该从序号为1001的字节数据开始发送”。
如果发送端连续收到三次确认序号相同(本例这里是1001)的响应报文,此时就会将对应的(本例这里是1001-2000)的数据包重新进行发送,这种机制被称为 高速重发控制(也叫 快重传).
此时当接收端收到1001-2000的数据包后,就会直接发送确认序号为7001的响应报文,因为2001-7000的数据接收端其实在之前就已经收到了。
- 第一个ACK丢失,通过「快重传」进行补发,实在不行就「超时重传」。
- 中间ACK的丢失了,说明前面的ACK没有丢失,于是滑动窗口向右进行滑动就转化为第一个ACK丢失的情况。
- 最后一个ACK丢失,说明前面的ACK没有丢失,于是滑动窗口向右进行滑动就转化为第一个ACK丢失的情况。
3、快重传与超时重传
- 快重传是当发送端连续收到三次相同的应答时就会触发快重传,而不像超时重传一样需要通过设置重传定时器,在固定的时间后才会进行重传,所以快重传的效率比较高。
- 超时重传以时间为驱动,而快速重传是以数据为驱动,虽然快重传能够快速判定数据包丢失,但快重传并不能完全取待超时重传,因为有时数据包丢失后可能并没有收到对方三次重复的应答,此时快重传机制就触发不了,而只能进行超时重传。
- 因此快重传虽然是一个效率上的提升,但超时重传却是所有重传机制的保底策略,也是必不可少的。
九、拥塞控制
1、拥塞控制的简单介绍
Ⅰ、为什么要有拥塞控制?
对于TCP通信的双方,有了「流量控制」来保证通信数据的传输速度,有了「滑动窗口」来保证数据传输的效率,但是这些机制考虑的都是通信的双方,而没有考虑通信时的网络。
例如两个主机在进行TCP通信的过程中,出现个别数据包丢包的情况是很正常的,此时可以通过快重传或超时重发对数据包进行补发,但如果双方在通信时出现了大量丢包,此时就不能认为是正常现象了,大概率是网络出现了问题。
- 例如大面积断电,导致各种网络设备瘫痪了(硬件的问题)。
- 或者是路由器转发压力过大宕机了(软件和硬件结合的问题)。
- 或者是网络中出现了网络拥塞,导致报文超时了(软件的问题)等。
所以需要有其他的机制来保证网络的可用性,于是就有了「拥塞控制」,其核心就是提供一个拥塞窗口来尽可能保证不会出现网络拥塞。
TCP不是万能的,对于一些硬件导致出现网络问题TCP无法解决,对于软件上面的一些问题,TCP能够提供一些解决方案。
Ⅱ、什么是拥塞窗口?
拥塞窗口是可能引起网络拥塞的阈值,如果一次发送的数据超过了拥塞窗口的大小就可能会引起网络拥塞
在编码中,拥塞窗口是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
我们在前面提到过滑动窗口的大小是:
min(对方的16位窗口大小,拥塞窗口的值)
这样设计的目的就是:保证通信数据在通过「网络」到达对方的「接收缓冲区」时都不会发生问题。
Ⅲ、如何解决网络拥塞?
网络出现大面积瘫痪时,通信双方作为网络当中两台小小的主机,看似并不能为此做些什么,但“雪崩的时候没有一片雪花是无辜的”,网络出现问题一定是网络中大部分主机共同作用的结果。
例如网络中的主机在同一时间节点都大量向网络当中塞数据,此时位于网络中某些关键节点的路由器下就可能排了很长的报文,最终导致报文无法在超时时间内到达对端主机,此时也就导致了丢包问题。
- 当网络出现拥塞问题时,通信双方虽然不能提出特别有效的解决方案,但双方主机可以做到不加重网络的负担,例如通信双方可以少发数据甚至不发数据,等待网络状况恢复后双方再慢慢恢复数据的传输速率。
需要注意的是,网络拥塞时影响的不只是一台主机,而几乎是该网络当中的所有主机,此时所有使用TCP传输控制协议的主机都会执行拥塞避免算法。
因此拥塞控制看似只是谈论的一台主机上的通信策略,实际这个策略是所有主机在网络崩溃后都会遵守的策略。一旦出现网络拥塞,该网络当中的所有主机都会受到影响,此时所有主机都要执行拥塞避免,这样才能有效缓解网络拥塞问题。通过这样的方式就能保证雪崩不会发生,或雪崩发生后可以尽快恢复。
2、拥塞控制的策略
拥塞控制的策略主要是四个算法:
- 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复
Ⅰ、慢启动
TCP引入了慢启动机制,在出现网络拥塞时或者刚开始通信时先发少量的数据探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
慢启动的算法规则:当发送方每收到一个 ACK,拥塞窗口的大小就会加 1。
这里我们先假定拥塞窗口 cwnd
和发送窗口(滑动窗口)相等,下面举个例子:
- 连接建立完成后,一开始初始化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)状态变量,一般来说TCP刚开始启动的时候 ssthresh
的大小是 65535 (对方窗口大小的最大值) 。
- 当
cwnd < ssthresh
时,使用慢启动算法。 - 当
cwnd >= ssthresh
时,就会使用「拥塞避免算法」。
Ⅱ、拥塞避免算法
进入拥塞避免算法后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。
接上前面的慢启动的例子,现假定 ssthresh
为 8:
- 当 8 个 ACK 应答确认到来时,每个确认增加 1/8,8 个 ACK 确认 cwnd 一共增加 1,于是这一次能够发送 9 个
MSS
大小的数据,变成了线性增长。
拥塞避免算法的变化过程如下图:
所以,我们可以发现,拥塞避免算法就是将原本慢启动算法的指数增长变成了线性增长,还是增长阶段,但是增长速度缓慢了一些。
就这么一直增长着后,网络就会慢慢进入了拥塞的状况了,于是就会出现丢包现象,这时就需要对丢失的数据包进行重传。
当触发了重传机制,也就进入了「拥塞发生算法」。
Ⅲ、拥塞发生
当网络出现拥塞,也就是会发生数据包重传,重传机制主要有两种,这两种使用的拥塞发送算法是不同的。
- 超时重传的拥塞发生算法
当发生了「超时重传」时,就会使用「超时重传的拥塞发生算法」。
这个时候,ssthresh
和 cwnd
的值会发生变化:
ssthresh
设为cwnd/2
。cwnd
通常会被重置为1,但是有些时候「超时重传的拥塞发生算法」可能会选择恢复到初始的cwnd
值,但这并不常见。通常,当发生「超时重传」时,网络拥塞已经很严重,因此将cwnd
重置为1更加合理。
问 :怎么查看系统的 cwnd
初始化值?
Linux
针对每一个 TCP 连接的 cwnd
初始化值是 10,也就是 10 个 MSS
,我们可以用 ss -nli | grep cwnd
命令查看每一个 TCP 连接的 cwnd
初始化值,但是要记住在发生「超时重传」时大概率不会使用这个初始值来给cwnd
重新赋值的。
如下图:
拥塞发生算法的变化如下图:
- 少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为发生了网络拥塞。
- 当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
- 拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。
- 快速重传的拥塞发生算法
前面我们讲过「快速重传」,发送端连续收到三次相同的应答时就会触发快重传,不必等待超时再重传。
当发生快速重传时,说明因为大部分都没有没丢,只丢了一小部分,TCP 认为这种情况网络拥塞不严重,则 ssthresh
和 cwnd
变化如下:
cwnd
=cwnd/2
,也就是设置为原来的一半。ssthresh
=cwnd
。- 进入快速恢复算法。
Ⅳ、快速恢复
快速重传和快速恢复算法一般同时使用,快速恢复算法是认为:你还能收到 3 个重复 ACK ,说明网络拥塞的情况也不那么糟糕,所以没有必要像 「超时重传」那么强烈的将cwnd
置为1
。
正如前面所说,进入快速恢复之前,cwnd
和 ssthresh
已被更新了,然后,进入快速恢复算法如下:
- 拥塞窗口
cwnd
=ssthresh + n
( n 的意思是确认有 n 个数据包被收到了); - 重传丢失的数据包;
- 如果再收到重复的 ACK,那么
cwnd
增加1
; - 如果收到新数据的 ACK 后,把
cwnd
设置为第一步中的ssthresh
的值,原因是该 ACK 确认了新的数据,说明从重复 ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
首先,快速恢复是拥塞发生后慢启动的优化,其首要目的仍然是降低 cwnd
来减缓拥塞,所以必然会出现 cwnd
从大到小的改变。
其次,cwnd
逐渐加1的过程的存在是为了尽快将丢失的数据包发给目标,从而解决拥塞的根本问题(三次相同的 ACK 导致的快速重传),所以这一过程中 cwnd
反而是逐渐增大的 。
- 在快速恢复的过程中,首先
ssthresh
=cwnd/2
,然后cwnd = ssthresh + 3
,表示网络可能出现了阻塞,所以需要减小cwnd
以避免,加 3 代表快速重传时已经确认接收到了 3 个重复的数据包; - 随后继续重传丢失的数据包,如果再收到重复的 ACK,那么
cwnd
增加 1。加 1 代表每个收到的重复的 ACK 包,都已经离开了网络。这个过程的目的是尽快将丢失的数据包发给目标。 - 如果收到新数据的 ACK 后,把
cwnd
设置为第一步中的ssthresh
的值,恢复过程结束。
这样的话「快速重传」也就没有像「超时重传」那样直接将cwnd
变为1,而是还在比较高的值,后续呈线性增长,快速恢复还是一个不错的优化。
十、TCP的一些杂项讨论
1、一种应答方式——延迟应答
TCP有了滑动窗口这个大杀器, 能够高效可靠的发送大量的数据. 但是如果接收数据的主机收到数据后立即进行ACK应答,此时返回的窗口可能比较小。
- 假设对方接收端缓冲区剩余空间大小为1M,对方一次收到500K的数据后,如果立即进行ACK应答,此时返回的窗口就是500K。
- 但实际接收端处理数据的速度很快,10ms之内就将接收缓冲区中500K的数据消费掉了。
- 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
- 如果接收端稍微等一会再进行ACK应答,比如等待200ms再应答,那么这时返回的窗口大小就是1M。
需要注意的是,延迟应答的目的不是为了保证可靠性,而是留出一点时间让接收缓冲区中的数据尽可能被上层应用层消费掉,此时在进行ACK响应的时候报告的窗口大小就可以更大,从而增大网络吞吐量,进而提高数据的传输效率。
此外,不是所有的数据包都可以延迟应答。
- 数量限制:每个N个包就应答一次。
- 时间限制:超过最大延迟时间就应答一次(这个时间不会导致误超时重传)。
延迟应答具体的数量和超时时间,依操作系统不同也有差异,一般N取2,超时时间取200ms(超时重传的时间一般为500ms)。
2、面向字节流
当创建一个TCP的socket
时,同时在内核中会创建一个发送缓冲区和一个接收缓冲区。
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去。
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据, 这个概念叫做 全双工
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
- 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
- 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
3、粘包问题
Ⅰ、什么是粘包?
- 粘包问题中的“包”,是指的应用层的数据包。
- 在TCP的协议头中,没有如同UDP一样的“报文长度”这样的字段。
- 站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中。
- 但站在应用层的角度,看到的只是一串连续的字节数据。
- 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包,所以粘包问题是一个应用层的问题,而且是应用层最先要解决的问题!
Ⅱ、如何解决粘包问题 ?
归根结底就是一句话: 明确两个包之间的边界。
- 对于定长的包, 保证每次都按固定大小读取即可。
- 对于变长的包:
- 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
- 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可);
Ⅲ、UDP是否存在粘包问题?
- UDP是一个一个把数据交付给应用层的,具有有很明确的数据边界。
- 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在,于是在交付时就可以根据 :udp报文长度字段 - udp8字节固定首部长度 得到一个完整的报文。
因此UDP是不存在粘包问题的,根本原因就是UDP的16位UDP长度字段和UDP报头的固定长度,因此UDP在传输层的时候就把报文和报文之间的边界明确了,而TCP存在粘包问题就是因为TCP是面向字节流的,TCP报文之间没有明确的边界。
4、TCP异常情况
- 进程终止
当客户端正常访问服务器时,如果客户端进程突然崩溃了,此时建立好的连接会怎么样?
文件描述符是随进程的,当一个进程退出时,该进程曾经打开的文件描述符都会自动关闭,因此当客户端进程崩溃退出时,相当于自动调用了close
函数关闭了对应的文件描述符,此时双方操作系统在底层会正常完成四次挥手,然后释放对应的连接资源。也就是说,进程崩溃退出时会释放文件描述符,TCP底层仍然可以发送FIN,和进程正常退出没有区别。
- 机器重启/关机
当客户端正常访问服务器时,如果将客户端主机突然重启/关机,此时建立好的连接会怎么样?
和进程终止的情况相同的,例如我们在windows
中关机/重启时,系统会给我们提示当前系统还有未关闭的进程,是否关闭进程?
所以系统关机/重启之前会关闭所有的进程,这就和进程终止的情况相同的,另外我们会发现当我们在关机之前运行的有多个网络程序时,关机的速度就会变慢,这是因为操作系统在底层进行四次挥手时也要消耗时间的!当挥手完毕才能够进行关机。
- 机器掉电/网线断开
当客户端正常访问服务器时,如果将客户端主机突然掉电/网线断开,此时建立好的连接会怎么样?
当客户端掉线后,服务器端在短时间内无法知道客户端掉线了的。
会有以下这些情况:
- 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset。
- 服务器端会维持与客户端建立的连接,但这个连接也不会一直维持,因为TCP是有保活策略的,TCP自己内置了一个保活定时器,会定期询问对方是否还在,如果对方不在,也会把连接释放。
- 此外,应用层的某些协议,也有一些类似的检测机制,例如基于长连接的HTTP,也会定期检测对方的存在状态。
另外,TCP的连接长短其实是由应用层决定的,因为TCP虽然进行连接管理工作,但是它是没有办法确定用户想要连接的时长的!
十一、TCP总结
1、TCP小结
TCP协议这么复杂就是因为TCP既要保证可靠性,同时又尽可能的提高性能。
可靠性:
检验和 | 序列号 |
确认应答 | 超时重传 |
连接管理 | 流量控制 |
拥塞控制 |
提高性能:
滑动窗口 | 快速重传 |
延迟应答 | 捎带应答 |
需要注意的是,TCP的这些机制有些能够通过TCP报头体现出来的,但还有一些是通过代码逻辑体现出来的,此外,TCP当中还设置了各种定时器。
TCP定时器:
- 重传定时器:为了控制丢失的报文段或丢弃的报文段,也就是对报文段确认的等待时间。
- 坚持定时器:专门为对方零窗口通知而设立的,也就是向对方发送窗口探测的时间间隔。
- 保活定时器:为了检查空闲连接的存在状态,也就是向对方发送探查报文的时间间隔。
- TIME_WAIT定时器:双方在四次挥手后,主动断开连接的一方需要等待的时长。
2、基于TCP的应用层协议
常见的基于TCP的应用层协议如下:
- HTTP(超文本传输协议)。
- HTTPS(安全数据传输协议)。
- SSH(安全外壳协议)。
- Telnet(远程终端协议)。
- FTP(文件传输协议)。
- SMTP(电子邮件传输协议)。
当然,也包括你自己写TCP程序时自定义的应用层协议。
3、理解传输控制协议
TCP的各种机制实际都没有谈及数据真正的发送,这些都叫做传输数据的策略。TCP协议是在网络数据传输当中做决策的,它提供的是理论支持,比如TCP要求当发出的报文在一段时间内收不到ACK应答就应该进行超时重传,而数据真正的发送实际是由底层的IP和MAC帧完成的。
TCP做决策,IP+MAC做执行,我们将它们统称为通信细节,它们最终的目的就是为了将数据传输到对端主机。而传输数据的目的是什么则是由应用层决定的。因此应用层决定的是通信的意义,而传输层及其往下的各层决定的是通信的方式。
参考资料:
[2] 传输层协议 ——— TCP协议