📘 第一章 了解Web及网络基础
📄 一句话概括

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 秒实验

  1. 把循环次数改成 2000,看 p99 有没有变化(大概率更差)。
  2. 改成同时开 10 个线程各建 200 次,看总耗时和单线程差多少。
  3. 把 create_connection 换成先 socket() 再 connect(), 看两段各占多少——建 socket 本身在 Windows 上并不便宜。

常见误区

  1. 「三次握手是为了安全」——它只证明双方能收发,不做身份验证。
  2. 「TCP 按包编号」——按字节编号,这是理解粘包与拆包的关键。
  3. 「平均延迟能代表体验」——极端值才决定用户感知,必须看 p95/p99。
自测三问(点开对答案) 1. 为什么握手要三次而不是两次? —— 两次只能证明单向可达,第三次 ACK 才能让服务端确认「我发的包客户端能收到」。 2. 序号为什么按字节算? —— 因为 TCP 是字节流。按字节编号才能表达「我收到了前 N 个字节」这种累计确认。 3. p99 比中位数高三十倍,说明什么? —— 说明有少数请求特别慢,原因通常是调度、计时器精度或锁竞争,不是带宽。

IP 送包、TCP 保可靠,还差一环:怎么先找到那台机器的地址。

相关笔记