内容整理自:https://xiaolincoding.com/network/3_tcp/tcp_feature.html
重传
超时重传
tcp 发送的一个段(segement)在发送后的一段时间(超时重传时间,RTO)没有收到应答(发送的数据丢失,或者 ACK 确认响应丢失),那么将重新发送这个数据段。
一般情况下,RTO 应该略大于 RTT (Round-Trip time)。
linux 如何计算 RTO
- 需要 TCP 通过采样 RTT 的时间,进行加权平均,算出一个平滑 RTT 的值。这个值是不断变化的,因为网络状况不断地变化。在最近采样的 RTT 的值比之前采样的 RTT 值有更大的权重。
- 除了采样 RTT,还要采样 RTT 的波动范围,这样就避免如果 RTT 有一个大的波动的话,很难被发现的情况。
具体的,由以下公式计算:
首次计算时:
R1:第一次测量得到的 RTT
SRTT = R1
DevRTT = R1 / 2
RTO = $\mu$ * SRTT + $\partial$ * DevRT
后续计算 RTO:
RTT:最近测量得到的 RTT
SRTT = SRTT + $\alpha$ (RTT - SRTT)
DevRTT = (1 - $\beta$) * DevRTT + $\beta$ * ($\left|RTT - SRTT\right|$)
RTO = $\mu * SRTT + \partial * DevRT$
SRTT 是最终的(平滑过后的)RTT(Retransmission Timeout),DevRTT 是 SRTT 和最近 RTT 的差距。
Linux 下:$\alpha = 0.125, \beta = 0.25 \mu = 1, \partial = 4$
此外,当出现一次超时时,会将下一次的超时时间设置为当前值的两倍。
快速重传

在接收端不论当前收到的数据段(segement)的 seq 是多少,都返回最后一个连续的 seq 的 ACK。
所以当发送端多次受到同一个 seq 的 ACK 时,大概率是这个 seq 的数据段丢失,那么需要重发这个数据段。
除了这个数据段丢失外,可能这个 seq 的数据段之后的一些数据段也丢失了。如果只在收到 3 个重复的 seq 的 ACK 之后重传一个数据段,那么当出现多个数据段丢失的情况下,重传丢失的数据段将耗费过多的时间。
于是就有了 SACK。
SACK
SACK (Selective Acknowledgment) 选择性确认。
在 TCP 的头部中加入一个字段来表示在 ACK 中以一组两个(最左和最右的 seq) seq 数表示已经受到的一个连续的数据段。(TCP的选项不能超过 40 个字节,所以边界不能超过 4 组)
需要发送和接受端同时支持,在 SYN 包中 SACK Permitted 选项二者都为真。
Duplicate SACK
在上面的对数据段的讨论中,我们没有考虑到接受端返回的 ACK 丢失的情况,在这种情况下,发送端会将受到的最后一个 ACK 的 seq 和当前未受到 ACK 的 seq 之间的数据段全部重发。这显然是没必要的。
那么在接收端受到这个的重发之后,返回的最新的 ACK 中除了携带最新的 seq 之外,还需要携带之前未收到的那段数据段(在 SACK 头部字段中以首尾的 seq 表示)。

D-SACK 可以解决下面几个问题:
- ACK 数据段丢失后发送端发起的重传(SACK 尾部 seq < ACK 的 seq)
- 网络延迟导致的中间某一段数据段未到达(SACK 首部 seq > ACK 的 seq)
同样需要发送端和接收端都支持。
滑动窗口
TCP 并非在前一个数据段收到 ACK 之后才传输下一个数据段。而是存在一个范围,发送的数据端的 seq 与最后一个 ACK 之间的差值不超过这个范围即可在前边的数据段没有收到 ACK 响应的情况下继续发送数据段。
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。

而 ACK 的 seq 也并不是表示只有这个 seq 的数据段收到了,而是表示截至这个 seq 的所有数据段都受到了,这个就是累计确认。
窗口大小
TCP 头里有一个字段叫 Window,也就是窗口大小。
这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
所以,通常窗口的大小是由接收方的窗口大小来决定的。
发送方发送的数据大小不能超过接收方的窗口大小,否则接收方就无法正常接收到数据。
发送方窗口:

接收端窗口:

因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。
流量控制
注意其中 Usable 是指发送方为了不发送超过接收方处理能力的数据量的当前最大发送数据量。在收到接收方的 Ack 响应之后,Usable 增大对应的值(收到确认的数据长度)。

窗口关闭
窗口探测 ( Window probe ) 报文避免因接收方窗口从 0 恢复时后的窗口报文丢失的情况。
在 3 次窗口探测之后可能直接关闭 TCP 连接。
避免小窗口
接收方设置一个上下限的阈值,降低超过阈值下限关闭窗口,上升超过上限恢复窗口。
发送方设置发送数据的窗口下限和数据下限或者收到服务端 ACK (需要服务端使用上面的策略)。
拥塞控制
swnd = min(cwnd, rwnd)
cwnd 初始值为 10
慢启动
(指数增长一点也不慢,只是基数小,从 1 开始)

拥塞避免算法
在慢启动大于一个阈值(ssthresh)之后就会使用
每当收到一个单位数据长度的 ACK (可能多个单位数据长度共用一个 ACK)时,cwnd 增加 1/cwnd。
这个阶段变为线性增长。
拥塞发生
当发生重传之后(超时或快速)
超时重传之后
ssthresh
设为cwnd/2
cwnd
重置为初始值(linux 中为 10)- 进入慢启动
快速重传之后
cwnd = cwnd/2
,也就是设置为原来的一半;ssthresh = cwnd
;- 进入快速恢复算法
快速恢复算法
- 拥塞窗口
cwnd = ssthresh + 3
( 3 的意思是确认有 3 个数据包被收到了) - 重传丢失的数据包
- 如果再收到重复的 ACK,那么 cwnd 增加 1
- 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态