第 10 章 内容协商与压缩:同一个资源的一千种形态
“同一个资源,怎么给不同的人不同的样子?” 这个看似简单的问题,答案里藏着 HTTP 里最优雅也最容易被误用的两套机制。
10.0 引子:移动端和 PC 返回不同内容,缓存全乱了
小美:老哥,移动端要求接口返回精简版字段,PC 要完整版,能不能同一个接口根据设备返回不同内容?
你:行,我在代码里读 User-Agent 判断是不是移动端。
老张:出事了。CDN 缓存全乱了——有的移动用户拿到了 PC 的完整版数据,有的 PC 用户拿到了移动端的精简版。
你:……因为缓存只认 URL,不认”设备”?
墨叔:你撞上了”内容协商”的第一个坑。今天这一章,把”同一个资源,多种形态”这件事彻底讲清楚——包括那个让无数人混淆的 Content-Encoding 和 Transfer-Encoding。
10.1 先把问题问对:什么是”内容协商”
墨叔:一个资源,可以有多种”表示(representation)”:
- 同一个”用户资料”,可以是 JSON、XML、HTML。
- 同一个”网页”,可以是英文、中文。
- 同一个”文件”,可以是 gzip 压缩版、原始版。
“内容协商”就是:客户端和服务器商量好,这次传输用哪种表示。
你:谁来决定用哪种?客户端还是服务器?
墨叔:这正是核心。HTTP 设计了三种协商方式,但最终只有一种活了下来。
三种协商方式
方式一:服务端驱动(Server-driven)—— 赢家。
客户端在请求里声明”我能接受什么”(Accept: application/json),服务器根据这个声明决定返回什么。
方式二:代理驱动(Agent-driven)—— 基本没人用。
服务器返回 300 Multiple Choices,列出所有可选表示,让客户端自己挑。太麻烦,多一次往返,没人用。
方式三:透明协商(Transparent)—— 理论存在。 中间缓存代理替双方协商。太复杂,现实中极少。
你:所以现实中只有”服务端驱动”这一种?
墨叔:对。核心机制就一句话:客户端用 Accept* 系列头声明”我能接受什么”,服务器据此选择,并在响应里用 Content-* 头声明”我这次给了你什么”。
请求:Accept: application/json <- 客户端说:我要 JSON
请求:Accept-Encoding: gzip, br <- 客户端说:我支持 gzip 和 br 压缩
请求:Accept-Language: zh-CN <- 客户端说:我偏好中文
响应:Content-Type: application/json <- 服务器说:给你 JSON
响应:Content-Encoding: gzip <- 服务器说:我用 gzip 压了
响应:Content-Language: zh-CN <- 服务器说:内容语言是中文
10.2 Accept 与 q 值:优先级是怎么算的
墨叔:Accept 头里有个容易被忽略但很精妙的东西——q 值(质量值,quality value)。
Accept: text/html, application/xhtml+xml, application/xml;q=0.9, */*;q=0.8你:这些 q=0.9、q=0.8 是什么?
墨叔:是优先级。客户端在说:“我最想要 text/html(q 默认是 1.0,最高),其次是 application/xhtml+xml(也是 1.0),再其次是 application/xml(0.9),最后任何格式都行(0.8)。”
匹配算法:服务器遍历自己能提供的格式,选客户端能接受且 q 值最高的那个。如果都不匹配,就返回 406 Not Acceptable。
你:所以 */* 是”兜底,什么都行”?
墨叔:对。*/* 表示”任何 MIME 类型都接受”(q=0.8 表示”不太想要,但实在没有也行”)。
一个实际的坑:很多人在接口里根本不看 Accept 头,直接返回写死的 JSON。这在”只有一种客户端”时没问题,但一旦有别的客户端(比如一个只想拿 XML 的老系统),就会出现”它声明了 Accept: application/xml,你却硬塞给它 JSON”的语义错位。要么尊重 Accept,要么就别声称支持内容协商。
10.3 Content-Encoding vs Transfer-Encoding:全章最大的坑
墨叔:现在讲全章最值得讲的一个辨析,也是面试和工作里高频混淆点:
Content-Encoding和Transfer-Encoding到底有什么区别?
你:这俩我一直稀里糊涂的。都是”编码”?
墨叔:天差地别。记住一句话:
Content-Encoding是”内容的压缩格式”,是资源表示的一部分;Transfer-Encoding是”传输的打包方式”,是传输过程的临时手段。
Content-Encoding:把内容压小了存/传
Content-Type: text/html <- 内容本质是 HTML
Content-Encoding: gzip <- 但为了省流量,用 gzip 压缩过了Content-Encoding 是端到端的——它描述的是”这个资源表示本身被压缩成了什么格式”。接收方必须解码(解压),才能得到真正的 HTML。
你:所以 gzip 是”内容的一种形态”?
墨叔:对。Content-Encoding: gzip 意味着”这个表示的字节,是 gzip 压缩后的”。浏览器拿到后先 gunzip,再当 HTML 解析。
Transfer-Encoding:把传输过程打包成段
Transfer-Encoding: chunked <- 传输时用了分块,跟内容无关Transfer-Encoding 是逐跳的(第 5 章讲过)——它描述的是”这一跳的传输,用了什么打包方式”(比如 chunked)。接收方解码后得到的是完整的实体,跟传输方式无关。
一张表彻底分清
| Content-Encoding | Transfer-Encoding | |
|---|---|---|
| 管什么 | 内容的压缩格式 | 传输的分段/打包 |
| 端到端/逐跳 | 端到端 | 逐跳 |
| 接收方要不要解 | 要(解压) | 要(去掉分段标记) |
| 典型值 | gzip / br / zstd | chunked |
| 例子 | 一个 gzip 的 HTML | 一个分块传输的流 |
你:那”一个 gzip 的 HTML,用 chunked 传输”,两个可以同时有?
墨叔:可以!这正是最好的例子:
Content-Type: text/html
Content-Encoding: gzip <- 内容被 gzip 压了
Transfer-Encoding: chunked <- 传输时又用了分块
顺序是这样的:先有”内容”(HTML)→ 用 Content-Encoding 压缩(变成 gzip 字节)→ 用 Transfer-Encoding 分块传输。一个管”内容长什么样”,一个管”怎么送过去”。
为什么这个辨析重要:因为很多人写代码时想”给响应压缩”,结果设置错了头——设了 Transfer-Encoding 以为在压缩,其实只是分了块。或者反过来,在逐跳的地方(代理)处理了 Content-Encoding(端到端),把不该它管的东西给改了。
10.4 压缩:gzip、br、zstd 与安全
墨叔:讲压缩,就讲三个点:选什么算法、代价是什么、以及一个必须知道的安全问题。
算法选择
| 算法 | 压缩率 | CPU 消耗 | 现状 |
|---|---|---|---|
| gzip | 中 | 低 | 最通用,兼容性最好 |
| br(Brotli) | 高(比 gzip 高约 20%) | 中 | 现代浏览器都支持,静态资源首选 |
| zstd | 高 | 低(解压快) | 较新,逐渐普及 |
你:那直接全用 br 不就完了?
墨叔:不行。因为压缩率和 CPU 是一对矛盾——br 压得小,但压缩时 CPU 更费。对于静态资源(压一次,服务一万次),br 很划算;对于动态内容(每个请求都要现压),br 的 CPU 成本可能扛不住,gzip 更划算。
所以实践中:静态资源用 br(构建时压好),动态响应用 gzip(实时压,CPU 友好)。
安全:BREACH 攻击
墨叔:压缩有一个经典的安全隐患——BREACH 攻击。
原理很精妙:压缩 + 加密 + 反射用户输入 = 泄露秘密。
想象一个页面,它会把用户输入的内容反射到响应里(比如搜索框,你搜”abc”,页面显示”你搜索了 abc”),而且响应是 HTTPS 加密 + gzip 压缩的。
攻击者不断发请求,让你反射不同内容,然后观察加密响应的长度变化:
- 因为 gzip 压缩,重复的内容会被压得更小。
- 如果攻击者猜到”秘密”的一部分(比如 CSRF token 的前几个字符),他把它构造进请求里,如果猜对了,响应里出现了重复,压缩后长度就变短。
- 攻击者通过”响应长度变短了没有”,来一个字符一个字符地猜出秘密。
你:所以压缩把”秘密”的信息,泄露到了”响应长度”里?
墨叔:对。这是一个非常聪明的侧信道攻击。防护办法:对敏感的动态响应(含 CSRF token、含用户反射内容的),禁用压缩;或者给响应加随机长度的填充。
10.5 范围请求:断点续传与分片下载
墨叔:讲一个实用技能——Range 请求,它实现了断点续传。
场景:下载 2GB 的文件,下到一半断了。重新下,如果从头再来,太蠢了。Range 请求让你”只下后半段”。
# 客户端:我要从第 1000 字节开始
GET /big.iso HTTP/1.1
Range: bytes=1000-
# 服务器:给你 1000 字节之后的部分
HTTP/1.1 206 Partial Content
Content-Range: bytes 1000-2097151/2097152
Content-Length: 2096152
Accept-Ranges: bytes你:206 Partial Content 是 Range 请求的标志?
墨叔:对。几个要点:
Accept-Ranges: bytes:服务器在第一次响应里声明”我支持范围请求”。没有它,客户端不该发 Range。206+Content-Range:告诉客户端”这是哪一段/总共多长”。If-Range:一个精妙的细节——客户端在断点续传时,可以带If-Range: <etag>,意思是”如果资源还是这个 ETag 没变,就给我续传;如果变了,就给我完整的新内容”。防止”资源变了,你却拿旧的后半段拼了个四不像”。
你:这个 If-Range 挺聪明的。
墨叔:对。它是”乐观并发”在传输层的应用——先确认资源没变,再做增量操作,否则退化回全量。这和第 3 章、第 8 章的 ETag 乐观锁是同一个思想。
10.6 分块传输与 SSE:为实时推送铺路
墨叔:内容协商的最后一块,也是为第 14 章 WebSocket 铺路的——SSE(Server-Sent Events,服务器推送事件)。
你:SSE 是什么?跟 WebSocket 啥关系?
墨叔:SSE 是一种**“单向的、纯 HTTP 的”实时推送**:服务器持续往一个 HTTP 响应里”流式”地写数据,客户端持续读。
HTTP/1.1 200 OK
Content-Type: text/event-stream <- 特殊 MIME 类型
Cache-Control: no-cache
data: 用户 123 上线了\n\n
data: 用户 456 下线了\n\n关键:这个响应是永不结束的(连接一直开着,服务器一直写)。这正是第 2 章”chunked 流式响应”的应用——服务器用 Transfer-Encoding: chunked 或”无限 body”来持续推送。
你:那 SSE 和 WebSocket 的区别?
墨叔:一句话总结(第 14 章会展开):
| SSE | WebSocket | |
|---|---|---|
| 方向 | 单向(服务器 → 客户端) | 双向 |
| 底层 | 纯 HTTP(复用现有的一切) | 协议升级后的独立协议 |
| 自动重连 | 浏览器原生支持 | 要自己做 |
| 复杂度 | 低 | 高 |
SSE 的定位是:“我只需要服务器推消息,不需要客户端回发,那我干嘛上 WebSocket 这么重的东西?” 这是”选对工具”的思维——不是所有实时场景都需要 WebSocket。
10.7 设计者视角:为什么”协商”是 HTTP 扩展性的又一面
墨叔:最后,把内容协商放到全书的框架里看。
内容协商揭示了一个和”首部扩展”(第 5 章)遥相呼应的设计思想:
HTTP 的很多”多样性”,不是靠”发明新协议”实现的,而是靠”让现有机制可协商”实现的。
- 多语言?
Accept-Language协商,不用发明”语言版 HTTP”。 - 多格式?
Accept协商,不用发明”JSON 专用协议”。 - 压缩?
Accept-Encoding协商,不用发明”压缩版 HTTP”。
你:所以”协商”本质是”同一个东西,声明式地支持多种形态”?
墨叔:精确。而且注意——协商和缓存是深度耦合的(引子里小美的问题就是证据)。这引出一个关键结论:
内容协商改变了”同一个 URL 的响应”,所以它必须用
Vary告诉缓存”我的内容依赖这些维度”。 否则就会出现引子里的缓存污染。
你:所以 Vary: User-Agent、Vary: Accept-Encoding 这些,是”协商”和”缓存”之间的桥梁?
墨叔:对。协商(让内容多样化)和缓存(让内容复用)是一对天生的矛盾。Vary 就是那个”告诉缓存’多样性按什么维度划分‘“的机制。你理解了”协商 + Vary + 缓存”这个三角,就真正理解了”同一个资源多种形态”这件事的全貌。
10.8 Go 落地:gzip 中间件与两个坑
墨叔:写一个 gzip 压缩中间件,顺便点两个坑:
func GzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 坑一:要检查客户端支不支持 gzip(Accept-Encoding)
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
w.Header().Add("Vary", "Accept-Encoding") // 坑二:必须加 Vary!
gz := gzip.NewWriter(w)
defer gz.Close()
gzw := &gzipResponseWriter{ResponseWriter: w, Writer: gz}
next.ServeHTTP(gzw, r)
})
}你:两个坑分别是什么?
墨叔:
- 不检查
Accept-Encoding就强行 gzip:会发给”不支持 gzip 的老客户端”一堆乱码。 - 压缩了但没加
Vary: Accept-Encoding:CDN 会”缓存压缩版,然后把它发给不支持压缩的客户端”(或反过来)——这就是引子里”缓存全乱”的又一个版本。
记住:只要你的响应会因某个请求头而变化,就必须加对应的 Vary。 这是”协商”和”缓存”之间不能省的桥梁。
10.9 排查清单:内容协商与压缩问题
第 1 步:客户端声明了什么 Accept* 头?
→ 服务器尊重了吗?还是写死了格式?
第 2 步:Content-Encoding 和 Transfer-Encoding 用对了吗?
→ 想压缩却设了 Transfer-Encoding?分清
第 3 步:压缩了,加 Vary 了吗?
→ 没加 Vary: Accept-Encoding = 缓存污染
第 4 步:敏感动态内容,禁用压缩了吗?
→ BREACH 攻击风险
第 5 步:Range 请求,If-Range 处理了吗?
→ 断点续传时资源变了怎么办
思考题
第 1 题(概念)
Content-Encoding 和 Transfer-Encoding 的区别是什么?请用一个具体的报文说明:一个 gzip 压缩的 JSON,用 chunked 传输时,完整的响应头应该是怎样的?
第 2 题(概念)
内容协商有三种方式(服务端驱动/代理驱动/透明协商),为什么最终只有”服务端驱动”活了下来?请从成本的角度分析。
第 3 题(设计)
你要设计一个文件下载接口,要求支持:
- 大文件(几个 GB)断点续传
- 同一份文件可能被很多用户下载,要省服务器流量
- 下载过程中文件被更新了,不能出现”新旧内容拼接”的错误
请设计这个接口,说明要用到的机制(Range、ETag、If-Range、缓存等)和交互时序。
第 4 题(生活类比)
墨叔:餐厅点餐时,“顾客说忌口”和 HTTP 的”内容协商”,有什么相似之处?
提示:想想——
- 顾客说”我不吃辣”(类似 Accept)
- 服务员端上”不辣的版本”(类似 Content-Type 响应)
- 如果顾客说”我都行”(类似
*/*) - 如果餐厅做不了不辣的(类似 406)
请映射,并回答:为什么”顾客先说忌口、服务员再决定”比”服务员先做、顾客再抱怨”更高效?
第 5 题(更深一层的发问)
内容协商让”同一个 URL 有多种形态”,但这和”缓存”(同一个 URL 应该能复用)产生了根本冲突。解决这个冲突的机制是 Vary。
现在请你深入思考:
Vary本质上是在做什么?(提示:它是在”URL”这个标识之上,叠加了什么?)- 如果内容依赖的维度太多(
Vary: User-Agent, Cookie, Accept, Accept-Encoding, ...),会发生什么?这暴露了”用 Vary 解决多样性与缓存冲突”的什么根本局限? - 基于这个局限,你会怎么设计一个”既要多样性、又要缓存命中率”的系统?
参考答案
第 1 题答案
区别:Content-Encoding 描述”内容被压缩成了什么格式”(端到端,接收方要解压),Transfer-Encoding 描述”传输用了什么打包方式”(逐跳,接收方要去掉分段标记)。
一个 gzip 压缩的 JSON,用 chunked 传输的完整响应头:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Encoding: gzip <- 内容:JSON 被 gzip 压缩了
Transfer-Encoding: chunked <- 传输:用分块发送
<chunked 编码的 gzip 字节流>处理顺序(接收方的视角):
- 先去掉 chunked 分段(处理 Transfer-Encoding),得到完整的”gzip 字节”。
- 再解压 gzip(处理 Content-Encoding),得到”JSON 文本”。
- 最后按
Content-Type: application/json解析。
一句话:Transfer-Encoding 管”字节怎么送”,Content-Encoding 管”送的内容是什么格式”,一个在外层、一个在里层,嵌套的。
第 2 题答案
三种方式的核心成本差异,在于**“谁来承担决策的责任和往返成本”**:
-
代理驱动(300 + 客户端挑):需要两次往返——客户端先发请求,服务器回 300 列选项,客户端再发一次选定。多一次 RTT,在高延迟网络下是灾难。而且客户端要实现”选表示”的逻辑,复杂。
-
透明协商(缓存代理协商):需要中间缓存代理理解并参与协商,对中间设备要求极高,而现实中中间设备五花八门,无法保证都支持。
-
服务端驱动(客户端声明、服务器决定):一次往返搞定。客户端在请求里顺手带上
Accept*头(几乎零成本),服务器直接决定返回什么。成本最低、最简单、对中间设备无要求。
所以服务端驱动赢的本质:
它把”协商”这件事,压进了”已经存在的请求-响应”这个单次往返里,没有新增任何往返或中间设备要求。 而另外两种方式,要么多一次往返(代理驱动),要么多一个”必须聪明的中间设备”(透明协商)。
这和第 7 章”管线化失败”、第 1 章”简单决定采用率”是同一个规律:在协议设计里,“成本最低、对生态要求最低”的方案,几乎总是赢家。优雅但昂贵的方案,会死于现实。
第 3 题答案
机制组合:Accept-Ranges + Range + 206 + ETag + If-Range + Cache-Control。
交互时序:
客户端 服务器
|--- GET /files/big.zip ----------->|
| | 返回:200 OK
| | Accept-Ranges: bytes
| | ETag: "hash_v1"
| | Content-Length: 2147483648
| | Cache-Control: public, max-age=86400
|<-- 200 + 部分内容(开始下载)------|
|
|(下载到 1GB 时,网络断了)
|
|--- GET /files/big.zip ----------->|
| Range: bytes=1073741824- |
| If-Range: "hash_v1" |
| | 情况 A:文件没变(ETag 还是 hash_v1)
| | → 206 + Content-Range: bytes 1073741824-2147483647/2147483648
|<-- 206 + 剩余部分(续传成功)------|
|
| | 情况 B:文件变了(ETag 变成 hash_v2)
| | → 200 + 完整新内容(忽略 Range)
|<-- 200 + 全部新内容(重新下载)----|
每个机制的作用:
Accept-Ranges: bytes:首次响应声明”支持断点”,客户端才知道能续传。Range: bytes=1073741824-:客户端声明”从 1GB 处继续”。206 Partial Content+Content-Range:服务器返回”这是后半段”。ETag+If-Range:这是防”新旧拼接”的关键。续传时带上If-Range: "hash_v1",意思是”如果文件还是 v1 这个版本,就给我续传;如果已经变成 v2,就别续传了,给我完整的新文件”。否则会出现”前半段是 v1 的、后半段是 v2 的”拼接出来的损坏文件。Cache-Control: public, max-age=86400:文件是共享的、相对不变的,允许 CDN 缓存,省服务器流量。
核心要点:断点续传不是”一个技巧”,而是”Range(增量)+ If-Range/ETag(一致性)+ 缓存(省流量)“三者的协同。 少了 If-Range,续传就可能拼出损坏的文件——这是很多”下载损坏”bug 的根因。
第 4 题答案
| 餐厅 | HTTP |
|---|---|
| 顾客说”我不吃辣” | Accept(客户端声明能接受什么) |
| 服务员端上”不辣的版本” | Content-Type(服务器声明给了什么) |
| 顾客说”我都行” | Accept: */* |
| 餐厅做不了不辣的 | 406 Not Acceptable |
为什么”先说忌口、再决定”比”先做、再抱怨”更高效?
因为前者的信息流是”提前的、单向的、一次性的”:
- 先问需求:顾客一句话”我不吃辣”,服务员就知道该做什么、不该做什么,一次做成。
- 后问抱怨(先做再问):服务员做了一份辣的,顾客说”我不吃辣”,于是倒掉重做——浪费了一份菜、浪费了时间,还可能再来一轮。
翻译成 HTTP:
“客户端先声明 Accept,服务器再决定” = 一次往返、一次成功。而”服务器先固定返回一种,客户端收到后不匹配再报错重来” = 多次往返、反复浪费。
更深一层:这揭示了”协商”的价值本质——
协商是把”双方的偏好”提前对齐,用”一点声明成本”(一个 Accept 头)换掉”反复试错的成本”(多次往返、重复传输)。
在低延迟环境(餐厅里、同机房)下,这个优化不明显;但在高延迟、高成本的网络环境(互联网)里,“提前对齐”省下的每一次往返、每一份重复传输,累积起来就是巨大的收益。协议设计里的很多”声明式”机制(Accept、Vary、Cache-Control),本质都是在”用一点元数据,省掉大量的试错”。
第 5 题答案
1. Vary 本质上在做什么?
Vary 本质上是在**“URL”这个一维标识之上,叠加了额外的维度,把它扩展成一个多维的缓存键。**
- 默认缓存键:
URL(一维)。 - 加了
Vary: Accept-Encoding后,缓存键变成:URL × Accept-Encoding(二维)。 - 加了
Vary: User-Agent后:URL × Accept-Encoding × User-Agent(三维)。
换句话说:Vary 是在承认”URL 不足以唯一标识内容”,然后在缓存层用额外的维度去”补齐”这个不足。
2. 维度太多会怎样?暴露了什么局限?
维度太多,缓存键就爆炸——每个维度的每个取值组合,都是一个独立的缓存条目。极端情况下:
Vary: User-Agent:用户代理成千上万种 → 缓存命中率趋近 0(第 8 章讲过)。Vary: Cookie:每个用户的 Cookie 都不同 → 等于每个用户一个缓存副本,共享缓存彻底失效。Vary: Accept, Accept-Encoding, Accept-Language:三个维度相乘,条目数量是三个维度的笛卡尔积。
根本局限:
Vary试图用”给缓存键加维度”来解决”内容依赖上下文”的问题,但”上下文”是无限维的,而缓存键的维度是有限的、且维度越多命中率越低。 所以Vary只能处理”低维度的、可枚举的”多样性(如压缩编码、语言),无法处理”高维度的、连续的”多样性(如每个用户的个性化内容)。
3. 怎么设计”既要多样性、又要命中率”的系统?
核心思路是**“分层缓存 + 拆分资源”**:
思路一:把”可共享的部分”和”个性化的部分”拆成两个资源。
- 可共享的(文章正文、公共配置)→ 一个 URL,允许 CDN 缓存,命中率极高。
- 个性化的(用户信息、收藏状态)→ 另一个 URL,
private缓存,或干脆动态请求不缓存。
这是最有效的办法:不要试图让”一个 URL”同时承载”公共内容”和”个性化内容”,而是把它们拆开。 公共的走共享缓存(省流量),个性的走私有缓存或动态(保证正确)。
思路二:把多样性”固化到 URL 里”,而不是用 Vary。
- 与其
GET /app+Vary: Accept-Language,不如GET /app/zh、GET /app/en——把语言变成 URL 的一部分。 - 这样缓存键天然就是
URL(一维),命中率可控,而且明确。
思路三:理解”缓存只能在某个体积上生效”。
缓存命中率,取决于”内容的可共享度”。 内容越个性化(依赖越多上下文),可共享度越低,缓存越难命中。这不是 Vary 的错,而是”个性化”和”复用”的本质矛盾。要命中率,就要牺牲个性化;要个性化,就要接受命中率下降。
最终结论:
Vary是”在 URL 上补维度”的权宜之计,适合”低维、可枚举”的多样性。真正高维的个性化,正确解法不是”加更多 Vary”,而是”拆分资源 + 让可共享部分走共享缓存”。 设计一个”既要多样性又要缓存”的系统,本质是仔细划分”什么能共享、什么不能共享”的边界——这个划分能力,就是第 8 章”私有 vs 共享缓存”在更高层次的运用。
一页纸小结
内容协商:客户端 Accept* 声明、服务器 Content-* 决定。三种方式里只有”服务端驱动”活下来,因为它零额外往返、零生态要求。
Content-Encoding vs Transfer-Encoding:前者是”内容压缩格式”(端到端,要解压),后者是”传输打包方式”(逐跳,去分段)。可以嵌套。
压缩:静态用 br(压一次服务万次),动态用 gzip(CPU 友好)。BREACH 攻击:压缩+加密+反射输入 = 泄露秘密。
Range 请求:断点续传。206 + Content-Range + If-Range(防新旧拼接)。
SSE:单向、纯 HTTP 的实时推送,是”不需要双向就别上 WebSocket”的轻量选择。
核心洞察:协商(多样性)和缓存(复用)是天生矛盾,Vary 是它们的桥梁——但 Vary 只适合低维多样性,高维个性化要”拆资源”。
下一章:内容、报文、连接都聊完了,但有一个贯穿始终的问题我一直没正面回答——你的数据,到底在哪一层被保护? 是 TCP?是 TLS?还是那个 Authorization 头?我要讲一个协议设计里最反直觉的结论:那个被寄予厚望的”认证方案” Digest,为什么最后死了? 答案会颠覆你对”技术优劣”的认知。