滑动窗口
因为 TCP 传输一段数据之后,并不会马上就被上层的应用层读取,所以在 TCP 层是有一层缓存的,而每个 TCP 连接的缓存大小是有限制的,这个大小限制着发送端和接收端的行为。例如接收端有 8K 的缓存窗口,那么意味着发送端一次性或者连续可以有效发送 8K 的数据过来,这里假设发送端每次发送 1K 数据过来,而接收端的数据没有被应用层读取,那么发送端在发送第一次之后,接收端就会告诉发送端我的窗口还有 7K,那么发送端继续发送,接收端反馈还有 6K,直到发送端发送 8 次之后,接收端就反馈给发送端没有接收窗口了。
发送端在遇到没有窗口的时候,会进行两个操作,分别是:
- 等待接收端通知有空余窗口
- 定时得发送一个字节的段(窗口探测),询问是否有窗口更新
这里不断变更的窗口就是所谓的滑动窗口了,滑动窗口虽然有利于网络的充分使用,但是架不住应用层的不正当使用。
延迟确认
假设我们是使用 SSH 远程访问一台机器的 VIM 编辑器,可能很多时候我们就好几秒才会按下一个键盘,例如下一行:K,那么这是不是意味着 TCP 就只发送了一个字节的有效数据给接收端,这看上去没什么问题,但是还有 TCP 头和 IP 头呢,这就导致了极大的网络资源浪费,服务器还得响应一个。所以针对这种情况,接收端就有一个 延迟确认 的操作,其实就是期望累计一定数量的数据,或者等待一定的时候之后,再响应给发送端,从而节省一些带宽。
实际的操作有 Nagle 算法,他对于大量小数据包的操作是,将多次的小数据包累计起来,先发送第一个到达的小数据包,然后累计后面的,直到接收到了第一个小数据包的结果之后,再发送第二个累计起来的数据包。在这种模型下,也就只有第一次才是发送的小数据包,较大程度上充分利用了网络资源。
低能窗口综合征
虽然 延迟确认 很不错,但是防不住接收端的另外一种情况,那就是如果接收端的应用是一个一个字节的读取,那么接收端就会一个一个字节窗口得向发送端确认窗口大小。
所以为了减少这种情况,Clark 算法会杜绝一个字节的窗口更新,会累计一定数量的空间之后再通知发送端。
累计确认
在 TCP 中,因为数据可能被拆分成多个包发送,而包到达的顺序不是确定的,所以就可能出现序号小的包后到的,序号大的包先到的情况。如果序号大的包后到,TCP 就发送未收到的包的 ACK 的话,可能会导致发送端重复发送已经收到的包,所以 TCP 的处理是会等待序号小的包到达之后,再发送接收到的连续最大序号的 ACK,这就叫做 TCP 的 累计确认
- 问题 1:收到序号大的包都可以先保存起来吗?受窗口大小影响吗?
- 问题 2:等待序号小的包有时间限制吗?
定时器
在前面中我至少说了两个定时器,分别是 TCP 发送端有一个定时器用于向接收端询问窗口大小,还有一个定时器是接收端延迟确认的最大时间,所以可以看出定时器在 TCP 中占有很重要的地位,事实上也是这样的,总的来说,TCP 中有四种类型的定时器,虽然有人总结了有 7 种,但是,实际上就只有 4 种,分别是:
1. 重传定时器(RTO,Retransmission TimeOut)
重传计时器可能是用得比较多的一个计时器了,我们都知道 TCP 是可靠数据传输,并且的分段传输,那么 TCP 如何判断每一个段都被接收端成功接收呢?那就是 ACK 了,但是,如果一直收不到 ACK 怎么办,所以就有了 RTO,这个机制好理解,但是 RTO 设置为多长合适这是个难题。
在最开始的时候,RTO 的周期是 2 倍的 RTT,RTT 可以很简单得计算得到,但是,如果设置为固定值,那么显然对于多变的网络环境来说是不合适的,所以 TCP 就采用动态的方式进行,一个比较常用的动态数值是:
RTO = SRTT + 4 * RTTVAR
其中 SRTT 是指数加权移动平均值: SRTT = α SRTT + (1-α) SRTT
RTTVAR 是往返时间变化值: RTTVAR = β RTTVAR + (1 - β)|SRTT - RTT|
这样的话,RTO 就会对真实情况变得敏感。
但是这里还有一个问题,那就是如果出现了重传的情况,怎么确认是第一次发送的 ACK 还是第二次的 ACK 呢?甚至于是更后面的重试 ACK?为了避免这个问题,常见的 TCP 实现对于重传的包是不会进行 RTO 更新的。对于重传,TCP 实现也会以“指数避让”的形式进行时间递增,同时,有一个最终的最长超时时间。
2. 持续定时器
这里所谓的持续定时器其实就是前面说的滑动窗口定时器,发送端如果长时间没有接收到窗口的刷新,那么就会在定时器结束之后发送一个询问包用于查询窗口的大小,从而确定自己是否可以再次发送。
3. 保活定时器
因为 TCP 是有状态的连接,如果一直没有数据需要传输的话,那么中间的节点都会保持着连接,那么如果确定发送端或者接收端已经因为意外的情况突然消失了呢?例如一台机器因为掉电离线了,那么后续的节点都会永远接收不到数据的,所以这里 TCP 为了避免这种情况,设置了一个 Keep Alive 计时器,如果长时间没有数据传输,时间长到保活定时器超时,那么接收端就会向发送端询问是否还在线,从而决定是否要释放连接。
但是,默认的保活定时器超时时间都很长,我遇见过的是 2 小时的,所以如果业务上有需要的话,最好自己在应用层上进行保活设计。
4. Timeout 定时器
最后一个定时器就是 TCP 结束的时候的定时器了,我们都知道 TCP 关闭的时候是四次挥手,当挥手完之后,连接处于 TIMED WAIT 状态时,会持续 2 个 RTT 的时间,目的就是为了让网络中的所有这个连接数据包都消失了。
但是,在一些高并发的网络上,这个定时器并不是必须的,而可以支持连接重用,在这种情况下,对于遗留的数据包会被忽略掉。