第 10 章 请求的上行:HTTP 与 HTTPS
副题:报文、连接、加密——浏览器和服务器之间,到底隔着什么
第 9 章我们让页面能渲染、能交互了。现在,用户在浏览器里点下"发布内容"——请求离开浏览器,走上网络。它要去哪?怎么去?路上会发生什么?本章跟着一次真实的请求走完网络段:先看慢请求怎么排查,再看请求的"语言"(HTTP 报文)、请求的"交通"(TCP 连接)、请求的"保险箱"(HTTPS 加密)。
本章要建立的认知
- HTTP 的核心实体是报文:请求报文和响应报文——第 7 章讲的接口约定(方法、状态码),都在报文里有固定的位置;
- HTTP 是无状态的协议——这个设计让它简单、可缓存、可扩展,代价是"记不住用户";
- HTTPS 就是 HTTP 外加一层加密(TLS)——它成为默认,是因为明文在互联网上等于裸奔。
10.1 慢请求之谜
小明点了"发布内容",页面转圈转了 8 秒才成功。这不是第一次了——"接口有时候慢"。
服务器没挂,页面能打开,其他接口也正常。"慢"比"挂"更常见:系统还活着,但用户在流失。第 8 章的白屏是"没画出来",本章的问题是"画出来了但要等"。
慢在哪?
打开开发者工具的 Network 面板,点开那个慢请求,浏览器把时间线切成了四段:
- 排队(Queuing):请求在浏览器里等出发。可能是并发连接数满了(同一域名通常限 6 个),也可能是前面的请求占着道。
- 连接(Stalled/Connection):建立 TCP 连接的时间。每次新连接都要先"握手",这是过路费。
- TTFB(Time To First Byte):请求发出后,到收到第一个响应字节。服务器处理慢、数据库慢,都体现在这里。
- 下载(Content Download):响应体传过来的时间。返回的数据越大,这段越长。
小明这个 8 秒的请求,时间线一目了然:连接段占了 5 秒。服务器处理并不慢,慢在每次请求都重新建立连接。浏览器和服务器之间没有现成的"高速公路",每次都要先"修路"——这就是 TCP 的三次握手。
排查口诀:先看时间线分段,再看慢在哪段。
- 排队慢 → 浏览器侧(并发限制、连接没复用)
- 连接慢 → 网络侧(握手、距离)
- TTFB 慢 → 服务端侧(第 12、14 章的战场)
- 下载慢 → 内容侧(响应太大)
"接口有时候慢"的"有时候",往往就藏在"每次请求都重新握手"里。握手有固定成本,请求越频繁,成本越明显。
浏览器限制同一域名并发连接数,是为了保护服务器——如果放开限制,一个页面同时开几十个连接,小服务器直接被打趴。这是 HTTP/1.1 时代的"交通规则"。HTTP/2 的多路复用(一个连接并行多个请求)把它绕开了,但"限制存在的理由"不会过时。
间歇性慢更难查。 不是每次都慢,说明常态路径没问题。慢的时候发生了什么?三个常见候选:
- 连接重建:连接池里的连接被服务器关了,这次请求重新握手
- 网络抖动:丢包触发 TCP 重传,传输层在悄悄补救
- 服务端偶发:某个请求赶上 GC 或慢查询(第 14 章)
不管哪种,Network 面板的时间线分段依然是最快的定位工具。分段会告诉你慢在传输还是慢在服务端,剩下的事交给对应章节。
还有一类"转圈"的原因预告给第 11 章:DNS 解析慢。浏览器要先问 DNS"www.example.com 是哪台服务器",DNS 没响应,请求根本发不出去。网络段的"慢"按顺序查:DNS → 连接 → 发送 → 等待(TTFB)→ 下载。每一段都有对应的主人。
小明的问题怎么修?
确认时间线(连接段 5 秒)→ 检查连接是否复用(Network 面板看 Connection ID,相同表示复用,不同表示重建)→ 发现前端代码每发一个请求就新建连接 → 把连接池用起来 → 5 秒变 0.3 秒。
服务器本身没问题,问题出在连接策略。网络段的慢,解法常常就在网络段自己身上。
10.2 报文:HTTP 的语言
浏览器和服务器之间对话,用的是 HTTP(HyperText Transfer Protocol)。第 7 章讲的接口约定——方法、URL、状态码——最终都要写成 报文(message)才能发出去。
报文的骨架永远四部分:起始行、首部、空行、主体。先看一个真实的请求:
POST /v1/contents HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer xxxxx
{
"title": "一台二手相机",
"body": "九成新,快门数两万……"
}
第一行是请求行:方法(POST)+ 路径(/v1/contents)+ 协议版本。下面几行是首部(Headers),键值对形式,描述请求的元信息。空行之后是主体,放实际要提交的数据。
再看响应:
HTTP/1.1 201 Created
Content-Type: application/json
{
"id": 1024,
"title": "一台二手相机"
}
第一行换成状态行:协议版本 + 状态码(201)+ 原因短语(Created)。下面同样是首部和主体。
GET 和 POST 在报文层面的区别直接写在报文上:GET 没有主体。 它只是"读",没什么可带的,请求行加首部就结束了。POST 带着内容来,主体里放要提交的数据。第 7 章说"GET 幂等、POST 不幂等",报文形态就是它的物理体现:GET 连内容都没有,自然不会产生副作用。
再看一个错误响应——库存不足时:
HTTP/1.1 409 Conflict
Content-Type: application/json
request-id: a1b2c3
{
"code": "OUT_OF_STOCK",
"message": "商品库存不足"
}
第 7 章设计的三层错误结构,在这里一字不差地写进了报文:409 是状态码(机器知道冲突)、OUT_OF_STOCK 是业务码(程序知道原因)、message 给人看、request-id 用来排障。第 7 章是"约定",本章是"约定写进协议的样子"。
把整张接口清单放回报文里看,对应关系是完整的:方法和路径在请求行,查询参数在 URL,请求体在主体,凭证在 Authorization 首部,分页信息在响应主体。第 7 章设计的每一个字,最后都会变成报文里的字节。
首部是元信息,主体是实际内容。 一个请求的首部通常几百字节——Host、Content-Type、Authorization 等。如果请求很频繁,首部就成了"重复的过路费"。HTTP/2 的头部压缩就是冲这个来的。
主体大小则看业务。第 7 章讲 DTO 时说过"少传多余字段",在报文层面就是让主体更小。响应太大时,还有一种分块传输(Transfer-Encoding: chunked)——服务器边生成边发,浏览器边收边显示,不用等全部生成完才开始下载,首字节到达更快。
常见首部先熟悉一下,后面章节会用到:
- Host:去哪台服务器
- Content-Type:主体是什么格式(JSON、表单、图片)
- Authorization:我是谁(第 13 章认证)
- Cache-Control:能不能缓存(第 16 章)
- Set-Cookie / Cookie:服务器让浏览器存小纸条,后续请求自动带上——HTTP 本身无状态,Cookie 是给无状态协议"补记忆"的第一块砖。第 13 章的 Session 和 JWT 之争,核心就是"这块砖怎么用"。
报文设计里有三个细节值得想想:
为什么首部是文本键值对,不是二进制? HTTP 诞生于 1990 年代初,设计目标之一是"人能读懂"。用文本协议,调试工具直接看就知道发生了什么。代价是文本比二进制占空间,HTTP/2 后来引入了二进制帧优化。
为什么要有空行? 它是首部和主体的分隔线。没有它,接收方不知道元信息在哪结束、内容从哪开始。
为什么状态码要带原因短语? 201 是给程序看的,Created 是给人看的——和第 7 章 error code / message 的设计同一个思路。
报文是你每次 F12 都能看到的实物。 打开开发者工具的 Network 面板,点任意请求,Headers 标签页里就是完整的报文。第 8 章用它排查白屏,本章用它读协议——同一套工具,看的是同一趟请求的不同层面。
10.3 无状态与连接
报文要发出去,得先有一条路。这条路就是 TCP 连接。但在讲连接之前,先理解 HTTP 的一个根本设定:无状态。每个 HTTP 请求都是独立的。浏览器发"请求 1:看首页",服务器响应;再发"请求 2:看评论",服务器把第二个请求当作"另一个陌生人"来处理——它不记得刚才谁看过首页。
这个设定带来了三个好处:
第一,简单。 服务器不用维护"谁在干什么"的内存状态,处理完就忘。
第二,可缓存。 同一个 GET 请求,任何时候结果都一样,缓存可以放心复用(第 11 章 CDN、第 16 章浏览器缓存都建立在这个基础上)。
第三,可扩展。 因为服务器不记状态,任意一台机器都能处理任意请求。加一台服务器就能分担流量,不需要把某个用户"粘"在特定机器上。这是 Web 能水平扩展的根本原因,也是第 23 章负载均衡、第 26 章服务拆分的基石。
无状态也有直接的代价:服务器记不住用户。 用户刚登录完,下一个请求服务器又问"你是谁"。这就是第 13 章会话机制(Session/JWT)的来源——无状态是 HTTP 的根基,会话机制是给这个根基打的补丁。
注意一个常见的混淆:无状态是应用层的事,连接是传输层的事。 HTTP 说"我不记状态",但 TCP 说"我要先建一条可靠通路"。两者不在同一层。
浏览器和服务器之间的通路叫 TCP(Transmission Control Protocol)。HTTP 报文就跑在这条通路上。建立 TCP 连接需要三次握手:
浏览器 服务器
│ │
│── SYN ────────────────→ │ 1. 浏览器:我要连你
│←──── SYN + ACK ────────│ 2. 服务器:好,我也连你
│── ACK ────────────────→ │ 3. 浏览器:收到,开始通信
│═════════════════════════│ 连接建立,发送 HTTP 报文
为什么是三次?因为双方都要确认对方能收到自己的消息。 两次不够——服务器不知道浏览器有没有收到它的回应;四次多余——第三次已经是确认的确认。这个设计从 1970 年代定下来,五十年没变过。
三次握手有固定成本:每次新建连接都要 1 个 RTT(round-trip time,一来一回的时间,国内跨省通常 20-50ms)。小明那个 8 秒请求里,5 秒花在连接上,就是因为每次请求都重新握手。
解法是连接复用。HTTP/1.1 默认开启 Keep-Alive:一个 TCP 连接处理完请求后不立即断开,继续服务下一个请求。浏览器侧也有连接池,同一域名的请求尽量复用已有连接。
但连接不是永远开着。服务器会设超时(比如 60 秒无请求就关闭),连接池里的连接也会被回收。所以"有时候慢"的另一个常见原因,就是连接池里的连接刚被服务器关掉,下一个请求不得不重新握手。
连接的真实成本比看上去更高,尤其是 HTTPS:
- TCP 握手:1 RTT
- TLS 握手:1-2 RTT(加密协商,10.4 展开)
- TCP 慢启动:新连接前几个包发得慢,逐渐提速
所以"新建连接贵"——付的是三重过路费叠加。这也解释了为什么连接复用的收益那么大——一次握手费之外,整套"修路"成本都省了。
TCP 为什么叫"可靠传输"?因为它在底层做了三件事:ACK 确认、超时重传、顺序重组。报文在网上可能丢包、乱序,TCP 负责把"可能出错的网络"包装成"看起来可靠的管道"。HTTP 不用操心丢包,TCP 都处理了。
还有两个机制在默默工作:
- 流量控制:接收方告诉发送方"我还能收多少",防止把对方撑爆。
- 拥塞控制:发送方探测网络容量,网络堵了自动降速。
这和 10.1 的慢请求有关。网络抖动时,TCP 会自动降速重传——你在时间线上看到"连接段变长",很多时候是 TCP 在背后补救。排查"网络慢"时,不要只怪服务器:慢可能是 TCP 在替网络的不靠谱买单。
回到小明的案例。8 秒请求,5 秒卡在连接段。排查方法:看 Network 面板的 Connection ID,如果每个请求的 ID 都不一样,说明没复用连接。修法就是把连接池用起来,让 Keep-Alive 生效。5 秒变 0.3 秒——网络段的慢,解法常常就在网络段自己身上。
最后把网络段用第 8 章的关键路径算一笔账:
一次 HTTPS 请求的完整时间 = DNS 解析(第 11 章)+ TCP 握手(1 RTT)+ TLS 握手(1-2 RTT)+ 发送请求 + TTFB(服务端处理)+ 下载响应
网络段占了关键路径的大半。第 8 章说"白屏时间 = 关键路径长度",本章给出了网络段的具体算法。两章合起来,"首屏时间"才有完整的账本。
还有一个浏览器限制要知道:HTTP/1.1 下,同一域名最多约 6 个并行连接。 页面同时发 20 个请求时,14 个在排队。这个限制是 HTTP/2 多路复用要解决的核心问题——"连接是稀缺资源"这个事实,贯穿了 HTTP 的整个演进史。
10.4 HTTPS:为什么加密成为默认
报文有了,连接建了,但还有一个致命问题:HTTP 报文是明文传输的。
这意味着,报文经过的每一台设备都能看到内容——公司的路由器、小区的光猫、运营商的机房、跨国的海底光缆。任何中间人不仅能看,还能改。运营商在网页里插广告、攻击者伪装成服务器骗你提交密码、篡改转账金额或收货地址,都是明文带来的风险。
所以 HTTPS 已经是默认项。它的公式就一行:HTTPS = HTTP + TLS。TLS(Transport Layer Security)是套在 HTTP 外面的一层加密壳,解决两件事:加密(只有浏览器和服务器能读懂内容)和身份验证(浏览器确认对面真的是 api.example.com,不是骗子)。
TLS 握手怎么工作? 简化版三步走:
浏览器 服务器
│ │
│── 我要安全连接 ──────────────────────→ │
│←── 这是我的证书(含公钥)─────────────│ ① 服务器出示身份证
│ (浏览器验证书:签名、域名、有效期) │
│── 用公钥加密"会话密钥" ─────────────→ │ ② 浏览器生成临时钥匙,公钥加密传过去
│←── 收到,开始加密通信 ───────────────→│ ③ 双方用这把临时钥匙对称加密
│════════ 加密通道建立 ═════════════════│
这里有两个关键设计,必须理解:
第一,为什么非对称和对称要混着用?
非对称加密(公钥/私钥)很安全,但很慢——比对称加密慢几个量级。所以 TLS 的设计是:非对称只干一件事——安全地把"会话密钥"送过去;送完就换对称加密(AES 之类)传数据。 非对称是护送员,对称是保险箱。护送员只跑一次(握手阶段),保险箱用全程(通信阶段)。各司其职。
第二,证书到底是干什么的?
公钥本身不能证明"我是 api.example.com"——任何人都能生成一对密钥自称官网。所以需要证书:由 CA(Certificate Authority,证书颁发机构)签名的身份证明。证书里写着域名、公钥、CA 签名。浏览器验证三步:签名是否合法、域名是否匹配、有没有过期。验证通过,才相信对面是真的。
证书通常是一条链:根 CA → 中间 CA → 网站证书。根 CA 的证书内置于浏览器,是信任的起点;中间层用来隔离风险——万一出事,不用动摇根证书。日常部署中,DV 证书(验证域名所有权,免费,Let's Encrypt 就是这类,支持自动续期)对大多数业务已经足够。证书到期忘了续,是每年都在发生的线上事故,所以续期一定要自动化。
TLS 本身也在演进:TLS 1.2(当前基线)到 TLS 1.3(2018),握手从 2 个 RTT 降到 1 个,更快也更安全。每次握手优化,都是网络段性能的实打实收益。
HTTPS 的代价要诚实说:
一是握手延迟。TLS 握手在 TCP 握手之后还要再来一轮,新连接的 HTTPS 比 HTTP 慢一拍。二是证书管理——要买(或申请免费的)、要部署、要续期,到期就是"连接不安全"的报错。但对比明文被看、被改、被冒充的风险,这些成本明确划算。HTTPS 成为默认,是因为明文已经不可接受。
还有一个加强手段先认识:HSTS(HTTP Strict Transport Security)。服务器通过响应头告诉浏览器"这个域名只准用 HTTPS 访问",防止"先走 HTTP 再被劫持跳转"的降级攻击。
最容易混淆的一点:HTTPS 加密 ≠ 用户认证。
HTTPS 保证的是传输通道安全——路上的内容没人能看、没人能改。但**"你是谁、有没有权限发帖"**是另一件事,第 13 章的登录和认证才管这个。
打个比方:HTTPS 是封好的信封(路上安全),用户认证是门禁(进门要查证件)。信封保证路上没人拆,门禁保证进门的是谁。两件事分开管,别混成一句"都 HTTPS 了怎么还要登录"。
还是用关键路径看:
TLS 握手发生在白屏之前——浏览器要先完成 TCP 握手,再完成 TLS 握手,才能发出第一个 HTTP 请求。网络差的时候,两轮握手就是 2-3 个 RTT,200 毫秒以上。所以排查白屏时,如果 HTML 迟迟没到,别只盯着 CSS,先看看网络段排队/连接/握手是不是卡住了。
这也呼应了第 16 章的优化方向:连接复用和 TLS 会话复用(session resumption),都是让"关键路径的网络段"变短的手段。
10.5 HTTP 的代价与边界
HTTP 有三笔收益:简单(文本报文,人直接能读)、无状态(任何服务器都能处理任何请求,天然支持缓存和水平扩展)、开放(谁都能实现,生态最大)。但也有三笔代价:无状态导致服务器记不住用户(靠 Session/JWT 打补丁)、明文传输不安全(靠 HTTPS 打补丁)、文本协议太占带宽(靠 HTTP/2/3 的二进制帧打补丁)。
你会发现一个模式:HTTP 的每一次升级,都是在给最初的设计打补丁。 无状态是 1990 年代的产物,会话机制、加密层、二进制帧都是后来长出来的答案。第 2 章说的演进循环,在协议身上一样成立。
版本演进很快,但记住一点:接口语义从来没变。 HTTP/1.1(1997,Keep-Alive、缓存控制)→ HTTP/2(2015,二进制帧、多路复用)→ HTTP/3(2022,基于 UDP 的 QUIC,解决 TCP 队头阻塞)。三个版本只动传输效率,不动业务语义——方法、状态码、URL 始终稳定。所以接口设计可以放心依赖 HTTP 语义,就是这个原因。
HTTP/1.1 有两个限制直接催生了后续版本:队头阻塞(一个连接上的请求按顺序处理,前面的响应慢了,后面的全部排队)和半双工(同一时刻只能单向传)。HTTP/2 用帧交错和多路复用解决了它们。协议升级的本质,就是旧设计的限制被业务逼出来了。
REST 能成立,完全因为 HTTP 是地基(第 7 章回扣)。REST 的方法、状态码、缓存头,全是 HTTP 的现成语义。REST 用的就是 HTTP 的现成语言,只是把它用得更规范。 它之所以能成为 Web 通用语,就是因为它建立在所有人都会的 HTTP 之上。
关于 HTTPS,2020 年之后已没有"要不要"的决策可言——它就是默认项。浏览器对 HTTP 标"不安全",行业标准就是默认加密。真正需要权衡的是"哪些内容要付额外性能成本":全站 HTTPS 是底线;图片和静态资源走 CDN + HTTPS(第 11 章);内网服务之间是否加密,视内部网络可信度而定。案例从第一天就全站 HTTPS,对应第 4 章需求清单里的"数据不能丢"——需求清单的每一行,都要在协议层找到落点。
再埋两个伏笔:
一是网络不可靠。 TCP 的 ACK 能保证报文不丢,但不保证应用层响应一定能回来。服务器可能处理到一半崩了,所以应用层得自己决定要不要重试(第 17 章的回调重试、幂等判断,都是应对这个基本事实)。网络不可靠是 Web 的底层假设。
二是方法语义与安全。 GET 不产生副作用,这是 Web 安全的地基之一。CSRF 攻击(第 19 章)能得手,往往就是因为某些系统用 GET 干了改数据的活。严格区分 GET 只读、POST/PUT/DELETE 才改,安全审计时能少一半麻烦。
10.6 本章对应表
| 业务诉求 | 技术选择 | 为什么 | 代价/取舍 |
|---|---|---|---|
| 前后端要对话 | HTTP 报文(起始行+首部+主体) | 统一的协议语言,简单、可读、可扩展 | 文本协议占空间(HTTP/2 二进制帧补丁) |
| 服务器要记住用户 | (无状态是 HTTP 的设计) | 简单、可缓存、可扩展 | 记不住用户——会话机制打补丁(第 13 章) |
| 连接要省 | Keep-Alive / 连接池 | 三次握手每次付 1 个 RTT,复用省过路费 | 连接管理有复杂度,排队等连接 |
| 数据不能明文 | HTTPS(HTTP + TLS) | 明文可被看、被改、被冒充 | 握手延迟 1-2 个 RTT、证书要买要续 |
| 对面是不是真的服务器 | 证书链(CA 签名) | 公钥不能自证身份,需要发证机关 | 证书体系有信任成本,CA 被攻破是灾难 |
| 接口"有时候慢" | 网络时间线排查(排队/连接/TTFB/下载) | 慢在哪一段看哪一段 | 每段的原因不同,要分别排查 |
HTTP 以无状态文本报文为基石,换来简单与可扩展,代价是会话、加密、二进制帧不断打补丁。连接复用省握手成本,HTTPS 防明文风险但增延迟与证书负担。排查慢请求可以按网络时间线分段定位。
本章小结
- HTTP 的核心实体是报文:请求报文和响应报文——第 7 章讲的接口约定,都在报文里有固定的位置;
- HTTP 是无状态的协议——简单、可缓存、可扩展,代价是"记不住用户"(第 13 章会话问题从这里来);
- HTTPS 就是 HTTP 外加一层加密(TLS)——它成为默认,是因为明文在互联网上等于裸奔;
- HTTP 的每次升级都是给原始设计打补丁(会话/加密/二进制帧)——第 2 章的演进循环在协议身上同样成立;
- 网络不可靠是 Web 的基本事实——超时重试、幂等判断(第 17 章)都是它的应对。
下一章
请求的报文写好了,连接建好了,加密也加上了——但还有一个问题:**浏览器怎么知道要去哪台服务器?**小明输入的是 www.example.com,可网络世界里没有"名字",只有 IP 地址。谁把域名翻译成 IP?翻译要经过哪些关卡?还有,为什么有的页面加载飞快、有的慢吞吞——除了服务器远近,还有什么在起作用?
下一章,请求的上行(下):DNS 与 CDN——域名怎么变成 IP,以及 CDN 为什么存在。