《系统架构师成长之路》第 3 期|计算机网络:TCP 三次握手与可靠传输
导读 :第 2 期我们用一次
malloc(16)把内存管理从头看到尾。本期进入计算机网络线:从"为什么 TCP 需要三次握手"出发,推导可靠传输的三大支柱------序号确认、超时重传(RTO 的数学)与滑动窗口,最后画出拥塞控制的 cwnd 曲线。读完你会真正理解:可靠,是拿什么换来的。
一、为什么需要 TCP:可靠不是免费的
IP 层只做"尽力而为"(best-effort):数据报可能丢失、重复、乱序。TCP 要在这条不可靠的管道上,向上层提供字节流式、可靠、有序的传输。可靠性的代价是:头部开销(20 字节)、握手延迟、拥塞控制下的吞吐波动。
一个架构师必须记住的对照:TCP 用"确认-重传"换可靠性,UDP 用"不确认"换实时性。飞控遥测、实时音视频这类容忍少量丢失、不能容忍延迟抖动的场景,往往选 UDP + 应用层自建重传。
二、三次握手:为什么必须是三次
先看时序(如图 3-1 所示):客户端发 SYN, seq=x;服务端回 SYN+ACK, seq=y, ack=x+1;客户端再回 ACK, seq=x+1, ack=y+1,连接建立。

图3-1 TCP 三次握手:①②③ 三次报文交互,双方各维护一套状态机;seq/ack 互相确认对方初始序号。
为什么两次不够?------反证法推导
假设只用两次握手(客户端 SYN,服务端 SYN+ACK,连接即建立)。构造一个真实会发生的时间线:
- 客户端发起连接请求 A(seq=x),报文在网络中滞留(比如路由器缓存队列拥塞,超时了)。
- 客户端等不到回应,发起连接请求 B(seq=x'),这次正常完成握手,数据传输完毕,连接关闭。
- 滞留的旧报文 A 此时才到达服务端 。服务端收到 A,误以为是新的连接请求,回复 SYN+ACK------于是服务端单方面认为连接建立,一直等待客户端发数据。
- 但客户端根本没有这个连接(它认为已经关闭),于是服务端资源被白白占用,且一旦收到旧连接的数据还会造成数据错乱。
三次握手如何解决 :服务端在收到 SYN 并回复 SYN+ACK 后,连接并未建立,必须等客户端的第三个 ACK 才进入 ESTABLISHED。旧报文 A 到达时,服务端虽然会回 SYN+ACK,但客户端不会应答(它没有这个连接),服务端超时后自行释放------错误连接建立不起来。
关键推导结论:三次握手让"发起方"掌握最终确认权------只有真正想建连的一端能完成第三次 ACK,滞留的旧请求永远无法"单方面"建立连接。
三次握手的两个工程细节
- SYN 泛洪(SYN Flood) :攻击者只发 SYN 不回 ACK,让服务端堆积半连接。对策:
tcp_syncookies(把连接信息编码进 SYN+ACK 的序号里,不占用半连接队列)。 - seq 初始值 ISN:RFC 793 建议 ISN 随时间递增(每 4 微秒 +1),防止旧连接的数据段混入新连接。
三、可靠传输支柱一:序号与确认
TCP 把字节流编号,每个字节一个序号。接收方确认连续收到的最后一个字节 :ack = 期望收到的下一个序号。
超时重传 RTO 的数学推导(RFC 6298)
第一步:RTT 采样。 每收到一个 ACK,测量发送到确认的时间差,得到样本 RTT。
第二步:加权平均平滑。 用指数加权移动平均(EWMA)估计"正常"的往返时延:
SRTT_new = (1 - α) · SRTT_old + α · RTT_sample
取 α = 1/8。推导技巧:越新的样本权重越大(α 越大对新样本越敏感);α 是 1/8 是因为工程上优先"稳"------网络抖动大时,平滑值不剧烈跳动。
第三步:估计方差。 RTT 本身是波动的,重传超时不能只盯平均值,要给一个"波动裕量"。RFC 6298 用 RTTVAR 近似方差:
RTTVAR_new = (1 - β) · RTTVAR_old + β · |RTT_sample - SRTT_old|
取 β = 1/4。这里的技巧是:用一阶差分 |RTT_sample − SRTT_old| 代替真正的方差公式,省掉平方/开方,工程上足够用。
第四步:合成 RTO:
RTO = SRTT + 4 · RTTVAR
推导含义:把 RTT 近似看成"均值 ± 4 倍标准差"覆盖的区间------4 倍保证绝大多数正常 ACK 不会误判超时;若真的超时,说明网络状态已变,RTO 按指数退避(×2)。
四、可靠传输支柱二:滑动窗口
光靠"发一个等一个"(停等协议)效率太低:链路往返一次只能发一个报文。TCP 用窗口允许一次性发送窗口内的多个报文,如图 3-2 所示。

图3-2 发送窗口示意:窗口大小 = min(cwnd, rwnd);累计确认 + 窗口滑动是"流水线"效率的关键。
累计确认与快重传
接收方只确认连续收到的字节 ,不必逐段回 ACK。若中间缺了一段,后续到达的段都会触发"重复 ACK"(值等于缺口的期望序号)。发送方连续收到 3 个重复 ACK ,判定该段丢失------不等超时立刻重传,叫快重传。
推导为什么是 3 而不是 1 或 2:个别报文乱序到达(如 4 先到、3 后到)也会产生 1~2 个重复 ACK;用 3 个做阈值,是在"误判丢包"(乱序)与"检测延迟"(超时)之间取的工程折中。
五、可靠传输支柱三:拥塞控制
窗口由两把锁共同决定:接收方通告的 rwnd(流量控制)+ 发送方维护的 cwnd(拥塞控制)。实际发送窗口 = min(cwnd, rwnd)。
cwnd 随时间的变化规律如图 3-3 所示,分四阶段:

图3-3 cwnd 随 RTT 的变化:横轴为 RTT 轮次,纵轴为 cwnd(单位 MSS)。
慢启动:指数增长
连接刚建立,不知道网络容量,从 cwnd=1(个 MSS)开始,每个 RTT 翻倍:
第 k 个 RTT 后:cwnd_k = 2^k (MSS)
推导:每收到一个 ACK,cwnd += 1------因为 ACK 确认了 cwnd 个段,一个 RTT 内全部收到后 cwnd 翻倍。指数增长直到 cwnd >= ssthresh(初始约 16 或 65535 字节)。
拥塞避免:线性增长
超过 ssthresh 后改加法增长,每个 RTT 只 +1 个 MSS:
cwnd = cwnd + 1 (每个 RTT)
推导技巧:相比慢启动的"每 ACK +1",这里是"每 RTT +1"------实现上每收到 ACK 只增加 1/cwnd,凑满一个 RTT 才 +1。目的:在接近网络容量时放慢增速,避免瞬间打爆网络。
丢包后:快重传 + 快恢复
检测到 3 个重复 ACK 时:
ssthresh = max(cwnd/2, 2) // 阈值降到一半(推导:保守假设网络容量只剩一半)
cwnd = ssthresh // 不回到 1,直接降到新阈值,继续线性增长
推导技巧:快恢复相对"超时重传"(cwnd 直接回 1)的改进在于------既然还能收到重复 ACK,说明网络链路仍在工作,没必要清空窗口;只有真正的超时(网络可能彻底拥塞)才 cwnd=1 重新慢启动。
六、架构视角:什么时候不用 TCP
TCP 的可靠是"端到端重传",代价是延迟不可控(最坏情况一个段重传多次、队头阻塞)。因此:
- 实时遥测/流媒体/游戏:用 UDP,丢包靠应用层补偿(如飞控 MAVLink over UDP、WebRTC);
- 需要可靠且能容忍延迟:TCP;
- 既要低延迟又要可靠:QUIC(在 UDP 之上实现类似 TCP 的可靠 + 多路复用,且握手 1-RTT)。
架构决策的本质:在可靠性、延迟、吞吐三者之间显式地做权衡,而不是默认全要。
小结
- 三次握手 = 发起方最终确认,防滞留旧 SYN 误建连(反证法推出"两次不够");
- 可靠传输 = 序号 + 累计确认 + RTO(EWMA 推导出
RTO = SRTT + 4·RTTVAR)+ 滑动窗口(min(cwnd, rwnd)); - 拥塞控制 = 慢启动(指数)/ 拥塞避免(线性)/ 快重传快恢复(阈值减半)三阶段;
- 架构权衡:TCP 可靠换延迟,UDP 实时换可靠,QUIC 试图兼得。
思考题
- 用反证法证明"四次握手"是多余的(提示:第三次 ACK 后双方都确认了什么?)。
- 如果 RTT 采样混入"重传报文的 ACK"(歧义问题),SRTT 会被拉大还是拉小?Karn 算法怎么处理?
- 为什么快恢复后 ssthresh 取 cwnd/2 而不是更小?网络容量假设在哪里成立、哪里不成立?
下期预告
第 4 期「HTTP/HTTPS:从一次请求到 TLS 握手」------把第 2 期"一次请求的六层旅程"落到 HTTP 协议层:报文结构、缓存、连接复用,以及 TLS 握手如何用非对称+对称加密保护数据。
参考资料
- RFC 793 --- Transmission Control Protocol(TCP 协议原始规范)
- RFC 6298 --- Computing TCP's Retransmission Timer(RTO 计算的权威推导)
- RFC 5681 --- TCP Congestion Control(慢启动/拥塞避免/快重传/快恢复)
- 《TCP/IP 详解 卷 1:协议》(第 17--21 章)
- Kurose & Ross《计算机网络:自顶向下方法》第 3 章
- TCP 拥塞控制动画演示(Interactive TCP Congestion Control)