TCP 把 IP 的「尽力而为」变成「一个字节都不丢」:靠序号、确认号和重传。 代价是额外一个往返的握手和维护状态的成本。
上一节说 IP 什么都不保证。 这一节补上保证——HTTP 之所以能只写「发一个请求」而不写「丢了怎么办」,全靠这一层。
一、三次握手只证明一件事
SYN → SYN+ACK → ACK,三个报文,一个往返。 它证明的是双方都能收发,不证明「你是你以为的那个人」——身份验证是 TLS 和认证层的事(第七、 八章)。 为什么不是两次?因为两次只能证明「客户端能发、服务端能收」,证明不了「服务端能发、客户端能收」。 第三次 ACK 补上这一半。 握手成本是一个完整往返。跨城 40 毫秒,就意味着还没发一个字节,四十毫秒已经花掉了。 这就是 1.2.3 节里持久连接那么值钱的原因。
二、序号与确认号是按字节算的
TCP 给每一个字节编号,不是给每个包编号。
一个 1460 字节的段,序号从 N 开始,下一个段的序号就是 N+1460。
接收方回 ACK N+1460,意思是「N 之前的我都收到了,下一个我要 N+1460」。
这种「累计确认」让丢包检测很自然:连收三个重复 ACK 就认为中间丢了,立刻重传(快重传),
不必等超时。
这也是为什么一次 recv 拿到的字节数跟应用报文无关:TCP 只看字节流,
不知道也不关心你在第几个字节画了边界。
三、实测:一次建连到底多贵
Demo 在本机回环上建了 200 次连接。中位数 0.63 毫秒,但 p99 到了 22.55 毫秒——差了三十多倍。 这不是网络问题,是 Windows 的计时器精度和线程调度造成的。 所以「本机测出来的延迟」只能当参考:真正要看的是分位数,不是平均值。 跨城按 40 毫秒算,一条连接每秒最多建 25 条。 如果要伺候一万个并发用户, 光建连这一项就需要极强的事件驱动模型——这就是为什么服务器端几乎都放弃了「一连接一线程」。
四、动手 Demo:量 200 次建连,看分位数而不是平均值
起一个常驻服务端,循环建连 200 次,每次记下耗时,排序后取各分位。 注意:这里量的是「建连」这一段,不含请求与响应。
ts = []
for _ in range(200):
a = time.perf_counter()
s = socket.create_connection((u'127.0.0.1', pt), timeout=2.0)
ts.append((time.perf_counter() - a) * 1000.0)
s.close()
ts.sort()
print(u'中位数 %.4f' % ts[len(ts) // 2])
print(u'p99 %.4f' % ts[int(len(ts) * 0.99)])
实跑输出:
--------------------------------------------------------------------
三次握手的成本:本机回环上量 200 次
--------------------------------------------------------------------
样本数 200
最小 0.3155 毫秒
中位数 0.6332 毫秒
p90 1.2299 毫秒
p99 22.5510 毫秒
最大 23.8316 毫秒
平均值 1.2094 毫秒
三次握手对应 1 个往返(SYN → SYN+ACK → ACK)
跨城按 40 毫秒算 握手就要 40 毫秒,还没发一个字节
跨城每秒最多建 25 条
本机回环上一次建连的中位数不到一毫秒,但 p99 能差几十倍——
这不是网络抖动,是 Windows 的计时器精度和线程调度在捣鬼。
内核在这不到一毫秒里做的其实就是三件事:
收 SYN、回 SYN+ACK、收 ACK 之后把连接放进已建立队列。
真实网络里这条路上还有 NAT、防火墙、负载均衡,每多一跳设备就多一次状态查询。
最小 0.3155 毫秒、中位数 0.6332、p99 22.5510、平均 1.2094——平均值明显高于中位数,
说明有少量极慢的样本在拉高均值。线上看延迟必须看 p99,看平均会被骗。
跨城每秒最多建 25 条:1000 ÷ 40。这个数很重要——它意味着「短连接」架构在跨城场景下天然有天花板。
30 秒实验
- 把循环次数改成 2000,看 p99 有没有变化(大概率更差)。
- 改成同时开 10 个线程各建 200 次,看总耗时和单线程差多少。
- 把
create_connection换成先socket()再connect(), 看两段各占多少——建 socket 本身在 Windows 上并不便宜。
常见误区
- 「三次握手是为了安全」——它只证明双方能收发,不做身份验证。
- 「TCP 按包编号」——按字节编号,这是理解粘包与拆包的关键。
- 「平均延迟能代表体验」——极端值才决定用户感知,必须看 p95/p99。
自测三问(点开对答案)
1. 为什么握手要三次而不是两次? —— 两次只能证明单向可达,第三次 ACK 才能让服务端确认「我发的包客户端能收到」。 2. 序号为什么按字节算? —— 因为 TCP 是字节流。按字节编号才能表达「我收到了前 N 个字节」这种累计确认。 3. p99 比中位数高三十倍,说明什么? —— 说明有少数请求特别慢,原因通常是调度、计时器精度或锁竞争,不是带宽。IP 送包、TCP 保可靠,还差一环:怎么先找到那台机器的地址。
- ☐ (本节完,下一节 1.5 负责域名解析的DNS服务)
相关笔记
- 36 课 · 图解TCP/IP(第5版)(三次握手、滑动窗口、拥塞控制)
- 📘 图解HTTP · 索引