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

在地址栏敲一个网址、按下回车,浏览器做的第一件事不是发请求,而是先查这个名字对应哪台机器。 HTTP 只是这条链路上的一环,前面还有 DNS 和 TCP 两道门。

📝 本节和前面几节的关系

这是全书的开场。 往后十章都在拆这一屏里发生的事:第二章看请求和响应的字面,第三章看报文内部, 第四章看那一串三位数字,第六章看首部,第七章看为什么还要加 TLS。 先把「一次访问分几段」立起来,后面才挂得住。

一、浏览器不是直接和服务器说话的

输入网址、回车之后,浏览器手里只有一串字符,比如 https://www.example.com/index.html。 它不知道这台机器在哪,也不知道怎么把字节送过去。它必须先问路。 问路这一步交给 DNS:把 www.example.com 翻成一个 IP 地址。 拿到地址之后,才轮到建立连接(TCP),连上之后才轮到 HTTP——HTTP 是跑在这条连接里的对话, 它自己不会把字节送到对端。 所以「用 HTTP 访问 Web」这句话省掉了两个前提。 真实顺序是:解析名字 → 建立连接 → 发请求 → 收响应 → 渲染。

二、本机实测:三段路各花了多久

下面这个 Demo 在沙箱里起了个真的 HTTP 服务端,然后按「解析 → 连接 → 等首字节」三段分别计时。 真跑出来暴露了一个课本不会提的坑:系统把 localhost 先解析成 IPv6 的 ::1, 而服务端只监听了 IPv4 的 127.0.0.1,于是第一条候选地址直接超时, 整整白等了六百多毫秒——占总耗时的九成以上。 这不是网络的错,是「一个名字对应多条地址」这件事的必然代价。 浏览器会做「Happy Eyeballs」:同时试 IPv6 和 IPv4,谁先通用谁。 自己写程序时如果只按顺序试,就会踩这个坑。

三、三段各自的量级

本机回环上三段都不到一毫秒,所以看起来毫无意义。换到真实网络,量级立刻变: | 环节 | 跨城典型耗时 | 受什么影响 | |---|---|---| | 域名解析 | 10~100 毫秒 | 缓存命中与否、递归层数 | | 建 TCP 连接 | 1 个往返 ≈ 40 毫秒 | 物理距离、经过几跳 | | 请求到首字节 | 1 个往返起 | 服务端处理时间 | | 传完正文 | 看体积 | 带宽与拥塞窗口 | 一个「什么都没做」的页面,光前两步就要一百毫秒上下。 这也是为什么复用连接(2.7 节)比压缩内容更立竿见影。

四、动手 Demo:把一次访问拆成三段分别计时

先起一个只回固定内容的服务端,再用裸 socket 走一遍。 三段各自 perf_counter() 卡一下,就知道时间花在哪。

with H.server() as port:
    t0 = time.perf_counter()
    infos = socket.getaddrinfo(u'localhost', port)
    t1 = time.perf_counter()
    first = infos[0][4][0]          # 系统给的第一条是 ::1
    try:
        s = socket.create_connection((first, port), timeout=0.6)
    except OSError:
        # 服务端只监听 127.0.0.1,退回 IPv4
        ipv4 = [x for x in infos if x[0] == socket.AF_INET][0][4][0]
        s = socket.create_connection((ipv4, port))
    t2 = time.perf_counter()
    s.sendall(REQ11)                # GET /index.html HTTP/1.1
    head = s.recv(1)                # 只要第一个字节
    t3 = time.perf_counter()

实跑输出:

--------------------------------------------------------------------
一个网址敲下去,三段路各花多久
--------------------------------------------------------------------
  解析 localhost 用了        18.993 毫秒
  候选地址                   2 条
    IPv6                     ::1
    IPv4                     127.0.0.1
  第一条候选                 ::1 连不上(TimeoutError,白等 613 毫秒)
  实际连上的                 127.0.0.1
  建 TCP 连接用了            0.848 毫秒
  发出到首个字节             3.176 毫秒
  整段合计                   636.471 毫秒
  其中白等掉的               613 毫秒(占 96%)
  响应总字节                 138
  这里踩了一个真坑:系统把 localhost 先解析成 IPv6 的 ::1,而服务端只监听了 127.0.0.1,于是第一条连不上。
  本机回环上三段都不到一毫秒;真实网络里域名解析和建连接各要一个往返。

输出里最值得注意的是那行「其中白等掉的」——它占了九成。 一个名字对应两条地址,试错一次要付一个超时的代价。 第二件事:响应总字节 138。这一屏的响应只有一百多个字节,其中真正的正文是 40 字节。 HTTP 的固定开销在这个尺度上很显眼,这正是第六章要讲的首部,也是 HTTP/2 要压头的原因。

30 秒实验

  1. 把 socket.getaddrinfo 换成 socket.gethostbyname 再跑一次,看候选地址还剩几条——它只回 IPv4。
  2. 把服务端改成监听 ::(全地址族),再跑一遍,看还会不会白等。
  3. 把 REQ11 里的 Connection: close 删掉,看客户端会不会卡住不返回。

常见误区

  1. 「输入网址就开始了 HTTP 通信」——中间还隔着 DNS 和 TCP,这两个都不通的时候, HTTP 一个字都发不出去。
  2. 「localhost 就是 127.0.0.1」——它可能先解析成 ::1,只监听 IPv4 的服务端会连不上。
  3. 「响应慢一定是网慢」——同一屏里能拆出解析、连接、首字节、传输四段,先定位再动手。
自测三问(点开对答案) 1. 从敲下网址到看到页面,按顺序有哪几个环节? —— 解析名字(DNS)→ 建立连接(TCP 三次握手)→ 发请求 → 收响应 → 渲染。HTTP 只占中间两环。 2. 为什么一个名字会对应两条地址? —— 因为同一台机器可以同时有 IPv4 和 IPv6 地址,解析器按系统策略给候选列表。 3. 响应 138 字节里正文只有 40 字节,这说明什么? —— 短请求里协议开销占比极高。所以 HTTP/2 要用 HPACK 压首部,HTTP/3 还要压帧头。

这一节只到「分几段」。下一节往回退一步,看 Web 当初是为了什么被造出来的。

相关笔记