技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTNET

GET 和 POST 的区别

所属馆藏
NET Protocol
文件格式
Markdown
原始路径
NET Protocol/01-GET和POST的区别
本文目录5 个章节
  1. 一、先看常见的“标准答案”
  2. 二、GET 和 POST 在报文上的区别
  3. 三、常见问题
  4. 四、用 Socket 观察 HTTP 报文
  5. 五、总结

GET 和 POST 的区别

Category(分类): NET Protocol Status: 未知

本文保留原文从“面试标准答案”到“报文和 Socket 实验”的讲解方式,并结合 RFC 9110、RFC 9112 以及现代浏览器行为修正容易误导的结论。

一、先看常见的“标准答案”

很多旧文章会把 GET 和 POST 的区别整理成下面的表格。这个表格可以帮助入门,但其中“POST 更安全”“GET 最大 2048 字符”“POST 不能缓存”等说法并不是 HTTP 标准结论。

分类GETPOST
后退按钮/刷新通常会重复执行 GET,通常不应产生业务状态变化可能重新提交请求体,浏览器可能提示用户确认;具体行为由浏览器决定
书签URL 包含查询参数,通常可以收藏并复现 GET可以收藏 URL,但通常不会保存原 POST 方法和请求体
缓存GET 响应通常可缓存,但受缓存首部和请求条件影响默认不应简单视为不可缓存;满足明确条件时 POST 响应也可以缓存,实际较少见
编码类型HTML 表单常使用 application/x-www-form-urlencoded;接口也可以约定其他格式可使用 application/x-www-form-urlencodedmultipart/form-dataapplication/jsonapplication/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
  • 请求首部:例如 HostContent-TypeContent-LengthCookieUser-Agent
  • 空行:分隔首部和内容;
  • 请求内容:可选的 body。

请求首部

请求首部是请求报文的一部分,用于描述客户端、目标资源、认证信息、缓存条件和请求内容等信息。

请求体

请求体是请求报文中承载数据的部分,开发者也常称为 body。它不是 POST 专属,PUT、PATCH 等方法经常使用 body;但 GET body 没有通用语义,实际应用不建议依赖。

响应报文通常由以下部分组成:

  1. 状态行:例如 HTTP/1.1 200 OK
  2. 响应首部:例如 Content-TypeContent-LengthCache-ControlSet-Cookie
  3. 空行
  4. 响应体: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 的统一限制。

URL 长度测试示意图

五、总结

  • 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 规范定义。

参考:

  1. RFC 9110:HTTP Semantics
  2. RFC 9112:HTTP/1.1
  3. RFC 9113:HTTP/2
  4. RFC 9114:HTTP/3
  5. MDN:HTTP 请求方法
  6. MDN:GET 方法
  7. MDN:POST 方法
457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS