读《图解HTTP》:HTTP 首部到底在传递什么信息?
本文是阅读《图解HTTP》第 6 章后的学习整理,结合个人理解做了少量补充。文中的表格用于归纳本章概念,不替代原书内容。
为什么要单独看 HTTP 首部
前面几篇已经讲过,请求和响应是 HTTP 通信的基本形式。但如果只看"请求行、状态行、主体",我们其实还看不懂一次 HTTP 通信的完整意图。
比如:
- 浏览器能接收什么格式的内容?
- 服务器返回的资源能不能缓存?
- 客户端请求的是完整资源,还是某一段字节范围?
- 服务器要求客户端使用哪种认证方式?
- Cookie 是谁下发的,又是谁带回来的?
这些信息大多不是放在主体里,而是放在 HTTP 首部字段里。
可以把 HTTP 报文简单理解成这样:
text
起始行
首部字段
空行
主体
首部字段就像报文身上的说明标签:它不一定是用户最终看到的内容,却决定了客户端和服务器如何理解、传输、缓存、认证和处理这份内容。
首部字段长什么样
HTTP 首部字段由字段名和字段值组成,中间用冒号分隔:
http
首部字段名: 字段值
例如:
http
Content-Type: text/html
这里 Content-Type 是字段名,text/html 是字段值。
一个首部字段也可以有多个值:
http
Keep-Alive: timeout=15, max=100
书中还特别提到一个容易被忽略的问题:如果同一个报文里出现多个相同字段名,不同浏览器或实现的处理结果可能不一致。有的会优先处理第一次出现的字段,有的会优先处理最后一次出现的字段。
所以在实际开发中,应该尽量避免生成重复含义的同名首部。
HTTP 首部分成哪几类
书中把 HTTP/1.1 的首部字段分成四类:
| 类型 | 使用位置 | 作用 |
|---|---|---|
| 通用首部字段 | 请求和响应都会使用 | 描述通信整体行为,比如缓存、连接管理、传输编码 |
| 请求首部字段 | 请求报文使用 | 补充客户端信息、请求条件、可接受内容等 |
| 响应首部字段 | 响应报文使用 | 补充服务器信息、认证要求、重定向位置等 |
| 实体首部字段 | 请求和响应的实体部分使用 | 描述实体主体本身,比如类型、长度、编码、修改时间 |
这四类不要死记,可以按问题来理解:
- 通用首部:这次通信整体怎么处理?
- 请求首部:客户端想要什么、能接受什么、带了什么条件?
- 响应首部:服务器怎么回应、要求什么、资源在哪里?
- 实体首部:报文主体是什么、怎么编码、有多大、什么时候改过?
书中列出了 HTTP/1.1 规范定义的 47 种首部字段。发布文章不适合逐字照搬成一大段,但我们可以先用一张总表建立全局印象:
| 类别 | 首部字段 |
|---|---|
| 通用首部 | Cache-Control、Connection、Date、Pragma、Trailer、Transfer-Encoding、Upgrade、Via、Warning |
| 请求首部 | Accept、Accept-Charset、Accept-Encoding、Accept-Language、Authorization、Expect、From、Host、If-Match、If-Modified-Since、If-None-Match、If-Range、If-Unmodified-Since、Max-Forwards、Proxy-Authorization、Range、Referer、TE、User-Agent |
| 响应首部 | Accept-Ranges、Age、ETag、Location、Proxy-Authenticate、Retry-After、Server、Vary、WWW-Authenticate |
| 实体首部 | Allow、Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、Expires、Last-Modified |
另外,HTTP 通信中也会使用一些不属于 HTTP/1.1 RFC2616 定义、但非常常见的首部,比如 Cookie、Set-Cookie、Content-Disposition 等。书中也把 Cookie 相关首部单独拿出来讲,因为它在 Web 状态管理中太常用了。
端到端首部和逐跳首部
书中还提到一个很重要的分类:端到端首部和逐跳首部。
| 类型 | 含义 | 典型理解 |
|---|---|---|
| 端到端首部 | 会转发给最终接收目标,并且必须保存在缓存生成的响应中 | 信息要一路传到最终目的地 |
| 逐跳首部 | 只对单次转发有效,不会继续转发给下一站 | 信息只在当前这一跳生效 |
HTTP/1.1 中典型的逐跳首部包括:
ConnectionKeep-AliveProxy-AuthenticateProxy-AuthorizationTrailerTETransfer-EncodingUpgrade
这个概念和代理转发场景很容易连起来理解:请求经过代理时,不是所有首部都应该原样传到最后。有些首部只服务于当前连接,到了下一跳就应该重新处理。
通用首部:控制通信整体行为
通用首部既可以出现在请求中,也可以出现在响应中。它们描述的是这次 HTTP 通信本身的处理方式。
最值得先记住的是这几个:
| 首部字段 | 作用 | 典型场景 |
|---|---|---|
Cache-Control |
控制缓存行为 | 浏览器缓存、代理缓存、敏感数据不缓存 |
Connection |
管理连接和逐跳首部 | 控制连接关闭、声明不再转发的首部 |
Date |
表示报文创建时间 | 日志、缓存判断 |
Pragma |
HTTP/1.0 遗留字段 | 兼容旧缓存控制 |
Trailer |
说明报文主体之后还会出现哪些首部 | 分块传输编码 |
Transfer-Encoding |
指定传输报文主体时采用的编码方式 | 分块传输 |
Upgrade |
切换到其他协议 | 协议升级 |
Via |
记录代理或网关经过路径 | 排查代理链路 |
Warning |
告知与缓存相关的警告 | 缓存过期、再验证失败等 |
其中 Cache-Control 是最容易在实际开发里碰到的。比如:
http
Cache-Control: max-age=3600
Cache-Control: no-cache
Cache-Control: no-store
这里最容易误解的是 no-cache。它并不是"完全不缓存",而是"可以缓存,但使用前必须向源服务器确认有效性"。真正表示不保存请求和响应内容的是 no-store。
Cache-Control 指令不能只记 no-cache
第 6 章里 Cache-Control 占了很大篇幅,因为它确实是缓存控制的核心。
常见请求指令可以这样理解:
| 请求指令 | 含义 |
|---|---|
no-cache |
不直接接收缓存过的响应,要求缓存服务器转发请求并确认有效性 |
no-store |
不保存请求或响应的任何内容 |
max-age |
只接收 Age 不超过指定秒数的缓存响应 |
max-stale |
即使缓存过期,也愿意接收一段时间内的响应 |
min-fresh |
希望响应在指定时间内仍然保持新鲜 |
only-if-cached |
只从缓存获取响应,不向源服务器确认 |
常见响应指令可以这样理解:
| 响应指令 | 含义 |
|---|---|
public |
响应可以被任意缓存保存 |
private |
响应只面向特定用户,通常不应被共享缓存返回给其他用户 |
no-cache |
缓存使用前必须向源服务器确认有效性 |
no-store |
不保存请求或响应的任何内容 |
max-age |
响应在指定秒数内可视为新鲜 |
s-maxage |
针对公共缓存服务器的最大缓存时间 |
must-revalidate |
缓存过期后必须向源服务器再验证 |
proxy-revalidate |
要求共享缓存服务器再验证 |
no-transform |
缓存不能改变实体主体的媒体类型或内容编码 |
这也是第 5 章讲缓存时留下的问题:缓存服务器并不是"有缓存就直接返回",它需要根据这些缓存指令判断能不能用、要不要重新验证。
Connection、Upgrade、Via 的关系
Connection 有两个作用:
- 控制不再转发给代理的首部字段
- 管理持久连接
比如使用协议升级时,通常会同时出现:
http
Upgrade: TLS/1.0
Connection: Upgrade
这表示 Upgrade 只对当前连接这一跳生效,不应该继续被代理无脑转发。
Via 则用于追踪报文经过的代理或网关。书中强调,经过代理时必须附加 Via,它既能追踪转发路径,也能避免请求回环。
Warning 主要用于告知用户一些与缓存相关的问题,比如响应已经过期、再验证失败、代理对内容做了转换等。
请求首部:客户端告诉服务器"我想要什么"
请求首部用于补充客户端的请求信息。它告诉服务器:客户端能处理什么内容、请求来自哪里、想访问哪个主机、是否带了认证信息、是否只想获取资源的一部分。
常见请求首部可以这样分组:
| 关注点 | 首部字段 | 作用 |
|---|---|---|
| 内容协商 | Accept |
告诉服务器客户端能处理哪些媒体类型 |
| 内容协商 | Accept-Charset |
告诉服务器客户端支持哪些字符集 |
| 内容协商 | Accept-Encoding |
告诉服务器客户端支持哪些内容编码 |
| 内容协商 | Accept-Language |
告诉服务器客户端偏好的自然语言 |
| 目标主机 | Host |
指定请求目标主机名和端口 |
| 认证 | Authorization |
携带客户端认证信息 |
| 期待行为 | Expect |
告诉服务器客户端期待某种特定行为,例如 100-continue |
| 用户联系信息 | From |
告知服务器用户的电子邮件地址 |
| 条件请求 | If-Match、If-None-Match |
根据 ETag 判断是否处理请求 |
| 条件请求 | If-Modified-Since、If-Unmodified-Since |
根据资源修改时间判断是否处理请求 |
| 范围请求 | Range、If-Range |
请求资源的某一部分 |
| 代理相关 | Max-Forwards、Proxy-Authorization |
限制转发次数,或向代理提供认证信息 |
| 传输编码 | TE |
告诉服务器客户端能处理哪些传输编码 |
| 客户端信息 | User-Agent、Referer |
表示客户端程序和请求来源 |
Host 为什么重要
Host 是 HTTP/1.1 中唯一必须包含的请求首部字段。
这和第 5 章的虚拟主机直接相关:多个域名可能解析到同一个 IP 地址,服务器必须通过 Host 判断客户端到底想访问哪个站点。
http
GET / HTTP/1.1
Host: www.example.com
如果没有 Host,虚拟主机就很难正确区分不同站点。
Accept 系列在做什么
Accept、Accept-Encoding、Accept-Language 这类字段,本质上是在做内容协商。
客户端不是简单地说"给我资源",而是在说:
- 我能接受 HTML,也能接受 JSON
- 我能处理 gzip 压缩
- 我更偏好中文内容
服务器再根据这些信息,尽量返回最合适的资源表示。
条件请求是一组字段,不是一个字段
第 6 章里很多 If- 开头的字段都属于附带条件的请求。
| 字段 | 条件含义 |
|---|---|
If-Match |
ETag 匹配时才处理请求 |
If-None-Match |
ETag 不匹配时才处理请求 |
If-Modified-Since |
指定时间后资源更新过,才处理请求 |
If-Unmodified-Since |
指定时间后资源未更新,才处理请求 |
If-Range |
ETag 或时间匹配时按范围请求处理,否则返回完整资源 |
这些字段和缓存验证、断点续传、并发修改控制都有关系。
Max-Forwards 用来排查代理链路
Max-Forwards 会指定请求最多还能经过多少台服务器。每经过一次转发,数值减 1;当变成 0 时,接收请求的服务器直接返回响应。
它常和 TRACE、OPTIONS 方法一起用于排查代理转发问题:如果请求在中间某一跳失败,可以通过控制 Max-Forwards 逐步定位问题发生在哪一段。
响应首部:服务器告诉客户端"我怎么回应"
响应首部用于补充服务器响应的信息。它可能告诉客户端资源位置、服务器信息、认证方式、范围请求能力,也可能影响缓存行为。
| 首部字段 | 作用 | 典型场景 |
|---|---|---|
Accept-Ranges |
告知是否支持范围请求 | 下载续传、媒体资源 |
Age |
告知源服务器在多久前创建了响应 | 缓存响应 |
ETag |
表示资源实体标识 | 缓存验证、并发控制 |
Location |
提供重定向目标 URI | 3xx 重定向 |
Proxy-Authenticate |
告知客户端代理服务器要求的认证信息 | 代理认证 |
Retry-After |
告知客户端多久后再次请求 | 503 或重定向响应 |
Server |
告知服务器软件信息 | 调试、排查 |
Vary |
指定缓存需要参考哪些请求首部 | 内容协商缓存 |
WWW-Authenticate |
告知客户端认证方案和质询信息 | 401 响应 |
ETag 和 Last-Modified 怎么配合缓存
严格说,ETag 是响应首部,Last-Modified 是实体首部,但它们经常一起出现在缓存验证场景里。
服务器第一次返回资源时,可以带上:
http
ETag: "abc123"
Last-Modified: Wed, 23 May 2012 09:59:55 GMT
之后客户端再请求同一资源时,就可以通过条件请求询问服务器:
http
If-None-Match: "abc123"
If-Modified-Since: Wed, 23 May 2012 09:59:55 GMT
如果资源没有变化,服务器就可以避免重复传输完整资源。
这里可以这样理解:
| 字段 | 判断依据 | 特点 |
|---|---|---|
ETag |
资源实体标识 | 粒度更细,能表示资源版本 |
Last-Modified |
最后修改时间 | 容易理解,但受时间精度影响 |
Vary 会影响缓存命中
Vary 用于告诉缓存服务器:缓存响应时,不能只看 URL,还要看某些请求首部字段。
例如:
http
Vary: Accept-Language
这表示即使请求的是同一个资源,如果 Accept-Language 不同,也不能简单返回同一份缓存。否则英文用户可能拿到中文页面,或者反过来。
Retry-After 告诉客户端什么时候再来
Retry-After 常配合 503 Service Unavailable 或 3xx 重定向使用。字段值可以是具体日期时间,也可以是几秒之后。
它传达的是一种节奏控制:服务器现在无法处理,或希望客户端稍后再发起请求。
实体首部:描述主体本身
实体首部关注的是报文主体本身,而不是通信链路。
常见字段包括:
| 首部字段 | 作用 |
|---|---|
Allow |
通知客户端指定资源支持哪些 HTTP 方法 |
Content-Type |
说明实体主体的媒体类型 |
Content-Encoding |
说明实体主体采用的内容编码 |
Content-Language |
说明实体主体使用的自然语言 |
Content-Length |
说明实体主体大小 |
Content-Location |
给出与报文主体对应的 URI |
Content-MD5 |
提供实体主体的报文摘要,用于检查完整性 |
Content-Range |
范围请求响应中,说明返回的是实体的哪一部分 |
Expires |
指定资源失效时间 |
Last-Modified |
指定资源最后修改时间 |
比如一个 HTML 响应可能包含:
http
Content-Type: text/html; charset=UTF-8
Content-Encoding: gzip
Content-Length: 12345
这三个字段分别说明:主体是什么类型、有没有压缩、主体有多大。
Allow 常出现在 405 Method Not Allowed 响应中,用来告诉客户端当前资源支持哪些方法:
http
Allow: GET, HEAD
Content-Range 则和范围请求配套使用。服务器返回 206 Partial Content 时,会用它说明这次返回的是资源的哪一段:
http
Content-Range: bytes 5001-10000/10000
Content-MD5 的目的在于让客户端检查报文主体是否完整。不过如果报文主体和 Content-MD5 字段本身都被篡改,客户端也无法仅凭它发现问题,这也是为什么传输安全不能只依赖单个字段。
Cookie 相关首部:HTTP 的状态补丁
HTTP 本身是无状态协议。为了让服务器识别"这次请求和上次请求来自同一个用户",Cookie 就派上了用场。
Cookie 相关首部主要有两个:
| 首部字段 | 方向 | 作用 |
|---|---|---|
Set-Cookie |
服务器 -> 客户端 | 服务器要求客户端保存 Cookie |
Cookie |
客户端 -> 服务器 | 客户端在后续请求中带回 Cookie |
典型流程是:
text
服务器响应:Set-Cookie: session_id=abc123
客户端后续请求:Cookie: session_id=abc123
书中提到的 Set-Cookie 属性包括:
expirespathdomainSecureHttpOnly
其中 Secure 表示 Cookie 仅在 HTTPS 连接中发送,HttpOnly 可以防止 JavaScript 通过 document.cookie 读取 Cookie。涉及登录态的 Cookie,尤其要关注这些安全属性。
Cookie 的几个属性含义也值得单独记一下:
| 属性 | 含义 |
|---|---|
NAME=VALUE |
Cookie 的名称和值,必需项 |
expires |
Cookie 有效期;不指定时通常到浏览器关闭前为止 |
path |
限制 Cookie 适用的服务器路径 |
domain |
限制 Cookie 适用的域名范围 |
Secure |
只在 HTTPS 安全通信时发送 Cookie |
HttpOnly |
限制 JavaScript 读取 Cookie,降低 XSS 窃取风险 |
书中还提到,服务器端没有一个"显式删除客户端 Cookie"的直接方法,常见做法是通过覆盖一个已过期的 Cookie,让客户端实现实质删除。
其他常见首部:安全和隐私相关
第 6 章最后还介绍了几个常见的非标准或扩展首部。
| 首部字段 | 类型 | 作用 |
|---|---|---|
X-Frame-Options |
响应首部 | 控制页面是否允许被 frame/iframe 嵌入,用于防点击劫持 |
X-XSS-Protection |
响应首部 | 控制浏览器 XSS 防护机制 |
DNT |
请求首部 | Do Not Track,表示用户拒绝被追踪的偏好 |
P3P |
响应首部 | 与 Web 隐私策略相关 |
其中 X-Frame-Options 常见取值包括:
DENYSAMEORIGIN
X-XSS-Protection 和 P3P 都带有明显的时代背景;现代安全策略中,还需要结合 CSP、SameSite Cookie 等机制一起看。这里的重点是理解:HTTP 首部也常被浏览器和服务器用来表达安全、隐私和兼容性策略。
几组最容易混的首部概念
| 易混点 | 区别 |
|---|---|
no-cache vs no-store |
no-cache 可以缓存,但使用前要验证;no-store 是不保存请求或响应内容。 |
ETag vs Last-Modified |
ETag 按资源标识判断;Last-Modified 按修改时间判断。 |
Content-Encoding vs Transfer-Encoding |
前者描述实体内容如何编码;后者描述传输过程中如何编码。 |
Authorization vs WWW-Authenticate |
前者是客户端提交认证信息;后者是服务器发起认证质询。 |
Cookie vs Set-Cookie |
前者是客户端带回 Cookie;后者是服务器下发 Cookie。 |
Host vs Server |
Host 表示客户端要访问谁;Server 表示服务器软件信息。 |
| 端到端首部 vs 逐跳首部 | 端到端首部传到最终目标;逐跳首部只对当前一次转发生效。 |
实践:用 curl 观察几个典型首部
下面是结合开发场景的补充实践,不是原书中的命令示例。
1. 查看响应首部
bash
curl -I https://www.example.com/
可以重点观察:
Content-TypeCache-ControlETagLast-ModifiedServer
2. 使用 Accept 系列首部做内容协商
bash
curl -H "Accept: application/json" https://api.example.com/data
curl -H "Accept-Language: zh-CN,zh;q=0.9" https://www.example.com/
curl -H "Accept-Encoding: gzip, deflate" --compressed https://www.example.com/
3. 使用条件请求验证缓存
bash
curl -H 'If-Modified-Since: Wed, 23 May 2012 09:59:55 GMT' -I https://www.example.com/
curl -H 'If-None-Match: "abc123"' -I https://www.example.com/
4. 发送 Cookie 和认证信息
bash
curl -H "Cookie: session_id=abc123" https://www.example.com/dashboard
curl -u username:password https://www.example.com/admin/
小结
HTTP 首部不是可有可无的附加信息,它决定了 HTTP 通信中很多关键行为:
- 内容协商靠
Accept系列 - 虚拟主机靠
Host - 缓存控制靠
Cache-Control、ETag、Last-Modified - 范围请求靠
Range - 认证交互靠
Authorization和WWW-Authenticate - 状态管理靠
Cookie和Set-Cookie - 代理链路可以通过
Via观察
如果说 HTTP 报文主体是"要传的内容",那么首部字段就是"如何传、如何理解、如何处理"的说明书。读懂首部,很多缓存、认证、压缩、重定向、代理相关的问题都会变得清晰。
不过,首部字段只能帮助双方传递控制信息,并不能解决 HTTP 的先天安全问题:明文会被窃听,身份可能被伪装,内容也可能被篡改。下一篇就顺着这个问题继续往下看:HTTPS 是怎样给 HTTP 补上安全能力的。