HTTP协议设计思想
# 11.HTTP协议设计思想
# 目录介绍
- 01.工作案例引入
- 1.1 一个前后端分离项目的HTTP踩坑记
- 1.2 踩坑背后的HTTP设计知识图谱
- 02.URI和URL设计思想
- 2.1 如何访问网络资源
- 2.2 什么是URI
- 2.3 为何要设计URL
- 2.4 常用网络协议
- 2.5 URI和URL设计思想
- 2.6 HTTP之URL
- 2.7 URL注意事项
- 03.理解HTTP超文本协议
- 3.0 HTTP诞生历史
- 3.1 HTTP协议介绍
- 3.2 在OSI中设计
- 3.3 什么叫超文本
- 3.4 什么叫做无状态
- 3.5 什么叫做无连接
- 3.6 HTTP协议特点
- 04.HTTP请求与响应
- 4.1 理解请求和响应
- 4.2 请求包的设计
- 4.3 响应包的设计
- 4.4 报文空行设计
- 4.5 看一个完整案例
- 05.HTTP一些场景
- 5.1 如何统计消息长度
- 5.2 判断传输是否完成
- 5.3 HTTP状态码设计
- 06.HTTP各个版本
- 6.1 Http1.0版本
- 6.2 HTTP1.1版本
- 6.3 Http2.0版本
- 07.HTTP方法的设计哲学
- 7.1 安全性与幂等性
- 7.2 RESTful设计思想
- 7.3 方法的实际应用
- 08.HTTP头部的设计艺术
- 8.1 内容协商机制
- 8.2 条件请求设计
- 8.3 跨域资源共享
- 09.综合案例:从0到1设计RESTful API
- 9.1 案例背景与目标
- 9.2 第一版:随意风格(能跑就行)
- 9.3 第二版:RESTful化(规范化)
- 9.4 第三版:高级特性(内容协商+缓存+CORS)
- 9.5 第四版:生产级设计(版本管理+幂等+安全)
- 9.6 四种方案横向对比
- 9.7 案例升华:HTTP设计的本质哲学
- 9.8 全文知识图谱回顾
- 10.思考题与作业
- 10.1 基础思考题
- 10.2 进阶思考题
- 10.3 动手作业
# 01.工作案例引入
# 1.1 一个前后端分离项目的HTTP踩坑记
场景:小张是一名全栈工程师,负责公司社交平台"动态广场"模块。前端 React,后端 Spring Boot,典型的 RESTful API 架构。开发环境一直顺利,直到联调上线。
踩坑 ① —— "中文用户名在 URL 里变成了乱码":用户反馈搜索接口 GET /api/users/search?keyword=张三 返回空结果,但数据库里明明有这个用户。小张在 Chrome DevTools 里看到请求 URL 变成了 keyword=%E5%BC%A0%E4%B8%89,后端日志也显示收到了 %E5%BC%A0%E4%B8%89。但 SQL 查不到——原来后端没有做 URL 解码。
踩坑 ② —— "Postman 里 DELETE 方法测试正常,浏览器调用就 405":小张用 Postman 测试 DELETE /api/posts/123 一切正常,但前端 fetch() 调用时浏览器报 405 Method Not Allowed。排查发现——前端先发了一个 OPTIONS 预检请求,后端没有处理,直接返回 405。
踩坑 ③ —— "用户说修改了头像,但展示的还是旧图":测试反馈用户上传了新头像,但其他人看到的还是旧头像。小张查了 CDN——新图片已推送,但浏览器缓存了旧图。因为响应头里没有 ETag 或 Cache-Control,浏览器按自己的策略缓存,刷新了也不生效。
踩坑 ④ —— "高峰期重复下单,同一个订单被创建了两次":双十一压测时发现,有些用户在网络超时后重试 POST /api/orders,结果创建了两个一模一样的订单。小张没做幂等处理——POST 不是幂等方法,重试会导致重复创建。
踩坑 ⑤ —— "移动端请求返回 XML,Web 端返回 JSON,客户端解析崩了":不同端的请求到达同一个后端接口,有的客户端收到了 XML 格式的响应。小张的代码里同时配置了 JSON 和 XML 的序列化器,但是没有根据请求头 Accept 做内容协商——Spring 按默认优先级返回了 XML。
疑惑链条:
- "URL 里的中文怎么变成百分号了?" → HTTP URL 只支持 ASCII,非 ASCII 字符需要百分号编码(Percent-Encoding),后端收到后必须解码
- "为什么浏览器多了一个 OPTIONS 请求?" → CORS 预检(Preflight)——跨域非简单请求(如 DELETE/PUT/自定义头)浏览器先发 OPTIONS 询问服务器是否允许
- "换头像为什么还显示旧图?" → HTTP 缓存机制——没有
ETag/Cache-Control头,浏览器凭自己的启发式策略缓存,不一致时无法及时刷新 - "POST 为什么会创建两次?" → POST 不是幂等方法。幂等性(Idempotency)是 HTTP 方法设计的核心概念——GET/PUT/DELETE 可以安全重试,POST 不能
- "为什么返回了 XML?" → 内容协商(Content Negotiation)——客户端通过
Accept头告诉服务器想要什么格式,服务器应据此返回对应的格式
小张这一串问题,本质都是在问:HTTP 的设计哲学是什么?URL 为什么要编码?方法之间的区别不只是名字不同?头部字段到底在干什么?——这正是"HTTP 协议设计思想"要回答的。
# 1.2 踩坑背后的HTTP设计知识图谱
把这次踩坑翻译成 HTTP 协议设计语言:
一次 POST /api/orders 请求的完整 HTTP 报文路径:
客户端构造请求
↓ ① URL编码
POST /api/orders HTTP/1.1\r\n ← 踩坑①的发生地(百分号编码)
Host: api.example.com\r\n
Content-Type: application/json\r\n
Accept: application/json\r\n ← 踩坑⑤的发生地(内容协商)
Authorization: Bearer xxx\r\n
\r\n
{"productId": 123, "quantity": 1} ← 踩坑④的发生地(非幂等操作)
↓ ② 浏览器检查是否跨域
OPTIONS 预检请求 ← 踩坑②的发生地(CORS)
↓ ③ 服务器处理并返回响应
HTTP/1.1 200 OK\r\n
ETag: "abc123"\r\n ← 踩坑③的发生地(条件请求/缓存)
Cache-Control: max-age=60\r\n
Content-Type: application/json\r\n
\r\n
{"orderId": 456}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
五类踩坑与后续章节的映射关系:
| 踩坑 | 症状 | 根因所在的知识点 | 对应章节 |
|---|---|---|---|
| ① | 中文变乱码 | URL 编码 → ASCII 限制 | 02.URL设计 |
| ② | OPTIONS 预检失败 | 跨域 → CORS 预检 | 08.跨域资源共享 |
| ③ | 头像不刷新 | 无缓存条件 → ETag/Cache-Control | 08.条件请求 |
| ④ | 重复下单 | POST 非幂等 → 幂等性设计 | 07.方法设计哲学 |
| ⑤ | 返回格式不一致 | 无内容协商 → Accept/C-Content-Type | 08.内容协商 |
本章的主线就是沿着这五类踩坑,一层一层拆解 HTTP 协议的设计理念。读完之后,你不仅能避开这些坑,还能理解为什么 RESTful API 强调"资源"概念、为什么 HTTP/2 用二进制分帧替代文本协议、为什么"无状态"反而是 HTTP 最强大的特性。
# 02.URI和URL设计思想
# 2.1 如何访问网络资源
疑问:我们每天都在使用网络,但网络资源到底是什么?怎么找到它?
网络资源是指存储在互联网上的任何可访问内容——网页、图片、视频、API接口、文件等。要访问网络资源,需要解决两个核心问题:
- 标识问题:如何唯一地标识一个资源?答案是 URI(统一资源标识符)
- 定位问题:如何找到资源的具体位置?答案是 URL(统一资源定位符)
两者的关系可以用一个生活中的例子来理解:
URI —— 相当于一个人的身份证号码,能唯一标识一个人
URL —— 相当于一个人的家庭住址,能定位到这个人
身份证号(URI):110101199001011234
家庭住址(URL):北京市东城区XX路XX号XX室
URL 是 URI 的子集,每个 URL 都是 URI,但 URI 不一定是 URL
2
3
4
5
6
7
访问网络资源的基本流程:
- 用户输入 URL(如
https://www.example.com/index.html) - 浏览器解析 URL,提取协议、主机名、端口、路径等信息
- 通过 DNS 将主机名解析为 IP 地址
- 建立 TCP 连接(HTTPS 还需 TLS 握手)
- 发送 HTTP 请求,携带路径和参数
- 服务器返回对应资源的响应数据
# 2.2 什么是URI
URI全称是Uniform Resource Identifier,也就是统一资源标识符,它是一种采用特定的语法标识一个资源的字符串表示。
URI所标识的资源可能是服务器上的一个文件,也可能是一个邮件地址、图书、主机名等。简单记为:URI是标识一个资源的字符串(这里先不必纠结标识的目标资源到底是什么,因为使用者一般不会见到资源的实体),从服务器接收到的只是资源的一种字节表示(二进制序列,从网络流中读取)。
通用URI的格式如下:
scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
URI一般由三部组成:
- ①访问资源的命名机制
- ②存放资源的主机名
- ③资源自身的名称,由路径表示,着重强调于资源。
# 2.3 为何要设计URL
URL全称是Uniform Resource Location,也就是统一资源位置。
- 实际上,URL就是一种特殊的URI,它除了标识一个资源,还会为资源提供一个特定的网络位置,客户端可以通过它来获取URL对应的资源。
- URL所表示的网络资源位置通常包括用于访问服务器的协议(如http、ftp等)、服务器的主机名或者IP地址、以及资源文件在该服务器上的路径。
- 典型的URL例如http://localhost/myProject/index.html,它指示本地服务器的myProject目录下有一个名为index.html的文档,这个文档可以通过http协议访问(实际上,URL不一定是指服务器中的真实的物理路径,因为我们一般在服务器中部署应用,如Servlet应用,URL访问的很可能是应用的接口,至于最终映射到什么资源可以由应用自身决定)。
URL(Uniform Resource Locator,统一资源定位符)的格式。通常而言,我们所熟悉的 URL 的常见定义格式为:
scheme://host[:port#]/path/.../[;url-params][?query-string][#anchor]
> scheme //有我们很熟悉的http、https、ftp以及著名的ed2k,迅雷的thunder等。
> host //HTTP服务器的IP地址或者域名
> port# //HTTP服务器的默认端口是80,这种情况下端口号可以省略。如果使用了别的端口,必须指明
> path //访问资源的路径
> url-params //所带参数
> query-string //发送给http服务器的数据
> anchor //锚点定位
2
3
4
5
6
7
8
格式属性介绍:
- protocol:URL中的协议(protocol)是相应于URI中的模式(schema)的另一个叫法。
- userInfo:URL中的用户信息(userInfo)是服务器的登录信息,这部分信息是可选的。
- port:URL中的端口号(port)是指服务器中应用的运行端口,默认端口为80。
- path:URL中的路径(path)用于表示服务器上的一个特定的目录。
- query:查询参数(query)一般是一个字符串,表示形式是key1=value1&key2=value2&keyn=valuen。
- fragment:片段(fragment)表示远程服务器资源的某个特定的部分。
URL一般由三部组成:
- ①协议(或称为服务方式)
- ②存有该资源的主机IP地址(有时也包括端口号)
- ③主机资源的具体地址。如目录和文件名等。
# 2.4 常用网络协议
URI当前的常用模式包括:
- data:链接中直接包含经过BASE64编码的数据。
- file:本地磁盘上的文件。
- ftp:FTP服务器。
- http:使用超文本传输协议。
- mailto:电子邮件的地址。
- magnet:可以通过对等网络下载的资源。
- telnet:基于Telnet的服务的连接。
- urn:统一资源名(Uniform Resource Name)。
# 2.5 URI和URL设计思想
URI和URL的设计思想:
- 统一性:提供一种统一的标识和定位机制,不同类型的资源都可以通过唯一的标识符进行访问。
- 唯一性:确保每个资源都有一个唯一的标识符,避免冲突和混淆。
- 可读性:使用人类可理解的字符串来表示资源的标识和定位。
- 层次性:URL支持层次结构,按照层次结构的方式组织各个部分。
- 可扩展性:允许通过添加额外的参数、查询字符串或片段标识符来扩展访问方式。
# 2.6 HTTP之URL
HTTP使用统一资源标识符(URI)来传输数据和建立连接。URL是一种特殊类型的URI,包含了用于查找某个资源的足够的信息。
以 http://www.yc.cn:8080/news/index.php?boardID=32&ID=24618&page=1#name 为例:
- 协议部分:
http: - 域名部分:
www.yc.cn - 端口部分:
8080(跟在域名后面,使用":"分隔) - 虚拟目录部分:
/news/ - 文件名部分:
index.php - 锚部分:
#name - 参数部分:
boardID=5&ID=24618&page=1
# 2.7 URL注意事项
URL 只能使用 ASCII 字符集来通过因特网进行发送。由于 URL 常常会包含 ASCII 集合之外的字符,URL 必须转换为有效的 ASCII 格式。
此时就需要对URL进行转义:将需要转码的字符转为16进制,然后编码成 %XY 格式。
实际输入:https://www.hao123.com/
服务器接收到:https%3A%2F%2Fwww.hao123.com%2F
2
回到踩坑①的场景:中文"张三"的 UTF-8 编码是 E5 BC A0 E4 B8 89,百分号编码后变成 %E5%BC%A0%E4%B8%89。前端浏览器会自动编码,但后端必须主动解码——Java 中 URLDecoder.decode(keyword, "UTF-8") 即可还原为"张三"。
# 03.理解HTTP超文本协议
# 3.0 HTTP诞生历史
1989 年,任职于欧洲核子研究中心(CERN)的蒂姆·伯纳斯 - 李(Tim Berners-Lee)发表了一篇论文,提出了在互联网上构建超链接文档系统的构想。这篇论文中他确立了三项关键技术。
- URI:即统一资源标识符,作为互联网上资源的唯一身份;
- HTML:即超文本标记语言,描述超文本文档;
- HTTP:即超文本传输协议,用来传输超文本。
这三项技术在如今的我们看来已经是稀松平常,但在当时却是了不得的大发明。基于它们,就可以把超文本系统完美地运行在互联网上,让各地的人们能够自由地共享信息,蒂姆把这个系统称为"万维网"(World Wide Web),也就是我们现在所熟知的 Web。
# 3.1 HTTP协议介绍
HTTP协议又叫做超文本传输协议,是一种无状态,无连接,以请求-响应方式运行的协议。
也可以理解为:HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。
# 3.2 在OSI中设计
HTTP协议属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议,在数据链路层使用MAC地址。
- TCP协议主要解决如何在IP层之上可靠的传递数据包。
- IP协议主要解决网络路由和寻址问题。
- MAC地址主要解决相邻两台主机之间的寻址传输问题。
# 3.3 什么叫超文本
HTTP 传输的内容是「超文本」。超文本,它是文字、图片、视频等的混合体,最关键有超链接,能从一个超文本跳转到另外一个超文本。
HTML 就是最常见的超文本了,它本身只是纯文字文件,但内部用很多标签定义了图片、视频等的链接,再经过浏览器的解释,呈现给我们的就是一个文字、有画面的网页了。
# 3.4 什么叫做无状态
出现HTTP无状态的场景说明
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即我们给服务器发送 HTTP 请求之后,服务器根据请求,会给我们发送想要的数据过来,但是发送完,不会记录任何信息,也就是说协议对于发送过的请求或响应都不做持久化处理。
HTTP的无状态是指会话的无状态,会话是由一组请求和响应组成的,围绕一件相关事情的请求和响应,这些不同的请求和响应之间是需要进行数据传递的,但HTTP是无状态指的是本次请求和响应,与下一次的请求和响应是没有关系的,不会发生数据传递。
HTTP 协议无状态特性的优点和缺点:
- 优点在于解放了服务器,每一次请求"点到为止"不会造成不必要连接占用
- 缺点在于每次请求会传输大量重复的内容信息
随着 Web 的不断发展,因无状态而导致业务处理变得棘手的情况增多:
- 比如客户在购物平台购物场景下,登录、下单、购物,需要登录后才能进行处理,单独靠HTTP是无法完成的。
- 发起登录的HTTP请求成功登录后,不跟踪客户的登录状态,不对后续购物请求进行用户登录数据信息传递,无法满足业务逻辑。
Cookie解决无状态困境:
- 每个请求之间都是独立的,对于之前的请求事务没有记忆的能力。所以就出现了像Cookie这种,用来保存一些状态的东西。
- Cookie就相当于一个通行证,第一次访问的时候给客户端发送一个Cookie,当客户端再次来的时候,拿着Cookie(通行证),那么服务器就知道这个是"老用户"。
# 3.5 什么叫做无连接
为什么HTTP要设计无连接,它的意图是什么?
无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,就断开连接。采用这种方式可以节省传输时间。这样主要是为了缓解服务器的压力,减小连接对服务器资源的占用。
HTTP设计无连接后期遇到的挑战:随着网页变得复杂,里面嵌入了很多图片,每次访问图片都需要建立一次 TCP 连接就显得很低效。
如何解决HTTP无连接遇到的效率低问题:Keep-Alive 被提出,目前的 HTTP/1.1 默认开启 Keep-Alive,通过在响应头中包含 Connection: keep-alive 字段来保持连接。
# 3.6 HTTP协议特点
HTTP 协议具有以下特点:
- 简单易用:设计简单明了,易于理解和使用。
- 无连接:每个请求-响应对都是独立的。
- 无状态:服务器不会记住之前的请求和响应——这恰好是RESTful API的核心约束之一。
- 支持灵活的数据格式:最常见的是HTML、XML和JSON。
- 可扩展性:通过使用头部字段和方法,可以定义和传递各种自定义的元数据和操作。
- 基于请求-响应模型:客户端发送请求,服务器返回响应。
- 支持缓存:可以在客户端或代理服务器上缓存响应,以减少重复请求和提高性能。
# 04.HTTP请求与响应
# 4.1 理解请求和响应
什么是Http报文?它是HTTP应用程序之间发送的数据块。这些数据块以一些文本形式的元信息开头,这些信息描述了报文的内容及含义,后面跟着可选的数据部分。
HTTP报文是面向文本的,报文中的每一个字段都是一些ASCII码串,各个字段的长度是不确定的。
HTTP有两类报文:请求报文(客户端)和响应报文(服务端)。
HTTP报文的流动方向:一次HTTP请求,HTTP报文会从"客户端"流到"代理"再流到"服务器",在服务器工作完成之后,报文又会从"服务器"流到"代理"再流到"客户端"。
# 4.2 请求包的设计
客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行、请求头部、空行和请求数据四个部分组成。
请求行: <method> <request-URL> <version>
头部: <headers>
主体: <entity-body>
2
3
- 请求行:所有的HTTP请求报文都以一个请求行作为开始,该行包含了一个方法和一个请求的URL,还包含HTTP的版本。
- 请求头部:用于说明是谁或什么在发送请求、请求源自何处,或者客户端的喜好及能力。
- 请求主体:可以添加任意的其他数据。
例子:
//下面是POST请求
POST /meme.php/home/user/login HTTP/1.1
Host: 114.215.86.90
Cache-Control: no-cache
Postman-Token: bd243d6b-da03-902f-0a2c-8e9377f6f6ed
Content-Type: application/x-www-form-urlencoded
tel=13637829200&password=123456
2
3
4
5
6
7
8
常见的请求头部:
User-Agent:产生请求的浏览器类型。Accept:客户端可识别的响应内容类型列表。Accept-Language:客户端可接受的自然语言。Accept-Encoding:客户端可接受的编码压缩格式。Host:请求的主机名,允许多个域名同处一个IP地址。Connection:连接方式(close 或keep-alive)。Cookie:存储于客户端扩展字段。
# 4.3 响应包的设计
响应报文结构格式:状态行、消息报头、空行和响应正文。
状态行: <version> <status> <reason-phrase>
响应头部: <headers>
响应主体: <entity-body>
2
3
- 状态行:包含HTTP版本、数字状态码、原因短语。
- 响应头部:为客户端提供额外信息。
- 响应主体:服务器返回给客户端的文本信息。
例子:
HTTP/1.1 200 OK
Date: Sat, 02 Jan 2016 13:20:55 GMT
Server: Apache/2.4.6 (CentOS) PHP/5.6.14
X-Powered-By: PHP/5.6.14
Content-Length: 78
Keep-Alive: timeout=5, max=100 Connection: Keep-Alive
Content-Type: application/json; charset=utf-8
{"status":202,"info":"\u6b64\u7528\u6237\u4e0d\u5b58\u5728\uff01","data":null}
2
3
4
5
6
7
8
9
常见的响应头部参数:
Content-Encoding:文档的编码(Encode)方法。Content-Length:表示内容长度。Content-Type:表示后面的文档属于什么MIME类型。Set-Cookie:设置和页面关联的Cookie。ETag:被请求变量的实体值。Cache-Control:指定所有缓存机制在整个请求/响应链中必须服从的指令。
# 4.4 报文空行设计
为什么有了 Content-Length 字段防止粘包还需要空行呢?
个人理解是,如果报文的数据部分为空(HTTP/HTTPS GET 方法的数据部分就为空)那么头部中的 Content-Length 字段将忽略不写,这样接收方将无法通过这个字段读取指定长度数据,也就无法避免粘包现象。通过预留空行的方式可以解决这个特殊情况!
HTTP 协议并没有规定报头部分的键值对有多少个,使用空行就相当于是报文的结束标记或报文和正文之间的分隔符(连续2个\r\n代表报头结束)。
HTTP 在传输层依赖 TCP 协议,TCP 是面向字节流的。如果没有这个空行,就会出现"粘包问题"。
# 4.5 看一个完整案例
请求接口:GET https://www.wanandroid.com/friend/json
General:
Request URL: https://www.wanandroid.com/friend/json
Request Method: GET
Status Code: 200 OK
Response Header:
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Request Header:
Accept: text/html,application/json;q=0.9
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Connection: keep-alive
Host: www.wanandroid.com
User-Agent: Mozilla/5.0 ...
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
回到踩坑⑤:注意上面的 Accept: text/html,application/json;q=0.9——客户端告诉服务器"我优先要 HTML(q=1.0 默认),其次 JSON(q=0.9)"。如果服务器返回了 XML 而不是 JSON,说明服务器没有根据 Accept 头做内容协商。
# 05.HTTP一些场景
# 5.1 如何统计消息长度
Keep-Alive方式下如何判断消息内容/长度的大小
两种判断方法:
- 使用消息首部字段Content-Length:表示实体内容长度,客户端(服务器)可以根据这个值来判断数据是否接收完成。
- 使用消息首部字段Transfer-Encoding:如果是动态页面等,服务器不可能预先知道内容大小,这时就可以使用Transfer-Encoding:chunk模式来传输数据了。
使用消息首部字段Transfer-Encoding统计长度:
- chunk编码将数据分成一块一块的发生。
- Chunked编码将使用若干个Chunk串连而成,由一个标明长度为0的chunk标示结束。
- 每个Chunk分为头部和正文两部分,头部内容指定正文的字符总数(十六进制的数字)。
# 5.2 判断传输是否完成
在保持持久连接的情况下,依赖 Content-Length 来确定数据发送完毕。
- Content-Length 在这里起到了一个响应实体已经发送结束的判断依据。这样的情况下,我们就要求 Content-Length 必须和内容实体的长度一致,如果不一致,就会出现各种问题。
- 如果 Content-Length 小于内容实体的长度,则会截断,反之则无法判定当前响应已经结束,会将请求持续挂起造成 Padding 状态。
在响应一个请求的时候,就需要知道它的内容实体的大小。但是在实际应用中,有些时候内容实体的长度并没有那么容易获得。
此时就需要一个新的机制,不依赖 Content-Length 的值,来判定当前内容实体是否传输完成,此时就需要 Transfer-Encoding 这个头部来判定。
# 5.3 HTTP状态码设计
服务器返回的响应报文中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。
| 状态码 | 类别 | 原因短语 |
|---|---|---|
| 1XX | Informational(信息性状态码) | 接收的请求正在处理 |
| 2XX | Success(成功状态码) | 请求正常处理完毕 |
| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
# 06.HTTP各个版本
# 6.1 Http1.0版本
1993 年,NCSA 开发出了 Mosaic,是第一个可以图文混排的浏览器,随后又在 1995 年开发出了服务器软件 Apache。
HTTP/1.0 版本在 1996 年正式发布。它在多方面增强了 0.9 版:
- 增加了 HEAD、POST 等新方法;
- 增加了响应状态码,标记可能的错误原因;
- 引入了协议版本号概念;
- 引入了 HTTP Header(头部)的概念;
- 传输的数据不再仅限于文本。
# 6.2 HTTP1.1版本
HTTP/1.1 是对 HTTP/1.0 的小幅度修正。但一个重要的区别是:它是一个"正式的标准"。
HTTP/1.1 主要的变更点有:
- 增加了 PUT、DELETE 等新的方法;
- 增加了缓存管理和控制;
- 明确了连接管理,允许持久连接;
- 允许响应数据分块(chunked),利于传输大文件;
- 强制要求 Host 头,让互联网主机托管成为可能。
# 6.3 Http2.0版本
Google 首先开发了自己的浏览器 Chrome,然后推出了新的 SPDY 协议,从实际的用户方来"倒逼"HTTP 协议的变革。
HTTP/2 的主要特点:
- 二进制协议,不再是纯文本;
- 可发起多个请求,废弃了 1.1 里的管道;
- 使用专用算法压缩头部,减少数据传输量;
- 允许服务器主动向客户端推送数据;
- 增强了安全性,"事实上"要求加密通信。
Http2.0相对于Http1.x来说提升是巨大的:
- 二进制格式:以帧为基本单位,一帧中除了包含数据外同时还包含该帧的标识:StreamIdentifier。
- 多路复用:多个请求共用一个TCP连接,多个请求可以同时在这个TCP连接上并发。
- header头部压缩:通过压缩header来减少请求的大小,减少流量消耗,提高效率。
虽然 HTTP/2 到今天已经多年,也衍生出了 gRPC 等新协议,但由于 HTTP/1.1 实在是太过经典和强势,目前它的普及率还比较低,大多数网站使用的仍然还是 HTTP/1.1。
# 07.HTTP方法的设计哲学
# 7.1 安全性与幂等性
HTTP方法的设计遵循了两个重要的语义属性:安全性(Safe)和幂等性(Idempotent)。
安全性:一个方法是安全的,意味着它不会修改服务器上的资源状态。客户端可以放心地调用安全方法,不用担心产生副作用。
幂等性:一个方法是幂等的,意味着执行一次和执行多次的效果相同。这对于网络重试非常重要——如果请求超时,客户端可以安全地重发幂等请求。
| 方法 | 安全 | 幂等 | 说明 |
|---|---|---|---|
| GET | 是 | 是 | 获取资源,不修改服务器状态 |
| HEAD | 是 | 是 | 与GET相同,但不返回响应体 |
| OPTIONS | 是 | 是 | 查询服务器支持的方法 |
| PUT | 否 | 是 | 替换整个资源(覆盖式更新) |
| DELETE | 否 | 是 | 删除资源(删一次和删多次效果一样) |
| POST | 否 | 否 | 创建资源(每次调用可能创建新资源) |
| PATCH | 否 | 否 | 局部更新资源 |
疑惑:为什么幂等性如此重要?
答疑:在分布式系统中,网络超时是常态。如果客户端发送了一个请求但没有收到响应,它无法判断服务器是否已经处理了请求。对于幂等方法,客户端可以安全重试。
非幂等操作的问题(POST创建订单)← 对应踩坑④:
客户端发送 POST /orders → 服务器创建订单#1001 → 响应丢失
客户端重发 POST /orders → 服务器又创建订单#1002
→ 结果:用户被重复扣款
解决方案:
1. 客户端生成唯一请求ID(幂等键)
2. 服务端根据请求ID去重
POST /orders Header: Idempotency-Key: abc-123
服务端检查abc-123是否已处理过,如果是则直接返回之前的结果
2
3
4
5
6
7
8
9
10
# 7.2 RESTful设计思想
REST(Representational State Transfer,表述性状态转移)是一种基于HTTP协议的API设计风格,由Roy Fielding在2000年的博士论文中提出。
REST的核心思想是:将服务器上的一切看作资源(Resource),每个资源有唯一的URI标识,通过HTTP方法(GET/POST/PUT/DELETE)对资源进行操作。
RESTful API设计示例(以新闻系统为例):
获取所有新闻列表: GET /api/news
获取单条新闻: GET /api/news/123
创建新闻: POST /api/news
更新新闻(全量): PUT /api/news/123
更新新闻(局部): PATCH /api/news/123
删除新闻: DELETE /api/news/123
获取新闻的评论: GET /api/news/123/comments
给新闻添加评论: POST /api/news/123/comments
2
3
4
5
6
7
8
9
10
REST的六大设计约束:
- 客户端-服务器分离:关注点分离,前后端可独立演进
- 无状态:每个请求包含所有必要信息,服务器不保存客户端状态
- 可缓存:响应明确标记是否可缓存,减少不必要的交互
- 统一接口:使用标准HTTP方法和状态码,接口一致可预测
- 分层系统:客户端无需知道是直连服务器还是经过中间代理
- 按需代码(可选):服务器可以返回可执行代码(如JavaScript)
# 7.3 方法的实际应用
在实际开发中,HTTP方法的使用往往需要权衡规范和实践:
理想vs现实:
理想的RESTful:
DELETE /api/users/123 → 删除用户
PUT /api/users/123 → 更新用户
现实中常见的做法:
POST /api/users/123/delete → 删除用户(很多前端框架方便处理)
POST /api/users/123/update → 更新用户
原因:
1. 某些防火墙/代理会拦截PUT/DELETE方法
2. HTML表单只支持GET和POST
3. 团队习惯和技术栈限制
2
3
4
5
6
7
8
9
10
11
12
13
14
HTTP状态码与方法的配合设计:
| 场景 | HTTP方法 | 成功状态码 | 含义 |
|---|---|---|---|
| 获取资源 | GET | 200 OK | 返回资源内容 |
| 创建资源 | POST | 201 Created | 资源创建成功 |
| 更新资源 | PUT/PATCH | 200 OK | 更新成功 |
| 删除资源 | DELETE | 204 No Content | 删除成功,无返回内容 |
| 资源未找到 | GET | 404 Not Found | 资源不存在 |
| 参数错误 | POST | 400 Bad Request | 请求格式错误 |
| 未授权 | 任意 | 401 Unauthorized | 需要登录 |
| 无权限 | 任意 | 403 Forbidden | 已登录但无权限 |
# 08.HTTP头部的设计艺术
# 8.1 内容协商机制
HTTP的内容协商是一种客户端和服务器协商最佳资源表现形式的机制。客户端通过请求头告诉服务器自己的偏好,服务器选择最合适的版本返回。
内容协商的三个维度:
1. 内容类型协商(MIME Type)
Accept: text/html, application/json;q=0.9, */*;q=0.1
→ 优先HTML,其次JSON,最后接受任何格式
q值表示优先级权重(0~1,默认1.0)
2. 语言协商
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
→ 优先简体中文,其次中文,最后英文
3. 编码协商
Accept-Encoding: gzip, deflate, br
→ 支持gzip、deflate、brotli压缩
2
3
4
5
6
7
8
9
10
11
12
13
14
回到踩坑⑤:小张的后端同时配置了 JSON 和 XML 序列化器,但没有根据 Accept 头做优先级选择。解决方式:Spring Boot 中配置 spring.mvc.contentnegotiation.favor-parameter=false,并确保 Accept 头的优先级正确处理。
# 8.2 条件请求设计
条件请求是HTTP缓存机制的核心,通过条件头部实现"只在满足条件时才执行请求"。
条件请求的两种方式:
1. 基于时间(Last-Modified / If-Modified-Since)
首次请求:
服务器返回 Last-Modified: Wed, 10 Apr 2026 08:00:00 GMT
后续请求:
客户端发送 If-Modified-Since: Wed, 10 Apr 2026 08:00:00 GMT
服务器判断:
资源未修改 → 304 Not Modified(不传输内容,节省带宽)
资源已修改 → 200 OK + 新内容
2. 基于内容指纹(ETag / If-None-Match)
首次请求:
服务器返回 ETag: "abc123"
后续请求:
客户端发送 If-None-Match: "abc123"
服务器判断:
ETag匹配 → 304 Not Modified
ETag不匹配 → 200 OK + 新内容
ETag比Last-Modified更精确:
- Last-Modified精度只到秒(1秒内多次修改无法区分)
- ETag基于内容计算,任何修改都会改变
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
回到踩坑③:用户上传新头像后,服务器没有更新 ETag,也没有设置 Cache-Control: max-age=0。浏览器拿着旧的缓存(甚至有旧的 ETag),一直显示旧头像。解决:每次头像变更时生成新的 ETag,或在响应中设置 Cache-Control: no-cache(需要验证才能使用缓存)。
# 8.3 跨域资源共享
**CORS(Cross-Origin Resource Sharing)**是现代Web开发中不可避免的话题。
疑惑:为什么浏览器要限制跨域请求?
答疑:这是浏览器的同源策略(Same-Origin Policy),是Web安全的基石。如果没有同源限制,恶意网站可以通过JavaScript读取你在银行网站上的数据。
同源的定义:协议 + 域名 + 端口 完全相同
https://www.example.com:443/path
│ │ │
协议 域名 端口
以下均为跨域:
https://www.example.com → http://www.example.com (协议不同)
https://www.example.com → https://api.example.com (域名不同)
https://www.example.com → https://www.example.com:8080 (端口不同)
2
3
4
5
6
7
8
9
10
CORS的工作机制:
简单请求(GET/POST,常规Content-Type):
浏览器直接发送请求,附带 Origin 头
服务器返回 Access-Control-Allow-Origin 头
浏览器检查是否允许 → 允许则正常处理,否则报错
预检请求(非简单请求,如DELETE/PUT/自定义头)← 对应踩坑②:
1. 浏览器先发OPTIONS预检请求
Origin: https://front.example.com
Access-Control-Request-Method: DELETE
2. 服务器返回允许的方法和头部
Access-Control-Allow-Origin: https://front.example.com
Access-Control-Allow-Methods: DELETE, PUT
Access-Control-Max-Age: 86400(预检结果缓存24小时)
3. 预检通过后,浏览器发送实际请求
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
回到踩坑②:小张的前端发 DELETE /api/posts/123 带自定义头 Authorization,触发了 OPTIONS 预检。后端没有处理 OPTIONS 请求。解决:Spring Boot 添加 @CrossOrigin 或配置全局 CORS Filter。
HTTP协议的设计体现了"简单的事情简单做,复杂的事情可以做"的哲学。从最基本的请求-响应模型,到缓存、内容协商、安全控制等高级特性,HTTP通过头部字段的扩展机制,在保持协议简洁性的同时,不断适应Web的发展需求。
# 09.综合案例:从0到1设计RESTful API
前面我们分别讲了 URL 设计、HTTP 方法哲学、状态码、头部协商、缓存和 CORS。但这些知识点如果是孤立地看,很难形成"设计一个好的 HTTP API"的系统能力。
本章用一个贯穿全文的实战案例——从一个"能跑就行"的博客 API 开始,经历四次重构,最终成为一个生产级的 RESTful API。读完这一节,你应该能形成"设计一个 HTTP API 时知道该关注哪些维度和为什么"的能力。
# 9.1 案例背景与目标
假设我们要为博客平台设计一套 API,支持文章、评论、用户三大模块。目标:
- 前端和移动端共用同一套 API
- 支持缓存(减少重复请求)
- 支持跨域(前端独立部署)
- 支持版本管理(API升级不破坏老客户端)
我们经历四次设计进化:
| 版本 | 设计风格 | 问题 | 对应踩坑 |
|---|---|---|---|
| V1 | 随意风格 | URL 命名混乱、方法滥用 | ① |
| V2 | RESTful化 | 规范化但缺高级特性 | ④ |
| V3 | 高级特性 | 支持缓存+CORS+内容协商 | ②③⑤ |
| V4 | 生产级 | 版本管理+幂等+安全 | 全部解决 |
# 9.2 第一版:随意风格——能跑就行
V1 API 设计(典型的历史遗留风格):
获取文章列表:POST /api/getPostList
获取文章详情:POST /api/getPostDetail?id=123
创建文章: POST /api/createPost
更新文章: POST /api/updatePost
删除文章: POST /api/deletePost?id=123
获取评论: POST /api/fetchComments?postId=123
添加评论: POST /api/addComment
问题诊断(对应踩坑①④):
❌ 所有操作都用 POST ——丢失了HTTP方法的语义
→ 获取操作应该用GET(安全+幂等)
→ 删除操作应该用DELETE(幂等)
→ 更新操作应该用PUT/PATCH
❌ URL设计不一致 —— getPostList vs fetchComments vs createPost
→ 没有统一的命名规范
❌ 查询参数位置混乱 —— 有的在URL路径,有的在查询字符串
→ 资源ID应该在URL路径中:/api/posts/123
❌ 没有状态码的合理使用 —— 不管成功失败都返回200
→ 创建成功应返回201 Created
→ 资源不存在应返回404 Not Found
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 9.3 第二版:RESTful化——规范化
引入 RESTful 设计原则:
V2 API 设计(RESTful风格):
文章模块:
GET /api/posts → 获取文章列表
GET /api/posts/123 → 获取文章详情
POST /api/posts → 创建文章
PUT /api/posts/123 → 全量更新文章
PATCH /api/posts/123 → 局部更新文章
DELETE /api/posts/123 → 删除文章
评论模块(嵌套资源):
GET /api/posts/123/comments → 获取文章评论
POST /api/posts/123/comments → 创建评论
DELETE /api/posts/123/comments/45 → 删除评论
用户模块:
GET /api/users/me → 获取当前用户信息
PUT /api/users/me → 更新当前用户
GET /api/users/me/posts → 获取当前用户的文章
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
关键设计决策:
✅ 资源导向:把一切看作资源(posts、comments、users)
✅ URL用名词复数:/api/posts 而不是 /api/getPostList
✅ 嵌套表达关系:/api/posts/123/comments 清晰表达从属关系
✅ HTTP方法承载语义:GET获取、POST创建、PUT全量更新、PATCH局部更新、DELETE删除
✅ 统一的状态码使用:
200 OK — 获取/更新成功
201 Created — 创建成功
204 No Content — 删除成功
400 Bad Request— 参数错误
404 Not Found — 资源不存在
残留问题:
❌ 没有处理POST幂等性(踩坑④)
❌ 没有缓存策略(踩坑③)
❌ 没有处理CORS(踩坑②)
❌ 没有内容协商(踩坑⑤)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 9.4 第三版:高级特性——缓存+CORS+内容协商
V3 API 设计(加上 HTTP 高级特性):
1. 缓存策略(解决踩坑③):
请求:GET /api/posts/123
响应头:
HTTP/1.1 200 OK
ETag: "d41d8cd98f00b204e9800998ecf8427e" ← 内容指纹
Last-Modified: Mon, 10 Jun 2026 08:00:00 GMT ← 最后修改时间
Cache-Control: private, max-age=60 ← 私有缓存60秒
Vary: Accept, Authorization ← 告诉缓存服务器按这些头区分版本
设计说明:
- ETag = MD5(响应体) → 内容变了ETag就变 → 客户端带 If-None-Match 验证
- Cache-Control: max-age=60 → 浏览器60秒内直接用缓存,不发请求
- Vary: Accept → 不同Accept头的请求视为不同资源的缓存
2. CORS配置(解决踩坑②):
响应头:
Access-Control-Allow-Origin: https://front.blog.com
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400
Access-Control-Allow-Credentials: true
OPTIONS 预检处理:
后端对所有路径统一处理 OPTIONS 请求,返回上述CORS头
3. 内容协商(解决踩坑⑤):
客户端发送:
Accept: application/json, application/xml;q=0.9
服务器检查 Accept 头中的优先级:
→ application/json (q=1.0) 优先 → 返回JSON
→ 如果JSON不可用 → 尝试XML (q=0.9)
→ 都不支持 → 返回 406 Not Acceptable
响应头:
Content-Type: application/json; charset=utf-8
Vary: Accept ← 告诉缓存:不同Accept的响应应分开缓存
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 9.5 第四版:生产级设计——版本管理+幂等+安全
V4 API 设计(生产级标准):
1. 版本管理:
GET /api/v1/posts ← URL版本号(直观,推荐)
或
GET /api/posts
Header: API-Version: 1 ← Header版本号(更RESTful)
2. 幂等性处理(解决踩坑④):
创建文章时的幂等键设计:
// 客户端生成唯一请求ID
POST /api/v1/posts
Header: Idempotency-Key: req-abc-123-xyz
Body: {"title": "Hello", "content": "..."}
// 服务端处理逻辑
1. 收到请求 → 查Redis: idempotency:req-abc-123-xyz
2. 命中 → 返回之前的结果(不重复创建)
3. 未命中 → 执行业务逻辑 → 将结果存入Redis(TTL=24h)
3. 标准错误响应体:
无论什么错误,统一返回格式:
{
"error": {
"code": "DUPLICATE_ORDER",
"message": "重复提交,请勿重复操作",
"details": [
{"field": "orderId", "reason": "order already exists"}
]
}
}
配合正确的HTTP状态码:
400 Bad Request → 客户端参数错误
401 Unauthorized → 未认证
403 Forbidden → 无权限
404 Not Found → 资源不存在
409 Conflict → 资源冲突(如重复创建)
422 Unprocessable Entity → 语义正确但业务校验不通过
429 Too Many Requests → 触发限流
4. 分页与过滤标准化:
GET /api/v1/posts?page=1&per_page=20&sort=-created_at&filter[status]=published
响应:
{
"data": [...],
"meta": {
"current_page": 1,
"per_page": 20,
"total_pages": 50,
"total_count": 1000
},
"links": {
"self": "/api/v1/posts?page=1",
"next": "/api/v1/posts?page=2",
"last": "/api/v1/posts?page=50"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# 9.6 四种方案横向对比
| 维度 | V1 随意风格 | V2 RESTful | V3 +高级特性 | V4 生产级 |
|---|---|---|---|---|
| URL命名 | 混乱(动词+名词混杂) | 统一(名词复数+层级) | 同V2 | 加版本前缀/v1 |
| HTTP方法 | 全部POST | GET/POST/PUT/DELETE | 同V2 | 同V2 |
| 状态码 | 全部200 | 2xx/4xx区分 | 同V2 | 细粒度的错误码 |
| 幂等性 | ❌ 无 | ❌ 无 | ❌ 无 | ✅ Idempotency-Key |
| 缓存 | ❌ | ❌ | ✅ ETag+Cache-Control | ✅ 同V3 |
| CORS | ❌ | ❌ | ✅ 全面处理 | ✅ |
| 内容协商 | ❌ | ❌ | ✅ Accept/Content-Type | ✅ |
| 版本管理 | ❌ | ❌ | ❌ | ✅ /v1前缀 |
| 分页规范 | 随意 | 基本 | 基本 | ✅ 元数据+链接 |
| 前后端分离 | ❌ | ○ | ✅ | ✅ |
| 对应踩坑 | ① | ④ | ②③⑤ | 全部解决 |
| 对应章节 | — | 7.2/7.3 | 8.1/8.2/8.3 | 7.1/8.1 |
# 9.7 案例升华:HTTP设计的本质哲学
经历了四次进化,回头看 HTTP 的设计哲学就豁然开朗了:
HTTP 协议设计的四个核心哲学,贯穿整个 V1→V4 的进化:
1. 统一接口(Uniform Interface)
→ V1的混乱URL → V2的RESTful命名 → V4的版本化资源
→ GET永远是获取,DELETE永远是删除
→ 接口的可预测性 > 灵活性
2. 无状态(Stateless)
→ 每个请求自包含所有信息(会话通过Cookie/JWT携带)
→ 服务器不需要"记住"任何客户端 → 天然支持水平扩展
→ 这是RESTful和传统Session方案最本质的区别
3. 分层协商(Layered Negotiation)
→ 内容协商:Accept/Content-Type → 客户端和服务器协商数据格式
→ 缓存协商:ETag/If-None-Match → 客户端和服务器协商是否需要传输
→ 跨域协商:OPTIONS预检 → 浏览器和服务器协商是否允许跨域
4. 自描述消息(Self-Descriptive Messages)
→ 请求自描述:方法(做什么) + URL(对谁做) + 头部(怎么做) + 体(数据)
→ 响应自描述:状态码(结果) + 头部(缓存/类型) + 体(数据)
→ 中间代理(Nginx、CDN)只看头部就能做正确决策
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
一个好的HTTP API的评价标准:
☑ 资源命名是否一致? /api/posts/123/comments ✓
☑ HTTP方法是否正确? GET(获取) DELETE(删除) ✓
☑ 状态码是否准确? 201 Created ≠ 200 OK ✓
☑ 是否支持缓存? ETag + Cache-Control ✓
☑ 是否处理跨域? Access-Control-* ✓
☑ 是否支持内容协商? Accept → Content-Type ✓
☑ 非幂等操作去重? Idempotency-Key ✓
☑ 错误信息是否可读? {"error": {"code":...}} ✓
☑ 是否版本化? /api/v1/... ✓
2
3
4
5
6
7
8
9
10
11
# 9.8 全文知识图谱回顾
走到这里,我们用"RESTful API 的四次进化"把全文核心串完了:
小张的五类踩坑
│
┌───────┬───────┼───────┬───────┐
│ │ │ │ │
①乱码 ②预检 ③缓存 ④重复 ⑤格式
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
URL编码 CORS 条件请求 幂等性 内容协商
Percent OPTIONS ETag Idempotent Accept
[2.7] [8.3] [8.2] [7.1] [8.1]
│ │ │ │ │
└───────┴───────┼───────┴───────┘
│
┌───────────┴───────────┐
│ │
HTTP报文结构 [4章] HTTP版本演进 [6章]
请求行/头/体/空行 0.9→1.0→1.1→2.0
│ │
└───────────┬───────────┘
│
V1→V2→V3→V4 RESTful API的四次进化
[第9章] 从随意到生产级
│
▼
统一接口 / 无状态 / 分层协商 / 自描述
HTTP协议的四大设计哲学 [9.7节]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
最终的方法论沉淀——设计一个 HTTP API 时,都应该问自己三个问题:
- 资源怎么命名?(URL是名词复数还是动词?层级怎么表达?→ 可读性+一致性)
- 动作怎么表达?(GET/POST/PUT/DELETE 用对了吗?幂等性考虑了吗?→ 语义正确性)
- 元信息怎么传递?(缓存头、CORS头、Accept 头设置了吗?→ 性能和兼容性)
把这三个问题问到位,你就从"能写接口"进化到了"懂 HTTP 协议设计的工程师"。
# 10.思考题与作业
# 10.1 基础思考题
URI vs URL:用生活中的例子举例说明 URI、URL、URN 三者的区别和关系。为什么说"URL 是 URI 的子集"?
HTTP的无状态设计:如果 HTTP 从诞生之初就设计为有状态(服务器记住每个客户端的状态),今天前后端分离的架构还能实现吗?为什么 RESTful API 把"无状态"作为核心约束?
幂等性的判断:以下操作哪些是幂等的?① 用户注册(POST)② 修改密码(PUT)③ 增加积分(POST)④ 设置用户生日(PUT)⑤ 给文章点赞(POST)。说出你的判断依据。
状态码 301 vs 302:永久重定向和临时重定向有什么区别?为什么搜索引擎对 301 和 302 的对待不同?在什么场景下用 301,什么场景下用 302?
# 10.2 进阶思考题
踩坑④的深度分析:分布式系统中保证幂等性,除了客户端生成 Idempotency-Key,还有哪些方案?如果客户端没有生成幂等键,服务器端如何防止重复提交?(提示:业务唯一键、数据库唯一约束、分布式锁)
CORS的安全边界:CORS 的
Access-Control-Allow-Origin: *和Access-Control-Allow-Credentials: true为什么不能同时使用?如果可以同时使用,会有什么安全风险?请构造一个攻击场景。API版本管理策略:URL版本号 vs Header版本号 vs Query参数版本号,三种方案各有什么优缺点?Twitter、Google、GitHub 分别用了哪种?为什么大厂倾向于用 URL 版本号?
HTTP/2 对RESTful的影响:HTTP/2 引入了多路复用和服务器推送。这对 RESTful API 设计有什么影响?REST 的"一个资源一个请求"模式在 HTTP/2 下还是最优的吗?
# 10.3 动手作业
作业一(必做):用 curl 体验 HTTP 协议细节。
# 1. 观察完整的请求-响应报文
curl -v https://api.github.com/users/octocat
# 2. 测试内容协商
curl -H "Accept: application/json" https://api.example.com/resource
curl -H "Accept: application/xml" https://api.example.com/resource
# 3. 测试条件请求(第二次请求应该返回304)
curl -v https://www.baidu.com > /dev/null
# 从第一次响应中获取 ETag,第二次带 If-None-Match
curl -v -H "If-None-Match: \"xxx\"" https://www.baidu.com
2
3
4
5
6
7
8
9
10
11
- 标注出请求行、请求头、状态行、响应头在
-v输出中的位置。 - 对比不同
Accept头下返回的Content-Type是否变化。
作业二(选做):重构一个 API。
- 找一个你参与过的项目的 API(或者选择 GitHub/微博的公开 API)。
- 评估它当前的 RESTful 成熟度(随意→RESTful→高级→生产级)。
- 写出重构方案,包括:URL 重命名、HTTP 方法修正、状态码优化、添加缓存头和 CORS 配置。
- 标注每个改动对应本章的哪个章节的知识点。
作业三(架构思考):设计一个新API。
- 设计一个简单的"图书馆"API,支持图书的增删改查、借阅、归还。
- 画出所有端点、HTTP方法、请求/响应格式。
- 标注分页、缓存、错误处理、幂等性设计的实现方案。