模块一 · 应用层
HTTP:浏览器和服务器怎么对话
上一章认识了客户端、服务器与网络服务。现在先假设浏览器已知道服务器地址,看看“请求首页”和“返回页面”这段对话怎样表达。这就是HTTP要规定的消息语义;传输细节在后续模块学习。
本章要解决的问题:上一章认识了客户端、服务器与网络服务。现在先假设浏览器已知道服务器地址,看看“请求首页”和“返回页面”这段对话怎样表达。这就是HTTP要规定的消息语义;传输细节在后续模块学习。
本章的证据来源:历史实测记录(curl -v 抓到的真实请求头与响应头,全文贴在正文里)。格式定义出自 RFC 9110 / RFC 9112。
先从一个例子理解概念
你说“给我首页”,服务器回“这是页面”。HTTP规定这段对话怎样写;随后再看方法、状态码、Cookie和缓存。
问与答:请求、响应与HTTP
客户端发出的消息叫请求,服务器返回的叫响应;HTTP规定它们的语义和处理方式。消息包含控制信息,可能还带具体内容。
要做什么与结果如何:方法、状态码
请求中的GET、POST等叫方法;响应中的200、404等数字叫状态码。方法表达操作语义,状态码表达结果类别,不是完整故障原因。
重复执行会怎样:幂等
相同请求重复执行,对服务器所请求的作用不额外改变,叫幂等。例如把同一资源替换成相同内容;它不要求每次返回文本完全相同,也不等于可以随意缓存。
协议不强制记会话:无状态与Cookie
HTTP的无状态不禁止应用保存用户会话。Cookie是客户端保存并随请求携带的数据,可只带session ID(会话编号),服务器用它查询保存的状态。
已经有的内容能否复用:缓存
把响应保存后按规则复用叫缓存;向服务器确认副本是否变化叫重新验证。缓存期限、请求条件和身份信息影响是否能复用,不只看GET这个名字。
下面再用这些概念看原理、命令和结果。规范与官方资料列在本章末尾,类比只帮助理解,不代替协议规则。
HTTP 是什么
HTTP 是应用层协议,定义了"客户端和服务器之间的对话格式"。
它的模型非常简单,只有两条规则:
- 请求-响应:客户端发一个请求,服务器回一个响应。这里讨论通常的请求/响应;协议扩展曾支持HTTP/2推送,持续响应流等也要按具体机制解释
- 无状态:每个请求都是独立的,协议本身不强制保存跨请求会话,但应用完全可以在服务器保存状态
无状态听着不方便 —— 那"登录状态"是怎么保持的?靠 Cookie(本章后面讲)。
报文长什么样
请求的结构:
GET /index.html HTTP/1.1 ← 方法 + 路径 + 版本
Host: example.com ← 头部(若干行)
User-Agent: curl/8.21.0
Accept: */*
← 空行,标记头部结束
(请求体,GET 通常没有) ← 主体响应的结构:
HTTP/1.1 200 OK ← 版本 + 状态码 + 原因短语
Content-Type: text/html ← 头部(若干行)
Content-Length: 1234
← 空行
(响应体:实际的网页内容) ← 主体两者结构几乎一样,只是第一行不同:请求的第一行是"我要什么",响应的第一行是"结果如何"。
方法:我要做什么
| 方法 | 语义 | 幂等? |
|---|---|---|
| GET | 获取资源 | 是 |
| HEAD | 只要头部,不要内容 | 是 |
| POST | 提交数据(通常会改变服务器状态) | 否 |
| PUT | 整体替换资源 | 是 |
| PATCH | 按补丁语义修改资源 | 不保证;某些补丁可以设计为幂等 |
| DELETE | 删除资源 | 是 |
| OPTIONS | 询问服务器支持什么 | 是 |
"幂等"是什么意思? 同一个请求发一次和发十次,效果一样。
GET幂等:读十次和读一次,服务器状态没变POST不幂等:提交订单发十次,可能下十个单
状态码:结果如何
| 范围 | 含义 | 常见值 |
|---|---|---|
| 1xx | 信息,继续 | 101 切换协议(升级到 WebSocket) |
| 2xx | 成功 | 200 OK、204 无内容 |
| 3xx | 重定向 | 301 永久、302 临时、304 未修改(用缓存) |
| 4xx | 客户端的错 | 400 请求有误、401 未认证、403 无权限、404 找不到、429 请求过多 |
| 5xx | 服务器的错 | 500 内部错误、502 网关错误、503 不可用、504 网关超时 |
记住这个划分方式:4xx/5xx是响应语义分类,不是故障责任证明。 错误路由、权限配置或资源发布问题也可表现为4xx;应结合请求和服务端日志定位。
实测:一次真实的 HTTP 对话
用 curl -v 访问一个网站,把请求和响应头完整抓下来(历史实测记录,流量经代理):
请求头(> 表示发出的):
> CONNECT www.cloudflare.com:443 HTTP/1.1
> Host: www.cloudflare.com:443
> User-Agent: curl/8.21.0
> Proxy-Connection: Keep-Alive
>
< HTTP/1.1 200 Connection established
<
> GET / HTTP/1.1
> Host: www.cloudflare.com
> User-Agent: curl/8.21.0
> Accept: */*
>响应头(< 表示收到的):
< HTTP/1.1 200 OK
< Date: Mon, 12 Oct 2026 16:21:06 GMT
< Content-Type: text/html; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< CF-Ray: a4976f8d0f56df84-BOS
< CF-Cache-Status: HIT
< Accept-Ranges: bytes
< Age: 3
< Cache-Control: max-age=10
< Server: cloudflare
< Vary: accept-encoding
< Strict-Transport-Security: max-age=31536000; includeSubDomains逐行解读几个关键的:
| 头 | 含义 |
|---|---|
Host: www.cloudflare.com | HTTP/1.1 里必需。因为一个 IP 上可能有很多网站,服务器靠这个字段知道你要哪个 |
User-Agent: curl/8.21.0 | 客户端自报身份(服务器常据此返回不同内容) |
Accept: */* | 我能接受任何类型的内容 |
Content-Type: text/html; charset=utf-8 | 内容是 HTML,用 UTF-8 编码 |
Transfer-Encoding: chunked | 内容是分块传的,事先不知道总长度(下面解释) |
Connection: keep-alive | 这条连接先别关,下一个请求还要用 |
CF-Cache-Status: HIT | 这个响应来自 Cloudflare 的缓存,不是回源取的 |
Age: 3 | 这份缓存在 CDN 上已经放了 3 秒 |
Cache-Control: max-age=10 | 新鲜度寿命10秒;还需计算Age与传输时间,不能收到后再无条件缓存10秒 |
Vary: accept-encoding | 缓存时要按 Accept-Encoding 区分(因为压缩格式不同,内容也不同) |
为什么 Host 头是必需的
这是 HTTP/1.1 的一个重要改进。
一个服务器 IP 上可能挂着几百个网站(虚拟主机)。如果请求里不说明"我要哪个网站",服务器根本不知道该返回谁的内容。
没有 Host 头,服务器只能返回默认站点 —— 可能是别人的网站
有 Host 头,服务器才能精确路由到你想要的那个站点所以 Host 是 HTTP/1.1 里唯一一个必须提供的头字段。
为什么会有 Transfer-Encoding: chunked
正常情况下,服务器应该先算出内容总长度,填在 Content-Length 里,再开始发。
但动态生成的页面往往算不出总长度 —— 数据库还在查,页面还在拼。这时服务器用 chunked 方式:边生成边发,每块前面标上这块的长度,最后发一个长度为 0 的块表示结束。
代价:客户端无法显示下载进度条(因为不知道总共多少字节)。下载文件时你看到进度条,说明服务器用了 Content-Length。
无状态与 Cookie
HTTP协议的无状态不禁止服务器保存应用会话。
常见登录设计:服务器保存用户会话,Cookie只携带随机session ID;后续请求携带该ID,服务器查询会话存储。另一种设计把签名的部分会话信息放在客户端,但这不是Cookie的唯一用途。
| 存储方式 | 客户端携带 | 服务器职责 |
|---|---|---|
| 服务端会话 | 会话ID | 保存/查询会话;可使用共享存储 |
| 客户端状态 | 签名或加密的状态/token | 验证完整性、权限和有效期;可能仍有撤销等状态 |
共享会话存储可支持多台应用服务器,不必一律使用会话粘性。Cookie只是HTTP状态管理机制,不能据此断言服务器不保存会话信息。RFC 6265 §8.4
持久连接:keep-alive
HTTP/1.0 的默认行为是:每个请求用一条新连接,请求完就关。
这很浪费 —— 每个请求都要重新做 TCP 三次握手(《运输层》模块),如果还有 TLS 还要再加一轮握手(下一章)。一个网页有几十个资源(图片、CSS、JS),就要建几十条连接。
HTTP/1.1 改成了默认持久连接:一条连接发完一个请求后不关,留着给下一个用。
历史实测记录的响应头里就有:
Connection: keep-alive这就是为什么你在浏览器里连续点几个页面,速度会比较快 —— 连接被复用了。
(也可以显式发 Connection: close 要求关闭。)
缓存:避免重复传输
一个网页的图片、CSS 可能几天都不变。每次都重新下载很浪费,所以 HTTP 有一整套缓存机制。
两级缓存判断:
| 机制 | 做法 | 结果 |
|---|---|---|
| 强缓存 | 服务器给 Cache-Control: max-age=10,客户端 10 秒内直接用本地副本,根本不发请求 | 最快 |
| 协商缓存 | 过了有效期,客户端发请求问服务器"变了吗",带上 If-None-Match | 服务器回 304 Not Modified(不带内容) |
历史实测记录的响应头里两者都有:
Cache-Control: max-age=10 ← 强缓存 10 秒
CF-Cache-Status: HIT ← CDN 也缓存了
Age: 3 ← 这份缓存 3 秒前生成的
Vary: accept-encoding ← 缓存要按编码格式区分304 Not Modified 是一个很聪明的设计:服务器不重发内容,只回一个状态码,客户端就用本地副本。省掉的是整个响应体的传输量。
实测观察:HTTPS 是怎么穿过 HTTP 代理的
注意上面请求头的第一段:
> CONNECT www.cloudflare.com:443 HTTP/1.1
< HTTP/1.1 200 Connection established这不是 HTTP 请求的一部分,而是"打通隧道"的过程。
本机装了 HTTP 代理(《网络层》模块讲过)。当 curl 要访问 HTTPS 网站时,它不能直接连(因为流量被导向代理),于是:
① 先向代理发一个 CONNECT 请求:「帮我在 www.cloudflare.com:443 上开一条隧道」
② 代理回 200,隧道建立
③ 之后的所有数据(包括 TLS 握手)都原样穿过这条隧道
④ 在这条隧道里,才真正发出 GET / HTTP/1.1关键点:代理只看到"要连哪个主机和端口",看不到隧道里的内容 —— 因为隧道里是加密的 TLS 数据。
常见误解
误解一:"HTTP 能记住我的登录状态"
HTTP协议无状态,应用会话可以保存在服务器。 Cookie可能只携带session ID,也可能携带签名状态;不是所有登录都必须使用Cookie。
误解二:"404 说明服务器坏了"
404表示未找到当前表示或不愿公开其存在。 请求地址错误、发布遗漏和服务器路由配置都可能导致它,不能保证服务器配置正确。
误解三:"POST 和 GET 的区别只是有没有请求体"
核心区别是幂等性。 GET是安全且幂等的方法,缓存仍需满足响应与请求的缓存规则,POST 不幂等(重试可能造成重复下单)。这个区别直接影响浏览器、代理、CDN 能对它做什么。
误解四:"HTTP 响应必须带 Content-Length"
不一定。动态生成的内容可以用 Transfer-Encoding: chunked 边生成边发(历史实测记录的响应就是 chunked)。代价是客户端无法显示进度。
本章小结
| 结论 | 类型 |
|---|---|
| HTTP 是请求-响应模型,默认无状态 | 标准 |
| 请求与响应结构相同,只是首行不同 | 标准 |
| 方法分幂等与非幂等,影响能否被缓存和重试 | 标准 |
| 4xx/5xx是响应语义分类,具体根因需结合请求与日志 | RFC 9110 |
Host 头是 HTTP/1.1 里唯一必须提供的头字段 | 标准 |
Transfer-Encoding: chunked 用于无法预知长度的响应 | 标准 |
| Cookie可携带会话ID或状态,服务器也可保存会话与使用共享存储 | RFC 6265 |
HTTP/1.1 默认持久连接(keep-alive) | 标准 |
缓存分强缓存(Cache-Control)与协商缓存(304) | 标准 |
实测:真实响应含 Connection: keep-alive、Transfer-Encoding: chunked、Cache-Control: max-age=10、CF-Cache-Status: HIT | 实测 |
实测:HTTPS 经 HTTP 代理需先用 CONNECT 建立隧道 | 实测 |
自测题
7 题 · 先自己答,再展开对照
HTTP 为什么需要
Host头?HTTP/1.0 里没有它会怎样?因为一个服务器 IP 上可能挂载着很多个网站(虚拟主机)。服务器需要靠Host知道"你要的是哪一个网站",否则只能返回默认站点 —— 可能会返回别人的网站。HTTP/1.0 里没有这个字段时,一个 IP 基本只能对应一个站点,非常浪费 IP 地址(而那时还没有 SNI 之类的机制)。GET和POST最本质的区别是什么?为什么这个区别会导致"POST 不能被自动重试"?GET用于获取资源且语义安全、幂等;POST让目标按内容处理,不保证幂等。HTTP层重试需考虑重复执行,普通TCP重传已按序号去重,不是重复下单原因。缓存也不是仅由幂等性决定。收到
502 Bad Gateway,问题在客户端还是服务器端?502表示网关/代理从上游收到无效响应,504表示未及时收到需要的上游响应;应查对应代理和上游日志,状态码不证明客户端请求或配置完全无关。HTTP 是无状态的,那服务器怎么知道你登录过?
常见方式是Cookie带session ID,服务器用它查询服务端会话;也可使用签名客户端状态等设计。Cookie不要求全部状态只存在客户端。Transfer-Encoding: chunked解决了什么问题?代价是什么?解决了"服务器事先算不出响应总长度"的问题。 动态生成的页面(还在查数据库、拼页面)无法预先知道总字节数,就无法填Content-Length。用 chunked 方式可以边生成边发,每块标注自己的长度,最后用长度为 0 的块表示结束。
代价是客户端无法显示下载进度 —— 因为不知道总量,就算不出百分比。304 Not Modified有什么好处?省掉整个响应体的传输。 客户端本地已经有缓存副本,发一个请求问服务器"内容变了没有";如果没变,服务器只回一个304 Not Modified状态码(不带响应体),客户端就用本地副本渲染。对于大图片、大 CSS 文件,这省下的流量非常可观。你通过公司代理访问一个 HTTPS 网站。代理能看到你访问了哪个网站吗?能看到你传输的内容吗?
能看到你访问了哪个网站,看不到你传输的内容。
因为 HTTPS 的流量是先建立 TLS 加密连接再发 HTTP 请求的,代理只负责"转发加密后的字节流"。对于明文代理,它通过CONNECT www.cloudflare.com:443知道目标主机和端口(历史实测记录确实如此),但隧道内部是密文。
例外情况:如果公司代理安装了自己的根证书做了"合法的中间人解密",那它就能看到全部内容 —— 这是企业环境里真实存在的做法,下一章会讲它的原理。
参考资料与适用范围
- 《计算机网络:自顶向下方法(原书第8版)》§2.1–2.2,纸书53–75页(PDF72–94页):先理解网络应用参与者与服务,再学习HTTP请求与响应。
- RFC 9110 §9、§15:方法幂等性和状态码语义
- RFC 9112 §6–9:HTTP/1.1消息与持久连接
- RFC 9111 §3–4:缓存条件、新鲜度与Age
- RFC 6265 §8.4:Cookie携带会话标识与服务端状态
- RFC 9293 §3.3–3.10:TCP状态、建立关闭、字节流与接口
修订核对日期:2026-10-13。规则按所列协议与实现范围理解;文中的原始命令输出是历史观察,不表示本次重新实测。跨版本、代理/隧道及不同捕获位置需要另行核对。