← 网络基础学习第 7 / 55 章基础45 分钟

模块一 · 应用层

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 是应用层协议,定义了"客户端和服务器之间的对话格式"。

它的模型非常简单,只有两条规则:

  1. 请求-响应:客户端发一个请求,服务器回一个响应。这里讨论通常的请求/响应;协议扩展曾支持HTTP/2推送,持续响应流等也要按具体机制解释
  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.comHTTP/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 题 · 先自己答,再展开对照

  1. HTTP 为什么需要 Host 头?HTTP/1.0 里没有它会怎样?
    因为一个服务器 IP 上可能挂载着很多个网站(虚拟主机)。服务器需要靠 Host 知道"你要的是哪一个网站",否则只能返回默认站点 —— 可能会返回别人的网站。HTTP/1.0 里没有这个字段时,一个 IP 基本只能对应一个站点,非常浪费 IP 地址(而那时还没有 SNI 之类的机制)。
  2. GET 和 POST 最本质的区别是什么?为什么这个区别会导致"POST 不能被自动重试"?
    GET用于获取资源且语义安全、幂等;POST让目标按内容处理,不保证幂等。HTTP层重试需考虑重复执行,普通TCP重传已按序号去重,不是重复下单原因。缓存也不是仅由幂等性决定。
  3. 收到 502 Bad Gateway,问题在客户端还是服务器端?
    502表示网关/代理从上游收到无效响应,504表示未及时收到需要的上游响应;应查对应代理和上游日志,状态码不证明客户端请求或配置完全无关。
  4. HTTP 是无状态的,那服务器怎么知道你登录过?
    常见方式是Cookie带session ID,服务器用它查询服务端会话;也可使用签名客户端状态等设计。Cookie不要求全部状态只存在客户端。
  5. Transfer-Encoding: chunked 解决了什么问题?代价是什么?
    解决了"服务器事先算不出响应总长度"的问题。 动态生成的页面(还在查数据库、拼页面)无法预先知道总字节数,就无法填 Content-Length。用 chunked 方式可以边生成边发,每块标注自己的长度,最后用长度为 0 的块表示结束。
    代价是客户端无法显示下载进度 —— 因为不知道总量,就算不出百分比。
  6. 304 Not Modified 有什么好处?
    省掉整个响应体的传输。 客户端本地已经有缓存副本,发一个请求问服务器"内容变了没有";如果没变,服务器只回一个 304 Not Modified 状态码(不带响应体),客户端就用本地副本渲染。对于大图片、大 CSS 文件,这省下的流量非常可观。
  7. 你通过公司代理访问一个 HTTPS 网站。代理能看到你访问了哪个网站吗?能看到你传输的内容吗?
    能看到你访问了哪个网站,看不到你传输的内容。
    因为 HTTPS 的流量是先建立 TLS 加密连接再发 HTTP 请求的,代理只负责"转发加密后的字节流"。对于明文代理,它通过 CONNECT www.cloudflare.com:443 知道目标主机和端口(历史实测记录确实如此),但隧道内部是密文。
    例外情况:如果公司代理安装了自己的根证书做了"合法的中间人解密",那它就能看到全部内容 —— 这是企业环境里真实存在的做法,下一章会讲它的原理。

参考资料与适用范围

修订核对日期:2026-10-13。规则按所列协议与实现范围理解;文中的原始命令输出是历史观察,不表示本次重新实测。跨版本、代理/隧道及不同捕获位置需要另行核对。

正文源自 m1-03-HTTP.md · 共 14 节 · 7 道自测题