GET 和 POST 的区别
Category(分类): NET Protocol Status: 未知
本文保留原文从“面试标准答案”到“报文和 Socket 实验”的讲解方式,并结合 RFC 9110、RFC 9112 以及现代浏览器行为修正容易误导的结论。
一、先看常见的“标准答案”
很多旧文章会把 GET 和 POST 的区别整理成下面的表格。这个表格可以帮助入门,但其中“POST 更安全”“GET 最大 2048 字符”“POST 不能缓存”等说法并不是 HTTP 标准结论。
| 分类 | GET | POST |
|---|---|---|
| 后退按钮/刷新 | 通常会重复执行 GET,通常不应产生业务状态变化 | 可能重新提交请求体,浏览器可能提示用户确认;具体行为由浏览器决定 |
| 书签 | URL 包含查询参数,通常可以收藏并复现 GET | 可以收藏 URL,但通常不会保存原 POST 方法和请求体 |
| 缓存 | GET 响应通常可缓存,但受缓存首部和请求条件影响 | 默认不应简单视为不可缓存;满足明确条件时 POST 响应也可以缓存,实际较少见 |
| 编码类型 | HTML 表单常使用 application/x-www-form-urlencoded;接口也可以约定其他格式 | 可使用 application/x-www-form-urlencoded、multipart/form-data、application/json、application/octet-stream 等 |
| 历史和日志 | 查询字符串通常会进入地址栏、浏览器历史、代理或服务器日志 | 请求体通常不显示在地址栏,但仍可能出现在开发者工具、代理、应用和服务器日志中;URL 本身仍可能被记录 |
| 数据长度 | 没有 HTTP 规定的统一最大值,实际受浏览器、代理、服务器和 CDN 限制 | 请求体也没有一个对所有实现通用的无限制上限,实际受实现、配置和资源限制 |
| 数据类型 | 查询字符串通常需要 URI 编码;不能把它简单说成“只允许 ASCII” | 请求体可以是文本或二进制,具体由 Content-Type 和应用协议决定 |
| 安全性 | 明文 HTTP 下不安全;URL 中的敏感数据还容易泄露 | 不会因为参数放在 body 中就自动安全;同样需要 HTTPS 和合理的日志、权限、CSRF 防护 |
| 可见性 | 查询参数出现在 URL 中,可能被分享、记录或作为 Referer 传播 | body 不会显示在地址栏,但并不等于只有通信双方可见 |
从 HTTP 规范来看
- GET:请求目标资源的当前表示。GET 被定义为 safe(安全方法)和幂等方法,GET 响应通常可以被缓存;
- POST:请求目标资源根据请求体中的内容进行资源特定的处理,例如提交表单、创建资源或触发某种业务处理。POST 默认不是 safe,也不是幂等的,但具体接口可以通过业务设计实现幂等;
- 安全方法不代表数据机密,而是表示请求的预期语义不应修改服务器状态;
- 幂等表示重复相同请求的预期服务器效果与执行一次相同,不代表每次响应状态码一定相同;
- POST 响应在满足显式新鲜度信息和其他条件时也可以缓存,只是实际使用比 GET 少得多。
“GET 用于查询、POST 用于提交”是常见且实用的约定,但不是所有接口的绝对规则。还应根据语义考虑 PUT、PATCH、DELETE、HEAD 和 OPTIONS 等方法。
二、GET 和 POST 在报文上的区别
先下一个更准确的结论:在 HTTP/1.1 中,GET 和 POST 使用相同的请求报文结构和传输机制,但它们的方法语义不同,不能只说是第一行的几个字符不同。
HTTP/1.1 的请求报文通常由以下部分组成:
请求行
请求首部
空行
请求内容(可选)
HTTP/2 和 HTTP/3 不再使用 HTTP/1.1 那样的文本请求行,而是使用 :method、:path、:scheme、:authority 等伪首部表达同样的语义。不过从应用层看,仍然存在 GET 和 POST 方法。
不带参数时
HTTP/1.1 中,两种请求的请求行不同:
GET /index.php HTTP/1.1
Host: localhost
POST /index.php HTTP/1.1
Host: localhost
Content-Length: 0
除了方法不同,实际首部也可能不同。是否存在 body 不是由方法名称单独决定的,而是由消息 framing 和客户端、服务端约定决定。
带参数时
常见的 GET 请求会把参数放入 URI 查询字符串:
GET /index.php?name=qiming.c&age=22 HTTP/1.1
Host: localhost
常见的 POST 请求会把数据放到请求体,并用 Content-Type 描述编码:
POST /index.php HTTP/1.1
Host: localhost
Content-Type: application/x-www-form-urlencoded
Content-Length: 20
name=qiming.c&age=22
上面的 Content-Length 只是示意,真实值应按请求体的字节数计算,而不是按字符数猜测。
POST 也可以带查询参数:
POST /index.php?source=web HTTP/1.1
Host: localhost
Content-Type: application/json
{"name":"qiming.c","age":22}
GET 请求理论上可以携带 content,但 RFC 9110 没有为 GET content 定义通用语义,客户端不应依赖这种用法,服务端和中间代理也可能拒绝它。因此实际接口通常把 GET 的筛选参数放在 query 中,把需要提交的内容放在 POST、PUT 或 PATCH 的 body 中。
GET 和 POST 是否都使用 TCP?
在 HTTP/1.1 和 HTTP/2 中,通常使用 TCP;HTTP/3 使用 QUIC。GET 和 POST 是 HTTP 方法,不是 TCP 连接,也不决定底层一定采用哪一种传输协议。
如果只观察 HTTP/1.1 的 Socket 实验,可以说二者都被封装成 TCP 字节流;但 TCP 不理解 GET、POST、缓存和请求体,它只负责传输字节。
三、常见问题
1. GET 方法的参数写法是固定的吗?
最常见的查询参数形式是在 ? 后使用键值对,并用 & 分隔:
https://example.com/user?name=chengqm&age=22
参数值需要进行 URI 百分号编码。例如空格、中文和特殊字符不能直接依赖裸字符传输。实际解析时应使用标准 URL API 或框架提供的查询参数解析器,不要简单用字符串切割代替完整解析。
路径参数也很常见:
https://example.com/user/name/chengqm/age/22
这不是查询字符串,而是路径的一部分。只要客户端和服务端约定一致就可以使用,但需要正确处理编码、斜杠、路径穿越和参数歧义。
2. POST 方法比 GET 方法安全吗?
不是。从传输角度看,如果使用明文 HTTP,GET 和 POST 都可能被网络节点抓包并读取完整报文。
HTTPS 才能提供传输过程中的机密性和完整性,但仍应注意:
- GET 查询参数更容易出现在浏览器历史、代理日志、访问日志和 Referer 中;
- POST body 也可能被服务器、代理、监控系统和应用日志记录;
- POST 仍然可能受到 CSRF 攻击;
- 登录密码、Token 等敏感数据不应放入 URL;
- 应通过 HTTPS、Cookie 安全属性、CSRF Token、Origin/Referer 校验和日志脱敏共同保护数据。
因此,“POST 更安全”只能理解为它通常不会把参数展示在地址栏,不能理解为它具有加密能力。
3. GET 方法的长度限制是怎么回事?
HTTP 没有规定一个所有浏览器、服务器和代理都必须遵守的 URL 最大长度。实际限制可能来自:
- 浏览器地址栏和网络 API;
- Web 服务器请求行限制;
- 反向代理和负载均衡器;
- CDN、防火墙和网关;
- 应用自身的路由和日志系统。
超过请求目标可处理范围时,服务器可能返回 414 URI Too Long。所以 2048 个字符不是 HTTP 的统一标准,也不是所有实现的安全上限。
POST 的请求体也不是“无限制”。服务端通常会限制 body 大小,以避免内存消耗、磁盘消耗和拒绝服务攻击;上传接口还可能受到反向代理和应用框架限制。
如果参数数量很多、包含较大内容或涉及隐私,应根据接口语义选择 POST 或其他方法,不要仅仅为了绕过 URL 长度限制而随意改变方法。
4. POST 方法会产生两个 TCP 数据包吗?
不会。POST 不会因为方法本身就必然把 Header 和 Body 分成两个 TCP 数据包。
TCP 没有应用层消息边界,HTTP 请求可能被拆成多个 TCP 段,也可能多个 HTTP 数据片段被合并到一次读取中。TCP 分段和 HTTP Header/Body 的逻辑边界没有一一对应关系。
有些客户端在请求较大 body 前会发送:
Expect: 100-continue
这表示客户端希望服务端先根据首部判断是否继续发送请求体。服务端可以返回:
HTTP/1.1 100 Continue
然后客户端再发送 body。Expect: 100-continue 不是 POST 专属,也不是所有客户端都会使用;客户端是否等待、等待多久以及是否直接发送 body,都取决于实现。
因此,抓包时看到 Header 和 Body 分成多个 TCP 包,不能证明这是 POST 的固定行为;看到它们在同一个 TCP 包中,也不能证明所有客户端都一定如此。
5. GET 和 POST 的请求体、请求头、请求报文是什么关系?
请求报文
请求报文(Request Message)是客户端发送的完整 HTTP 请求,通常包括:
- 请求行:HTTP/1.1 中包含方法、请求目标和协议版本,例如
GET / HTTP/1.1; - 请求首部:例如
Host、Content-Type、Content-Length、Cookie、User-Agent; - 空行:分隔首部和内容;
- 请求内容:可选的 body。
请求首部
请求首部是请求报文的一部分,用于描述客户端、目标资源、认证信息、缓存条件和请求内容等信息。
请求体
请求体是请求报文中承载数据的部分,开发者也常称为 body。它不是 POST 专属,PUT、PATCH 等方法经常使用 body;但 GET body 没有通用语义,实际应用不建议依赖。
响应报文通常由以下部分组成:
- 状态行:例如
HTTP/1.1 200 OK; - 响应首部:例如
Content-Type、Content-Length、Cache-Control、Set-Cookie; - 空行;
- 响应体:HTML、JSON、图片或其他表示数据。
204 No Content 没有响应内容;304 Not Modified 通常也不携带响应体。响应体是否存在应根据 HTTP 状态码和消息 framing 判断,而不是只看请求方法。
四、用 Socket 观察 HTTP 报文
如果对 GET 和 POST 的报文区别有疑惑,可以启动一个简化的 HTTP/1.1 Socket 服务端,观察请求行、首部和请求体。
下面代码仅用于学习 HTTP 报文结构:它按 Content-Length 读取请求体,不支持 HTTP/1.1 chunked 请求、不支持 HTTP/2/HTTP/3,也没有实现完整的 HTTP 安全和并发能力。
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import socket
HOST, PORT = '', 23333
def receive_request(client_connection):
buffer = b''
# 先读取 HTTP/1.1 首部,直到遇到空行。
while b'\r\n\r\n' not in buffer:
chunk = client_connection.recv(4096)
if not chunk:
return buffer
buffer += chunk
if len(buffer) > 1024 * 1024:
raise ValueError('request headers are too large')
header_bytes, body = buffer.split(b'\r\n\r\n', 1)
header_lines = header_bytes.decode('iso-8859-1').split('\r\n')
headers = {}
for line in header_lines[1:]:
name, separator, value = line.partition(':')
if separator:
headers[name.lower().strip()] = value.strip()
transfer_encoding = headers.get('transfer-encoding', '').lower()
if transfer_encoding and transfer_encoding != 'identity':
raise NotImplementedError('this demo does not parse chunked encoding')
content_length = int(headers.get('content-length', '0'))
while len(body) < content_length:
chunk = client_connection.recv(4096)
if not chunk:
break
body += chunk
return header_bytes + b'\r\n\r\n' + body[:content_length]
def handle_request(client_connection):
try:
request = receive_request(client_connection)
print(request.decode('iso-8859-1', errors='replace'))
response_body = b'''<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello, World!</title>
</head>
<body>
<p style="color: green">Hello, World!</p>
</body>
</html>
'''
response = (
b'HTTP/1.1 200 OK\r\n'
b'Content-Type: text/html; charset=utf-8\r\n'
+ f'Content-Length: {len(response_body)}\r\n'.encode()
+ b'Connection: close\r\n'
+ b'\r\n'
+ response_body
)
client_connection.sendall(response)
finally:
client_connection.close()
def server_run():
listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
listen_socket.bind((HOST, PORT))
listen_socket.listen(5)
print(f'Serving HTTP on port {PORT} ...')
try:
while True:
client_connection, _ = listen_socket.accept()
handle_request(client_connection)
finally:
listen_socket.close()
if __name__ == '__main__':
server_run()
这个示例只为观察报文,真实生产环境不能直接使用。生产 HTTP 服务还需要处理并发、超时、请求行限制、重复首部、Host 校验、TLS、chunked 编码、错误响应、日志和请求走私防护。
启动后可以使用浏览器、curl 或 Postman 请求:
curl 'http://127.0.0.1:23333/index.php?name=qiming.c&age=22'
curl -X POST 'http://127.0.0.1:23333/index.php' \
-H 'Content-Type: application/x-www-form-urlencoded' \
--data 'name=qiming.c&age=22'

服务端打印出的内容可以帮助观察请求行、请求首部、空行和请求体的区别。

如果需要观察 Header 和 Body 是否分开传输,应使用抓包工具查看 TCP 流和 HTTP 解析结果,但不能把 TCP 数据包边界当成 HTTP 报文边界。

如果需要测试较长的 URL,可以逐渐增加查询字符串长度,并分别观察浏览器、代理和服务端的限制。超过某个实现的限制时可能得到 414 URI Too Long,但这个限制不能推广为 HTTP 的统一限制。

五、总结
- GET 和 POST 都是 HTTP 方法,不是 TCP 连接;
- GET 主要用于获取资源表示,规范语义上 safe、幂等,响应通常可缓存;
- POST 用于让目标资源处理请求内容,默认不是 safe,也不是幂等,但具体语义取决于接口设计;
- GET 查询参数通常放在 URL 中,POST 数据通常放在 body 中,但这不是绝对的语法限制;
- GET body 没有 RFC 9110 定义的通用语义,实际接口不应依赖;
- POST body 也不是无限大,服务器、代理和应用通常会设置大小限制;
- GET 和 POST 都可能使用 HTTPS,POST 不会自动带来安全性;
Content-Type决定请求内容的媒体类型,不由 GET 或 POST 单独决定;- TCP 分包、合包和 HTTP Header/Body 的逻辑边界不是一一对应的;
100 Continue是 HTTP 临时响应机制,不是 POST 必然产生两个 TCP 数据包;- HTTP/1.1 的报文格式与 HTTP/2、HTTP/3 的线格式不同,但方法语义仍由 HTTP 规范定义。
参考: