§ 4.5 · Section

发送请求与服务器响应

Request & Response in Action · RFC 9110-9112

前面三节全是准备工作:找到地址、接上线、验明身份加上锁。这一节终于开口说正事了——浏览器把要求说出去,服务器那头经过好几道手把东西找出来,再把结果送回。这是整段旅程里唯一真正"传输内容"的一步,也是服务器唯一有机会出错的一步。

生活场景
🏪 你终于站到了柜台前,开口说了第一句话

号码查到了、电话拨通了、身份也互相验明了。现在你说:

"我要取 8848 号那件货。我是老客户,卡号在这儿。麻烦给我压缩包装,我拿不了太大的箱子。另外如果这货和上次那批一样,就不用重新给我了。"

柜员听完,转身去后面办:先看你卡号有效不有效(网关鉴权)→ 到货架区找 8848(应用处理)→ 发现要查库存记录(数据库查询)→ 按你要求打压缩包(压缩响应)→ 递给你并说一句"这是货,另外提醒你三天后有活动"(响应头)。

注意你那句话里的信息密度:一个动作(取)、一个目标(8848)、三个附加条件(我是谁、怎么包装、什么情况下不用给)。HTTP 请求的结构和这句话一模一样——一个方法、一个目标、一堆头部字段。
HTTP 的报文格式在 § 3.1 已经详细讲过。这一节要看的是:它在一次真实访问里怎么发、发出去之后服务器那头到底经过了几道手、以及回来的响应头怎么反过来影响你下一次访问。

上一步刚完成了什么,这一步要干什么

接力棒交到这一节时,手里有的是一条已验明身份的加密通道(§ 4.4 的产物),再加上 § 4.1 解析出来的那几样东西:方法(GET)、路径(/item/8848)、主机名。

这一节要干的事:把这些信息组装成一份规范的请求报文发出去,然后接住服务器的回复。听着简单,但这一步是整段旅程里唯一"服务器有机会犯错"的一步——前面四步全是通信层面的,出错只有"连不上"这一种;这一步开始涉及业务逻辑,出错方式一下丰富起来(404、500、403、超时、返回了错的内容)。

干完之后交给下一节的东西是:

这一节的时间成本有两部分,性质完全不同:一个来回的网络时间(由距离决定,你改不了)+ 服务器处理时间(由服务器性能与业务复杂度决定)。把这两部分分开看,是排查"网站慢"的第一步。

先把这一节的术语翻译成人话

HTTP 术语在 § 3.1 已经过了一遍,这里补的是"实战流程视角"才会碰到的那些:

术语换成大白话生活里对应的东西
请求行(Request Line)说白了就是"你开口那句话的主干:干什么 + 对什么"窗口说的"我要取 8848 号"
请求头(Request Headers)说白了就是"一堆附加说明,一行一条"快递面单上那些栏目:易碎、代收、指定时间
TTFB(Time To First Byte,首字节时间)说白了就是"从你说完话到对方吐出第一个字的间隔"你问完话,对方"嗯……"了一下才开始答的那段沉默
反向代理(Reverse Proxy)说白了就是"你以为在跟店家说话,其实先经过了一个前台"医院的分诊台:所有人先到它这儿,它再分到各科室
网关(Gateway / API Gateway)说白了就是"进门第一道关卡,管查票和限流"景区大门的安检与验票口
负载均衡(Load Balancer)说白了就是"把客人分到几个窗口去,别都挤一个"银行大堂经理引导排队的人去不同窗口
状态码(Status Code)说白了就是"这次办得怎么样的一个三位数编号"挂号机吐出的小票:成功、已满、该科室不存在
重定向(Redirect,3xx)说白了就是"这事不在我这儿办,你去那个窗口""社保业务不在 3 号窗,请到 7 号窗"
协商缓存(304 Not Modified)说白了就是"你手上那份还能用,我不重新给你了""你上次拿的那份表格没改版,接着用"
内容编码(Content-Encoding,压缩)说白了就是"我把东西压扁了寄,你收到自己撑开"洗衣店的真空压缩袋:体积小一大半,用时拆开就恢复
分块传输(Chunked Transfer)说白了就是"我边做边给你,不等全做完才一起端上来"餐厅先上凉菜,热菜陆续端,而不是全做好一起上
Cookie说白了就是"人家发给你的一个小凭据,下次来要带着"寄存柜给的便签牌号——服务方不记你是谁,靠你把牌子带来
幂等(Idempotent)说白了就是"这动作做一次和做十次结果一样"电梯上行键:按十次也还是上一趟

这张表里最值得先记住的是 TTFB,因为它是排查"网站慢"最关键的一个指标。TTFB 把"网络慢"和"服务器慢"分开了:TTFB 长而下载快 → 服务器处理慢;TTFB 短而下载慢 → 带宽或内容太大。这两种病的治法完全不同。

Analogy · 一次请求响应就是"在窗口办一件事"

打个比方,把整个请求响应过程放到银行窗口上,每一部分都有精确的对应:

你的那句话 = 请求报文。
· "我要取" = 方法(GET / POST / PUT / DELETE,动作类型)
· "8848 号那件" = 请求目标(路径)
· "我是老客户,卡号在这儿" = Cookie 头(身份凭据)
· "给我压缩包装" = Accept-Encoding 头(我能接受压缩过的内容)
· "如果和上次那批一样就不用给了" = If-None-Match 头(协商缓存)

柜员转身去办的那段时间 = 服务器处理时间。这段时间里你什么都看不到,只能干等——这就是 TTFB 那段沉默。

柜员回来递给你的东西 = 响应报文。
· "给你"(或"该货已下架") = 状态码(200 或 404)
· "这是压缩包装的,一共 3 公斤" = Content-EncodingContent-Length
· "这批货的批次号是 ABC,下次报这个号我就知道你要哪批" = ETag
· "这货一周内不会变,你先放着用" = Cache-Control
· 货本身 = 响应体(那份 HTML)

整个 HTTP 的设计,本质上就是把"人到窗口办一件事"这个过程写成了机器能严格执行的格式。而它最聪明的地方在于:这些附加条件(头部)是可选的、可扩展的——今天多加一个"请给我英文版"的说明,明天多加一个"我支持新的压缩格式",老服务器看不懂就当没说,照样能办事。相当于面单上多填了一栏备注,快递员不认识那栏就跳过,不影响整件事成立。

浏览器实际发出去的那份请求

现在看真东西。你在手机上点开那个商品页,浏览器通过加密通道发出的报文,解密之后大致是这样(为了看清结构,这里按 HTTP/1.1 的文本格式呈现;HTTP/2 与 HTTP/3 会把它压成二进制,但字段的含义完全一样):

GET /item/8848 HTTP/1.1
Host: shop.example.com
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ...
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Accept-Encoding: gzip, deflate, br
Referer: https://m.weixin.example/chat
Cookie: session_id=a1b2c3d4e5; uid=88192
If-None-Match: "v3-8848-20260806"
Connection: keep-alive
                        ← 空行,头部到此结束
                        ← GET 请求通常没有请求体

这份报文的语法规则(请求行、头部、空行、请求体这四段结构,以及为什么换行必须是 CRLF)在 § 3.1 已经详细讲过。这里我们换个角度看:每一行都在替你争取一件事。逐行拆一遍:

这一行它在替你争取什么不写会怎样
GET /item/8848说清动作和目标——我要"取",取的是这个没有这行就不成立,这是请求的主干
Host告诉服务器"我找的是哪个网站"(一个 IP 上常有几十个站)HTTP/1.1 里不写是违规的,服务器会返回 400
User-Agent报出你的设备与浏览器,服务器可能据此给移动版页面可能收到桌面版页面,在手机上难看
Accept"我能接受这些格式的内容",还带优先级(q 值)服务器只能猜,可能给你一个你处理不了的格式
Accept-Language"我优先要中文"——多语言网站靠它决定给哪种语言可能收到英文页面
Accept-Encoding"我能解开压缩的内容"服务器不敢压缩,传输量可能大好几倍
Referer"我是从哪个页面点过来的"(这个词的拼写是历史上的笔误,规范里就这么写)网站统计不到来源;某些防盗链功能会失效
Cookie"这是我的凭据"——服务器靠它认出你是谁会被当成陌生人,登录状态丢失
If-None-Match"我手上有版本 v3 那份,如果没变就别重发了"服务器只能把内容完整重发一遍,浪费流量与时间
Connection: keep-alive"这条线别断,我等下还有请求"连接被关,下一个资源要重新握手(§ 4.3 讲过这笔开销)

看完这张表,你应该能体会到一件事:HTTP 请求头是一场"预先谈判"。浏览器在开口的同一句话里,就把"我是谁、我能接受什么、我已经有什么、我希望怎么处理"全说清了,好让服务器一次就给出最合适的回复。

换成大白话:这相当于一个会办事的人到窗口的说话方式。不会办事的人只说"我要取货",然后被反复追问"卡号呢""要什么包装""你上次那份还在不在",来回问三遍。会办事的人一口气把该说的全说完,柜员一次就办妥了。而每一次"追问"在网络上就是一个来回——所以把信息一次说全,本身就是性能优化。

Host 头:一个字段撑起了整个虚拟主机时代

上面那些头部里,Host 最不起眼但历史意义最大。它是 HTTP/1.1 相对 1.0 最重要的强制新增项。

为什么必须有它?因为 IP 地址稀缺,而域名不稀缺(§ 1.3 讲过 IPv4 地址枯竭的问题)。如果一个 IP 只能放一个网站,那全世界能有的网站数量就被 IP 数量卡死了。

没有 Host 头时(HTTP/1.0 早期):
  请求只写:GET /item/8848 HTTP/1.0
  服务器收到后一脸茫然:我这台机器上跑着 30 个网站,
  你这个 /item/8848 是要哪个网站的?

有了 Host 头之后:
  GET /item/8848 HTTP/1.1
  Host: shop.example.com     ← 这一行解决了全部问题

于是一台服务器、一个 IP,可以承载成千上万个网站。
这种做法叫「虚拟主机」(Virtual Host)。

换成大白话:这相当于一栋写字楼共用一个门牌号。快递员送到楼下,光有门牌号不够,他还得知道"送几层几室"。Host 头就是那个"几层几室"——它让一个物理地址能容纳无数个逻辑地址。

你可能注意到这和 § 4.4 讲的 SNI 干的是同一件事。没错,它们的作用完全平行,只是发生在不同层、解决不同阶段的同一个问题:

SNIHost
发生在什么时候TLS 握手时(§ 4.4)发 HTTP 请求时(本节)
解决什么问题服务器该出示哪份证书服务器该返回哪个网站的内容
是否加密明文在 HTTPS 下是加密的
为什么不能只留一个证书要在加密建立之前发,所以必须有一个握手期的字段请求是加密之后才发的,用它更安全也更规范

相当于你进写字楼大门时跟保安说了一次"找 20 楼某公司"(SNI,大门口,公开听得见),进电梯上去后又跟那家公司前台说了一次"我找技术部老李"(Host,屋里说的,外面听不见)。两次都必须说,因为听的人不同、目的不同。

请求发出去之后:服务器那头的好几道手

请求离开你的设备后,通常不会直接抵达那个"跑业务代码的程序"。现代网站的架构里,一个请求往往要过好几道手。这就是 TTFB 那段沉默里发生的事。

你的请求到达机房,典型路径:

  ①  CDN 边缘节点(如果网站用了 CDN)
      · 这份内容我缓存里有吗?有 → 直接返回,压根不去源站 ★
      · 没有 → 往后转发

  ②  负载均衡器
      · 后面有 20 台服务器,这个请求分给谁?
      · 顺便检查各台是否健康,挂了的不分给它

  ③  反向代理 / 网关
      · 解 TLS(很多架构在这一层解密,后面走内网明文)
      · 查鉴权:这个 Cookie 有效吗?
      · 限流:这个 IP 一分钟内请求太多了吗?
      · 路由:/item/... 这类路径该交给哪个后端服务?

  ④  应用服务器(真正跑业务代码的地方)
      · 解析路径,取出商品 ID 8848
      · 查缓存(如 Redis):这个商品的数据我内存里有吗?
      · 没有 → 查数据库

  ⑤  数据库
      · 执行查询,返回商品信息
      · ★ 这一层往往是整条链路最慢的一环

  ⑥  应用服务器把数据套进模板,生成 HTML

  ⑦  一路返回:应用 → 网关 → 负载均衡 → CDN → 你

★ 你在开发者工具里看到的那个 TTFB 数字,
  是上面①到⑦全部时间 + 一个网络来回的总和。

这张图解释了一个很常被问的问题:为什么有的网站快、有的慢,明明我的网速一样?因为TTFB 里"服务器处理"那一部分跟你的网络毫无关系,它取决于对方的架构、缓存命中情况、数据库有多忙。

相当于你在两家餐厅点同一道菜。一家后厨提前备好了料、五分钟上菜;另一家现杀现洗、四十分钟。而你从家到两家餐厅的路程是一样的。路程是网络,备料是服务器架构——抱怨路太远是解决不了后厨慢的。

注意第 ① 步那个 ★ 号。CDN 命中的话,你的请求压根到不了真正的服务器——它在离你几十公里的边缘节点就被应答了。这就是为什么 CDN 能同时降低两笔成本:缩短了网络距离,还省掉了服务器处理时间。它是本章讲的所有优化手段里效果最直接的一个。

服务器返回的那份响应

处理完了,服务器把结果发回来。响应报文的结构和请求是对称的:状态行 + 响应头 + 空行 + 响应体。真实的样子大致如下:

HTTP/1.1 200 OK
Date: Sat, 08 Aug 2026 06:12:44 GMT
Content-Type: text/html; charset=UTF-8
Content-Encoding: br
Content-Length: 14203
Cache-Control: max-age=300, public
ETag: "v3-8848-20260808"
Vary: Accept-Encoding, Accept-Language
Set-Cookie: last_view=8848; Path=/; HttpOnly; Secure; SameSite=Lax
Strict-Transport-Security: max-age=31536000; includeSubDomains
Server: nginx
                        ← 空行
<!DOCTYPE html>
<html lang="zh-CN">
<head><title>某款商品 - 商城</title>
  <link rel="stylesheet" href="/css/main.css">
  ...
                        ← 这一大段就是 § 4.6 渲染的原料

逐条看这些响应头分别在干什么——它们的共同特点是:每一条都在指挥浏览器接下来该怎么做。

响应头它在告诉浏览器什么换成大白话不给会怎样
Content-Type这份内容是什么类型、什么字符编码"这是一份 HTML,用 UTF-8 编码读"可能出现乱码,或者浏览器把网页当纯文本显示
Content-Encoding内容被哪种方式压缩过"我压扁了寄,你收到自己撑开"浏览器不知道要解压,看到一堆乱码
Content-Length正文一共多少字节"这箱货净重 14203 克"浏览器不知道什么时候读完(§ 3.1 讲过这个字段写错的严重后果)
Cache-Control这份内容能缓存多久、谁能缓存"五分钟内你直接用,别再来问我"浏览器只能保守处理,下次可能又白跑一趟
ETag这份内容的"版本指纹""这批货的批次号是 v3"没法做协商缓存,只能整份重传
Vary缓存时要区分哪些请求头"中文版和英文版是两批货,别混着存"可能给中文用户返回缓存里的英文页面
Set-Cookie给你一个凭据,下次来带着"这是寄存牌,下次凭它认你"没法维持登录状态
Strict-Transport-Security以后只准用 HTTPS 找我(§ 4.1 讲的 HSTS 就是这条头设的)"以后只走监控走廊"留下明文请求的缝隙

其中 Vary 最容易被忽略,但它造成的 bug 最诡异,值得单独说。它的意思是"这份缓存的有效性依赖于请求里的某几个头"。

打个比方:一家店按顾客口味做了两种版本的货——微辣版和特辣版。如果仓库不区分地把它们堆在同一个格子里,那下一个说"我要微辣"的人可能拿到特辣。Vary: Accept-Language 就是在告诉所有缓存:"按语言分格存放,别混。"

真实世界里,忘写 Vary 导致的经典故障是:某个用户访问了英文版,页面被 CDN 缓存下来;后面所有中文用户都收到了那份英文页面。这类 bug 极难排查,因为它只在特定顺序下出现,而且开发者本地永远复现不出来。

状态码:三位数字里的分诊逻辑

状态码的完整清单在 § 3.1 讲过(那里也有一个可点击查询的组件)。这一节只讲一件事:在真实的访问流程里,不同状态码会把你导向完全不同的后续路径。

状态码浏览器接下来会怎么做对整段旅程的影响
200 OK接收内容,交给渲染引擎正常路径,直接进 § 4.6
301 / 308 永久重定向换个地址重新走一遍,并且记住这个跳转多花一个完整来回;下次访问会直接去新地址
302 / 307 临时重定向换个地址重新走一遍,但不记住每次访问都要多花一个来回
304 Not Modified用本地缓存那份省掉了传内容,但那个来回还是花了
403 Forbidden显示错误页你身份不够——凭据可能过期了
404 Not Found显示错误页路径写错了,或者内容已删(回到 § 4.1 的路径排查)
500 服务器内部错误显示错误页服务器代码炸了——跟你的网络毫无关系
502 / 504 网关错误显示错误页前面那道网关连不上后端、或等后端超时了
503 服务不可用显示错误页服务器过载或在维护

这里有一个非常实用的区分,几乎没人一开始就搞清:4xx 和 5xx 的锅在不同的人身上。

这个区分的实际价值:看到 4xx 你该检查自己的操作,看到 5xx 你重试或联系对方就行,检查自己纯属白费功夫。

还要特别说一下 502 和 504 这一对,因为它们透露了服务器的内部结构:

502 Bad Gateway   ← 网关说:"我联系不上后面那台服务器"
504 Gateway Timeout ← 网关说:"我联系上了,但它半天不回话,我等不下去了"

★ 这两个状态码本身就证明了「服务器不是一台机器」。
  能报出"网关"错误,说明前面确实站着一个分诊台,
  而真正干活的在它后面。

相当于医院分诊台告诉你:
  502 = "那个科室的电话打不通"(可能整个科室没人了)
  504 = "电话通了但一直没人接完这通"(人在但忙得顾不上)

这也解释了为什么你偶尔会看到"网站挂了但错误页很漂亮"——因为报错的是网关(它还活得很好),挂掉的是它后面的应用服务器。说白了,前台好得很,是后厨没人了。

重定向:那个你察觉不到的额外来回

重定向(3xx)是本节最值得从"流程视角"重新理解的一项。因为每一次重定向,都意味着整段旅程要从某一步重新走一遍。

你输入:example.com

实际可能发生的连锁:

  第 1 趟  http://example.com
           → 301 跳到 https://example.com
           (多花:一个完整来回)

  第 2 趟  https://example.com
           → 301 跳到 https://www.example.com
           (多花:一次 DNS 查询 + 建连接 + TLS 握手 + 一个来回)
           ★ 因为域名变了,前面几节全部要重做

  第 3 趟  https://www.example.com
           → 200 终于拿到内容

一共三趟。而用户的感受只是"这网站有点慢"。

换成大白话:这相当于你去办事,第一个窗口说"这业务搬到二楼了",二楼窗口说"哦我们又搬到隔壁楼了"。三趟跑下来事情办成了,但时间花了三倍。而如果第一个窗口一开始就贴张纸写清"本业务在隔壁楼三层",你只需要跑一趟。

这就是网站优化里"减少重定向链"这一条的全部道理。而 § 4.1 讲的 HSTS 预加载正是为了消掉上面那第 1 趟——让浏览器不必先去问一次就知道该用 HTTPS。

顺便区分 301 和 302,这一对在实践中差别很大:

301 永久重定向302 临时重定向
含义"这地址永久换了,以后直接去新的""这次先去那边,但下次还来问我"
浏览器会不会记住(可能长期缓存这条跳转)不会(每次都要问一遍)
性能好——只有第一次多花一个来回差——每次都多花一个来回
风险配错了很难挽回——用户浏览器已经记住了,改回来他也不去问了配错了影响小,改了立刻生效
生活里对应门上贴"本店已永久迁至新址"——熟客从此直接去新店门上贴"今日装修,临时在隔壁营业"——明天还得来看一眼

最后那一行的风险值得记住:301 是会被浏览器长期记住的,所以它是一个"不太容易反悔"的动作。相当于你在通讯录里把某人的号码永久改了,之后就算原号又启用了,你也不会再去打原号。说白了:不确定要不要永久搬家时,先用 302。

压缩与分块传输:让内容更快到眼前的两招

响应体真正开始传的时候,有两个机制在悄悄提速。它们的思路完全不同,但都很朴素。

第一招:压缩。HTML、CSS、JS 都是纯文本,而文本的压缩率极高——大量重复的标签名、属性名、空格,压完常常只剩几分之一。

它是怎么谈成的?就是靠前面那对头部:

你说:   Accept-Encoding: gzip, deflate, br
         ("我能解开这三种压缩格式")

它回:   Content-Encoding: br
         ("那我用 br 这种压缩,你自己解开")

★ 这是一次典型的"能力协商":
  客户端报出自己支持什么,服务器从中挑一个。
  老客户端不支持新格式?那它就不写在清单里,服务器自然不用。
  —— 新旧共存,谁都不用被迫升级。

相当于洗衣店的真空压缩袋:同样一床被子,压完体积只剩三分之一,运起来轻松得多,你收到自己一撑就恢复。代价是两头都要花一点力气(服务器压、你解),但省下的传输时间远远超过这点计算成本。

不过有一类内容不该再压:图片(JPEG、PNG、WebP)、视频、已经打包好的压缩文件。因为它们内部已经压过了,再压一遍不但省不下几个字节,还白费两头的算力说白了,真空袋对已经抽过真空的东西没用。

第二招:分块传输(Chunked Transfer Encoding)。它解决的是另一个问题:如果服务器要生成的内容很大、或者要边算边给,它压根不知道 Content-Length 该填多少。

普通方式:
  服务器必须先把整份内容生成完,数清字节数,
  填好 Content-Length,然后才开始发。
  → 用户在这段时间里什么都看不到

分块方式:
  服务器每生成一块就发一块,每块前面标上"这块多少字节",
  最后发一个长度为 0 的块表示"完了"。
  → 用户能很早看到内容的开头部分

相当于餐厅上菜:
  普通方式 = 所有菜做完一起端上来(干等)
  分块方式 = 凉菜先上,热菜陆续端(边等边吃)

这个机制对用户感知的速度影响极大,而且值得注意的是:它并没有让总时间变短,只是让"开始看到东西"这个时刻提前了。

本质上就是:同一顿饭吃完的时间可能一样,但"从坐下到第一口"这段等待,决定了你觉得这家店快还是慢。网页也一样——用户感知的快慢,主要取决于第一屏什么时候出现,而不是最后一个字节什么时候到。这一点在 § 4.6 讲渲染时会进一步展开。

Cookie:无状态协议怎么记住你是谁

HTTP 有个根本特性叫无状态(Stateless,§ 3.1 与 § 1.2 都讲过)——服务器默认不记得你上次来过。每一个请求在它眼里都是全新的陌生人。

那登录状态是怎么维持的?靠 Cookie服务器不记,让你自己记,每次来的时候带着。

第一次访问(登录成功):
  服务器 → 你:Set-Cookie: session_id=a1b2c3d4e5; HttpOnly; Secure

之后每一次请求:
  你 → 服务器:Cookie: session_id=a1b2c3d4e5
  服务器一看这个编号,去自己的记录里查:"哦,这是 88192 号用户"

★ 关键点:服务器不需要"记住你",
  它只需要能"认出你手上那个编号"。

相当于超市门口的寄存柜。柜台不会记住你长什么样、存了什么,它只给你一个便签牌号;你回来把牌子递上去,它按号开柜。记忆的负担从服务方转移到了客户手上——这正是 HTTP 能在云上无限扩容的根本原因。

为什么这个转移这么重要?因为如果服务器必须"记住每个用户",那同一个用户的每次请求都必须落到同一台服务器上(否则另一台不认识他)。而把记忆交给客户端之后,任何一台服务器都能服务任何一个用户——加机器就能扛更多流量。

换成大白话:如果每个柜员只认自己接待过的客人,那客人就得永远找同一个柜员,柜员一休假业务就断。而凭牌号办事的话,任何柜员都能接任何客人,人多了就多开窗口。

那几个 Cookie 属性各自在干什么,值得记一下——它们全是安全属性:

属性作用换成大白话
HttpOnly禁止网页脚本读取这个 Cookie"这个牌号只能交给柜台,不许给店里其他人看"——防止脚本偷走凭据
Secure只在 HTTPS 连接上发送"只在加密通道里出示,明文场合一律不掏出来"
SameSite限制跨站请求时是否携带"别人家的网页替我发的请求,别把我的牌号带上"——防跨站伪造
Path / Domain限定这个 Cookie 在哪些路径与域名下有效"这张牌只在这一层楼、这几个柜台有效"
Max-Age / Expires多久后失效"这张牌当天有效,过期作废"

顺便提醒一件实际后果:Cookie 会随每一个请求发出去,包括那些图片、CSS、JS 请求。所以 Cookie 存太多东西会让每个请求都变胖。这也是为什么很多网站把静态资源放在一个单独的域名下——那个域名下没有 Cookie,请求头能小一大截。相当于你去取一瓶水也要把整个证件夹掏出来,不如把"要出示证件的柜台"和"随便拿的货架"分开设置。

整节小结:一次请求响应的完整时间线

把本节内容按真实顺序串起来,接在 § 4.4 的第 ⑦ 步之后:

接过一条已验明身份的加密通道
  │
  ├─① 组装请求报文:请求行 + 十来条头部
  │      (每一条头都在替你争取一件事:身份、语言、压缩、缓存)
  │
  ├─② 通过加密通道发出去(体积通常只有几百到几千字节)
  │
  ├─③ ═══ 等待期 · 这就是 TTFB 那段沉默 ═══
  │      服务器侧依次经过:
  │        CDN 边缘节点 → 命中就直接返回 ★
  │        负载均衡器   → 分给哪台
  │        网关         → 解 TLS、鉴权、限流、路由
  │        应用服务器   → 跑业务代码、查缓存
  │        数据库       → 往往是最慢的一环
  │        套模板生成 HTML → 压缩 → 返回
  │
  ├─④ 收到状态行:看这三位数字决定下一步
  │      200 → 继续收内容
  │      3xx → 换地址重走一遍(可能要从 § 4.2 重新开始)
  │      304 → 用本地缓存那份
  │      4xx → 你的请求有问题
  │      5xx → 对方办不了,跟你的网络无关
  │
  ├─⑤ 收响应头:这些头会指挥浏览器接下来的行为
  │      Content-Type   → 按什么类型和编码解读
  │      Content-Encoding → 要不要解压
  │      Cache-Control / ETag → 这份要不要存、存多久
  │      Set-Cookie     → 记下这个凭据
  │      Vary           → 缓存时要按哪些维度分格
  │
  ├─⑥ 收响应体(可能是分块陆续到达的)
  │
  └─⑦ 交出一份 HTML 文本 → 进入渲染 → 这就是 § 4.6 的起点

耗时量级:
  网络往返            由距离决定(同城十毫秒级,跨洲百毫秒级)
  服务器处理          从毫秒级到秒级,完全取决于对方架构与负载
  下载内容            由体积与带宽决定
  (TTFB = 网络往返 + 服务器处理;把这两者分开是排查的第一步)

这张图里最实用的一条是第 ④ 步那个状态码分支看清状态码,你就知道该找谁:4xx 找自己、5xx 找对方、3xx 是白跑了一趟、304 说明缓存机制在起作用。

一个页面不止一个请求:几十个请求的调度

前面讲的都是"那一个请求"——拿 HTML 的那个。但现实里,一个页面的加载是几十甚至上百个请求的合奏。这一点常被忽略,却是理解网页性能的关键。

请求 1:GET /item/8848        → 拿到 HTML
   ↓  浏览器开始解析这份 HTML,边解析边发现引用了别的东西
请求 2:GET /css/main.css      ← 从 <link> 标签发现的
请求 3:GET /js/app.js         ← 从 <script> 标签发现的
请求 4:GET /img/8848-1.jpg    ← 从 <img> 标签发现的
请求 5~40:更多图片、字体、图标、统计脚本……

★ 关键:这些请求不是等第一个完全处理完才发的。
  浏览器一边读 HTML 一边就把发现的资源排进队列,尽量并行去取。
  这个"边读边预先取"的机制叫预加载扫描(Preload Scanner)。

相当于装修工头拿到图纸的做法:他不会看完整本图纸才开始订料,而是翻到哪一页就把那页要的材料先订出去——瓷砖、水泥、电线各家供货商同时在备货。如果非要一样一样顺着来,工期要长好几倍。

但这里有个很重要的约束:不是所有资源都能并行取,因为有些有依赖关系,还有些会挡住后面的活儿。

资源类型会不会挡住渲染为什么
CSS 样式表样式没到位就画,用户会看到"没穿衣服"的页面,所以浏览器宁愿等(§ 4.6 详讲)
普通的同步脚本会,而且挡得最狠脚本可能修改页面结构,所以解析必须停下来等它下载并执行完
标了 async / defer 的脚本不会明确告诉浏览器"我不急,你先干别的"
图片不会图没到就先留个空位,来了再填
字体可能造成文字闪烁字体没到时用备用字体,到了再换,于是文字会跳一下

这张表解释了一个非常常见的现象:为什么有些网页会先白屏一两秒,然后突然整页出来。因为它顶部有一个挡住渲染的脚本或样式表,浏览器在排队等它,什么都不敢画。而如果把这些不急的东西标成异步,页面就能先把能画的画出来,用户的感受会好得多。

换成大白话:一支流水线上,某个工位不管三七二十一非要等某个零件到齐才肯往下传,后面所有工位全部干等。而其实那个零件只影响最后一道装饰工序——它压根不该卡住整条线。

HTTP/1.1、2、3:同一句话的三种说法

前面展示的请求报文是 HTTP/1.1 的文本格式。真实世界里你的浏览器可能在用 2 或 3。要强调的是:换版本换的是"怎么把这句话装进网络包",不是"这句话说什么"。方法、路径、头部字段的含义完全一样。

HTTP/1.1HTTP/2HTTP/3
报文形式纯文本,人能直接读二进制帧二进制帧
底层用什么TCPTCPQUIC(跑在 UDP 上)
一条连接上能同时跑几个请求一个(要排队)很多个(多路复用)很多个
头部压缩没有——每个请求都把那十几行头重发一遍
一个包丢了会怎样那条连接上后面的都得等仍会等(因为底层 TCP 要求按序交付)只影响那一个请求
生活里对应一条队伍,前面办完才轮到后面一个窗口能同时受理多人,但如果某人的材料掉了,所有人一起等各人的材料互不影响

第五行那个"仍会等"是 HTTP/2 最著名的遗留问题,业内叫队头阻塞(Head-of-Line Blocking)。HTTP/2 在应用层解决了排队问题,但它底下的 TCP 仍然要求"按顺序交付"——所以某个包在路上丢了,TCP 就把后面所有已经到达的数据全扣着不给上层,直到那个包补上。

换成大白话:这相当于一条传送带。HTTP/1.1 是"一件一件放",HTTP/2 改成了"多件并排放",效率大增。但传送带的规矩是"必须按放上去的顺序取下来"——所以中间某一件掉了,后面那些明明已经运到的,也只能停在带子上等。HTTP/3 换掉 TCP 的根本理由就是这个:它要摆脱"必须按序"这条规矩。

这里能看出一个很值得记住的模式:当你在某一层做足了优化,瓶颈就会掉到下一层去。HTTP/2 把应用层榨干了,问题就露在传输层;于是 HTTP/3 干脆连传输层一起换。说白了,优化就是不断把瓶颈往下推,直到推到物理定律那儿推不动为止——而那个推不动的东西,就是 § 1.6 讲的光速带来的传播时延。

请求方法:为什么必须区分 GET 和 POST

最后补一块本节流程视角下很实用的内容。请求行里那个动词(方法)不只是形式,它决定了浏览器和路上所有缓存怎么对待这个请求。

方法意思安全吗(不改变数据)幂等吗(做多次结果一样)能被缓存吗
GET取一份东西
HEAD只要头部,不要内容
POST提交一份东西不是不是通常不能
PUT把某个东西整体替换成这份不是(替换十次和一次结果相同)不能
DELETE删掉某个东西不是(删十次和一次结果相同)不能

"安全"和"幂等"这两个词值得单独说清,因为它们很容易被误解。

这两个性质有非常实际的后果,而且都跟"重试"有关:浏览器和中间设备可以放心重试幂等请求,绝不敢随便重试非幂等请求。

这就解释了你一定遇到过的一个现象:刷新一个提交过表单的页面时,浏览器会弹窗问"确认要重新提交吗"。而刷新一个普通页面它从不问。

换成大白话:重新看一遍某份材料,没人需要征求你同意;但"要不要再交一遍申请表",柜员一定会先问你一句——因为交两遍可能真的办出两笔业务来。说白了:能重来的动作可以悄悄重来,不能重来的动作必须先问。

这也回头解释了 § 4.4 讲的 0-RTT 只能用于幂等请求那条限制:因为 0-RTT 的数据可能被重放,而"重放"和"重试"在服务器看来是同一件事——所以只有那些"多做几次也无害"的请求才敢用它。

最后提一个实践中的常见错误:把"改数据"的操作写成 GET。比如 GET /delete?id=8848。这看着能用,但危险得很——因为整个网络世界都假定 GET 是安全的:浏览器会预取它、代理会缓存它、爬虫会随手访问它。相当于你把"销户"这个业务放在了"查询"窗口,还写着"随便看"——结果谁路过瞄一眼,账户就被销了。

顺便说清另一个常见混淆:"GET 不能带请求体""POST 更安全"这两句话都不严谨。准确的说法是——GET 在规范上不禁止带请求体,但实践中带了往往没人处理,所以不要带;至于安不安全,POST 只是把数据从地址栏移到了请求体里,明文传输时它一样被路上的人看得见——真正提供保密的是 § 4.4 的 TLS,不是方法名。

换成大白话:把密码从写在信封外面改成写在信纸上,确实不会被随手瞄到,也不会被抄进快递记录(这是 POST 相对 GET 的真实好处);但只要信封不封口,拆开一看照样清清楚楚。封口这件事只有加密能做。

Recap · 收束

这一节是整段旅程里唯一真正传输内容的一步,也是唯一"对方有机会犯错"的一步——前四步出错只有"连不上"一种,这一步开始有 404、500、403、超时好几种。
报文格式见 § 3.1(四段结构、CRLF、Content-Length 写错的后果、状态码全表)。本节换成流程视角看三件事:
一 · 请求头是一场"预先谈判"。浏览器在开口的同一句话里就说清了"我是谁(Cookie)、我能接受什么(Accept 系列)、我已经有什么(If-None-Match)、这条线别断(Connection)"。把信息一次说全本身就是性能优化——因为每一次追问就是一个来回。其中 Host 头撑起了整个虚拟主机时代,它和 § 4.4 的 SNI 作用平行但发生在不同层。
二 · 服务器不是一台机器。一个请求要过 CDN → 负载均衡 → 网关 → 应用 → 数据库好几道手,TTFB 就是这几道手加一个网络来回的总和TTFB 长而下载快说明服务器慢,TTFB 短而下载慢说明内容太大或带宽不足——这两种病的治法完全不同。而 CDN 命中的话请求压根到不了源站,它同时省掉了距离与处理时间。
三 · 响应头在指挥浏览器接下来做什么。Content-Type 决定怎么解读、Content-Encoding 决定要不要解压、Cache-ControlETag 决定下次还要不要来问、Set-Cookie 给你凭据、Vary 决定缓存怎么分格(漏掉它会导致中文用户拿到缓存里的英文页)。
三条最实用的判断:4xx 找自己、5xx 找对方;② 每次重定向都是白跑一个来回,301 会被浏览器长期记住所以不易反悔,不确定就先用 302;③ Cookie 的意义是把记忆负担从服务器转移给客户端,这正是 HTTP 能无限加机器扩容的根本原因。
下一步交出什么:一份 HTML 文本。但它现在还只是一堆字符,屏幕上什么都没有。把这堆字符变成你看得见的画面,是最后一节的事——而那一节的性质跟前五节完全不同:它不再等来回,它开始拼命地算。

☰ 主页
Xue Hai Wu Ya · Network · § 4.5 · 发送请求与服务器响应