听说『99% 的人都理解错了 HTTP 中 GET 与 POST 的区别』?
Category(分类): NET Protocol Status: 未知
写在前面
今天看到一篇标题十分吓人的文章:
99%的人都理解错了 HTTP 中 GET 与 POST 的区别
这篇文章前半部分讨论了大家经常提到的 GET 和 POST 区别,重点在后面。原文提出了一个传播很广的说法:
GET 产生一个 TCP 数据包;POST 产生两个 TCP 数据包。
并进一步解释:
对于 GET 请求,浏览器会把 HTTP Header 和 Data 一并发送出去;而对于 POST,浏览器先发送 Header,服务器响应
100 Continue,浏览器再发送 Data,服务器响应200 OK。
这个说法把某些浏览器或 XHR 实现中的抓包现象,误写成了 GET 和 POST 的协议区别。本文保留这段历史讨论,但按照现代 HTTP 规范重新梳理它的适用范围。
一、先说结论
GET 和 POST 没有固定的 TCP 数据包数量。
下面这些说法都不能作为 HTTP 规范结论:
GET 一定是一个 TCP 包
POST 一定是两个 TCP 包
POST 一定先收到 100 Continue 才发送请求体
实际传输多少个 TCP 段,可能受到以下因素影响:
- 请求行、首部和请求体的实际大小;
- TCP MSS、路径 MTU 和分段方式;
- 客户端 Socket 写入和缓冲策略;
- Nagle 算法、TCP_NODELAY 和延迟 ACK;
- TLS 记录切分和加密层缓冲;
- HTTP/1.1、HTTP/2 或 HTTP/3 的实现;
- 代理、负载均衡器和中间网络设备;
- 丢包、重传和拥塞控制。
而且 HTTP/3 使用 QUIC over UDP,根本不存在“GET 和 POST 各产生几个 TCP 包”的问题。
二、这段说法是怎么来的?
为了验证上述说法,原文引用了一篇文章:
XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?
这篇英文文章讨论的是某个浏览器版本中 XMLHttpRequest(XHR)发送 POST 时的具体网络表现,而不是 HTTP 规范规定的 GET 和 POST 必须使用不同数量的数据包。
原文还记录了当时的浏览器测试结果:
- Chrome 55:观察到 two packets;
- Safari 10:观察到 two packets;
- Firefox 49:没有观察到 two packets。
这些结果可以作为当时浏览器实现的历史记录,但不能推广成所有浏览器、所有请求库、所有操作系统和所有网络环境的行为。浏览器版本、请求大小、是否启用 TLS、连接是否复用,以及抓包位置变化后,结果都可能不同。
三、POST 不一定等待 100 Continue
原文批评了以下说法,这个批评的方向是正确的:
POST 先发送 Header,服务器响应
100 Continue,浏览器再发送 Data。
1. 100 Continue 需要 Expect 请求头配合
现代 HTTP 规范定义了:当客户端发送请求头时,如果请求包含较大的请求内容,可以带上:
Expect: 100-continue
它表达的意思是:客户端希望服务器先根据请求头判断是否愿意接收请求体,然后再发送请求体。
服务器收到后可以:
- 返回
100 Continue,提示客户端继续发送请求体; - 直接返回最终响应,例如
401 Unauthorized、403 Forbidden或413 Content Too Large; - 在请求体已经到达或没有请求体时,省略
100 Continue。
客户端收到 100 Continue 后才继续发送请求体,是一种由 Expect 触发的协商流程,不是 POST 方法的固定流程。
2. 普通 POST 不必等待 100 Continue
普通浏览器表单提交、Fetch 或 XHR POST 通常不会自动带上 Expect: 100-continue 并等待服务器响应。客户端可以把请求头和请求体连续交给操作系统发送,底层是否分成一个或多个 TCP 段由实现决定。
即使客户端使用了 Expect: 100-continue,100 Continue 也是 HTTP 层的临时响应,不是 TCP 握手,也不是一个专门的 TCP 数据包。客户端还应设置等待策略,不能因为一直没有收到 100 就无限等待。
因此,原文中“普通 POST 一定先收到服务器 100 Continue 才发送 body”的说法是错误的。
四、XHR 不等于 POST
原文指出:英文标题讨论的是 XMLHttpRequest,而中文文章却把 XMLHttpRequest 直接写成了 POST。这一点是正确的。
XHR 是浏览器提供的 JavaScript API,用于发起 HTTP 请求。它可以发起:
- GET;
- POST;
- PUT;
- PATCH;
- DELETE;
- HEAD 等请求。
Fetch API 也是类似的请求接口。XHR、Fetch、Axios 是发起请求的工具或 API,GET、POST 是 HTTP 方法,两者不是同一个概念。
因此,准确的说法应当是:
某个浏览器版本在通过 XHR 发送某类 POST 请求时,观察到请求数据被分成不同的网络写入或 TCP 段。
不能把它简化为“POST 天生发送两个 TCP 包”。
五、HTTP 规范中真正的 GET 与 POST 区别
当我们讨论 GET 和 POST 的区别时,应该首先讨论 HTTP 方法语义,而不是某个浏览器版本的实现细节。
| 对比项 | GET | POST |
|---|---|---|
| 主要语义 | 请求传输目标资源的当前表示 | 请求目标资源按照自身语义处理提交的数据 |
| 安全方法 | 是 | 否 |
| 幂等性 | 是 | 默认不是 |
| 默认缓存性 | 通常可缓存 | 通常不可缓存,满足明确条件时才可能缓存 |
| 常见参数位置 | URI 查询参数 | 请求体,也可以有查询参数 |
| 请求内容语义 | 没有通用定义,服务器可能拒绝或忽略 | 请求内容通常是资源需要处理的表示 |
| 常见用途 | 查询、读取、获取资源 | 表单提交、创建、上传、触发业务操作 |
1. GET 的安全性和幂等性
GET 是安全方法,表示客户端的请求意图是读取资源,而不是要求服务器修改资源。安全方法仍可能产生日志、统计等附带效果,“安全”不代表服务器完全没有任何内部变化。
GET 也是幂等的。重复发送相同 GET,按照规范的预期效果应当与发送一次相同。响应状态码、响应时间和日志数量仍可能不同,但请求的目标资源状态不应因为 GET 而被改变。
2. POST 默认不是幂等的
POST 请求目标资源处理请求体中的数据,常见于:
- 提交表单;
- 创建资源;
- 上传文件;
- 发起订单;
- 执行某个业务动作。
重复提交 POST 可能创建两条记录、生成两个订单或执行两次操作,因此规范不保证 POST 幂等。如果业务确实需要重试,应通过幂等键、请求 ID 或业务去重机制实现,而不是假设 POST 自动幂等。
3. GET 和 POST 都可以有请求内容,但语义不同
HTTP 消息的请求内容是否存在,与 TCP 分成几个数据包是两回事。
GET 请求可以在语法层面携带内容,但现代 HTTP 规范指出,GET 请求内容没有一般性的定义,不能改变 GET 的目标或语义,服务器也可能拒绝。因此,客户端除非与目标服务器有明确约定,否则不应在 GET 中发送请求体。
POST 的请求内容则是其常见语义的一部分,服务器会根据资源定义处理它。请求体可以使用 Content-Length 或 Transfer-Encoding 等消息 framing 方式传输。
4. 缓存不是“GET 永远缓存、POST 永远不缓存”
GET 响应通常可以被缓存,但仍受 Cache-Control、Expires、Vary 等首部影响。
POST 响应通常不被通用缓存直接复用,但在提供明确的新鲜度信息、Content-Location 等条件下,规范允许特定缓存场景。实际缓存行为还取决于浏览器、CDN 和代理配置。
5. GET 参数在 URL,POST 参数在 body?
这是常见使用习惯,不是绝对规则:
- GET 常把查询条件放在 URI 的 query 部分;
- POST 常把提交数据放在请求体;
- POST 也可以带 query 参数;
- GET 请求体没有通用语义,不建议用它传业务数据。
6. GET 比 POST 更安全吗?
不能这样比较。GET 和 POST 都可能传输敏感数据,都应使用 HTTPS。
GET 查询参数可能出现在:
- 浏览器历史记录;
- Web 服务器访问日志;
- 代理日志;
- 监控系统;
- Referer(具体取决于 Referrer-Policy);
- 缓存键。
POST 请求体通常不显示在地址栏,但并不天然加密,也可能被服务器、代理、日志中间件记录。安全性主要取决于 TLS、日志策略、认证授权、CSRF 防护和应用实现,而不是仅由 GET/POST 决定。
六、为什么不能用 TCP 包数量区分 GET 和 POST?
1. HTTP 方法属于应用层语义
GET 和 POST 定义的是服务器如何理解请求,以及请求具有什么安全性、幂等性和缓存语义。TCP 只负责传输字节流,不理解 GET、POST、URL 或 HTTP 首部。
即使是 HTTP/1.1,请求也可能出现以下情况:
GET:请求头较小,可能被一次写入,也可能被拆成多个 TCP 段
POST:请求头和小请求体可能连续写入,也可能被拆成多个 TCP 段
POST:大请求体通常需要很多 TCP 段
一个 TCP 段也不等于一个 HTTP 请求,一个 HTTP 请求也不等于一个 TCP 段。TCP 的分段、重组、确认和重传都不会保留 HTTP 方法的边界。
2. HTTP/2 和 HTTP/3 进一步说明了这个问题
HTTP/2 通常运行在 TCP 之上。HTTP 请求会被编码成 HEADERS、DATA 等帧,并在多个 Stream 中复用。抓包时看到的是 HTTP/2 帧、TLS 记录和 TCP 段的组合,不能按 HTTP/1.1 的经验数包。
HTTP/3 运行在 QUIC 之上,QUIC 使用 UDP 而不是 TCP。此时继续讨论“GET 一个 TCP 包、POST 两个 TCP 包”就完全不适用了。
3. 抓包结果受观察位置影响
在浏览器开发者工具、操作系统网卡、代理服务器、TLS 终止点或服务端网卡处抓包,看到的内容可能不同。还需要区分:
- TCP 三次握手;
- TCP 数据段;
- TCP ACK;
- TLS 握手和 TLS Application Data;
- HTTP/2 帧;
- 重传数据段;
- QUIC UDP 数据报。
所以抓包可以帮助分析某一次实现的行为,但不能仅凭一次抓包总结 HTTP 方法的规范差异。
七、一个更准确的测试结论
如果复现旧文章中的浏览器测试,应当这样描述:
在特定浏览器版本、特定请求大小、特定协议版本和特定网络环境下,通过 XHR 或其他 API 发出的 POST 请求,可能被浏览器分成多次 Socket 写入,并在抓包中表现为多个 TCP 数据段。其他浏览器或其他配置可能采用不同的发送方式。
还应说明:
- “多个 Socket 写入”不等于“多个 TCP 数据包”;
- “多个 TCP 数据包”不等于“POST 的协议规定”;
- 是否有
100 Continue取决于Expect和服务器处理; - TCP 可能合并或拆分这些写入;
- 开启 HTTPS 后还会经过 TLS 记录层封装;
- HTTP/2 和 HTTP/3 的传输模型又不同。
八、原文观点总结
原文认为“99% 的人理解错了 GET 和 POST”属于标题党,这个评价可以保留,但更准确的总结应当是:
- 不能把某一浏览器实现的抓包行为当成 GET/POST 的协议规定;
- XHR 是请求 API,POST 是 HTTP 方法,两者不是同一层次的概念;
100 Continue与Expect: 100-continue有关,不是所有 POST 的固定步骤;- GET 与 POST 的核心差异应该从 HTTP 方法语义、请求内容、是否安全、幂等性和缓存性来解释;
- TCP 数据包数量属于传输实现细节,不能作为 GET/POST 的通用区别;
- 在现代 HTTP/2 和 HTTP/3 环境中,用 TCP 包数量区分 GET 和 POST 更加不成立。
九、总结
GET 和 POST 的区别,首先是 HTTP 规范中的方法语义,其次才是浏览器和请求库的具体实现。
- GET 通常用于获取资源,是安全且幂等的;
- POST 通常用于提交数据或触发资源处理,默认不保证幂等;
- GET 请求内容没有通用语义,POST 通常使用请求体携带要处理的内容;
- GET 和 POST 都可能被拆成多个 TCP 段;
- POST 不一定触发
100 Continue; Expect: 100-continue是可选的请求协商机制;- XHR、Fetch 和 Axios 是请求 API,不等于 POST;
- GET 和 POST 都需要 HTTPS 才能保护传输中的敏感信息。
参考资料
发布于 2017-01-26 19:38(本文根据现代 HTTP 规范补充整理)