文章目录
- 前言
- 序号与确认序号
- 超时重传
- RTO
- Jacobson算法
- 内核中超时时间的计算
- 滑动窗口
- 滑动窗口
- 延迟应答
- 流量控制
- 拥塞控制
- 慢启动
- 拥塞避免
- 快重传
- 快速恢复
- 保活机制
- 参考资料
前言
TCP(Transmission Control Protocol,传输控制协议)是互联网最重要的协议之一,为应用层提供可靠的、面向连接的数据传输服务。它广泛应用于 HTTP、FTP、SMTP 等协议中,保障数据能够准确、有序地到达目标设备。本文将主要介绍TCP其他保证可靠性机制
序号与确认序号
TCP是面向字节流的协议,数据在传输时并不以数据包为单位,而是以字节为单位进行传输。每个 TCP 数据段都会携带一个序号,指示数据段中第一个字节的位置, 如果将字节流看作在两个应用程序间的单向流动,则可以看作 TCP 用序号对每个字节进行计数。序号是32 bit的无符号数,序号到达 23^2-1后又从0开始, 由于IP协议是不可靠无序的, 不同的 IP 报文可能通过不同的路由到达目标主机的顺序不同, TCP 要保证可靠性, 就需要对这些乱序的报文进行排序, TCP 报文中的 32位序号就是用于报文排序
首部 32位确认序号用于确保数据传输的可靠性和顺序性。它是接收端告诉发送端,在这个确认序号之前的报文都收到了,
例如:如果发送方发送了1000 - 1099 和1100 - 1199的数据, 如果接收端已经成功接收到这两个报文的数据,那么它的确认序号将是 1200,表示1200以前的报文都收到了, 如果发送丢包, 比如接收方只接收到了 1000 - 1099 的数据, ,那么它的确认序号将是 1100, 如果是前一个报文1000 - 1099 丢失后一个报文收到, 根据确认序号的意义, 此时确认序号不可能是1100和1200, 确认序号将是1000
超时重传
TCP 提供可靠的运输层。它使用的方法之一就是确认从另一端收到的数据。但数据和确认都有可能会丢失。TCP 通过在发送时设置一个定时器来解决这种问题。如果当定时器到期时还没有收到确认,它就重传该数据
RTO
超时重传机制中的关键是 重传超时时间(Retransmission Timeout,RTO),即发送方等待接收方确认的最大时间。RTO的选择是动态的,不是一个固定值,而是根据网络的当前状况来调整
- 《TCP-IP详解卷1:协议》中表示可以通过一下公式估计
RTO = A + 4D
- RFC-793 3.7 给出的计算方法为
SRTT = (ALPHA * SRTT) + ((1 - ALPHA) * RTT)
RTO = min[UBOUND, max[LBOUND, (BETA * SRTT)]]
- 其中,UBOUND是超时的上限(例如1分钟),LBOUND是超时的下限(例如1秒),ALPHA是平滑因子(例如0.8到0.9),BETA是延迟方差因子(例如1.3到2.0)。
- RFC-1122 4.2.3.1 指出RFC-793中建议的计算重传超时的算法现在被认为是不充分的, 并重新给出计算
主机TCP必须实现Karn算法和Jacobson算法来计算重传超时(“RTO”)。
- Jacobson算法用于计算平滑的往返时间(“RTT”),它包括一个简单的方差度量[TCP:7]。
- Karn算法用于选择RTT测量,确保不明确的往返时间不会破坏平滑往返时间的计算[TCP:6]。
该实现还必须包括“指数回退”机制,用于相同段的连续RTO值计算。SYN段的重传应使用与数据段相同的算法。
Jacobson算法
SRTT = aRTT + (1-a)SRTT
RTTVAR = (1-b)RTTVAR + b|SRTT-RTT|
RTO=SRTT+max(G,4×RTTVAR)
- a 是平滑因子,通常设定为7/8
- b 是另一种平滑因子,通常设置为 0.25
RFC-1122 4.2.3.1 指出:
以下值应当用于初始化新连接的估算参数
(a) RTT = 0秒。
(b) RTO = 3秒。(平滑方差应初始化为能够产生此RTO的值)。
RTO的推荐上下限已知在大规模互联网中不足以满足要求。下限应以秒的分数来衡量(以适应高速局域网),上限应为2*MSL,即240秒。
内核中超时时间的计算
参考Linux2.6内核源码中, 发现RTO初始化使用了RFC 1122
#define HZ 100
#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ)) /* RFC 1122 initial RTO value */
在static int tcp_v4_init_sock(struct sock *sk)中初始化时就会使用TCP_TIMEOUT_INIT初始化一个连接的RTO
static int tcp_v4_init_sock(struct sock *sk)
{
struct tcp_sock *tp = tcp_sk(sk);
skb_queue_head_init(&tp->out_of_order_queue);
tcp_init_xmit_timers(sk);
tcp_prequeue_init(tp);
tp->rto = TCP_TIMEOUT_INIT;//初始化RTO
tp->mdev = TCP_TIMEOUT_INIT;
......
}
在超时重传时, tatic void tcp_retransmit_timer(struct sock *sk)对RTO做处理
static void tcp_retransmit_timer(struct sock *sk)
{
......
/**
* 如果重传超时,检查当前资源使用情况并决定是否重传。
* 如果重传次数达到上限,则需要强制关闭套接字。
* 如果仅资源使用达到上限,则不重传。
*/
if (tcp_write_timeout(sk))
goto out;
......
out_reset_timer:
/* 设置重传超时时间,然后重置重传定时器 */
tp->rto = min(tp->rto << 1, TCP_RTO_MAX);//2倍扩
tcp_reset_xmit_timer(sk, TCP_TIME_RETRANS, tp->rto);
if (tp->retransmits > sysctl_tcp_retries1)
__sk_dst_reset(sk);
......
}
#define TCP_RTO_MAX ((unsigned)(120*HZ))
可以看出RTO初始值为3秒, 最大值为120秒, 每次超时重传2倍增加, 不超过120秒, 并且如果重传次数达到上限就会关闭该连接
滑动窗口
滑动窗口
对每一个发送的数据段, 都要给一个ACK确认应答, 收到ACK后再发送下一个数据段.这样做有一个比较大的缺点, 就是性能较差, 尤其是数据往返的时间较长的时候, 那么我们一次发送多条数据, 以此提高性能
滑动窗口(是缓冲区的一部分,size=min(对方接收缓冲区剩余空间的大小, 拥塞窗口大小)
- 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值, 比如一个滑动窗口有四个段
- 发送前四个段的时候, 不需要等待任何 ACK, 直接发送
- 收到第一个 ACK 后, 滑动窗口向后移动, 继续发送第五个段的数据, 依次类推
- 操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉
延迟应答
如果接收数据的主机的接收缓冲区很小, 此时如果立刻返回ACK应答, 这时候返回的窗口也比较小, 这样发送方在后续的传输中就可能会受到限制,因为它只能发送少量的数据, 为了提高传输效率, 接收方可以延迟一段时间再发送ACK, 等待一段时间让上层将接收缓冲区的数据读取, 那么自身的接收能力就提高了, 返回的窗口大小更大, 发送方就可以一次发送更多的数据, 当然也不是所有包都能延迟应答, 一些实时性较高的场景会导致应用层性能下降
流量控制
接收端处理数据的速度是有限的, 如果发送方太快以至于接收方来不及处理的, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应, 因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度, 这个机制就叫做流量控制(Flow Control)
- 接收端将自己可以接收的缓冲区大小放入TCP首部中的 16位窗口大小字段和附加首部选项 ( 16位可能不够 ) 通知对方自己的接收能力
- 如果接收端缓冲区满了,就会将窗口置为0, 这时发送方不再发送数据,但是会定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
拥塞控制
TCP 协议中用于防止网络出现过度拥塞的机制。它的目标是确保网络在高负载下依然能够有效传输数据,避免由于拥塞引起的丢包和延迟问题, 因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵, 在不清楚当前网络状态下, 贸然发送大量的数据可能会加重网络拥堵程度, 首先是慢启动拥塞窗口指数增长, 拥塞窗口达到阈值后成线性增长, 直到发生大量丢包, 认为网络堵塞, 拥塞窗口阈值将置为当前拥塞窗口大小一半, 然后将拥塞窗口设置1, 重新慢启动
慢启动
慢启动为发送方的 TCP 增加了另一个窗口:拥塞窗口(congestion window),记为 cwnd
-
初始时,TCP 的 拥塞窗口 被设置为一个较小的值,通常为 1 或 2 个最大报文段(MSS,Maximum Segment Size)。
-
每收到一个 确认(ACK),拥塞窗口会 增加 1 个 MSS。
-
在慢启动阶段,拥塞窗口每经过一个往返时间(RTT),就会指数增长。这种指数增长非常快速,直到达到网络的承载能力或进入下一个阶段。
-
当TCP开始启动的时候,慢启动阈值等于窗口最大值
-
当拥塞窗口达到阈值时,进入 拥塞避免阶段
拥塞避免
当慢启动的拥塞窗口超过慢启动阈值的时候,不再按照指数方式增长,而是按照线性方式增长
- 在拥塞避免阶段,拥塞窗口每经过一个 RTT 就增加一个 MSS(线性增长),而不是像慢启动阶段那样指数增长。
- 线性增长的目的是平稳地增加数据传输速率,避免网络中出现拥塞
- 当网络状态稳定且没有发生拥塞时,TCP 会逐步增加发送窗口,充分利用带宽
- 当拥塞发生时(超时或收到重复确认),拥塞窗口阈值被设置为当前窗口大小的一半(拥塞窗口和接收方通告窗口大小的最小值,但最少为 2个报文段)。此外,如果是超时引起了拥塞,则拥塞窗口被设置为1个报文段(重新开始慢启动)。
快重传
当发送方接收到 三个重复的 ACK,它推测网络中丢失了某些数据包。发送方会立即进行重传,这时会进入快速恢复阶段
结合之前的确认序号, 比如发送方发送了序号1000 - 1099 1100 - 1199 1200 - 1299 1300 - 1399 这四个报文, 如果第一个报文丢失, 因为确认序号表示该序号的以前的报文都收到了, 第一个报文没有收到, 即时后面的报文都收到了, 接收方对于后面几个报文的应答中确认序号依然是1000, 此时发送方收到三个确认序号相同的ACK, 发送方就知道是1000这个报文丢失了, 此时发送方会立即进行重传
快速恢复
快速恢复是在发生丢包情况下, 用来快速恢复数据的发送速率,并避免过度减小拥塞窗口, 通常和快重传搭配,
- 将 拥塞窗口设置为当前的慢启动阈值,即 cwnd = ssthresh。
- 将 慢启动阈值(ssthresh)设置为当前的 cwnd / 2,目的是减少发送速率,避免过多的数据继续传输到网络中。
- 发送方继续发送数据,但 cwnd 不会像慢启动那样从 1 MSS 开始增大,而是通过线性增长恢复发送速率。
保活机制
TCP 保活机制是确保 TCP 连接在长时间空闲的情况下不会因为网络故障或对方设备问题而中断的机制。它通过周期性地发送小的数据包来检查连接的状态,确保通信双方都处于活动状态。
通过一个半开放连接发送数据会导致返回一个复位, 但那是在来自正在发送数据的客户端。如果客户已经消失了,使得在服务器上留下一个半开放连接,而服务器又在等待来自客户的数据,则服务器将永远等待下去。保活功能就是试图在服务器端检测到这种半开放的连接
- 检测死连接:TCP 保活机制主要用于检测长时间没有数据传输的连接是否仍然处于活动状态。如果对方没有响应,发送方可以确定连接已经断开或发生了网络问题。
- 避免空闲连接超时:一些防火墙和路由器可能会在连接空闲时间较长时自动断开连接。TCP 保活机制可以防止这些设备因空闲超时而丢弃连接
TCP 保活机制通过周期性地发送保活探测包来检查连接是否仍然有效
- 空闲连接:在 TCP 连接长时间没有数据传输时,发送方开始启动保活机制。这个空闲时间通常由 TCP 保活探测间隔(TCP_KEEPIDLE) 配置。
- 发送保活探测包:如果没有接收到数据包,TCP 会按照一定的间隔周期发送小的 保活探测包(通常是 1 字节的数据包),并等待接收方的响应。
- 等待接收方响应:如果接收方仍然存在并且连接正常,它会返回一个 ACK 来确认收到保活探测包, 如果发送方连续发送了多个保活探测包都没能得到响应, 那么发送方会认为该连接已经关闭, 从而关闭连接
参考资料
- 《TCP/IP详解 卷1: 协议》
- Linux2.6内核源码
- RFC - 793(RFC 793: Transmission Control Protocol)
- RFC - 1122(RFC 1122: Requirements for Internet Hosts - Communication Layers)