HTTP 报文结构:方法、状态码、头部与实体

上一篇我们看了 HTTP 怎么一代代演进,但无论 HTTP/2 怎么二进制分帧、HTTP/3 怎么换到 QUIC,那套"方法、状态码、头部、实体"的语义始终没变。这一篇我们就把这套语义彻底拆开——它才是你每天打交道的东西。

你在浏览器开发者工具的 Network 面板、在网关日志、在 curl -v 输出里看到的每一行,其实都是 HTTP 报文的一部分。但很多人只是"认识"它们,说不清每个字段在协议机制里承担什么职责、为什么要分这么多部分。报文异常时,也就只会大概感觉"接口报错了",没法顺着报文结构精准定位。

所以这一篇先机制后设计:先把一份报文从第一行到最后一字节逐段拆开,看清每一部分装什么、起什么作用,再回头说为什么 HTTP 要这样组织报文。 我们以最直观的 HTTP/1.1 文本格式为例(HTTP/2/3 只是编码不同,语义一致)。

一、先看一份报文的整体骨架

一份 HTTP/1.1 报文,无论请求还是响应,都是同一套四段式结构:

1
2
3
4
起始行(请求行 / 状态行)
头部字段(一行一个,Key: Value)
空行(CRLF,分隔头部和实体)
实体(可选,正文)

请求报文举例:

1
2
3
4
5
6
POST /api/users HTTP/1.1          ← 请求行:方法 + 路径 + 版本
Host: api.example.com ← 头部开始
Content-Type: application/json
Content-Length: 27
← 空行(关键分隔符)
{"name":"alice","age":20} ← 实体

响应报文举例:

1
2
3
4
5
HTTP/1.1 201 Created              ← 状态行:版本 + 状态码 + 原因短语
Content-Type: application/json
Content-Length: 38
← 空行
{"id":1001,"name":"alice"} ← 实体

整套结构的核心机制是那个空行:它是头部和实体的唯一分界。解析器读到一个空行(连续两个 CRLF),就知道"头部结束了,后面是实体"。这个看似不起眼的空行,是 HTTP 报文能被正确切分的关键。

记住这个四段骨架——起始行说"要干什么/结果如何",头部说"附加信息",空行说"头部到此为止",实体说"具体内容是什么"。请求和响应共享这套结构,只是起始行不同。

二、请求行:方法 + 目标 + 版本

请求行只有一行,三个部分用空格分隔:方法 请求目标 协议版本

方法表达的是意图(语义),不只是名字:

  • GET:获取资源,应当安全(不改变服务器状态)且幂等(多次请求效果相同)。
  • POST:提交数据、触发处理,不幂等(提交两次可能创建两条记录)。
  • PUT:整体替换/更新资源,幂等(用同样的数据 PUT 多次结果一致)。
  • DELETE:删除资源,幂等
  • PATCH:局部更新。
  • HEAD:和 GET 一样但只要响应头、不要实体(用于探测)。
  • OPTIONS:询问服务器支持哪些方法(CORS 预检就用它)。

方法的安全性幂等性不是学术名词,而是有真实工程后果的契约。比如重试逻辑:网络超时后,幂等的 GET、PUT、DELETE 可以放心重试,而 POST 重试就可能产生重复提交——支付、下单接口的"防重"问题,根子就在这里。

请求目标通常是路径加查询串(/api/users?page=2)。协议版本标明 HTTP/1.1 等。

三、状态行:状态码是服务端的分类反馈

响应的状态行是 版本 状态码 原因短语,如 HTTP/1.1 404 Not Found。状态码不是杂乱的"错误编号表",而是按首位数字分成五大类的结构化反馈:

  • 1xx 信息:请求已收到、继续处理(如 100 Continue,少见)。
  • 2xx 成功200 OK201 Created(已创建)、204 No Content(成功但无实体)。
  • 3xx 重定向301 Moved Permanently(永久)、302 Found(临时)、304 Not Modified(缓存仍有效,配合 ETag)。
  • 4xx 客户端错误400 Bad Request(请求格式错)、401 Unauthorized(未认证)、403 Forbidden(已认证但无权限)、404 Not Found429 Too Many Requests(限流)。
  • 5xx 服务端错误500 Internal Server Error502 Bad Gateway(网关从上游收到无效响应)、503 Service Unavailable(暂时不可用)、504 Gateway Timeout(网关等上游超时)。

状态码的首位数字本身就携带信息:4xx 让你查客户端(请求、权限、参数),5xx 让你查服务端。一个 404 和一个 502 都叫"失败",但前者是"东西不存在",后者是"网关后面的上游出问题了"——指向的排查层次完全不同。能从状态码首位迅速定位"该查谁",是工程基本功。

四、头部:在正文之外携带元信息

头部是一组 Key: Value,承担了 HTTP 里绝大部分"附加语义"。为什么有这么多?因为请求和响应除了"主体内容",还有大量关于内容、连接、缓存、认证的信息要传递,这些都塞进头部。按职责分几类看就不乱了:

描述实体的(实体头):

  • Content-Type:实体的媒体类型(application/jsontext/htmlimage/png),告诉对方怎么解析正文。
  • Content-Length:实体字节数。解析器靠它知道实体到哪结束。
  • Content-Encoding:实体的压缩方式(gzipbr)。

控制连接与传输的:

  • Host:目标域名。HTTP/1.1 必带——一台服务器靠它区分托管的多个虚拟主机。
  • Connection: keep-alive:复用连接(上一篇讲的持久连接)。
  • Transfer-Encoding: chunked:分块传输,不预先知道总长度时用。

认证与状态的:

  • Authorization:携带认证凭据(如 Bearer <token>)。
  • Cookie / Set-Cookie:客户端带上 / 服务端下发会话状态。HTTP 本身无状态,状态全靠它俩维持。

缓存与协商的:

  • Cache-ControlETagLast-Modified:控制缓存行为和缓存校验。
  • AcceptAccept-EncodingAccept-Language:客户端表达"我希望收到什么格式/编码/语言"。

五、实体与内容协商:传的是什么、双方怎么商量

空行之后是实体,承载真正的内容——HTML、JSON、图片、视频、文件片段都行。实体本身只是一串字节,它是什么、多长、怎么编码,全靠头部来描述Content-Type / Content-Length / Content-Encoding)。这是一种干净的分离:实体管"内容",头部管"关于内容的说明"。

内容协商是其中很优雅的一套机制:客户端在请求里用 Accept 系列头表达偏好,服务端据此挑选最合适的表示返回,并用对应的 Content-* 头说明实际给了什么。

  • 客户端发 Accept-Encoding: gzip, br,服务端可以选 gzip 压缩,回 Content-Encoding: gzip
  • 客户端发 Accept-Language: zh-CN,服务端返回中文版本。

实体和头部的分离,本质是"数据"和"元数据"的分离。同一份资源可以有多种表示(不同语言、不同压缩、不同格式),客户端用 Accept 说想要哪种,服务端用 Content-* 说实际给了哪种。理解这套协商,后端做压缩、多语言、文件下载、跨端兼容时就有章可循。

六、看懂机制后,再问:为什么这样设计报文

为什么用纯文本、一行一个头部? HTTP/1.x 选文本,是为了可读、易调试、易扩展。你能直接用 telnetcurl -v 看懂报文,中间代理也能轻松解析转发。代价是解析效率和体积——这正是 HTTP/2 改用二进制分帧、HPACK 压缩头部要优化的(语义不变、编码变高效)。

为什么把方法、状态码、头部、实体拆这么清楚? 职责分离。方法表达意图、状态码表达结果、头部表达元信息、实体承载内容——四者各管一段,互不耦合。这种正交设计让 HTTP 极易扩展:要加新能力(比如新的认证方式、新的缓存策略),往往只需加一个新头部,不动其他部分。

为什么 HTTP 要无状态、靠 Cookie 补状态? 无状态让服务器不必为每个客户端维护会话内存,天然利于水平扩展和负载均衡(任意一台服务器都能处理任意请求)。但真实业务需要"登录态",于是用 Cookie/Set-Cookie 把状态外置到客户端携带。这是"协议保持无状态、状态按需附加"的经典权衡。

七、和工程实践 / 后端开发的联系

报文结构是排障时最快的切入点——很多问题在协议层就暴露了,不必先翻业务代码:

  • 接口异常先看报文:方法对不对、路径对不对、Host 对不对、状态码几 xx、响应体有无错误信息、关键头部是否缺失。一份 curl -v 往往比读半天代码更快定位。
  • 跨域失败看响应头:浏览器报 CORS 错误时,查响应的 Access-Control-Allow-Origin 等头是否正确,以及 OPTIONS 预检是否通过。
  • 登录态问题看 CookieSet-CookieDomainPathSameSiteSecure 属性配错,会导致 Cookie 没带上、登录态丢失。
  • 下载异常看 Content-DispositionContent-Type:文件名乱码、浏览器直接打开而非下载,都和这两个头有关。
  • 502 vs 504 vs 500:502 是网关从上游收到坏响应(上游崩了或协议不对),504 是网关等上游超时,500 是应用自己抛异常。三者排查方向不同,看状态码就能分流。

学习这一部分最容易踩的坑

1. 把状态码当成无规律的错误编号

状态码首位数字是分类信息:3xx 重定向、4xx 客户端问题、5xx 服务端问题。不按类理解,就没法用它快速判断"该查客户端还是服务端"。

2. 忽视方法的幂等性

GET/PUT/DELETE 幂等、可安全重试,POST 不幂等、重试会重复提交。做超时重试、防重设计时不考虑这点,就会出现重复下单、重复扣款。

3. 以为 HTTP/2、HTTP/3 报文结构变了

变的是编码方式(二进制分帧、头部压缩),方法、状态码、头部、实体这套语义完全没变。理解语义在哪个版本都通用。

4. 分不清 401 和 403

401 Unauthorized 其实是"未认证"(没登录或凭据无效),403 Forbidden 是"已认证但无权限"。名字容易误导,混了就会在排查权限问题时找错方向。

总结

这一篇我们把 HTTP 报文逐段拆开,坚持先看机制、再说设计:

  • 报文是四段式骨架:起始行、头部、空行、实体,空行是头部与实体的关键分界;
  • 请求行的方法表达意图,其安全性、幂等性直接决定能否安全重试;
  • 状态码按首位分五类,4xx 指向客户端、5xx 指向服务端,是排障的快速分流器;
  • 头部在正文之外携带元信息,按描述实体、控制连接、认证状态、缓存协商分类记忆;
  • 实体承载内容、头部描述内容,二者分离支撑了内容协商(AcceptContent-*);
  • 文本格式利于调试、职责分离利于扩展、无状态利于扩容——这是 HTTP 报文设计的三条主线。

把这套语义吃透,HTTP 就从"浏览器自动帮我发的东西",变成你能主动解读和设计的一门语言。下一篇我们给这门语言加上安全外衣——看 HTTPS 是怎么在不可信网络里建立可信通道的。

参考资源