编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • 计算机原理

  • 网络协议

    • README
    • 通过看新闻熟悉网络
    • 通过购物熟悉加密
    • 从0到1部书电商网站
    • 请求网络的通用流程
    • 网络编程模型的概念
    • 传输协议TCP和UDP
    • Socket的发展和设计
    • 传输数据的设计思想
    • 网络域名解析的流程
    • HTTP服务设计流程
    • 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 动手作业
    • HTTPS协议设计策略
    • HTTP连接和跳转
    • HTTP代理和缓存设计
    • 如何去排查网络故障
    • WebSocket实时通信
    • HTTP3与QUIC协议
  • 操作系统

  • 数据库原理

  • 计算机
  • 网络协议
杨充
2021-09-21
目录

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}
1
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接口、文件等。要访问网络资源,需要解决两个核心问题:

  1. 标识问题:如何唯一地标识一个资源?答案是 URI(统一资源标识符)
  2. 定位问题:如何找到资源的具体位置?答案是 URL(统一资源定位符)

两者的关系可以用一个生活中的例子来理解:

URI —— 相当于一个人的身份证号码,能唯一标识一个人
URL —— 相当于一个人的家庭住址,能定位到这个人

身份证号(URI):110101199001011234
家庭住址(URL):北京市东城区XX路XX号XX室

URL 是 URI 的子集,每个 URL 都是 URI,但 URI 不一定是 URL
1
2
3
4
5
6
7

访问网络资源的基本流程:

  1. 用户输入 URL(如 https://www.example.com/index.html)
  2. 浏览器解析 URL,提取协议、主机名、端口、路径等信息
  3. 通过 DNS 将主机名解析为 IP 地址
  4. 建立 TCP 连接(HTTPS 还需 TLS 握手)
  5. 发送 HTTP 请求,携带路径和参数
  6. 服务器返回对应资源的响应数据

# 2.2 什么是URI

URI全称是Uniform Resource Identifier,也就是统一资源标识符,它是一种采用特定的语法标识一个资源的字符串表示。

URI所标识的资源可能是服务器上的一个文件,也可能是一个邮件地址、图书、主机名等。简单记为:URI是标识一个资源的字符串(这里先不必纠结标识的目标资源到底是什么,因为使用者一般不会见到资源的实体),从服务器接收到的只是资源的一种字节表示(二进制序列,从网络流中读取)。

通用URI的格式如下:

scheme:[//[user[:password]@]host[:port]][/path][?query][#fragment]
1

URI一般由三部组成:

  1. ①访问资源的命名机制
  2. ②存放资源的主机名
  3. ③资源自身的名称,由路径表示,着重强调于资源。

# 2.3 为何要设计URL

URL全称是Uniform Resource Location,也就是统一资源位置。

  1. 实际上,URL就是一种特殊的URI,它除了标识一个资源,还会为资源提供一个特定的网络位置,客户端可以通过它来获取URL对应的资源。
  2. URL所表示的网络资源位置通常包括用于访问服务器的协议(如http、ftp等)、服务器的主机名或者IP地址、以及资源文件在该服务器上的路径。
  3. 典型的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 //锚点定位
1
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的设计思想:

  1. 统一性:提供一种统一的标识和定位机制,不同类型的资源都可以通过唯一的标识符进行访问。
  2. 唯一性:确保每个资源都有一个唯一的标识符,避免冲突和混淆。
  3. 可读性:使用人类可理解的字符串来表示资源的标识和定位。
  4. 层次性:URL支持层次结构,按照层次结构的方式组织各个部分。
  5. 可扩展性:允许通过添加额外的参数、查询字符串或片段标识符来扩展访问方式。

# 2.6 HTTP之URL

HTTP使用统一资源标识符(URI)来传输数据和建立连接。URL是一种特殊类型的URI,包含了用于查找某个资源的足够的信息。

以 http://www.yc.cn:8080/news/index.php?boardID=32&ID=24618&page=1#name 为例:

  1. 协议部分:http:
  2. 域名部分:www.yc.cn
  3. 端口部分:8080(跟在域名后面,使用":"分隔)
  4. 虚拟目录部分:/news/
  5. 文件名部分:index.php
  6. 锚部分:#name
  7. 参数部分: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
1
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)发表了一篇论文,提出了在互联网上构建超链接文档系统的构想。这篇论文中他确立了三项关键技术。

  1. URI:即统一资源标识符,作为互联网上资源的唯一身份;
  2. HTML:即超文本标记语言,描述超文本文档;
  3. HTTP:即超文本传输协议,用来传输超文本。

这三项技术在如今的我们看来已经是稀松平常,但在当时却是了不得的大发明。基于它们,就可以把超文本系统完美地运行在互联网上,让各地的人们能够自由地共享信息,蒂姆把这个系统称为"万维网"(World Wide Web),也就是我们现在所熟知的 Web。

# 3.1 HTTP协议介绍

HTTP协议又叫做超文本传输协议,是一种无状态,无连接,以请求-响应方式运行的协议。

也可以理解为:HTTP 是一个在计算机世界里专门在「两点」之间「传输」文字、图片、音频、视频等「超文本」数据的「约定和规范」。

# 3.2 在OSI中设计

HTTP协议属于应用层协议,在传输层使用TCP协议,在网络层使用IP协议,在数据链路层使用MAC地址。

  1. TCP协议主要解决如何在IP层之上可靠的传递数据包。
  2. IP协议主要解决网络路由和寻址问题。
  3. MAC地址主要解决相邻两台主机之间的寻址传输问题。

# 3.3 什么叫超文本

HTTP 传输的内容是「超文本」。超文本,它是文字、图片、视频等的混合体,最关键有超链接,能从一个超文本跳转到另外一个超文本。

HTML 就是最常见的超文本了,它本身只是纯文字文件,但内部用很多标签定义了图片、视频等的链接,再经过浏览器的解释,呈现给我们的就是一个文字、有画面的网页了。

# 3.4 什么叫做无状态

出现HTTP无状态的场景说明

无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即我们给服务器发送 HTTP 请求之后,服务器根据请求,会给我们发送想要的数据过来,但是发送完,不会记录任何信息,也就是说协议对于发送过的请求或响应都不做持久化处理。

HTTP的无状态是指会话的无状态,会话是由一组请求和响应组成的,围绕一件相关事情的请求和响应,这些不同的请求和响应之间是需要进行数据传递的,但HTTP是无状态指的是本次请求和响应,与下一次的请求和响应是没有关系的,不会发生数据传递。

HTTP 协议无状态特性的优点和缺点:

  1. 优点在于解放了服务器,每一次请求"点到为止"不会造成不必要连接占用
  2. 缺点在于每次请求会传输大量重复的内容信息

随着 Web 的不断发展,因无状态而导致业务处理变得棘手的情况增多:

  1. 比如客户在购物平台购物场景下,登录、下单、购物,需要登录后才能进行处理,单独靠HTTP是无法完成的。
  2. 发起登录的HTTP请求成功登录后,不跟踪客户的登录状态,不对后续购物请求进行用户登录数据信息传递,无法满足业务逻辑。

Cookie解决无状态困境:

  1. 每个请求之间都是独立的,对于之前的请求事务没有记忆的能力。所以就出现了像Cookie这种,用来保存一些状态的东西。
  2. Cookie就相当于一个通行证,第一次访问的时候给客户端发送一个Cookie,当客户端再次来的时候,拿着Cookie(通行证),那么服务器就知道这个是"老用户"。

# 3.5 什么叫做无连接

为什么HTTP要设计无连接,它的意图是什么?

无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,就断开连接。采用这种方式可以节省传输时间。这样主要是为了缓解服务器的压力,减小连接对服务器资源的占用。

HTTP设计无连接后期遇到的挑战:随着网页变得复杂,里面嵌入了很多图片,每次访问图片都需要建立一次 TCP 连接就显得很低效。

如何解决HTTP无连接遇到的效率低问题:Keep-Alive 被提出,目前的 HTTP/1.1 默认开启 Keep-Alive,通过在响应头中包含 Connection: keep-alive 字段来保持连接。

# 3.6 HTTP协议特点

HTTP 协议具有以下特点:

  1. 简单易用:设计简单明了,易于理解和使用。
  2. 无连接:每个请求-响应对都是独立的。
  3. 无状态:服务器不会记住之前的请求和响应——这恰好是RESTful API的核心约束之一。
  4. 支持灵活的数据格式:最常见的是HTML、XML和JSON。
  5. 可扩展性:通过使用头部字段和方法,可以定义和传递各种自定义的元数据和操作。
  6. 基于请求-响应模型:客户端发送请求,服务器返回响应。
  7. 支持缓存:可以在客户端或代理服务器上缓存响应,以减少重复请求和提高性能。

# 04.HTTP请求与响应

# 4.1 理解请求和响应

什么是Http报文?它是HTTP应用程序之间发送的数据块。这些数据块以一些文本形式的元信息开头,这些信息描述了报文的内容及含义,后面跟着可选的数据部分。

HTTP报文是面向文本的,报文中的每一个字段都是一些ASCII码串,各个字段的长度是不确定的。

HTTP有两类报文:请求报文(客户端)和响应报文(服务端)。

HTTP报文的流动方向:一次HTTP请求,HTTP报文会从"客户端"流到"代理"再流到"服务器",在服务器工作完成之后,报文又会从"服务器"流到"代理"再流到"客户端"。

# 4.2 请求包的设计

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行、请求头部、空行和请求数据四个部分组成。

请求行: <method> <request-URL> <version>
头部:   <headers>
主体:   <entity-body>
1
2
3
  1. 请求行:所有的HTTP请求报文都以一个请求行作为开始,该行包含了一个方法和一个请求的URL,还包含HTTP的版本。
  2. 请求头部:用于说明是谁或什么在发送请求、请求源自何处,或者客户端的喜好及能力。
  3. 请求主体:可以添加任意的其他数据。

例子:

//下面是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
1
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>
1
2
3
  1. 状态行:包含HTTP版本、数字状态码、原因短语。
  2. 响应头部:为客户端提供额外信息。
  3. 响应主体:服务器返回给客户端的文本信息。

例子:

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}
1
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 ...
1
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方式下如何判断消息内容/长度的大小

两种判断方法:

  1. 使用消息首部字段Content-Length:表示实体内容长度,客户端(服务器)可以根据这个值来判断数据是否接收完成。
  2. 使用消息首部字段Transfer-Encoding:如果是动态页面等,服务器不可能预先知道内容大小,这时就可以使用Transfer-Encoding:chunk模式来传输数据了。

使用消息首部字段Transfer-Encoding统计长度:

  • chunk编码将数据分成一块一块的发生。
  • Chunked编码将使用若干个Chunk串连而成,由一个标明长度为0的chunk标示结束。
  • 每个Chunk分为头部和正文两部分,头部内容指定正文的字符总数(十六进制的数字)。

# 5.2 判断传输是否完成

在保持持久连接的情况下,依赖 Content-Length 来确定数据发送完毕。

  1. Content-Length 在这里起到了一个响应实体已经发送结束的判断依据。这样的情况下,我们就要求 Content-Length 必须和内容实体的长度一致,如果不一致,就会出现各种问题。
  2. 如果 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 版:

  1. 增加了 HEAD、POST 等新方法;
  2. 增加了响应状态码,标记可能的错误原因;
  3. 引入了协议版本号概念;
  4. 引入了 HTTP Header(头部)的概念;
  5. 传输的数据不再仅限于文本。

# 6.2 HTTP1.1版本

HTTP/1.1 是对 HTTP/1.0 的小幅度修正。但一个重要的区别是:它是一个"正式的标准"。

HTTP/1.1 主要的变更点有:

  1. 增加了 PUT、DELETE 等新的方法;
  2. 增加了缓存管理和控制;
  3. 明确了连接管理,允许持久连接;
  4. 允许响应数据分块(chunked),利于传输大文件;
  5. 强制要求 Host 头,让互联网主机托管成为可能。

# 6.3 Http2.0版本

Google 首先开发了自己的浏览器 Chrome,然后推出了新的 SPDY 协议,从实际的用户方来"倒逼"HTTP 协议的变革。

HTTP/2 的主要特点:

  1. 二进制协议,不再是纯文本;
  2. 可发起多个请求,废弃了 1.1 里的管道;
  3. 使用专用算法压缩头部,减少数据传输量;
  4. 允许服务器主动向客户端推送数据;
  5. 增强了安全性,"事实上"要求加密通信。

Http2.0相对于Http1.x来说提升是巨大的:

  1. 二进制格式:以帧为基本单位,一帧中除了包含数据外同时还包含该帧的标识:StreamIdentifier。
  2. 多路复用:多个请求共用一个TCP连接,多个请求可以同时在这个TCP连接上并发。
  3. 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是否已处理过,如果是则直接返回之前的结果
1
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
1
2
3
4
5
6
7
8
9
10

REST的六大设计约束:

  1. 客户端-服务器分离:关注点分离,前后端可独立演进
  2. 无状态:每个请求包含所有必要信息,服务器不保存客户端状态
  3. 可缓存:响应明确标记是否可缓存,减少不必要的交互
  4. 统一接口:使用标准HTTP方法和状态码,接口一致可预测
  5. 分层系统:客户端无需知道是直连服务器还是经过中间代理
  6. 按需代码(可选):服务器可以返回可执行代码(如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. 团队习惯和技术栈限制
1
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压缩
1
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基于内容计算,任何修改都会改变
1
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 (端口不同)
1
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. 预检通过后,浏览器发送实际请求
1
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
1
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              → 获取当前用户的文章
1
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(踩坑②)
  ❌ 没有内容协商(踩坑⑤)
1
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的响应应分开缓存
1
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"
  }
}
1
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)只看头部就能做正确决策
1
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/...               ✓
1
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节]
1
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 时,都应该问自己三个问题:

  1. 资源怎么命名?(URL是名词复数还是动词?层级怎么表达?→ 可读性+一致性)
  2. 动作怎么表达?(GET/POST/PUT/DELETE 用对了吗?幂等性考虑了吗?→ 语义正确性)
  3. 元信息怎么传递?(缓存头、CORS头、Accept 头设置了吗?→ 性能和兼容性)

把这三个问题问到位,你就从"能写接口"进化到了"懂 HTTP 协议设计的工程师"。

# 10.思考题与作业

# 10.1 基础思考题

  1. URI vs URL:用生活中的例子举例说明 URI、URL、URN 三者的区别和关系。为什么说"URL 是 URI 的子集"?

  2. HTTP的无状态设计:如果 HTTP 从诞生之初就设计为有状态(服务器记住每个客户端的状态),今天前后端分离的架构还能实现吗?为什么 RESTful API 把"无状态"作为核心约束?

  3. 幂等性的判断:以下操作哪些是幂等的?① 用户注册(POST)② 修改密码(PUT)③ 增加积分(POST)④ 设置用户生日(PUT)⑤ 给文章点赞(POST)。说出你的判断依据。

  4. 状态码 301 vs 302:永久重定向和临时重定向有什么区别?为什么搜索引擎对 301 和 302 的对待不同?在什么场景下用 301,什么场景下用 302?

# 10.2 进阶思考题

  1. 踩坑④的深度分析:分布式系统中保证幂等性,除了客户端生成 Idempotency-Key,还有哪些方案?如果客户端没有生成幂等键,服务器端如何防止重复提交?(提示:业务唯一键、数据库唯一约束、分布式锁)

  2. CORS的安全边界:CORS 的 Access-Control-Allow-Origin: * 和 Access-Control-Allow-Credentials: true 为什么不能同时使用?如果可以同时使用,会有什么安全风险?请构造一个攻击场景。

  3. API版本管理策略:URL版本号 vs Header版本号 vs Query参数版本号,三种方案各有什么优缺点?Twitter、Google、GitHub 分别用了哪种?为什么大厂倾向于用 URL 版本号?

  4. 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
1
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方法、请求/响应格式。
  • 标注分页、缓存、错误处理、幂等性设计的实现方案。
上次更新: 2026/06/09, 15:47:57
HTTP服务设计流程
HTTPS协议设计策略

← HTTP服务设计流程 HTTPS协议设计策略→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式