HTTP协议 ⭐⭐⭐
HTTP 概述
HTTP(HyperText Transfer Protocol,超文本传输协议)是整个万维网(World Wide Web)数据通信的基石。它定义了客户端(通常是浏览器)与服务器之间如何请求和传输超文本文档(HTML)、图片、视频、API 数据等资源的规则。
从协议栈的角度来看,HTTP 是一个典型的 应用层协议(Application Layer Protocol),它运行在 TCP(或在 HTTP/3 中运行在 QUIC/UDP)之上。当你在浏览器中输入一个网址并按下回车时,浏览器就会通过 HTTP 协议与目标服务器建立通信,获取你所请求的网页资源。
HTTP 协议之所以能成为互联网最广泛使用的应用层协议之一,与它的两个核心设计理念密不可分:无状态(Stateless) 和 请求-响应模型(Request-Response Model)。理解这两个概念,就等于抓住了 HTTP 协议的灵魂。
上图展示了 HTTP 通信的全貌:客户端构造请求 → 经由网络传输 → 服务器解析并返回响应。整个过程看似简单,但其中蕴含了精妙的协议设计。
无状态(Stateless)
什么是"无状态"?
HTTP 协议的无状态性是指:服务器不会在两次请求之间保留任何关于客户端的信息。换言之,每一次 HTTP 请求对于服务器来说都是全新的、独立的、互不关联的事务(transaction)。
举一个生活中的类比:想象你每天去同一家咖啡店点咖啡。如果店员是"无状态"的,那么他每次都不会记得你,每次你都需要重新报出你的名字、你想要的咖啡种类和杯型。他不会因为你昨天来过,就自动帮你准备你"常喝的"那杯。
用更技术化的表达:
The server retains no memory of previous interactions with the client. Each request must contain all the information necessary for the server to fulfill it.
这意味着,如果你先发送了一个登录请求(POST /login),紧接着又发送了一个获取个人信息的请求(GET /profile),服务器本身并不知道第二个请求和第一个请求来自同一个"已登录"的用户——除非你在第二个请求中显式地携带了身份凭证。
上面的时序图清晰地展示了无状态性的实际影响:第二次请求如果不携带 Token,服务器根本不知道"你是谁"。
为什么 HTTP 要设计成无状态的?
这个设计决策并非偶然,它带来了若干关键的工程优势:
| 优势 | 说明 |
|---|---|
| 简洁性(Simplicity) | 服务器不需要为每个客户端维护会话记录,协议实现更简单、更不容易出错 |
| 可扩展性(Scalability) | 因为不保存状态,任何一台服务器都可以处理任何一个请求。这让负载均衡(Load Balancing) 和水平扩展(Horizontal Scaling) 变得非常容易 |
| 可靠性(Reliability) | 某一台服务器宕机了,请求可以立刻路由到其他服务器,不会丢失"会话上下文",因为本来就没有 |
| 缓存友好(Cache-Friendly) | 无状态的请求更容易被中间代理和 CDN 缓存,因为相同的请求总是返回相同的结果 |
想象一下:如果 HTTP 是有状态的,那么当你的网站有百万用户同时在线时,服务器需要在内存中为每个用户维护一份会话状态。这不仅消耗大量资源,还让集群部署变成噩梦——因为用户的后续请求必须被路由到记录了他状态的那台特定服务器上(这就是所谓的 sticky session,粘性会话)。
无状态的"弥补方案"——状态管理机制
虽然 HTTP 本身是无状态的,但现实中的 Web 应用几乎都需要"记住用户"。为了在无状态协议之上实现有状态的体验,人们发展出了多种机制:
1. Cookie(浏览器端存储)
服务器在响应头中通过 Set-Cookie 字段下发一段小数据,浏览器会自动保存它,并在后续每次请求中通过 Cookie 请求头自动回传。这是最经典的状态保持方案。
// 服务器响应(第一次)
HTTP/1.1 200 OK
Set-Cookie: session_id=abc123; Path=/; HttpOnly // 服务器下发 Cookie
// 客户端后续请求(自动携带)
GET /profile HTTP/1.1
Host: example.com
Cookie: session_id=abc123 // 浏览器自动带上 Cookie2. Session(服务端存储)
Cookie 只在客户端存一个 session_id,而真正的用户状态(如用户名、权限等)保存在服务器端的 Session 存储中(内存、Redis、数据库等)。服务器通过 session_id 查找对应的状态。
3. Token(如 JWT)
JSON Web Token 是一种自包含(self-contained)的令牌。用户的身份信息被编码在 Token 本身中,服务器无需查询数据库即可验证身份。这种方案天然适合分布式系统,因为它让服务器真正保持了无状态。
核心区别一目了然:Cookie+Session 方案中服务器仍需存储状态(有状态后端),而 JWT 方案中服务器本身不存储任何东西(真正无状态后端)。
一句话总结
HTTP 无状态 ≠ 应用无状态。HTTP 的无状态是协议层面的设计选择,而应用层面的状态管理是通过 Cookie、Session、Token 等机制在协议之上叠加实现的。
请求-响应模型(Request-Response Model)
基本概念
HTTP 遵循严格的 请求-响应模型(Request-Response Model):通信永远由客户端主动发起请求,服务器被动地返回响应。
这个模型的核心规则可以概括为三条:
- 客户端先说话:服务器永远不会主动向客户端推送数据(在传统 HTTP 语境下)
- 一问一答:每一个请求(Request)对应且仅对应一个响应(Response)
- 请求驱动:没有请求就没有响应;响应不能独立存在
这就像打电话给客服:你必须先拨号(发起请求),客服才能接听并回答你(返回响应)。客服不会突然主动打电话给你。
加载一个网页通常不是一次请求就能完成的。浏览器先请求 HTML 文档,解析后发现还需要 CSS、JavaScript、图片等资源,于是再发起多次请求。每次请求都是独立的一问一答。
请求-响应的完整生命周期
一次完整的 HTTP 请求-响应过程,在底层经历了以下步骤:
逐步拆解:
| 阶段 | 做了什么 | 关键点 |
|---|---|---|
| DNS 解析 | 将域名(如 www.example.com)转换为 IP 地址 | 有本地缓存、递归查询等优化 |
| TCP 三次握手 | 建立客户端与服务器之间的可靠传输通道 | SYN → SYN+ACK → ACK |
| 发送 HTTP 请求 | 客户端将请求报文(方法、URL、头部、正文)通过 TCP 发送给服务器 | 报文结构将在下一章详细展开 |
| 服务器处理 | 服务器解析请求,执行对应逻辑(查数据库、读文件等) | 可能涉及反向代理、后端服务调用 |
| 返回 HTTP 响应 | 服务器将响应报文(状态码、头部、正文)回传给客户端 | 状态码标识处理结果 |
| 连接管理 | 根据 Connection 头部决定关闭还是复用(Keep-Alive)TCP 连接 | HTTP/1.1 默认 Keep-Alive |
连接复用与持久连接(Keep-Alive)
在 HTTP/1.0 中,每次请求-响应都需要建立一个新的 TCP 连接,用完即关。这意味着加载一个包含 30 个资源的网页,就需要进行 30 次 TCP 三次握手,开销巨大。
HTTP/1.1 引入了 持久连接(Persistent Connection),也叫 Keep-Alive。它的核心思想是:一个 TCP 连接上可以传输多个请求-响应对,避免了重复建立和断开连接的开销。
对比非常直观:HTTP/1.0 中 N 个请求需要 N 次 TCP 握手;HTTP/1.1 中 N 个请求只需要 1 次 TCP 握手。
但 HTTP/1.1 的持久连接仍有一个致命问题——队头阻塞(Head-of-Line Blocking, HOL Blocking):在同一个 TCP 连接上,请求必须按顺序发送和接收。如果第一个请求的响应很慢,后面的请求即使已经准备好也必须排队等待。
这也是后来 HTTP/2 引入多路复用(Multiplexing) 的核心动因:在一个 TCP 连接上,多个请求-响应可以并行交错传输,彼此互不阻塞。
请求-响应模型的局限性
传统的请求-响应模型有一个天然的限制:服务器无法主动推送数据给客户端。但在很多场景下(如聊天应用、实时股票行情、在线游戏),服务器需要在有新数据时立即通知客户端。
为了突破这个限制,业界发展出了几种方案:
| 方案 | 原理 | 优缺点 |
|---|---|---|
| 轮询(Polling) | 客户端每隔固定时间发一次请求查询新数据 | 简单但浪费带宽,实时性差 |
| 长轮询(Long Polling) | 客户端发请求后,服务器"挂起"连接直到有新数据才响应 | 减少无效请求,但服务器资源占用高 |
| SSE(Server-Sent Events) | 服务器通过一个持久 HTTP 连接单向推送数据给客户端 | 轻量,但只能服务器→客户端单向 |
| WebSocket | 在 HTTP 握手后升级为全双工协议,双向实时通信 | 最强大,但协议复杂度更高 |
可以看到,轮询(Polling)模式下客户端需要不断地"问"服务器,大量请求是无效的。而 WebSocket 建立连接后,双方都可以随时主动发消息,效率大幅提升。
一句话总结
请求-响应模型是 HTTP 的通信范式:客户端是主动方,服务器是被动方。这个简单的模型催生了整个 Web 生态,但也催生了 WebSocket 等补充方案来应对实时通信的需求。
📝 练习题
以下关于 HTTP 协议特性的描述,正确的是?
A. HTTP 是有状态协议,服务器会自动记住客户端的登录信息
B. HTTP 请求-响应模型中,服务器可以在没有收到请求的情况下主动向客户端发送数据
C. HTTP 的无状态性意味着 Web 应用无法实现用户登录功能
D. HTTP 本身是无状态的,但可以通过 Cookie、Session、Token 等机制在应用层实现状态管理
【答案】 D
【解析】 HTTP 协议在设计上是 无状态的(Stateless),服务器不会在两次请求之间自动保留任何关于客户端的信息,因此 A 错误。请求-响应模型要求客户端先发起请求,服务器才能返回响应,服务器不能主动推送(这是 WebSocket 的能力),因此 B 错误。C 选项的逻辑错误在于混淆了"协议无状态"和"应用无状态"——虽然 HTTP 本身不记忆状态,但应用可以通过在请求中携带 Cookie/Token 等凭证来实现登录态管理。D 选项完整准确地描述了 HTTP 无状态性以及实际工程中的补救方案,是正确答案。
HTTP 报文结构 ⭐
HTTP 协议的核心在于客户端与服务器之间交换报文(Message)。无论是浏览器发出的请求,还是服务器返回的响应,都遵循一套严格且清晰的文本格式。理解 HTTP 报文结构,就等于掌握了"HTTP 说的每一句话的语法"。
HTTP 报文本质上是一段纯文本(plain text),由 ASCII 字符构成,具有良好的人类可读性(Human-readable)。这也是早期 HTTP 协议设计的一大优势——开发者可以直接用 telnet 或抓包工具(如 Wireshark、Fiddler)阅读原始报文内容,极大地降低了调试门槛。
从宏观上看,HTTP 报文分为两大类:
- 请求报文(Request Message):由客户端发往服务器,描述"我想要什么"。
- 响应报文(Response Message):由服务器返回给客户端,描述"我给你什么"。
尽管两者的用途不同,但它们的整体骨架却高度一致,都由三大部分组成:起始行(Start Line)、头部(Headers)、正文体(Body)。它们之间通过特定的分隔符组织在一起。
需要特别注意的是,头部和正文体之间必须有一个空行(Blank Line),这个空行本质上就是一个单独的 CRLF(\r\n)。它是 HTTP 解析器区分"元数据结束、正文开始"的唯一标志。如果缺少这个空行,解析器将无法正确分离头部与正文,导致报文解析失败。
下面我们逐一深入每个组成部分。
请求行 / 状态行(Start Line)
起始行是 HTTP 报文的第一行,它承载了整条报文最核心的概要信息。对于请求报文和响应报文,起始行的格式有所不同,分别称为请求行(Request Line)和状态行(Status Line / Response Line)。
请求行(Request Line)
请求行由三个字段组成,以空格分隔,末尾以 CRLF 结束:
Method SP Request-URI SP HTTP-Version CRLF
举一个最经典的例子:
GET /api/users?page=1 HTTP/1.1\r\n我们来逐段拆解:
| 字段 | 示例值 | 说明 |
|---|---|---|
| Method(方法) | GET | 告诉服务器客户端要执行什么操作,常见的有 GET、POST、PUT、DELETE 等 |
| Request-URI(请求目标) | /api/users?page=1 | 标识目标资源的路径,可包含查询字符串(Query String) |
| HTTP-Version(协议版本) | HTTP/1.1 | 声明客户端使用的 HTTP 协议版本 |
关于 Request-URI 的几种形式:
Request-URI 并非总是一个简单路径,在不同场景下它有不同形式:
- Origin Form(最常见):
/path/to/resource?query=value,绝大多数浏览器请求使用此形式。 - Absolute Form:
http://www.example.com/path,主要用于向**代理服务器(Proxy)**发起请求时。 - Authority Form:
www.example.com:443,仅用于CONNECT方法(建立隧道)。 - Asterisk Form:
*,仅用于OPTIONS方法,表示对服务器整体(而非某个资源)的查询。
状态行(Status Line)
响应报文的第一行称为状态行,格式如下:
HTTP-Version SP Status-Code SP Reason-Phrase CRLF
典型示例:
HTTP/1.1 200 OK\r\nHTTP/1.1 404 Not Found\r\n| 字段 | 示例值 | 说明 |
|---|---|---|
| HTTP-Version | HTTP/1.1 | 服务器使用的 HTTP 协议版本 |
| Status-Code(状态码) | 200 | 三位数字,指示请求处理结果的类别(后续章节详述) |
| Reason-Phrase(原因短语) | OK | 状态码的可读文字描述,仅供人类阅读,不影响协议解析 |
注意:在 HTTP/2 及更高版本中,Reason-Phrase 已被移除。HTTP/2 采用二进制帧(Binary Frame)传输,不再有文本形式的起始行。状态码本身已足够表达语义,Reason-Phrase 被认为是冗余的。
下面通过一个完整的请求-响应对来直观感受起始行的作用:
-- 请求报文 --
POST /api/login HTTP/1.1 ← 请求行:方法=POST,路径=/api/login,版本=1.1
Host: www.example.com ← 以下为请求头
Content-Type: application/json
Content-Length: 42
{"username":"admin","password":"123456"} ← 请求体-- 响应报文 --
HTTP/1.1 200 OK ← 状态行:版本=1.1,状态码=200,短语=OK
Content-Type: application/json ← 以下为响应头
Set-Cookie: session=abc123
Content-Length: 27
{"status":"login success"} ← 响应体请求头 / 响应头(Headers)
头部(Headers)位于起始行之后、空行之前,是一组 Key-Value 键值对,用于携带关于报文的元数据(Metadata)。每个头部字段独占一行,格式为:
Header-Name: Header-Value CRLF
冒号后面通常跟一个空格(虽然规范允许省略,但绝大多数实现都会加上)。Header-Name 是大小写不敏感的(Case-Insensitive),但按照惯例采用首字母大写-连字符的形式(如 Content-Type)。
头部的分类
HTTP 头部按照适用场景,可以划分为以下几类:
① 通用头(General Headers)
请求和响应报文中都可以出现的头部字段,描述的是报文本身的属性,而非被传输资源的属性。
| 头字段 | 作用 | 示例 |
|---|---|---|
Date | 报文创建的日期和时间 | Date: Thu, 27 Feb 2026 08:00:00 GMT |
Connection | 控制连接是否保持(Keep-Alive / Close) | Connection: keep-alive |
Cache-Control | 缓存策略指令 | Cache-Control: no-cache |
Transfer-Encoding | 传输编码方式 | Transfer-Encoding: chunked |
② 请求头(Request Headers)
仅出现在请求报文中,提供关于客户端环境、期望内容格式等信息。
| 头字段 | 作用 | 示例 |
|---|---|---|
Host | 指定目标服务器的域名和端口(HTTP/1.1 强制必须) | Host: api.example.com |
User-Agent | 标识客户端软件类型和版本 | User-Agent: Mozilla/5.0 ... |
Accept | 客户端能接受的 MIME 类型列表 | Accept: text/html, application/json |
Accept-Encoding | 客户端支持的内容编码(压缩算法) | Accept-Encoding: gzip, deflate, br |
Accept-Language | 客户端偏好的语言 | Accept-Language: zh-CN,en;q=0.9 |
Authorization | 携带认证凭据 | Authorization: Bearer eyJhbGci... |
Cookie | 携带之前服务器设置的 Cookie | Cookie: session=abc123 |
Referer | 来源页面 URL(注意这是历史拼写错误,正确应为 Referrer) | Referer: https://www.google.com/ |
If-None-Match | 条件请求,携带 ETag 值 | If-None-Match: "v2.6" |
Host头的重要性:在 HTTP/1.0 时代,一个 IP 地址通常对应一个网站。但随着虚拟主机(Virtual Hosting)技术普及,同一台服务器(同一个 IP)可能承载多个不同域名的网站。此时服务器就需要通过Host头来判断客户端究竟想访问哪个站点。因此 HTTP/1.1 规范将Host定义为唯一一个强制必需的请求头。
③ 响应头(Response Headers)
仅出现在响应报文中,提供关于服务器和响应行为的补充信息。
| 头字段 | 作用 | 示例 |
|---|---|---|
Server | 标识服务器软件 | Server: nginx/1.24.0 |
Set-Cookie | 向客户端设置 Cookie | Set-Cookie: id=a3fWa; Path=/; HttpOnly |
Location | 重定向目标 URL(配合 3xx 状态码使用) | Location: https://new.example.com/ |
WWW-Authenticate | 要求客户端进行认证(配合 401 使用) | WWW-Authenticate: Basic realm="site" |
Access-Control-Allow-Origin | CORS 跨域允许的来源 | Access-Control-Allow-Origin: * |
④ 实体头(Entity Headers)
描述报文正文体(Body)的属性,请求和响应中都可以出现。
| 头字段 | 作用 | 示例 |
|---|---|---|
Content-Type | 正文体的 MIME 类型 | Content-Type: application/json; charset=utf-8 |
Content-Length | 正文体的字节长度 | Content-Length: 1024 |
Content-Encoding | 正文体的压缩编码 | Content-Encoding: gzip |
Content-Language | 正文体的语言 | Content-Language: zh-CN |
内容协商机制(Content Negotiation)
Headers 中有一组非常巧妙的设计——请求侧的 Accept-* 系列与响应侧的 Content-* 系列形成配对,实现了客户端和服务器之间的"自动协商":
这种机制的好处是:客户端告知自己的能力("我能处理 JSON 和 HTML,支持 gzip 压缩"),服务器从中选择最合适的方式进行响应。这就是所谓的服务端驱动的内容协商(Server-Driven Content Negotiation)。
质量因子 q(Quality Factor)
在 Accept 系列头部中,客户端还可以通过 **q 值(权重)**来表达偏好程度:
Accept: text/html;q=0.9, application/json;q=1.0, text/plain;q=0.5q 值范围为 0 ~ 1,默认值为 1(最高优先级)。上例表示:客户端最优先希望收到 JSON,其次 HTML,最后纯文本。服务器会根据 q 值和自身能力,选择一种最佳的内容格式返回。
请求体 / 响应体(Body)
正文体(Body)位于空行之后,是报文中实际要传输的数据载荷(Payload)。不同于起始行和头部是文本形式的元数据,Body 可以承载任意类型的数据——文本、JSON、XML、图片、视频、二进制流等。
请求体(Request Body)
并非所有 HTTP 方法都需要请求体:
| 方法 | 是否通常包含请求体 | 典型场景 |
|---|---|---|
GET | ❌ 不包含 | 获取资源,参数通过 URL 查询字符串传递 |
HEAD | ❌ 不包含 | 与 GET 类似,但只返回头部 |
POST | ✅ 包含 | 提交表单数据、上传文件、发送 JSON |
PUT | ✅ 包含 | 更新资源的完整内容 |
PATCH | ✅ 包含 | 更新资源的部分字段 |
DELETE | ⚠️ 可选 | 通常不包含,但规范未禁止 |
请求体的数据编码格式由 Content-Type 头决定,以下是最常见的几种:
① application/x-www-form-urlencoded
这是 HTML 表单默认的提交方式,数据以 key=value&key=value 的形式编码,特殊字符会被 URL 编码(Percent-Encoding):
POST /api/login HTTP/1.1
Host: www.example.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 29
username=admin&password=123456② application/json
现代 Web API(RESTful)最主流的数据格式,请求体为 JSON 字符串:
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json
Content-Length: 52
{"username":"admin","email":"admin@example.com"}③ multipart/form-data
用于文件上传或混合数据传输。请求体由多个部分(Part)组成,每个部分由 boundary 分隔符分隔:
POST /api/upload HTTP/1.1
Host: www.example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA
------WebKitFormBoundary7MA
Content-Disposition: form-data; name="title"
My Photo
------WebKitFormBoundary7MA
Content-Disposition: form-data; name="file"; filename="photo.jpg"
Content-Type: image/jpeg
[...二进制图片数据...]
------WebKitFormBoundary7MA--每个 Part 都有自己的头部(如 Content-Disposition、Content-Type),可以独立描述该部分的类型和元信息。最后一个 boundary 以 -- 结尾表示整个 body 终结。
响应体(Response Body)
服务器返回的响应体包含客户端请求的实际资源内容。常见的响应体类型:
| Content-Type | 数据类型 | 场景 |
|---|---|---|
text/html | HTML 文档 | 浏览器页面渲染 |
application/json | JSON 数据 | RESTful API 响应 |
text/css | CSS 样式表 | 网页样式 |
application/javascript | JavaScript 脚本 | 网页脚本 |
image/png, image/jpeg | 图片二进制 | 图片资源 |
application/octet-stream | 通用二进制流 | 文件下载 |
Body 的长度确定机制
HTTP 解析器如何知道 Body 在哪里结束?这是一个关键问题,有三种主要机制:
Chunked Transfer Encoding(分块传输编码) 是 HTTP/1.1 引入的重要特性,特别适用于服务器在生成响应时还不知道总长度的场景(如流式数据、动态内容生成)。格式如下:
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
1a ← 十六进制表示该块有 26 个字节
This is the first chunk.
10 ← 十六进制表示该块有 16 个字节
And the second.
0 ← 大小为0,标志所有块传输完毕
每个 chunk 由「大小行(十六进制)+ CRLF + 数据 + CRLF」组成,最后以大小为 0 的终止块结尾。
实际工程中的重要细节:当
Transfer-Encoding: chunked和Content-Length同时出现时,Transfer-Encoding优先级更高,Content-Length会被忽略。这是 RFC 7230 明确规定的,目的是防止因不一致导致的安全漏洞(如 HTTP Request Smuggling 攻击)。
完整报文实例(综合)
最后,让我们用一个完整的 POST 请求及其响应,完整呈现 HTTP 报文的所有组成部分:
-- ====== 请求报文 ====== --
POST /api/v1/articles HTTP/1.1 ← 请求行: 方法 + URI + 版本
Host: blog.example.com ← 请求头开始
User-Agent: Mozilla/5.0 (Windows NT 10.0)
Accept: application/json
Content-Type: application/json; charset=utf-8
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
Content-Length: 73
Cookie: session=s3cr3t_token
← 空行(CRLF) — 头部与正文体的分界线
{"title":"HTTP Deep Dive","content":"Understanding HTTP message structure."}-- ====== 响应报文 ====== --
HTTP/1.1 201 Created ← 状态行: 版本 + 状态码 + 原因短语
Content-Type: application/json; charset=utf-8 ← 响应头开始
Location: /api/v1/articles/42
Cache-Control: no-cache
Set-Cookie: session=s3cr3t_token; Path=/; HttpOnly
Content-Length: 58
Date: Thu, 27 Feb 2026 08:30:00 GMT
← 空行(CRLF)
{"id":42,"title":"HTTP Deep Dive","status":"published"}这个例子展示了:客户端发送一个 JSON 格式的文章数据,服务器成功创建后返回 201 Created,同时通过 Location 头告知新资源的 URI,通过 Set-Cookie 维护会话状态。
📝 练习题
题目 1:以下关于 HTTP 报文结构的描述,错误的是:
A. HTTP 请求报文的第一行称为请求行,由方法、URI 和 HTTP 版本三部分组成
B. HTTP 头部字段名(Header Name)是大小写敏感的(Case-Sensitive)
C. 头部与正文体之间必须有一个空行(即一个单独的 CRLF)作为分隔
D. Transfer-Encoding: chunked 与 Content-Length 同时出现时,前者优先级更高
【答案】 B
【解析】 根据 HTTP 规范(RFC 7230),HTTP 头部字段名是大小写不敏感的(Case-Insensitive),即 Content-Type、content-type、CONTENT-TYPE 在协议层面是等价的。选项 A 正确描述了请求行的三段式结构(Method SP Request-URI SP HTTP-Version);选项 C 正确,空行 CRLF 是解析器分离头部与正文体的关键标志;选项 D 正确,RFC 7230 明确规定当两者冲突时以 Transfer-Encoding 为准,以防止 HTTP Smuggling 等安全问题。因此 B 是错误的描述。
📝 练习题
题目 2:某客户端向服务器上传一个头像图片和一个用户名字段,应使用哪种 Content-Type 最合适?
A. application/json
B. application/x-www-form-urlencoded
C. multipart/form-data
D. text/plain
【答案】 C
【解析】 当请求需要同时上传文件(二进制数据)和普通表单字段时,multipart/form-data 是唯一合理的选择。它通过 boundary 分隔符将报文体划分为多个独立的 Part,每个 Part 可以拥有不同的 Content-Type(文本 Part 为 text/plain,图片 Part 为 image/jpeg 等)。选项 A 的 JSON 格式不适合直接携带二进制文件(虽然可以用 Base64 编码,但会使体积膨胀约 33%,效率低下);选项 B 的 URL 编码格式只适用于纯文本键值对,无法高效携带二进制文件;选项 D 的纯文本格式不具备结构化能力。
HTTP 方法 ⭐
HTTP 协议定义了一组 请求方法(Request Methods),用于指示客户端希望对目标资源执行何种操作。这些方法有时也被称为 HTTP 动词(HTTP Verbs),因为它们本质上描述的就是一个"动作"。在 RESTful API 设计哲学中,HTTP 方法与资源的 CRUD(Create / Read / Update / Delete)操作形成了优雅的映射关系,使得 Web 服务的接口语义清晰、风格统一。
HTTP/1.1 规范(RFC 7231)中定义了以下常用方法:
| 方法 | 语义 | 是否安全 | 是否幂等 | 是否有请求体 |
|---|---|---|---|---|
| GET | 获取资源 | ✅ | ✅ | ❌(通常无) |
| POST | 提交数据 / 创建资源 | ❌ | ❌ | ✅ |
| PUT | 整体替换 / 更新资源 | ❌ | ✅ | ✅ |
| DELETE | 删除资源 | ❌ | ✅ | ❌(通常无) |
| PATCH | 局部更新资源 | ❌ | ❌ | ✅ |
| HEAD | 与 GET 相同但不返回 body | ✅ | ✅ | ❌ |
| OPTIONS | 查询服务器支持的方法 | ✅ | ✅ | ❌ |
在深入各个方法之前,先理解两个关键概念:
- 安全性(Safe):该方法是否只读,不会修改服务器上的资源状态。GET、HEAD、OPTIONS 被定义为安全方法。
- 幂等性(Idempotent):同一请求执行一次与执行多次,效果完全一致。GET、PUT、DELETE 均为幂等的,而 POST 不是——每次 POST 都可能创建一个新资源。
GET(获取资源)
GET 是 HTTP 中最基础、最常用的方法,语义是 从服务器检索(retrieve)指定资源的表示。你在浏览器地址栏输入 URL 按下回车,触发的就是一次 GET 请求;页面中加载图片、CSS、JS 文件,同样都是 GET。
核心特征:
- 只读操作:GET 请求不应该对服务器资源产生任何副作用(side effect)。服务器处理 GET 请求时只负责"读取"和"返回",不改变资源状态。
- 参数通过 URL 传递:GET 的请求参数附加在 URL 的查询字符串(Query String)中,格式为
?key1=value1&key2=value2。 - 无请求体:规范上 GET 请求不应包含 Request Body(虽然技术上可以发送,但绝大多数服务器和框架会忽略它)。
- 可缓存(Cacheable):浏览器和中间代理可以对 GET 响应进行缓存,以提升性能。
- 可收藏 / 可分享:GET 请求的完整信息编码在 URL 中,因此可以被加入书签、通过链接分享。
典型请求示例:
GET /api/users?page=1&size=20 HTTP/1.1 # 请求行:方法 + 路径(含查询参数) + 协议版本
Host: api.example.com # 目标主机
Accept: application/json # 期望返回 JSON 格式
Authorization: Bearer eyJhbGciOiJIUz... # 携带认证令牌
Cache-Control: no-cache # 本次不使用缓存HTTP/1.1 200 OK # 状态行:协议版本 + 状态码 + 原因短语
Content-Type: application/json; charset=utf-8 # 响应体的媒体类型
Content-Length: 1024 # 响应体字节长度
{ # 响应体:JSON 格式的用户列表
"total": 200,
"page": 1,
"data": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
}URL 长度限制问题:
虽然 HTTP 规范本身没有对 URL 长度做硬性限制,但实际环境中存在约束:
| 环境 | 大致限制 |
|---|---|
| 大多数浏览器 | ~2,048 – 8,192 字符 |
| Nginx 默认 | 8KB(含请求行+请求头) |
| Apache 默认 | ~8,190 字符 |
| CDN / WAF | 各家不同,通常 4KB–16KB |
当参数数据量较大时(如传输 JSON 对象、文件内容),GET 就不再合适了,应改用 POST。
POST(提交数据)
POST 方法的语义是 向服务器提交数据,请求服务器对其进行处理。最典型的场景就是提交表单、上传文件、创建新资源等。
核心特征:
- 非安全、非幂等:POST 请求会改变服务器状态。连续发送两次相同的 POST 请求,可能创建两个相同的资源——这就是"非幂等"的含义。
- 数据在请求体中:与 GET 不同,POST 的数据放在 Request Body 中,理论上没有大小限制(实际受服务器配置约束)。
- 不可缓存:浏览器默认不缓存 POST 的响应。
- 不可收藏:POST 请求无法通过 URL 完整重现,因此不能被加入书签。
常见的 Content-Type(请求体编码方式):
| Content-Type | 用途 | 数据格式 |
|---|---|---|
application/x-www-form-urlencoded | HTML 表单默认提交 | key1=val1&key2=val2 |
multipart/form-data | 文件上传、混合数据 | 分段 boundary 编码 |
application/json | RESTful API 常用 | {"key": "value"} |
text/xml | SOAP 等老式接口 | XML 文档 |
典型请求示例(JSON 格式创建用户):
POST /api/users HTTP/1.1 # 请求行:POST 方法
Host: api.example.com # 目标主机
Content-Type: application/json # 请求体为 JSON 编码
Content-Length: 68 # 请求体字节长度
Authorization: Bearer eyJhbGciOiJIUz... # 携带认证令牌
{ # 请求体
"name": "Charlie",
"email": "charlie@example.com",
"role": "admin"
}HTTP/1.1 201 Created # 201 表示资源创建成功
Location: /api/users/42 # 新资源的 URI 地址
Content-Type: application/json
{
"id": 42, # 服务器分配的唯一 ID
"name": "Charlie",
"email": "charlie@example.com",
"role": "admin",
"createdAt": "2025-06-15T10:30:00Z" # 服务器记录的创建时间
}典型请求示例(表单文件上传):
POST /api/upload HTTP/1.1 # 文件上传请求
Host: api.example.com
Content-Type: multipart/form-data; boundary=----FormBoundary7MA4YWxkTrZu0gW
Content-Length: 23456
------FormBoundary7MA4YWxkTrZu0gW # 第一部分:文本字段
Content-Disposition: form-data; name="desc"
Profile Photo # 字段值
------FormBoundary7MA4YWxkTrZu0gW # 第二部分:文件内容
Content-Disposition: form-data; name="file"; filename="avatar.png"
Content-Type: image/png
<二进制图片数据...> # 文件的原始字节流
------FormBoundary7MA4YWxkTrZu0gW-- # 终止边界multipart/form-data 使用 boundary(边界符) 将请求体分隔为多个独立部分,每部分可以是不同类型的数据(文本、文件等),这使得单个 POST 请求能同时传输结构化字段和二进制文件。
PUT(更新资源)
PUT 方法的语义是 用请求体中的数据完整替换目标资源。如果目标资源不存在,某些实现下 PUT 还可以创建它(但更推荐用 POST 创建)。
核心特征:
- 幂等:对同一 URI 多次 PUT 相同的数据,最终效果与执行一次完全相同——资源就是那个状态。这是 PUT 与 POST 最关键的区别。
- 整体替换:PUT 期望客户端发送资源的完整表示。如果只想修改某几个字段,应该用 PATCH(局部更新)。
- 数据在请求体中:和 POST 一样,PUT 的有效载荷放在 Body 里。
典型请求示例:
PUT /api/users/42 HTTP/1.1 # 指定要更新的资源 URI
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUz...
{ # 必须发送完整的资源对象
"id": 42,
"name": "Charlie Updated", # 修改了名字
"email": "charlie_new@example.com", # 修改了邮箱
"role": "admin" # 即使不变也需要带上
}HTTP/1.1 200 OK # 更新成功
Content-Type: application/json
{
"id": 42,
"name": "Charlie Updated",
"email": "charlie_new@example.com",
"role": "admin",
"updatedAt": "2025-06-15T11:00:00Z"
}PUT vs PATCH 的直观对比:
用一个形象的比喻:PUT 就像"推倒重建整栋房子",PATCH 就像"只翻新其中一个房间"。
DELETE(删除资源)
DELETE 方法的语义很直接——请求服务器删除指定 URI 所标识的资源。
核心特征:
- 幂等:第一次 DELETE 成功删除资源后,后续对同一 URI 的 DELETE 请求不会再产生新的副作用(可能返回 404,但服务器状态没有进一步改变)。
- 通常无请求体:大多数实现不需要 Body,仅通过 URI 定位资源。
- 响应设计灵活:可以返回
200 OK(含被删除资源的信息),也可以返回204 No Content(不含响应体),具体取决于 API 设计风格。
典型请求示例:
DELETE /api/users/42 HTTP/1.1 # 删除 ID 为 42 的用户
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUz... # 删除操作通常需要鉴权HTTP/1.1 204 No Content # 删除成功,无响应体返回软删除 vs 硬删除:
实际工程中,DELETE 请求到达后端后,服务器并不一定真的从数据库中移除记录。很多系统采用 软删除(Soft Delete)策略:
- 硬删除(Hard Delete):
DELETE FROM users WHERE id = 42;—— 数据被永久抹除,不可恢复。 - 软删除(Soft Delete):
UPDATE users SET deleted_at = NOW() WHERE id = 42;—— 只标记为已删除,数据仍然存在于数据库中,可以审计和恢复。
前端和 API 消费者看到的行为是一致的(资源"消失"了),但后端实现不同。这是架构设计层面的选择,与 HTTP 协议本身无关。
GET vs POST ⭐
这是面试中的超高频问题,也是初学者最容易混淆的点。下面从多个维度做系统性对比:
全维度对比表:
| 维度 | GET | POST |
|---|---|---|
| 语义 | 获取资源(读) | 提交数据 / 创建资源(写) |
| 参数位置 | URL 查询字符串(Query String) | 请求体(Request Body) |
| 数据可见性 | 参数暴露在地址栏、浏览器历史、服务器日志中 | 参数在 Body 中,地址栏不可见 |
| 数据长度 | 受 URL 长度限制(通常 2KB–8KB) | 理论无限制,受服务器配置控制 |
| 缓存 | ✅ 可被浏览器/代理缓存 | ❌ 默认不缓存 |
| 书签 | ✅ 可以保存为书签 | ❌ 无法保存为书签 |
| 浏览器回退 | 无害(直接从缓存读取) | 浏览器会弹窗提示重新提交表单 |
| 安全性 | Safe(不修改资源) | Unsafe(可能修改资源) |
| 幂等性 | ✅ 幂等 | ❌ 非幂等 |
| 编码类型 | 仅 application/x-www-form-urlencoded | 支持多种(JSON、form-data 等) |
| TCP 数据包 | 通常一个(header + 无 body) | 某些浏览器会发两个(先 header 再 body) |
深入解析几个容易被误解的点:
1)"GET 不安全,POST 安全" —— 这是一个典型误区
很多初学者认为因为 GET 的参数在 URL 中"看得到",所以不安全;而 POST 的参数在 Body 中"看不到",所以安全。这个理解是错误的。
- HTTP 是明文协议,无论 GET 还是 POST,数据在网络传输中都是裸奔的,用抓包工具(Wireshark、Fiddler)都能轻松看到 Body 内容。
- 真正的安全必须依赖 HTTPS(TLS 加密传输层),而非 HTTP 方法的选择。
- GET 参数暴露在 URL 中的确更容易泄漏(通过浏览器历史、Referer 头、服务器日志),但这是"隐私性"问题,不是"传输安全性"问题。
2)TCP 数据包次数差异
在某些浏览器实现(如 Firefox)中,POST 请求会被拆分为两步发送:
第一步:发送 Header(含 Expect: 100-continue)
↓ 等待服务器返回 100 Continue
第二步:发送 Body而 GET 请求由于没有 Body,Header 和整个请求通常在一个 TCP 数据包中发出。但这是浏览器的实现行为,不是 HTTP 协议的强制要求。Chrome 等浏览器的 POST 通常也是一次性发送的。
3)幂等性的实际工程影响
幂等性不仅仅是一个理论概念,它直接影响重试策略的设计:
在支付等关键场景中,为了让 POST 请求也能安全重试,通常会在请求头中加入 幂等键(Idempotency Key):
POST /api/payments HTTP/1.1 # 创建支付请求
Host: api.example.com
Content-Type: application/json
Idempotency-Key: a1b2c3d4-e5f6-7890 # 客户端生成的唯一标识
{
"amount": 99.99, # 支付金额
"currency": "CNY", # 币种
"merchant": "shop-001" # 商户编号
}服务端收到请求后,先根据 Idempotency-Key 查询是否已处理过:若已处理则直接返回之前的结果;若未处理则执行业务逻辑并缓存结果。这样即使网络抖动导致客户端重复提交,也不会发生"重复扣款"的问题。
4)RESTful 风格下的方法选择指南
资源操作映射表 (CRUD → HTTP Methods)
┌──────────────┬──────────┬──────────────────────────┐
│ 操作 │ 方法 │ URI 示例 │
├──────────────┼──────────┼──────────────────────────┤
│ 查询用户列表 │ GET │ GET /api/users │
│ 查询单个用户 │ GET │ GET /api/users/42 │
│ 创建新用户 │ POST │ POST /api/users │
│ 全量更新用户 │ PUT │ PUT /api/users/42 │
│ 局部更新用户 │ PATCH │ PATCH /api/users/42 │
│ 删除用户 │ DELETE │ DELETE /api/users/42 │
└──────────────┴──────────┴──────────────────────────┘这张表是 RESTful API 设计的核心参照。URI 应该描述的是名词(资源),而 HTTP 方法承载的是动词(操作)。避免出现像 POST /api/deleteUser 这样将动作编码在 URL 中的反模式。
📝 练习题
以下关于 HTTP 方法的描述,正确的是?
A. GET 请求因为参数在 URL 中明文可见,所以比 POST 请求更不安全,POST 的 Body 数据是加密的
B. PUT 和 POST 都可以用于更新资源,但 PUT 是幂等的,多次执行效果一致;POST 不是幂等的,多次执行可能产生多个新资源
C. DELETE 方法必须从数据库中永久移除记录,否则不符合 HTTP 规范
D. GET 请求在任何情况下都不允许携带 Request Body,发送即报错
【答案】 B
【解析】
- A 错误:HTTP 本身是明文协议,POST 的 Body 在网络传输中并不加密。GET 和 POST 在传输安全性上没有本质区别,真正的加密要依靠 HTTPS(TLS)。GET 参数暴露在 URL 中确实更容易泄漏到日志和浏览器历史中,但这属于"隐私风险"而非"传输安全"问题。
- B 正确:PUT 的语义是"用完整数据替换目标资源",无论执行多少次,资源最终都是同一个状态,因此是幂等的。而 POST 的语义是"提交/创建",每次调用都可能生成一个新的资源实例,因此是非幂等的。
- C 错误:HTTP 规范只定义了 DELETE 的语义是"删除资源",但并不规定后端的具体实现。软删除(Soft Delete)标记
deleted_at是业界非常常见的实践,完全符合规范。 - D 错误:HTTP 规范并没有禁止 GET 请求携带 Body(RFC 7231 的描述是"没有定义语义"),但大多数服务器和中间件会忽略 GET 的 Body。实际工程中不推荐这样做,但技术上不会"报错"。例如 Elasticsearch 早期版本就允许在 GET 请求中携带 JSON Body 来描述复杂查询条件。
HTTP 状态码 (HTTP Status Codes) ⭐⭐
HTTP 状态码(Status Code)是服务器对客户端请求的"回执单"。每当浏览器或任何 HTTP 客户端向服务器发起请求后,服务器都会在响应的 状态行(Status Line)中返回一个三位数字的状态码,用以告知客户端本次请求的处理结果。状态码的设计哲学非常清晰——通过 首位数字 将所有响应划分为五大类别,使得开发者仅凭第一位数字就能快速判断响应的大致语义。
理解状态码对于 Web 开发、接口调试、运维排障都至关重要。一个经验丰富的工程师,往往能根据状态码在几秒内定位问题方向:是客户端参数写错了(4xx),还是服务端代码崩溃了(5xx),亦或是资源已经搬家了(3xx)。
下面这张总览图展示了五大类状态码的分类逻辑与典型代码:
1xx — 信息性状态码 (Informational)
1xx 类状态码属于 临时响应(Interim Response),它告诉客户端:请求已被接收,服务器正在继续处理,请稍候。在日常 Web 开发中,1xx 状态码相对少见,开发者往往不需要手动处理它们,但理解其原理对于掌握 HTTP 协议的完整流程非常有帮助。
100 Continue
100 Continue 是 HTTP/1.1 引入的一个重要优化机制。它解决的核心问题是:避免客户端在服务器可能拒绝请求的情况下,白白传输大量请求体数据。
其工作流程如下:
- 客户端希望发送一个带有大体积请求体的请求(例如上传一个 500MB 的视频文件)。
- 客户端先只发送请求头,并在其中包含
Expect: 100-continue头字段,暂时不发送请求体。 - 服务器收到请求头后进行预检查(如验证身份、检查文件大小限制等):
- 如果一切合规,服务器返回
100 Continue,客户端收到后才开始发送请求体。 - 如果不合规(如文件太大),服务器直接返回
413 Payload Too Large等错误码,客户端就避免了传输无意义的大数据。
- 如果一切合规,服务器返回
这种"先试探、再发送"的机制,在网络带宽珍贵的场景下(如移动端上传、跨洋网络传输)尤为有价值。
101 Switching Protocols
101 Switching Protocols 表示服务器同意切换协议。最经典的场景是 WebSocket 握手。客户端通过 HTTP 请求发起协议升级,服务器同意后返回 101,之后双方就从 HTTP 协议"升级"为 WebSocket 协议进行全双工通信。
// 客户端发起升级请求
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket // 希望升级到 WebSocket
Connection: Upgrade // 标记这是一个升级请求
Sec-WebSocket-Key: dGhlIHNhbXBsZQ== // WebSocket 安全密钥
// 服务器同意升级
HTTP/1.1 101 Switching Protocols
Upgrade: websocket // 确认升级到 WebSocket
Connection: Upgrade // 确认连接升级
Sec-WebSocket-Accept: s3pPLMBiTx... // 服务端返回的校验值返回 101 之后,该 TCP 连接上跑的就不再是 HTTP 报文了,而是 WebSocket 的帧(Frame)格式。
2xx — 成功状态码 (Success)
2xx 类状态码是开发者最乐于见到的一族——它意味着客户端的请求被服务器 成功接收、理解并处理。但"成功"的含义并非千篇一律,不同的 2xx 状态码表达着不同的成功语义。
200 OK
200 OK 是 HTTP 中最常见、最通用的成功状态码。它表示请求已成功,服务器返回了所请求的资源或处理结果。
不同请求方法下,200 响应体的含义有细微差异:
| 请求方法 | 200 响应体含义 |
|---|---|
| GET | 响应体包含所请求的目标资源 |
| POST | 响应体包含操作处理的结果或描述 |
| PUT | 响应体包含被修改后资源的当前状态 |
| DELETE | 响应体包含删除操作的状态描述 |
实际开发中,绝大多数 RESTful API 在操作成功时都会返回 200,响应体通常是 JSON 格式:
// GET /api/users/42 的 200 响应体
{
"code": 0, // 业务状态码: 0表示成功
"message": "success", // 提示信息
"data": { // 实际数据
"id": 42,
"name": "Alice",
"email": "alice@example.com"
}
}201 Created
201 Created 表示请求已成功,并且服务器创建了一个新资源。这是 POST 请求最"标准"的成功响应——当你往数据库中插入了一条新记录、创建了一个新用户、上传了一个新文件时,最语义化的做法就是返回 201 而不是泛泛的 200。
服务器在返回 201 时,通常会在响应头中通过 Location 字段告知客户端新资源的 URI:
HTTP/1.1 201 Created
Location: /api/users/43 // 新资源的访问地址
Content-Type: application/json
{
"id": 43,
"name": "Bob",
"created_at": "2025-06-15T10:30:00Z"
}这样客户端就能立刻知道新资源存放在哪,无需额外查询。
204 No Content
204 No Content 表示请求已成功处理,但响应体中没有任何内容返回。注意——这不是"出错了所以没内容",而是"成功了但确实没有需要返回的东西"。
最典型的使用场景包括:
- DELETE 请求:删除成功后,被删除的资源已经不存在了,没有内容可返回。
- PUT 请求:更新成功后,如果客户端不需要看到更新后的资源状态,返回 204 即可。
- 自动保存:前端周期性地自动保存草稿到后端,后端处理成功不需要返回数据,仅返回 204 告知"已保存"。
// 删除用户请求
DELETE /api/users/42 HTTP/1.1
Host: api.example.com
// 服务器成功删除, 无需返回内容
HTTP/1.1 204 No Content
// 注意: 没有响应体!200 vs 201 vs 204 选择指南:GET 成功用 200;POST 创建新资源用 201;成功但无需返回数据用 204。精确使用状态码是 RESTful API 设计规范化的标志。
3xx — 重定向状态码 (Redirection)
3xx 类状态码告诉客户端:你请求的资源已不在原来的位置了,需要进一步操作(通常是访问另一个 URL)才能完成请求。重定向在 Web 世界中无处不在——域名迁移、短链接跳转、HTTPS 强制跳转、登录后回跳等场景都离不开它。
301 Moved Permanently — 永久重定向
301 Moved Permanently 表示请求的资源已经被永久移动到了新的 URI。这是一种非常强烈的信号,它告诉客户端和搜索引擎:
- 这个旧地址以后都不要再用了。
- 请更新你的书签、缓存、索引到新地址。
- 浏览器会缓存这个重定向——下次访问旧地址时,浏览器可能直接跳转到新地址,不再向服务器发请求。
// 旧域名永久迁移到新域名
GET /home HTTP/1.1
Host: old-site.com
HTTP/1.1 301 Moved Permanently
Location: https://new-site.com/home // 新的永久地址典型应用场景:
- 网站整体从
http://迁移到https:// - 域名更换(
old-brand.com→new-brand.com) - URL 结构重构(
/page.html→/page)
SEO 影响:搜索引擎会将旧页面的权重(PageRank)转移给新页面,这对 SEO 非常友好。
302 Found — 临时重定向
302 Found(在 HTTP/1.0 中叫 302 Moved Temporarily)表示资源临时被移到了另一个 URI。与 301 的关键区别在于:
- 旧地址仍然有效,未来还会继续使用。
- 浏览器不会缓存此重定向——每次访问旧地址,都会重新向服务器发请求。
- 搜索引擎不会将权重转移给新地址。
// 用户未登录, 临时重定向到登录页
GET /dashboard HTTP/1.1
Host: app.example.com
HTTP/1.1 302 Found
Location: /login?redirect=/dashboard // 临时跳转到登录页典型应用场景:
- 用户未登录时,临时跳转到登录页
- A/B 测试中临时将用户导向不同版本
- 系统维护时临时跳转到维护公告页
下面用一张对比图清晰展示 301 和 302 在浏览器行为上的核心差异:
补充说明:HTTP/1.1 还引入了
307 Temporary Redirect和308 Permanent Redirect。它们与 302/301 的区别在于——307/308 严格禁止在重定向时改变请求方法(如 POST 不会被降级为 GET),而 301/302 在实际浏览器实现中,可能将 POST 重定向后变为 GET。这个行为差异在涉及表单提交重定向时尤其需要注意。
304 Not Modified — 缓存命中
304 Not Modified 是一个非常特殊的"重定向"状态码——它并不是让你去访问另一个 URL,而是告诉你:你本地缓存的资源仍然是最新的,直接用缓存即可,不必重新下载。
304 是 HTTP 协商缓存(Conditional Request / Cache Validation)机制的核心。其工作流程如下:
这里涉及两对缓存校验头:
| 服务器响应头 | 客户端请求头 | 校验方式 |
|---|---|---|
Last-Modified (资源最后修改时间) | If-Modified-Since | 基于时间比较 |
ETag (资源的唯一指纹) | If-None-Match | 基于内容哈希比较 |
ETag优先级高于Last-Modified:因为Last-Modified精度只到秒级,且某些场景下文件时间可能变化但内容不变(如重新部署但文件内容未改),而ETag直接反映内容本身。- 304 响应没有响应体:这正是它的价值——对于大文件(如图片、JS、CSS),避免重复传输可以极大地节省带宽、加速页面加载。
4xx — 客户端错误状态码 (Client Error)
4xx 类状态码明确表示:错误出在客户端。可能是请求语法有误、缺少认证信息、权限不足、或者请求了一个不存在的资源。作为后端开发者,合理区分和使用 4xx 状态码,能让 API 的调用方快速定位问题而不必反复联系你排查。
400 Bad Request — 错误的请求
400 Bad Request 是客户端错误中最通用的一个,表示服务器 无法理解或无法处理 客户端发送的请求,通常是因为请求本身存在语法或语义层面的问题。
常见的触发场景包括:
- JSON 格式错误:请求体不是合法的 JSON(如少了逗号、引号不匹配)。
- 缺少必填参数:API 要求传
user_id,但客户端漏传了。 - 参数类型错误:某个字段要求是整数,客户端传了字符串。
- 参数值超出范围:年龄字段传了 -5 或 300。
// 请求: POST /api/users 缺少必填字段 name
// 请求体:
{
"email": "alice@example.com"
// 缺少 "name" 字段!
}
// 响应: 400 Bad Request
{
"code": 400,
"message": "Validation failed", // 校验失败
"errors": [
{
"field": "name", // 出错字段
"reason": "name is required" // 错误原因
}
]
}最佳实践:返回 400 时,响应体中应详细描述错误原因和出错字段,帮助调用方快速修复。泛泛的 "Bad Request" 对调试毫无帮助。
401 Unauthorized — 未认证
401 Unauthorized 的名称有些 misleading(误导)——它实际表达的不是"未授权",而是 "未认证"(Unauthenticated)。服务器不知道你是谁,因为你没有提供有效的身份凭证。
触发条件:
- 请求完全没有携带认证信息(如没带 Token、没带 Cookie)。
- 携带的认证信息已过期或无效(如 JWT Token 过期)。
// 未携带 Token 访问受保护资源
GET /api/profile HTTP/1.1
Host: api.example.com
// 注意: 没有 Authorization 头!
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="api" // 告知客户端应使用 Bearer Token 认证
Content-Type: application/json
{
"code": 401,
"message": "Authentication required. Please provide a valid token."
}服务器在返回 401 时,必须包含 WWW-Authenticate 响应头,告知客户端应该使用何种认证方案(如 Basic、Bearer、Digest 等)。
403 Forbidden — 禁止访问
403 Forbidden 表示服务器理解了请求、也知道你是谁,但是你 没有权限 访问该资源。与 401 的核心区别是:
| 状态码 | 含义 | 类比 |
|---|---|---|
| 401 | 我不知道你是谁(Identity unknown) | 酒店前台:请先出示房卡 |
| 403 | 我知道你是谁,但你不够格(Permission denied) | 酒店前台:您的房卡无法进入总统套房 |
// 普通用户尝试访问管理员接口
GET /api/admin/settings HTTP/1.1
Host: api.example.com
Authorization: Bearer <valid-user-token> // Token 有效, 但身份是普通用户
HTTP/1.1 403 Forbidden
Content-Type: application/json
{
"code": 403,
"message": "Access denied. Admin privileges required."
}关键点:收到 403 后,即使重新认证(重新登录)也无济于事,因为问题不在于身份不明,而在于权限不够。只有提升权限(如联系管理员授权)才能解决。
404 Not Found — 资源不存在
404 Not Found 可能是普通用户最熟悉的 HTTP 状态码。它表示服务器上找不到你请求的资源。
常见触发原因:
- URL 路径拼写错误(如
/api/uesrs/42把users拼错了)。 - 资源已被删除或从未存在。
- 路由未配置(后端没有定义这个 endpoint)。
GET /api/users/99999 HTTP/1.1
Host: api.example.com
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"code": 404,
"message": "User with id 99999 not found."
}安全小技巧:某些场景下,出于安全考虑,服务器会故意对"有权限限制的资源"也返回 404 而不是 403——这样攻击者就无法探测某个资源是否存在。比如 GitHub 上访问一个私有仓库,未授权时返回的是 404 而非 403,防止信息泄露。
下面将 4xx 系列中最容易混淆的四个状态码放在一起做一个速查对比:
5xx — 服务端错误状态码 (Server Error)
5xx 类状态码表示:客户端的请求可能是完全合法的,但服务器在处理过程中出了问题。这类错误对用户体验的杀伤力最大,对开发团队来说则意味着需要立即排查和修复。线上系统一旦出现大量 5xx,通常会触发告警(Alert),运维和开发需要第一时间响应。
500 Internal Server Error — 服务器内部错误
500 Internal Server Error 是服务端错误中最通用的"兜底"状态码,表示服务器遇到了一个 意料之外的异常(Unexpected Exception),导致无法完成请求。它就像医生说的"不明原因的发烧"——知道服务器出问题了,但具体是什么问题需要进一步诊断。
常见触发原因非常广泛:
- 后端代码未捕获的空指针异常(NullPointerException)。
- 数据库查询语句语法错误。
- 除零错误、数组越界等运行时异常。
- 依赖的配置文件缺失或格式错误。
- 第三方 SDK 调用失败且未做容错处理。
# Python Flask 示例: 一个可能导致 500 的接口
@app.route('/api/users/<int:user_id>') # 路由定义: 获取指定用户
def get_user(user_id):
user = db.query(User, user_id) # 从数据库查询用户
# 如果 user 为 None, 下面这行就会抛出 AttributeError!
return jsonify({ # 构造 JSON 响应
"name": user.name, # user 为 None 时 -> 500!
"email": user.email # 访问 None 的属性触发异常
})
# ✅ 正确写法: 加入空值检查
@app.route('/api/users/<int:user_id>') # 路由定义: 获取指定用户
def get_user_safe(user_id):
user = db.query(User, user_id) # 从数据库查询用户
if user is None: # 检查查询结果是否为空
return jsonify({ # 为空时返回 404 而非让代码崩溃
"code": 404,
"message": "User not found"
}), 404 # 显式返回 404 状态码
return jsonify({ # 非空时正常返回用户数据
"name": user.name,
"email": user.email
}), 200 # 显式返回 200 状态码生产环境注意事项:服务器返回 500 时,绝对不要在响应体中暴露完整的堆栈跟踪(Stack Trace)或内部错误详情——这些信息可能被攻击者利用。正确做法是在日志系统中记录详细错误,对外只返回通用的错误提示。
502 Bad Gateway — 错误的网关
502 Bad Gateway 表示服务器作为 网关或代理(Gateway / Proxy)时,从上游服务器(Upstream Server)收到了一个无效的响应。
要理解 502,首先需要理解现代 Web 架构中的反向代理模型。在生产环境中,用户的请求通常不会直接到达应用服务器,而是先经过一层 反向代理(如 Nginx、AWS ALB、Cloudflare):
当 Nginx 把请求转发给后端的 App Server 时,如果 App Server:
- 已经崩溃(进程挂了)
- 返回了不合法的 HTTP 响应(乱码、协议格式错误)
- 连接被意外重置(Connection Reset)
那么 Nginx 就无法从上游获取有效响应,只好向客户端返回 502 Bad Gateway。
常见排查方向:
| 排查方向 | 具体操作 |
|---|---|
| 上游服务是否存活 | 检查 App Server 进程是否运行中 (ps aux, systemctl status) |
| 上游服务是否健康 | 直接绕过代理访问上游,看是否正常返回 |
| 端口连通性 | telnet 或 curl 测试代理到上游的网络连通性 |
| 代理配置是否正确 | 检查 Nginx upstream 配置的地址和端口 |
503 Service Unavailable — 服务不可用
503 Service Unavailable 表示服务器暂时无法处理请求。与 500 的关键区别在于——503 通常是一个临时性的问题,过一段时间可能会自动恢复。
触发 503 的常见场景:
- 服务器过载(Overloaded):流量洪峰(如大促、秒杀)导致请求量超出服务器处理能力。
- 计划维护(Scheduled Maintenance):服务器正在进行升级部署,主动拒绝新请求。
- 线程池/连接池耗尽:所有工作线程都在忙碌,无法接受新的请求。
- 熔断生效(Circuit Breaker Open):微服务架构中,下游服务故障导致熔断器打开,上游主动返回 503。
HTTP/1.1 503 Service Unavailable
Content-Type: application/json
Retry-After: 30 // 告知客户端: 30秒后重试
{
"code": 503,
"message": "Server is temporarily overloaded. Please retry after 30 seconds."
}注意响应头中的 Retry-After 字段——这是 503 独有的重要头字段。它告诉客户端"请在多少秒后重试"(也可以是一个具体的日期时间)。设计良好的客户端应遵守此头字段,避免无脑重试导致"雪崩效应"(Thundering Herd)。
500 vs 502 vs 503 一览对比
| 状态码 | 核心含义 | 谁出了问题 | 是否临时性 | 典型场景 |
|---|---|---|---|---|
| 500 | 服务器内部代码异常 | 应用本身 | 不确定 | 空指针、除零错误 |
| 502 | 网关从上游收到无效响应 | 上游服务 | 不确定 | 后端进程崩溃 |
| 503 | 服务器暂时无法处理 | 容量/策略 | 通常是临时的 | 过载、维护中 |
下面这张时序图展示了一个典型的 502 和 503 的区别场景:
面试高频考点:当面试官问"502 和 503 的区别"时,关键要答出——502 的根因是 上游服务异常(代理能联系上游但收到无效响应或连接失败),而 503 是 服务本身暂时无法处理(过载或维护)。502 是"转发失败",503 是"自身忙不过来"。
📝 练习题 1
用户访问 https://old-domain.com/about 时,服务器希望将用户永久引导到 https://new-domain.com/about,并且让搜索引擎将旧页面的权重转移到新页面。应该返回哪个状态码?
A. 302 Found
B. 304 Not Modified
C. 301 Moved Permanently
D. 307 Temporary Redirect
【答案】 C
【解析】 301 Moved Permanently 表示资源已永久迁移到新地址。浏览器会缓存此重定向结果,搜索引擎也会将旧 URL 的 PageRank 权重转移到新 URL。302 和 307 都是临时重定向,搜索引擎不会做权重转移;304 用于缓存协商,与重定向跳转无关。域名迁移这种永久性变更,使用 301 是唯一正确的选择。
📝 练习题 2
某公司的 API 网关(Nginx)在转发请求到后端微服务时,发现目标服务的进程已经崩溃(Connection Refused)。此时 Nginx 应返回给客户端什么状态码?如果后端微服务正在运行但因流量过大主动拒绝了请求,又该返回什么状态码?
A. 两种情况都返回 500 Internal Server Error
B. 第一种返回 502 Bad Gateway,第二种返回 503 Service Unavailable
C. 两种情况都返回 502 Bad Gateway
D. 第一种返回 503 Service Unavailable,第二种返回 502 Bad Gateway
【答案】 B
【解析】 第一种场景中,Nginx 作为反向代理向上游服务转发请求,但上游进程已崩溃、连接被拒绝(Connection Refused),Nginx 无法获取有效的上游响应,这正是 502 Bad Gateway 的标准定义——网关从上游收到无效响应。第二种场景中,后端服务本身是运行着的,但因为负载过高主动拒绝请求,此时服务处于"暂时不可用"状态,语义上对应 503 Service Unavailable,且通常应附带 Retry-After 头建议客户端稍后重试。500 是服务器内部代码异常的通用兜底码,不适用于这两种网关层面的场景。
常用请求头(Common HTTP Headers)
HTTP 头部(Headers)是客户端与服务器之间传递元信息的核心机制。每一个 Header 都以 Key: Value 的形式出现在报文的头部区域,它们不承载业务数据本身,却决定了数据如何被解析、如何被缓存、用户是否有权访问等关键行为。可以说,Headers 是 HTTP 协议的"控制面板"——请求体是货物,Headers 就是货物上的标签和运单。
常用请求头大致可以分为以下几个功能域:
| 功能域 | 代表 Header | 核心职责 |
|---|---|---|
| 内容协商 (Content Negotiation) | Content-Type, Accept | 告知/协商数据的 MIME 类型 |
| 状态管理 (State Management) | Cookie | 在无状态协议上维持会话 |
| 身份认证 (Authentication) | Authorization | 携带凭证,证明请求者身份 |
| 缓存控制 (Caching) | Cache-Control | 精细化管理资源的缓存策略 |
下面逐一深入讲解。
Content-Type
Content-Type 是最基础也是最重要的 Header 之一,它告诉接收方:我发给你的消息体(Body)是什么格式的数据。其完整语法为:
Content-Type: media-type; charset=encoding; boundary=something
其中 media-type 遵循 MIME(Multipurpose Internet Mail Extensions) 标准,常见类型如下:
| MIME 类型 | 说明 | 典型场景 |
|---|---|---|
application/json | JSON 格式 | RESTful API 数据交互 |
application/x-www-form-urlencoded | 表单键值对(URL 编码) | HTML <form> 默认提交 |
multipart/form-data | 多部分表单(支持二进制) | 文件上传 |
text/html | HTML 文档 | 浏览器页面渲染 |
text/plain | 纯文本 | 日志、简单文本响应 |
application/xml | XML 格式 | SOAP 协议、旧式接口 |
application/octet-stream | 任意二进制流 | 文件下载、未知类型兜底 |
深入理解三种表单提交类型
这是面试和实际开发中最常混淆的知识点,我们逐个拆解。
1. application/x-www-form-urlencoded
这是 HTML 表单的默认编码方式。所有数据被编码为 key1=value1&key2=value2 的格式,特殊字符会被 URL 编码(如空格变为 + 或 %20)。
POST /login HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded // 表单默认编码
Content-Length: 29 // Body 长度
username=admin&password=s%40fe // @ 被编码为 %40这种方式的特点是简单高效,但只适合纯文本键值对。如果值中包含大量特殊字符或二进制数据,编码后体积会膨胀。
2. multipart/form-data
当需要上传文件时,必须使用此类型。它将 Body 分成多个"部分(part)",每个部分用一个 boundary(分隔符) 隔开,每部分可以有自己独立的 Content-Type。
POST /upload HTTP/1.1
Host: example.com
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW // 分隔符开始第一部分
Content-Disposition: form-data; name="username" // 普通字段
admin // 字段值
------WebKitFormBoundary7MA4YWxkTrZu0gW // 分隔符开始第二部分
Content-Disposition: form-data; name="avatar"; filename="me.png" // 文件字段
Content-Type: image/png // 该部分的独立类型
[...PNG 二进制数据...] // 文件内容(二进制)
------WebKitFormBoundary7MA4YWxkTrZu0gW-- // 结束标记(注意末尾的 --)boundary 是由客户端随机生成的字符串,保证不会与 Body 内容冲突。服务器根据 boundary 来拆分和解析各部分。
3. application/json
现代 API 开发的主流选择。Body 直接就是一个 JSON 字符串,结构清晰,支持嵌套对象和数组。
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/json // 声明为 JSON
Content-Length: 68 // Body 长度
{ // JSON 格式正文
"username": "admin", // 字符串类型
"age": 25, // 数值类型
"roles": ["admin", "editor"] // 数组类型
}⚠️ 易错点:很多后端框架(如 Spring Boot 的
@RequestBody)默认按Content-Type选择解析器。如果前端发了 JSON 但没设Content-Type: application/json,后端可能解析失败返回415 Unsupported Media Type。
Content-Type 在请求与响应中的区别
这一点非常容易被忽视——Content-Type 既可以出现在请求头中,也可以出现在响应头中,含义略有不同:
Accept
Accept 头部是内容协商(Content Negotiation) 机制的核心,它由客户端发出,告诉服务器:我能接受哪些格式的响应。服务器收到后,会从自身支持的格式中选择一个最佳匹配返回。
基本语法:
Accept: media-type1;q=weight1, media-type2;q=weight2, ...
其中 q(quality factor)表示优先级权重,取值范围 0 ~ 1,默认为 1(最高优先级),越大越优先。
实际示例
GET /api/data HTTP/1.1
Host: example.com
Accept: application/json;q=0.9, application/xml;q=0.8, text/html;q=1.0, */*;q=0.1服务器收到后,会按权重排序:
| 优先级 | MIME 类型 | q 值 | 说明 |
|---|---|---|---|
| 1 | text/html | 1.0 | 最优先 |
| 2 | application/json | 0.9 | 其次 |
| 3 | application/xml | 0.8 | 再次 |
| 4 | */* | 0.1 | 兜底:接受任何类型,但优先级最低 |
如果服务器无法满足 Accept 中任何一种类型,应当返回 406 Not Acceptable 状态码。
Accept 家族
Accept 实际上是一整个协商"家族",分别协商响应的不同维度:
| Header | 协商维度 | 示例 |
|---|---|---|
Accept | 数据格式(MIME) | application/json, text/html |
Accept-Language | 语言 | zh-CN, en-US;q=0.8 |
Accept-Encoding | 压缩算法 | gzip, deflate, br |
Accept-Charset | 字符集 | utf-8, iso-8859-1;q=0.5 |
服务器会通过对应的响应头(Content-Type, Content-Language, Content-Encoding)来告知最终选择。
Accept 与 Content-Type 的关系
这两个 Header 经常被混淆,但它们的角色完全不同:
一句话总结:Accept 是"我要什么",Content-Type 是"我给什么"。
Cookie
HTTP 协议是无状态的(Stateless)——服务器不会自动记住上一次请求是谁发的。但现实中,登录态、购物车、用户偏好等场景都需要跨请求维持状态。Cookie 就是为解决这个矛盾而生的机制。
Cookie 的工作流程
整个机制涉及两个 Header:
Set-Cookie(响应头):服务器 → 客户端,"把这个 Cookie 存起来"。Cookie(请求头):客户端 → 服务器,"上次你让我存的 Cookie 还给你"。
浏览器在收到 Set-Cookie 后,会自动在后续符合条件的请求中带上 Cookie 头——开发者无需手动操作(这也是 Cookie 方便但同时危险的原因)。
Set-Cookie 的完整属性
一个生产级的 Set-Cookie 往往附带多个属性来控制安全性和作用范围:
Set-Cookie: session_id=abc123; Path=/; Domain=.example.com; Max-Age=3600; Secure; HttpOnly; SameSite=Lax| 属性 | 含义 | 安全影响 |
|---|---|---|
Path=/ | Cookie 对该路径及其子路径生效 | 限制作用范围 |
Domain=.example.com | Cookie 对该域名及其子域名生效 | .example.com 覆盖 a.example.com |
Max-Age=3600 | Cookie 存活时间(秒),过期自动删除 | 不设则为 Session Cookie(关浏览器即失效) |
Expires=Thu, 01 Jan 2026 00:00:00 GMT | 绝对过期时间(与 Max-Age 二选一) | 同上 |
Secure | 仅通过 HTTPS 传输 | 防止明文网络被窃听 |
HttpOnly | JavaScript 无法通过 document.cookie 读取 | 防御 XSS 攻击 |
SameSite=Strict/Lax/None | 跨站请求时是否携带 Cookie | 防御 CSRF 攻击 |
SameSite 属性详解
SameSite 是现代浏览器防御 CSRF(Cross-Site Request Forgery,跨站请求伪造) 的关键属性:
Strict:最严格。任何跨站请求(包括点击链接跳转)都不携带该 Cookie。安全性最高,但用户体验可能受损(比如从邮件中点击链接到目标站,会丢失登录态)。Lax(现代浏览器默认值):允许顶级导航的 GET 请求携带 Cookie(如点击链接),但跨站的 POST、iframe、AJAX 不携带。兼顾安全与体验。None:允许所有跨站请求携带 Cookie,但必须同时设置Secure,否则浏览器会拒绝。适用于需要跨站的合法场景(如第三方登录、嵌入式支付)。
Cookie 的局限性
| 局限 | 说明 |
|---|---|
| 大小限制 | 单个 Cookie 通常不超过 4KB,每个域名下 Cookie 总数通常限制为 50 个左右 |
| 每次请求都携带 | Cookie 会附加在每个符合条件的请求中,增加带宽开销 |
| 安全风险 | 若未正确设置 HttpOnly/Secure/SameSite,易受 XSS 和 CSRF 攻击 |
| 隐私争议 | 第三方 Cookie(Third-party Cookie)常被用于跨站追踪,各大浏览器正逐步禁用 |
💡 现代替代方案:对于纯 API 场景(如移动端、SPA),更推荐使用
Authorization头部携带 JWT(JSON Web Token),避免 Cookie 的诸多限制。
Authorization
Authorization 头部用于身份认证(Authentication),客户端通过它向服务器证明"我是谁"。与 Cookie 不同,Authorization 是显式的、每次手动设置的,更适合 API 场景。
常见认证方案(Scheme)
Authorization 的值由 方案名(Scheme) 和 凭证(Credentials) 两部分组成:
Authorization: <Scheme> <Credentials>
1. Basic 认证
最简单的方案:将 用户名:密码 进行 Base64 编码后放入 Header。
GET /admin HTTP/1.1
Host: example.com
Authorization: Basic YWRtaW46cGFzc3dvcmQ= // Base64("admin:password")⚠️ 严重警告:Base64 不是加密,任何人都可以解码!Basic 认证必须配合 HTTPS 使用,否则等同于明文传输密码。
2. Bearer Token 认证(最主流)
Bearer 意为"持有者"——"谁持有这个 Token,谁就有权限"。这是当前 OAuth 2.0 和 JWT 生态的标准方案。
GET /api/profile HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... // JWT Token典型的 Bearer Token 工作流程:
3. Digest 认证
比 Basic 更安全,使用摘要算法(如 MD5) 对密码进行哈希,避免明文传输。但由于复杂度高且 MD5 本身已不安全,现实中较少使用,已基本被 Bearer Token 取代。
Cookie 认证 vs Authorization 认证对比
| 维度 | Cookie 方案 | Authorization (Bearer) 方案 |
|---|---|---|
| 存储位置 | 浏览器自动管理(Cookie 存储) | 开发者手动管理(localStorage / 内存) |
| 携带方式 | 浏览器自动附加 | 开发者手动设置 Header |
| 跨域支持 | 受同源策略和 SameSite 限制 | 天然支持跨域(CORS 配置即可) |
| CSRF 风险 | 高(Cookie 自动携带是根因) | 极低(Token 不会被自动携带) |
| 适用场景 | 传统 Web 应用(服务端渲染) | SPA、移动端、微服务 API |
| 服务端状态 | 通常有状态(Session 存服务端) | 通常无状态(JWT 自包含信息) |
Cache-Control
缓存是 Web 性能优化的基石。Cache-Control 是 HTTP/1.1 引入的最强大、最灵活的缓存控制 Header,它可以出现在请求头和响应头中,通过一系列指令(Directives) 精细化控制缓存行为。
核心指令一览
| 指令 | 位置 | 含义 |
|---|---|---|
public | 响应 | 任何中间节点(CDN、代理)和浏览器都可以缓存 |
private | 响应 | 仅浏览器可以缓存,CDN/代理不可缓存 |
no-cache | 请求/响应 | 可以缓存,但每次使用前必须向服务器验证(协商缓存) |
no-store | 请求/响应 | 完全禁止缓存,每次都从服务器获取 |
max-age=N | 响应 | 缓存有效期为 N 秒(从响应生成时刻起算) |
s-maxage=N | 响应 | 专门给共享缓存(CDN/代理)用的 max-age,覆盖 max-age |
must-revalidate | 响应 | 缓存过期后,必须向源服务器验证,不允许使用过期缓存 |
no-transform | 响应 | 禁止中间代理修改响应内容(如压缩图片) |
⚠️ 经典误区:
no-cache≠ "不缓存"!no-cache的真正含义是"缓存但每次都要验证"。真正的"不缓存"是no-store。
强缓存 vs 协商缓存
HTTP 缓存机制分为两大阶段,Cache-Control 在其中扮演控制角色:
阶段一:强缓存(Strong Cache)
浏览器在发送请求之前,先检查本地是否有缓存且是否在有效期内。如果缓存有效,直接使用,根本不会发出网络请求。这是性能最高的方式。
控制强缓存的 Header 主要有两个(HTTP/1.1 优先使用前者):
| Header | 版本 | 格式 | 说明 |
|---|---|---|---|
Cache-Control: max-age=3600 | HTTP/1.1 | 相对时间(秒) | 从响应生成起 3600 秒内有效 |
Expires: Thu, 01 Jan 2026 00:00:00 GMT | HTTP/1.0 | 绝对时间 | 指定过期时间点(依赖客户端时钟,易出错) |
当两者同时存在时,Cache-Control: max-age 优先级更高,Expires 作为降级兜底。
阶段二:协商缓存(Negotiation Cache)
当强缓存过期后,浏览器不会直接丢弃缓存,而是带着验证信息去问服务器:"这个资源变了吗?"。服务器如果判断资源未变,返回 304 Not Modified(不带 Body),浏览器继续用本地缓存;如果变了,返回 200 OK 加新资源。
协商缓存有两组 Header 配对:
| 请求头 | 响应头 | 验证依据 | 精度 |
|---|---|---|---|
If-None-Match | ETag | 资源内容的哈希/指纹 | 精确到内容字节 |
If-Modified-Since | Last-Modified | 资源最后修改时间 | 精确到秒 |
ETag 优先级高于 Last-Modified。当两者都存在时,服务器会先检查 ETag。
协商缓存完整交互流程
典型缓存策略实践
不同类型的资源应使用不同的缓存策略:
| 资源类型 | 推荐策略 | 示例 Header |
|---|---|---|
| HTML 页面 | 不缓存或协商缓存 | Cache-Control: no-cache |
| 带 hash 的 JS/CSS | 长期强缓存 | Cache-Control: public, max-age=31536000, immutable |
| API 响应 | 通常不缓存 | Cache-Control: no-store |
| 图片/字体 | 中等时长强缓存 | Cache-Control: public, max-age=86400 |
| 用户敏感数据 | 禁止任何缓存 | Cache-Control: private, no-store |
为什么 HTML 用 no-cache 而静态资源用长缓存?这是现代前端部署的经典模式:HTML 作为"入口文件"每次都向服务器验证是否更新,而 JS/CSS 文件名中包含内容 hash(如 app.3a7f2b.js),内容变化则文件名变化,因此可以放心地设置超长缓存——旧文件名永远不会被请求到。
Cache-Control 在请求头中的用途
虽然 Cache-Control 更多出现在响应头中,但它在请求头中同样有意义:
| 请求头指令 | 含义 |
|---|---|
Cache-Control: no-cache | 告诉中间代理:不要给我缓存的版本,去源服务器拿(强制刷新) |
Cache-Control: no-store | 告诉中间代理和浏览器:不要存储这个请求和响应 |
Cache-Control: max-age=0 | 等效于 no-cache,要求验证 |
Cache-Control: only-if-cached | 仅当有缓存时返回,否则返回 504 Gateway Timeout(离线模式) |
💡 浏览器行为:当你按
Ctrl+F5(强制刷新)时,浏览器会在请求头中加上Cache-Control: no-cache(或no-store),跳过所有缓存层直接向源服务器请求。而普通的F5刷新只会跳过强缓存,但仍走协商缓存流程。
Cache-Control vs 旧版缓存 Header
| 维度 | Cache-Control (HTTP/1.1) | Expires / Pragma (HTTP/1.0) |
|---|---|---|
| 时间表达 | 相对时间 (max-age=3600) | 绝对时间 (Thu, 01 Jan 2026 GMT) |
| 精细度 | 多种指令灵活组合 | 只能粗粒度控制 |
| 可靠性 | 不依赖客户端时钟 | 依赖客户端时钟(时钟偏差会出错) |
| 优先级 | ✅ 高 | 低(作为降级兜底) |
Pragma: no-cache 是 HTTP/1.0 的遗留写法,等效于 Cache-Control: no-cache。现代应用中只需设置 Cache-Control 即可,但为了兼容极老的代理服务器,有些项目会同时写两者。
📝 练习题 1
以下关于 HTTP 缓存的说法,正确的是?
A. Cache-Control: no-cache 表示浏览器完全不缓存该资源
B. ETag 的优先级低于 Last-Modified,服务器会优先检查 Last-Modified
C. 当 Cache-Control: max-age=3600 和 Expires 同时存在时,Expires 优先级更高
D. 强缓存命中时,浏览器不会向服务器发送任何网络请求
【答案】 D
【解析】 强缓存(Strong Cache)的核心特征就是完全不发起网络请求,浏览器直接从本地缓存读取资源并返回 200 (from disk cache) 或 200 (from memory cache)。选项 A 错误,no-cache 的含义是"可以缓存,但每次使用前必须向服务器验证",不缓存应该用 no-store。选项 B 错误,ETag 优先级高于 Last-Modified。选项 C 错误,Cache-Control 是 HTTP/1.1 标准,优先级高于 HTTP/1.0 的 Expires。
📝 练习题 2
前端发送了如下请求,但服务器返回了 415 Unsupported Media Type,最可能的原因是什么?
POST /api/users HTTP/1.1
Host: api.example.com
Content-Type: application/x-www-form-urlencoded
{"username":"admin","age":25}A. 请求方法应该使用 PUT 而不是 POST
B. Body 格式为 JSON,但 Content-Type 声明为 application/x-www-form-urlencoded,导致服务器解析器不匹配
C. 缺少 Accept: application/json 请求头
D. 缺少 Authorization 请求头,服务器拒绝了未认证的请求
【答案】 B
【解析】 415 Unsupported Media Type 表示服务器无法处理请求中声明的媒体类型。本题中,Body 实际是 JSON 格式 ({"username":...}),但 Content-Type 却写成了 application/x-www-form-urlencoded。服务器的后端框架会根据 Content-Type 选择对应的解析器——用 URL-encoded 解析器去解析 JSON 字符串,自然会失败。正确做法是将 Content-Type 改为 application/json。选项 A 中 POST 和 PUT 都可以创建资源,与 415 无关。选项 C 中 Accept 影响的是响应格式,不会导致 415。选项 D 缺少认证应返回 401 Unauthorized,而不是 415。
本章小结
本章围绕 HTTP(HyperText Transfer Protocol) 协议展开,从宏观概述到微观报文字段,系统梳理了 Web 通信中最核心的应用层协议。下面我们以"回顾 → 全景图 → 高频考点 → 练习"的顺序做一次完整的收束。
知识脉络回顾
HTTP 概述部分,我们明确了两个基本特性。其一是 无状态(Stateless)——服务器不会在两次请求之间保留任何客户端信息,每次请求都是独立的事务。这一设计极大地简化了服务器实现,但也催生了 Cookie、Session、Token 等状态保持方案。其二是 请求-响应模型(Request-Response Model)——通信永远由客户端发起请求,服务器被动返回响应,这种"一问一答"的范式贯穿了 HTTP/1.0 到 HTTP/1.1 的整个生命周期(直到 HTTP/2 的 Server Push 才部分打破)。
HTTP 报文结构部分,我们将一个 HTTP 报文拆分为三大块:起始行(Start Line)、头部字段(Headers)、消息体(Body)。请求报文的起始行称为 请求行(Request Line),包含方法、URI、协议版本;响应报文的起始行称为 状态行(Status Line),包含协议版本、状态码、原因短语。头部和消息体之间以一个 空行(CRLF) 分隔——这个看似不起眼的空行是协议解析的关键定界符。
HTTP 方法部分,我们重点讨论了四大语义动词:GET 获取资源、POST 提交数据、PUT 整体更新资源、DELETE 删除资源,并深入对比了面试高频考点 GET vs POST 在语义、幂等性、参数位置、缓存行为等维度上的差异。
状态码部分,我们按五大分类逐一解读:1xx 信息性、2xx 成功、3xx 重定向、4xx 客户端错误、5xx 服务端错误,并对 200/201/204、301/302/304、400/401/403/404、500/502/503 等高频状态码做了场景化分析。
常用请求头部分,我们学习了 Content-Type(声明消息体媒体类型)、Accept(内容协商)、Cookie(状态保持)、Authorization(身份认证)、Cache-Control(缓存策略)五大核心头字段,它们几乎覆盖了日常开发中 90% 以上的请求头使用场景。
HTTP 协议全景图
一次完整 HTTP 交互的生命周期
将所有知识点串联在一条时间线上,有助于理解它们之间的协作关系:
从上图可以看到:方法决定了"做什么",请求头描述了"怎么做"和"我是谁",状态码反馈了"做得怎么样",响应头补充了"后续怎么办"(缓存、重定向等)。这四块知识不是孤立的,而是 一个请求生命周期中不同阶段的接力。
高频面试考点速查表
| 考点 | 关键结论 | 易错点 |
|---|---|---|
| 无状态 | 服务器不记忆客户端,需要 Cookie/Session 补偿 | 无状态 ≠ 无连接,HTTP/1.1 默认 Keep-Alive |
| GET vs POST | GET 幂等可缓存,参数在 URL;POST 非幂等不可缓存,参数在 Body | 规范上 GET 也可以有 Body,但几乎没有实现支持 |
| 301 vs 302 | 301 永久重定向(搜索引擎更新索引);302 临时重定向 | 301 浏览器会缓存重定向结果,调试时容易踩坑 |
| 304 Not Modified | 配合 If-Modified-Since / If-None-Match 使用 | 304 响应不包含消息体,节省带宽 |
| 401 vs 403 | 401 未认证(需要登录);403 已认证但无权限 | 401 应附带 WWW-Authenticate 头提示认证方式 |
| Cookie vs Authorization | Cookie 自动携带、有同源策略;Authorization 手动设置、灵活跨域 | Cookie 存在 CSRF 风险,Token 方案更适合现代前后端分离架构 |
| Cache-Control | no-cache 可缓存但每次需验证;no-store 完全禁止缓存 | no-cache 的名字极具误导性,它不是"不缓存" |
| Content-Type | application/json 是现代 API 主流;multipart/form-data 用于文件上传 | application/x-www-form-urlencoded 才是 HTML 表单默认类型 |
本章核心公式
用一句话概括 HTTP 的设计哲学:
HTTP = 无状态的请求-响应协议 + 语义化的方法动词 + 自描述的头部字段 + 标准化的状态码体系
它的简洁与强大并存:简洁在于"一问一答"的交互模型和纯文本的报文格式(至少在 HTTP/1.x 时代),强大在于通过头部字段的灵活组合,可以表达缓存策略、内容协商、身份认证、连接管理等极其丰富的语义。理解 HTTP 协议,就是理解 现代 Web 架构的通信基石。
📝 练习题 1
以下关于 HTTP 协议的描述,错误 的是:
A. HTTP 是无状态协议,服务器默认不会记录客户端的历史请求信息
B. GET 请求是幂等的,多次执行同一 GET 请求应返回相同结果
C. 状态码 304 表示资源未修改,响应报文中会包含完整的资源内容以供客户端校验
D. Cache-Control: no-cache 表示缓存在使用前必须向服务器进行验证,而非完全不缓存
【答案】 C
【解析】 304 Not Modified 的核心价值就在于 不传输消息体。当客户端通过条件请求(If-Modified-Since 或 If-None-Match)询问服务器"资源是否变化"时,若未变化,服务器只返回 304 状态码和必要的头部,不包含响应体(Body),从而节省带宽。选项 C 说"包含完整的资源内容"恰好相反。A 项准确描述了无状态特性;B 项是 GET 幂等性的标准定义;D 项正确区分了 no-cache(需验证)与 no-store(不缓存)。
📝 练习题 2
某前后端分离项目中,前端通过 fetch 发送 JSON 数据到后端 API,后端收到请求后返回 401 Unauthorized。以下排查方向中,最不可能 是问题根因的是:
A. 请求未携带 Authorization 头,服务器无法验证身份
B. 请求头中 Content-Type 设置为 text/plain,导致服务器无法解析 JSON 请求体
C. 客户端的 JWT Token 已过期,服务器校验失败后返回 401
D. 请求触发了 CORS 预检(preflight),预检请求本身未通过认证
【答案】 B
【解析】 401 Unauthorized 明确语义为 身份认证失败,与请求体的解析格式无关。如果 Content-Type 设置错误导致服务器无法解析 JSON,服务器更可能返回的是 400 Bad Request(请求格式有误)或 415 Unsupported Media Type(不支持的媒体类型),而不是 401。A 项(缺少认证头)、C 项(Token 过期)都是导致 401 的经典原因。D 项也值得注意——在 CORS 场景下,浏览器会先发送 OPTIONS 预检请求,如果服务器对预检请求也做了认证拦截,就会返回 401,导致正式请求根本发不出去,这是实际开发中常见的坑。因此 B 是最不可能导致 401 的选项。