目录
[1. TCP 的重传机制](#1. TCP 的重传机制)
[(3)SACK 方法](#(3)SACK 方法)
[(4)Duplicate SACK](#(4)Duplicate SACK)
[① ACK 丟包](#① ACK 丟包)
[② 网络延时](#② 网络延时)
[补充:D-SACK 的好处](#补充:D-SACK 的好处)
[(5)综述 TCP 重传机制是怎么实现的?](#(5)综述 TCP 重传机制是怎么实现的?)
[① 序号与确认号](#① 序号与确认号)
[② 超时检测](#② 超时检测)
[③ 重传策略](#③ 重传策略)
[④ 快速重传和快速恢复](#④ 快速重传和快速恢复)
[2. TCP 的滑动窗口](#2. TCP 的滑动窗口)
[3. TCP 的流量控制](#3. TCP 的流量控制)
[(2)综述 TCP 流量控制是怎么实现的?](#(2)综述 TCP 流量控制是怎么实现的?)
[① 滑动窗口大小](#① 滑动窗口大小)
[② 接收方窗口大小](#② 接收方窗口大小)
[③ 流量控制的目标](#③ 流量控制的目标)
[④ 动态调整](#④ 动态调整)
[⑤ 确认机制](#⑤ 确认机制)
[4. TCP 的拥塞控制](#4. TCP 的拥塞控制)
[(7)综述 TCP 拥塞控制是怎么实现的?](#(7)综述 TCP 拥塞控制是怎么实现的?)
[① 慢启动(Slow Start)](#① 慢启动(Slow Start))
[② 拥塞避免(Congestion Avoidance)](#② 拥塞避免(Congestion Avoidance))
[③ 快速重传(Fast Retransmit)](#③ 快速重传(Fast Retransmit))
[④ 快速恢复(Fast Recovery)](#④ 快速恢复(Fast Recovery))
[1. Keep-Alive 是什么?](#1. Keep-Alive 是什么?)
[2. Keep-Alive 的优缺点?](#2. Keep-Alive 的优缺点?)
[3. TCP KeepAlive 是什么?](#3. TCP KeepAlive 是什么?)
[4. TCP 的 KeepAlive 和 HTTP 的 Keep-Alive 是一个东西吗?](#4. TCP 的 KeepAlive 和 HTTP 的 Keep-Alive 是一个东西吗?)
[(1)TCP 的 KeepAlive](#(1)TCP 的 KeepAlive)
[(2)HTTP 的 Keep-Alive](#(2)HTTP 的 Keep-Alive)
一、问题综述
1. TCP 的重传机制
(1)超时重传
重传机制的 其中一个 方式,就是++在 发送 数据时,设定 一个 定时器,当 超过 指定的 时间后,没有 收到 对方的 ACK 确认 应答报文,就会 重发 该数据++ ,也就是 我们 常说的 超时重传。TCP 会在以下 两种情况 发生 超时重传:
- 数据包丢失。
- 确认应答丢失。
**超时时间 应该 设置为 多少呢?**先来了解一下什么是 RTT(Round-Trip Time 往返时延)。
RTT 指的是数据 发送时刻 到 接收到确认的 时刻的 差值,也就是 包的 往返时间。超时重传时间是以**RTO(Retransmission Timeout 超时重传时间)**表示。
上图中 有 两种 超时时间 不同的 情况:
- 当 超时时间 RTO 较大时,重发就慢,丢了老半天才重发,没有效率,性能差;
- 当 超时时间 RTO 较小时,会 导致 可能 并没有丢 就重发,于是 重发的 就快,会 增加 网络拥塞,导致 更多的 超时,更多的超时 导致更多的重发。
精确的测量 超时时间 RTO 的值是 非常重要的,这可 让我们的 重传机制 更高效。根据 上述的 两种情况,我们 可以 得知,超时重传时间 RTO 的值 应该 略大于 报文往返 RTT 的值。
实际上「报文往返 RTT 的值」是 经常变化的,因为 我们的网络 也是 时常 变化的。也就 因为「报文往返 RTT 的值」是 经常 波动变化的,所以「超时重传时间 RTO 的值」应该 是一个 动态变化的值。
如果 超时重发的 数据,再次 超时的 时候,又需要 重传的 时候,TCP 的策略是 超时间隔 加倍 。也就是 每当 遇到一次 超时重传的 时候,都会将 下一次 超时时间间隔 设为 先前值的 两倍 。两次超时,就 说明 网络环境 差,不宜 频繁 反复发送。
超时 触发 重传存在的 问题是,超时 周期 可能相对 较长。
(2)快速重传
TCP 还有另外一种 快速重传(Fast Retransmit)机制 ,它++不以 时间为 驱动,而是 以 数据驱动 重传++。
在上图,发送方 发出了 1,2,3,4,5 份 数据:
- 第一份 Seq1 先送到了,于是就 Ack 回 2;
- 结果 Seq2 因为 某些原因 没收到,Seq3 到达了,于是还是 Ack 回 2;
- 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到;
- 发送端 收到了 三个 Ack=2 的确认,知道了 Seq2 还没有收到,就会 在定时器过期 之前,重传 丢失的 Seq2。
- 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6。
所以,快速重传的 工作方式是 ****当收到 三个相同的 ACK 报文时,会在 定时器 过期 之前,重传丢失的 报文段。这解决了一个问题,就是 超时时间的问题。
还有 另外一个 问题,就是 重传的 时候,是 重传一个,还是 重传 所有的 问题。
举个例子,假设 发送方 发了 6 个 数据,编号的 顺序是 Seq1~Seq6,但是 Seq2、Seq3 都丢失了,那么 接收方 在收到 Seq4、Seq5、Seq6 时,都是回复 ACK2 给发送方,但是 发送方 并不清楚 这 连续的 ACK2 是 接收方收 到 哪个报文 而 回复的,那是 ++选择重传 Seq2 一个报文,还是 重传 Seq2 之后 已发送的 所有报文++(Seq2、Seq3、Seq4、Seq5、Seq6)呢?
- 如果 只选择 重传 Seq2 一个 报文,那么 重传的效率 很低。因为 对于 丢失的 Seq3 报文,还得 在后续 收到 三个 重复的 ACK3 才能触发重传。
- 如果 选择 重传 Seq2 之后 已发送的 所有报文,虽然 能同时 重传 已丢失的 Seq2 和 Seq3 报文,但是 Seq4、Seq5、Seq6 的 报文是 已经被 接收过了,对于重传 Seq4 ~Seq6 折部分数据 相当于做了 一次 无用功,浪费资源。
可以 看到,不管是 重传 一个报文,还是 重传 已发送的 报文,都 存在问题。为了 解决 不知道该重传哪些 TCP 报文,于是就有 SACK 方法。
(3)SACK 方法
还有一种 实现 重传机制的方式 叫:SACK(Selective Acknowledgment,选择性确认)。
这种方式 ++需要 在 TCP 头部「选项」字段 里加一个 SACK 的 东西++ ,它 可以 将 已收到的 数据的 信息 发送给「发送方」,这样 发送方 就可以 知道 哪些 数据 收到了,哪些数据 没收到,知道了 这些 信息,就可以 只重传 丢失的 数据。
下图中,发送方 收到了 三次同样 的 ACK 确认报文,于是 就会触发 快速重发机制,通过 SACK 信息 发现只有 200~299 这段 数据 丢失,则重发时,就 只选择了这个 TCP 段 进行重复。
最后 返回 ACK 600 表示为,600 之前的所有数据 均被接收到了。
(4)Duplicate SACK
Duplicate 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 包丢了;
- 可以 知道 是不是「发送方」的 数据包 被 网络延迟了;
- 可以 知道 网络中 是不是 把「发送方」的 数据包 给 复制了;
(5)综述 TCP 重传机制是怎么实现的?
当 发送方 的 数据 在传输过程中 丢失、损坏或延迟,接收方 可以 请求发送方 重新传输 这些数据。
① 序号与确认号
在 TCP 通信 中,每个 发送的字节 都有一个 唯一的****序号,而 每个 接收的字节 都有一个****确认号。发送方 维护了 一个 发送窗口 ,接收方 维护了 一个 接收窗口。发送方 会 持续 发送数据,并 等待接收方的 确认。
② 超时检测
发送方 为 每个 发送的数据段 设置一个 定时器,这个 定时器 的时长 称为 超时时间。发送方 假设在 这个超时时间 内,数据 能够 到达 接收方 并得到 确认。如果 在超时时间内 没有 收到确认,发送方 会认为 数据丢失或损坏,触发重传。
③ 重传策略
当 发送方 在 超时时间内 没有 收到确认,它会 认为 数据丢失,然后 重新 发送相应的 数据段。如果 只有一个 数据段丢失,发送方 只会 重传 丢失的 数据段。如果 有多个 数据段 丢失,发送方 可能会 使用 更复杂的 算法来 决定哪些 数据 需要重传。
④ 快速重传和快速恢复
为了 更快地 发现丢失的 数据,接收方 可以 使用 快速 重传策略。当 接收方 连续 接收到 相同的 确认号时,它会 立即 向发送方 发送 冗余的 确认,以 触发 发送方 进行重传。此外,发送方 在接收到 快速重传的 确认后,不需要 等到 超时 再次发送,而是 可以 使用快速 恢复算法 继续 发送未 丢失的数据。
2. TCP 的滑动窗口
TCP 是 每 发送一个 数据,都要 进行 一次 确认应答。当 上一个 数据包 收到 了应答了,再发送 下一个。这个 模式 就有点 像 我和你 面对面 聊天,你一句我一句。但 这种方式的 缺点是 效率比较低的。
这样的 传输方式 有一个 缺点:数据包的 往返时间 越长,通信的 效率就越低。
为解决这个问题,TCP 引入了 窗口 这个概念。即使 在 往返时间 较长 的 情况下,它也 不会 降低 网络通信的 效率。
有了窗口,就可以 指定 窗口大小,窗口大小 就是 指无需 等待 确认应答,而可以 继续 发送 数据的最大值。
窗口的实现 实际上 是 操作系统 开辟的 一个 缓存空间,发送方主机 在 等到 确认应答返回 之前,必须 在 缓冲区中 保留 已发送的 数据。如果 按期收到 确认应答,此时 数据 就 可以从缓存区 清除。
假设 窗口 大小为 3 个 TCP 段,那么 发送方 就可以「连续发送」3 个 TCP 段,并且 中途 若有ACK 丢失,可以 通过**「下一个确认 应答 进行确认」**。如下图:
图中的 ACK 600 确认应答 报文丢失,也没关系,因为可以通过下一个确认应答进行确认,只要发送方 收到了 ACK 700 确认应答,就 意味着 700 之前的 所有数据「接收方」都收到了。这个模式就叫 累计确认 或者 累计应答。
TCP 头 里有一个 字段叫 window,也就是 窗口大小。这个字段 是 接收端 告诉 发送端 自己 还有多少 缓冲区 可以 接收数据。于是 发送端 就 可以 根据 这个接收端 的 处理能力 来 发送数据,而不会 导致 接收端 处理 不过来。所以,通常 窗口的大小 是 由接收方的 窗口大小 来决定的。发送方发送的 数据大小 不能 超过 接收方的 窗口大小,否则 接收方 就无法 正常 接收到 数据。
(1)发送方的滑动窗口
过程一:
下图就是 发送方缓存 的数据,根据处理 的情况分成 四个部分,其中 深蓝色方框是 发送窗口,紫色方框是 可用窗口:
过程二:
发送方 把 数据「全部」都一下 发送 出去后,可用窗口 的 大小 就为 0 了,表明 可用窗口 耗尽,在没收到 ACK 确认 之前是 无法 继续 发送数据了。
过程三:
当 收到 之前发送 的数据 32~36 字节 的 ACK 确认应答 后,如果 发送窗口 的 大小 没有变化,则滑动窗口 往 右边移动 5 个 字节,因为有 5 个 字节的 数据 被应答 确认,接下来 52~56 字节 又变成了 可用窗口,那么 后续也就 可以 发送 52~56 这 5 个 字节的 数据了。
(2)接收方的滑动窗口
3. TCP 的流量控制
发送方 不能无脑的 发数据 给接收方,要考虑 接收方 处理能力。如果 一直无脑的 发数据 给对方,但 对方 处理 不过来,那么就会 导致 触发 重发机制,从而 导致 网络流量的 无端的浪费。为了解 决这种 现象发生,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。
(1)操作系统缓冲区与滑动窗口的关系
上面的 流量控制 例子,我们 假定了 发送窗口 和 接收窗口 是不变的 ,但是实际上,发送窗口 和 接收窗口 中所存放的 字节数,都是放在 操作系统 内存缓冲区中 的,而 操作系统的 缓冲区,会被 操作系统 调整。当 应用进程 没办法 及时 读取缓冲区的 内容时,也会 对我们的 缓冲区 造成影响。
那 操作系统的 缓冲区,是 如何影响 发送窗口 和 接收窗口 的呢?
举个例子。当 应用程序 没有 及时 读取缓存时,发送窗口 和 接收窗口的 变化。考虑以下场景:
- 客户端 作为 发送方,服务端 作为 接收方,发送窗口 和 接收窗口 初始大小为 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(360 - 120 - 140),最后 发送 确认信息 时,通告 窗口大小 给对方。
- 此时 ++客户端 因为 还 没有 收到服务端 的 通告窗口 报文,所以 不知道 此时 接收窗口 收缩成了 100++,客户端 只会 看 自己的 可用窗口 还有 220,所以 客户端就 发送了 180 字节 数据,于是 可用 窗口 减少到 40。
- 服务端 收到了 180 字节 数据 时,发现数据大小 超过了 接收窗口 的大小 ,于是 就把 数据包 丢失了。
- 客户端 收到 第 2 步 时,服务端 发送的 确认报文 和 通告窗口 报文,尝试 减少 发送窗口 到 100,把 窗口的 右端 向左 收缩了 80,此时 ++可用窗口的 大小 就会 出现 诡异的 负值++。
所以,如果 发生了 先减少 缓存,再 收缩窗口,就会 出现 丢包的 现象 。为了 防止 这种情况 发生,TCP 规定 是 不允许 同时 减少缓存 又 收缩窗口的,而是 采用 先收缩窗口,过段时间 再 减少缓存,这样就 可以 避免了 丢包情况。
(2)综述 TCP 流量控制是怎么实现的?
流量控制 就是 让 发送方 发送速率 不要过快,让 接收方 来得及 接收。利用 滑动窗口机制 就可以实施 流量控制,主要方法 就是 动态调整 发送方 和 接收方 之间 数据传输速率。
① 滑动窗口大小
在 TCP 通信中,每个 TCP 报文段 都包含一个 窗口字段,该 字段 指示 发送方 可以 发送多少 字节的 数据 而不等待 确认 。这个 窗口大小 是 动态调整的。
② 接收方窗口大小
接收方 通过 TCP 报文 中的 窗口字段 告诉 发送方 自己 当前的 可接收窗口大小 。这是 接收方 缓冲区 中还 有多少 可用空间。
③ 流量控制的目标
流量控制的 目标 是 确保 发送方 不要发送 超过 接收方 缓冲区容量 的数据。如果 接收方的 缓冲区 快满了,它会 减小 窗口大小,通知 发送方 暂停发送,以防止 溢出。
④ 动态调整
++发送方 会 根据 接收方的 窗口大小 动态 调整发送数据的 速率。++如果 接收方的 窗口大小 增加,发送方 可以 加速 发送数据。如果 窗口大小 减小,发送方将 减缓 发送数据的 速率。
⑤ 确认机制
接收方会 定期 发送 确认(ACK)报文,++告知 发送方 已成功 接收数据++ 。这也 与 流量控制 密切相关,因为 接收方 可以 ++通过 ACK 报文 中的 窗口字段 来通知 发送方它的 当前窗口大小++。
4. TCP 的拥塞控制
(1)为什么要有拥塞控制,不是有流量控制了吗?
流量控制是 避免「发送方」的数据 填满「接收方」的缓存,但是 并不知道 网络的中 发生了什么。 一般来说,计算机网络 都处在一个 共享的 环境。因此 也有可能会 因为 其他主机之间的 通信使得 网络拥堵。
在 网络 出现拥堵 时,如果 继续 发送 大量数据包,可能会 导致 数据包时延、丢失等 ,这时 TCP 就会 重传数据,但是 一重传 就会 导致 网络的 负担更重,于是会 导致 更大的 延迟 以及 更多的 丢包,这个情况就会 进入 恶性循环 被不断地 放大。
所以,TCP 不能忽略 网络上 发生的 事,它被 设计成一个 无私的 协议,当 网络 发送拥塞 时,TCP 会 自我牺牲,降低 发送的 数据量。 于是,就有了拥塞控制 ,控制的 目的 就是 避免「发送方」的 数据 填满 整个网络。
为了 在「发送方」调节 所要 发送数据的 量,定义了一个 叫做「拥塞窗口」的 概念。
(2)什么是拥塞窗口?和发送窗口有什么关系呢?
拥塞窗口 cwnd 是 发送方 维护的 一个的 状态变量,它会 根据网络 的 拥塞程度 动态变化的。++发送窗口 swnd 和 接收窗口 rwnd++ 是 约等于的 关系 ,由于 加入了 拥塞窗口 的概念后,此时 ++发送窗口的值 是 swnd=min(cwnd, rwnd)++ ,也就是 拥塞窗口 和 接收窗口 中的 最小值。
拥塞窗口 cwnd 变化的规则:
- 只要网络中没有出现拥塞,cwnd 就会增大;
- 但网络中出现了拥塞,cwnd 就减少;
(3)慢启动
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 时,就会 使用「拥塞避免算法」。
(4)拥塞避免算法
**当 拥塞窗口 cwnd「超过」慢启动门限 ssthresh 就会进入 拥塞避免算法。**一般来说 ssthresh 的大小是 65535 字节。
进入 拥塞避免算法 后,它的规则是:每当收到一个 ACK 时,cwnd 增加 1/cwnd。
接上慢启动的例子,现假定 ssthresh 为 8:当 8 个 ACK 应答 确认到来 时,每个 确认 增加 1/8,8 个 ACK 确认 cwnd 一共 增加 1,于是 这一次能够 发送 9 个 MSS 大小的数据,变成了 线性增长。拥塞避免算法 的变化过程 如下图:
可以发现,拥塞避免算法 就是 将原本 慢启动算法的 指数 增长变成了 线性增长,还是 增长阶段,但是 增长 速度 缓慢了一些。
就这么 一直增长着 后,网络就会 慢慢进入了 拥塞的 状况了,于是就会出现 丢包现象,这时就 需要 对丢失的 数据包 进行重传。当触发了 重传机制,也就进入了**「拥塞发生算法」**。
(5)超时重传、快速重传
当发生了**「超时重传」**,则就会 使用 拥塞发生算法 。这个时候,ssthresh(慢启动门限) 和 cwnd(拥塞窗口) 的值会发生变化:
- SSthresh 设为 cwnd/2,
- cwnd 重置 1(是恢复为 cwnd 初始化值,这里假定 cwnd 初始化 值 1)
接着,就 重新开始 慢启动,慢启动 是会 突然减少 数据流的。一旦「超时重传」,马上 回到解放前。但是 这种方式 太激进了,反应也很强烈,会造成网络卡顿。
发生 快速重传的拥塞发生算法 还有更好的 方式,就是**「快速重传算法」**。
当 接收方 发现 丢了一个 中间包 的时候,发送 三次前 一个包的 ACK,于是 发送端 就会 快速地 重传,不必 等待 超时再重传。TCP 认为 这种情况 不严重,因为 大部分 没丢,只丢了 一小部分,则 ssthresh 和 cwnd 变化如下:
- cwnd = cwnd/2,也就是设置为原来的一半。
- ssthresh = cwnd。
- 进入快速恢复算法。
(6)快速恢复
快速重传 和 快速恢复算法 一般 同时使用 ,快速恢复算法 是认为,你 ++还能 收到 3 个 重复 ACK 说明网络 也不那么 糟糕,所以 没有必要 像 RTO 超时 那么强烈。++
进入快速恢复算法如下:
- 拥塞窗口 cwnd = ssthresh + 3(3 的意思是 确认有 3 个 数据包 被收到了);
- 重传 丢失的 数据包;
- 如果 再收到 重复的 ACK,那么 cwnd 增加 1;
- 如果 收到 新数据的 ACK 后,把 cwnd 设置 第一步 中的 ssthresh 的值,原因是 该 ACK 确认了 新的数据,说明从 duplicated ACK 时的 数据都 已收到,该 恢复过程 已经结束,可以 回到 恢复之前的状态了,也即 再次 进入 拥塞避免 状态;
快速恢复算法的变化过程如下图:
也就是 没有像「超时重传」一夜回到 解放前,而是 还在 比较高的值,后续 呈线性增长。
(7)综述 TCP 拥塞控制是怎么实现的?
**TCP 拥塞控制 可以 在 网络 出现拥塞时 动态地 调整 数据传输的 速率,以 防止 网络过载。**TCP 拥塞控制 的 主要机制 包括 以下 几个方面:
① 慢启动(Slow Start)
初始阶段,TCP 发送方 会以 较小的 发送窗口 开始 传输数据。随着 每次 成功 收到 确认的数据,发送方 逐渐 增加 发送窗口的 大小,实现 指数级的 增长,这称为 慢启动。这有 助于在 网络 刚开始 传输时 谨慎地 逐步 增加速率,以避免 引发 拥塞。
② 拥塞避免(Congestion Avoidance)
一旦 达到 一定的 阈值(通常是 慢启动 阈值),TCP 发送方 就会 进入 拥塞避免 阶段 。在 拥塞避免阶段,发送方 以 线性增加 的方式 增加 发送窗口的 大小,而不再是 指数级的 增长。这 有助于控制 发送速率,以 避免 引起 网络拥塞。
③ 快速重传(Fast Retransmit)
如果 发送方 连续 收到相同的 确认,++它会 认为 发生了 数据包的 丢失,并会 快速重传 未确认的 数据包,而 不必 等待超时++。这 有助于 更快地 恢复 由于 拥塞引起的 数据包 丢失。
④ 快速恢复(Fast Recovery)
在 发生 快速重传 后,TCP 进入 快速恢复阶段。在 这个阶段,发送方 不会 回到 慢启动 阶段,而是 将慢启动阈值 设置为 当前窗口的 一半,并 将拥塞窗口 大小 设置为 慢启动阈值 加上 已确认 但 未被 快速重传 的 数据块的 数量。这 有助于 更快地 从拥塞中 恢复。
二、其他相关问题
1. Keep-Alive 是什么?
Keep-Alive 是一种 HTTP 协议的 机制,也被称为 HTTP 长连接。
++在启用 Keep-alive 的情况下,客户端 和 服务器 在 完成一个 HTTP 请求和响应 后,并 不 立即关闭连接,而是 继续 保持 连接 处于 打开状态++。 在 连接保持打开的 情况下,客户端 可以 继续 发送其他请求,服务器 可以 继续 发送响应,而 无需重新 建立连接,减少了 连接的 建立 和 关闭的 开销,从而 提高 性能 和 效率。
HTTP1.0 中 需要 配置 长连接,在 请求头中 配置 connection:Keep-Alive,而 HTTP1.1 中 默认开启了 长连接。
2. Keep-Alive 的优缺点?
(1)优点
- TCP 连接 的 建立和关闭 需要 时间和资源,通过 保持 连接打开,可以 减少 这些开销,从而 提高性能和效率。
- 客户端 可以在 同一个 连接上 同时发送 多个请求 ,服务器 可以 并行地 处理这些 请求,提高 并发性能。
- Keep-alive 连接中的 多个请求 共享 同一个 连接的 头部信息(如用户代理、Cookie 等),减少了头部信息的 重复传输。
(2)缺点
长时间的 持久连接 可能会 占用 服务器资源 ,特别是 在高并发的 情况下。为了 平衡资源 利用和性能,服务器 和 客户端 通常会 设置 Keep-alive 的 超时时间,以便 在一段时间内 保持 连接打开,超过 该时间则 关闭连接。
3. TCP KeepAlive 是什么?
TCP Keep-Alive 是在 操作系统 和 网络协议栈 级别 实现的,它 通过 发送 特定的 探测数据包 来维护连接的 活跃性。
- 在启用 TCP Keep-Alive 的情况下,操作系统 会 定期发送一些 特定的 探测数据包 到连接的 另一端。++这些 数据包 通常是 空的,没有 实际的 数据内容。++
- ++如果 一端 收到了 探测数据包,它会 回复一个 确认(ACK)数据包++。如果 一段时间内 没有 收到确认 数据包,发送端 将认为 连接 可能 已经断开,从而 触发 连接关闭。
TCP Keep-Alive 的 主要目的是 检测 连接是否 处于 空闲状态,即 没有 实际 数据传输。它 不仅可以 检测到 连接断开,还可以 在空闲 连接 超过 一定时间时 释放连接,从而 释放资源。
4. TCP 的 KeepAlive 和 HTTP 的 Keep-Alive 是一个东西吗?
TCP 的 KeepAlive 和 HTTP 的 Keep-Alive 并不是 同一个概念,它们 实现的 层面 和 意图 均有所不同。
(1)TCP 的 KeepAlive
- TCP 的 KeepAlive 是一个由**TCP 层(内核态)**实现的 机制,用于检测 TCP 连接的状 态,确保连接的 有效性。
- 当 TCP 连接 在一段时间 内 没有 数据交互时,内核 会 发送探测报文 来检测 对方 是否 仍然在线。如果 连续 多个探测报文 都没有 回应,TCP 会报告 连接 已死亡。
目的:确保 TCP 连接的 有效性,避免 因为 网络故障 或 对方宕机等 原因 导致的 连接失效。
(2)HTTP 的 Keep-Alive
- HTTP 的 Keep-Alive 是一个由 **应用层(用户态)**实现的 机制,允许 在 单个 TCP 连接上 发送多个 HTTP 请求和响应。
- 通过 HTTP 请求头 和 响应头 中的 Connection 字段来 指示 是否要 保持连接。如果值为 "keep-alive",则表示 希望 保持连接;如果值为 "close",则表示 请求完成后 关闭连接。
目的:提高网 络效率,减少延迟,并 降低服务器 和 客户端的 资源消耗。