一、滑动窗口存在的意义
在前一篇博客中,我们介绍到了 TCP 的确认应答机制,当两台计算机之间建立连接之后,就可以进行传输数据了, TCP 每发送一个数据,都要进行一次确认应答,发送一个 ACK应答包,当上一个数据包确认应答了,再发送下一个,从而保证数据的可靠传输。
这种传输方式,虽然可靠但是缺点明显,每次只能发送一条数据,并且只有当收到 ACK确认包才能发送下一条数据,传输数据非常低下。
为了解决这个问题,TCP 引入了滑动窗口。
二、滑动窗口机制的实现
1、窗口的实质
窗口实际上是操作系统开辟的一块缓冲区,发送方发送数据将会将数据存放在缓冲区里,如果发送方收到接收方回复的确认应答,这个数据就会从缓冲区里面删除。但是,窗口不是发送缓冲区,也不是接收缓冲区。
2、滑动窗口机制的实现过程
1、基本过程
上一篇博客讨论到了确认应答策略,对每一个发送的数据报,都要给一个ACK确认应答,收到ACK后再发送下一个数据段,这样做会有一个比较大的缺点,就是性能较差,尤其是往返时间较长的时候。

既然这样一发一收的方式性能较低,那么我们一次可以发送多条数据(其实是将多个段的等待时间重叠在一起了),就可以大大的提示性能

- 窗口大小是指舞曲等待确认应答而可以继续发送数据的最大值,上图的窗口大小就是4000个字节(四个段)
- 发送前四个段的时候,不需要等待任何 ACK 应答,直接发送
- 收到第一个 ACK 后, 滑动窗口向后移动,继续发送第五个段的数据;依此类推
- 操作系统内核为了维护这个滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉
- 窗口越大,网络的吞吐量越大

那么如果出现了丢包,如何进行重传,分 2 种情况讨论
**情况一:**数据包已经抵达,但是 ACK 丢了

由于接收方缓冲区可以根据数据包报头的序号,对接收来的数据包按先后进行排序,使得接收方也可以按序读取。
这一操作使得接收方发送的ACK号 Y 表示 所有序号小于 Y 的数据都已被正确、按序接收。这也称为 TCP 的累计确认机制。所以在第一种情况下,部分 ACK 丢了并不要紧,因为可以通过后续的 ACK 进行累计确认。
情况二:数据包在传输过程中丢了,导致接收方没收到

当某一段报文段丢失之后,发送端会一直收到 1001 这样的 ACK,就像是在提醒发送端 "我想要的是 1001" 一样
如果发送端主机连续收到同一个 "1001" 的应答,代表只收到了 1001 之前的数据包,就会将对应的数据 1001 - 2000 重新发送
这个时候接收端收到了 1001 之后,再次返回的 ACK 就是 7001了, 因为 2001-7000 的数据包接收端气质之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
这种机制被称为 " 高速重发控制"(也叫"快重传")
**3、**TCP 滑动窗口的计算
TCP 滑动窗口是发送方实际能发送的数据量,它的的大小是一个动态调整的值,主要受 3 个因素控制:
-
接收窗口(rwnd)--- 接收方能力限制,由流量控制
-
拥塞窗口(cwnd)--- 网络状况限制
-
系统最大窗口 --- 协议/系统限制
最终窗口窗口大小 = min(rwnd, cwnd)
1、接收窗口 (Receiver Window, rwnd)
接收窗口是接收方根据自己缓冲区可用空间声明的窗口大小, 在 TCP 首部中, 有一个16位窗口字段,就是存放了窗口大小信息;16位数字的最大就是 65535 字节。实际上, TCP 首部 40 字节选项中还包含了一个窗口扩大因子 M, 实际窗口大小是窗口字段的值左移 M 位。

接收窗口大小 = 接收缓冲区总大小(操作系统设置)- (已接收的最后字节序号 - 应用已读取的最后字节序号)
rwnd = receive_buffer_size - (last_byte_received - last_byte_read)
流量控制
接收方会根据本地缓冲区状态重新计算 rwnd ,并通过ACK包通告给发送方, 这一过程称为流量控制。
由于接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。
因此 TCP 支持根据接收端的处理能力,来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)
- 接收端可以将自己可以接收的缓冲区大小放入 TCP 首部的 "窗口大小" 字段, 通过 ACK确认包通知发送端;
- 窗口字段越大,说明网络的吞吐量越高;
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;
- 发送端接收到这个窗口后,就会减慢自己的发送速度
- 如果接收端缓冲区满了,就会将窗口设置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。

2、拥塞窗口(Congestion Window, cwnd)
这是 TCP 最复杂的部分,包含多个算法阶段,拥塞窗口的大小通过拥塞控制来调整
拥塞控制
虽然 TCP 有滑动窗口这个大杀器,能够高效可靠的发送大量的数据。但是由于网络上有很多计算机,可能当前的网络状态就已经比较拥堵,在不清粗当前网络状态下,如果在开始阶段就发送大量的数据,是有可能雪上加霜的
TCP 引入慢启动机制,先发少量的数据,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据;

- 此处引入一个概念为 "堵塞窗口"
- 发送开始的时候,定义拥堵窗口大小为 1
- 每次收到一个 ACK 应答,拥堵窗口加 1
- 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口作比较,取较小的值作为实际发送的窗口
| RTT轮次 | 开始时cwnd (段数) | 本RTT发送 数据包数量 | 本RTT将收到 ACK数量 | ACK到达顺序 | cwnd更新过程 (每次ACK到达时) | RTT结束时 cwnd值 | 增长率 |
|---|---|---|---|---|---|---|---|
| 0 | 1 | 1 | 1 | ACK#1 | cwnd = 1 + 1 = 2 | 2 | ×2 |
| 1 | 2 | 2 | 2 | ACK#1 | cwnd = 2 + 1 = 3 | 4 | ×2 |
| ACK#2 | cwnd = 3 + 1 = 4 | ||||||
| 2 | 4 | 4 | 4 | ACK#1 | cwnd = 4 + 1 = 5 | 8 | ×2 |
| ACK#2 | cwnd = 5 + 1 = 6 | ||||||
| ACK#3 | cwnd = 6 + 1 = 7 | ||||||
| ACK#4 | cwnd = 7 + 1 = 8 | ||||||
| 3 | 8 | 8 | 8 | ACK#1 | cwnd = 8 + 1 = 9 | 16 | ×2 |
| ACK#2 | cwnd = 9 + 1 = 10 | ||||||
| ACK#3 | cwnd = 10 + 1 = 11 | ||||||
| ACK#4 | cwnd = 11 + 1 = 12 | ||||||
| ACK#5 | cwnd = 12 + 1 = 13 | ||||||
| ACK#6 | cwnd = 13 + 1 = 14 | ||||||
| ACK#7 | cwnd = 14 + 1 = 15 | ||||||
| ACK#8 | cwnd = 15 + 1 = 16 | ||||||
| 4 | 16 | 16 | 16 | ACK#1-16 | (每次+1,共加16) | 32 | ×2 |
像上面这样的拥塞窗口增长速度,是指数级别的,"慢启动" 只是指开始时慢,但是增长速度非常快
- 为了不增长的那么快,因此不能使拥塞窗口简单的加倍
- 此处引入一个叫做慢启动的阈值
- 当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长
- 当 TCP 开始启动的时候,慢启动阈值等于窗口最大值
- 在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1
少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为是网络堵塞;
当 TCP 通信开始后,网络吞吐量会逐渐上升,当网络发生堵塞,吞吐量会立即下降
