4. HTTP 响应报文结构:从状态行到响应体

聊完了请求,咱们来看看响应。说白了,服务器收到你的请求后,总得给个回话。这个回话就是 HTTP 响应报文。它的结构其实和请求报文很像,但细节上完全不同。

我个人习惯把响应报文拆成四块来看:状态行、响应头、空行、响应体。嗯,咱们一块一块拆开说。

核心记忆点:HTTP 响应报文 = 状态行 + 响应头 + 空行 + 响应体。缺一不可。

4.1 状态行:服务器的“一句话总结”

状态行是响应报文的第一行。它用一句话告诉客户端:这次请求到底怎么样了。

它的格式长这样:

HTTP/版本号 状态码 状态描述\r\n

举个例子:

HTTP/1.1 200 OK\r\n

这里面有三个部分:

  • 协议版本:通常是 HTTP/1.1 或 HTTP/2。我刚开始做开发时,经常看到 HTTP/1.0 的老古董,现在基本绝迹了。
  • 状态码:三位数字,200 表示成功,404 表示找不到,500 表示服务器内部错误。这是最关键的。
  • 状态描述:一段简短的人类可读文本,比如 "OK"、"Not Found"、"Internal Server Error"。说白了,就是给程序员看的提示。

小技巧:状态描述其实可以自定义,但主流浏览器和客户端只认状态码。我曾经见过有人把 200 的状态描述改成 "Everything is fine, bro",虽然能跑,但建议别这么干。

4.2 状态码分类:5 个家族,各司其职

状态码分 5 类,每类代表一种结果。我整理了一张表,方便你对照:

分类 范围 含义 常见例子
1xx 100-199 信息性响应 100 Continue(继续发送请求体)
2xx 200-299 成功 200 OK、201 Created、204 No Content
3xx 300-399 重定向 301 Moved Permanently、302 Found、304 Not Modified
4xx 400-499 客户端错误 400 Bad Request、401 Unauthorized、404 Not Found
5xx 500-599 服务器错误 500 Internal Server Error、502 Bad Gateway、503 Service Unavailable

为什么会这样分类?你想想看,第一位数字就决定了这个响应的“大方向”。看到 2xx,你就知道请求成功了;看到 4xx,赶紧检查你的请求参数;看到 5xx,那是服务器背锅了。

避坑指南:我曾经在项目中遇到一个诡异的问题——客户端一直报 404,但接口路径明明是对的。查了半天才发现,是 Nginx 配置里把请求转发到了错误的 upstream。所以,看到 404 别急着改代码,先确认请求到底到了哪台机器。

4.3 响应头:服务器的“自我介绍”

状态行之后,就是一堆响应头字段。它们告诉客户端:我返回的是什么类型的数据、有多大、要不要缓存、要不要设置 Cookie 等等。

常见的响应头有这些:

  • Server:服务器软件信息。比如 Server: nginx/1.18.0。我个人建议在生产环境隐藏具体版本号,避免被针对性攻击。
  • Content-Type:响应体的 MIME 类型。比如 Content-Type: text/html; charset=utf-8 表示返回的是 HTML 页面。
  • Content-Length:响应体的字节长度。比如 Content-Length: 1024 表示响应体有 1024 个字节。
  • Set-Cookie:让客户端设置 Cookie。比如 Set-Cookie: sessionId=abc123; Path=/; HttpOnly。
  • Cache-Control:缓存策略。比如 Cache-Control: max-age=3600 表示缓存一小时。

来看一个完整的响应头示例:

HTTP/1.1 200 OK
Server: nginx/1.18.0
Content-Type: application/json; charset=utf-8
Content-Length: 256
Set-Cookie: sessionId=abc123; Path=/; HttpOnly
Cache-Control: no-cache
Date: Mon, 15 Jan 2024 12:00:00 GMT

{"status": "success", "data": {"id": 1, "name": "test"}}

小技巧:Content-Length 和实际响应体长度不一致时,浏览器会报错。我遇到过因为 Gzip 压缩后忘记更新 Content-Length 导致页面白屏的问题。嗯,血的教训。

4.4 空行:分隔符,但很重要

响应头和响应体之间,有一个空行。它由 \r\n 组成。别看它不起眼,它是 HTTP 协议解析的关键——告诉解析器:响应头结束了,接下来是响应体。

如果没有这个空行,解析器会一直把响应体当成响应头来解析,结果就是乱码或解析失败。

4.5 响应体:真正的“干货”

响应体就是服务器返回的实际数据。它可以是任何东西:

  • HTML 页面:浏览器渲染的网页源码。
  • JSON 数据:API 接口返回的结构化数据。
  • 图片/视频:二进制文件。
  • 纯文本:比如 robots.txt 文件。
  • 空:比如 204 No Content 响应,就没有响应体。

响应体的格式由 Content-Type 头决定。比如 Content-Type: application/json 告诉客户端:请用 JSON 解析器来解析这段数据。

重点:响应体可以是空的,但空行必须存在。即使没有响应体,空行也不能省略。这是协议规范,没得商量。

4.6 一张图看懂响应报文结构

说了这么多,不如一张图来得直观。下面是我画的 HTTP 响应报文结构图:

HTTP 响应报文结构图 HTTP 响应报文结构 状态行 HTTP/1.1 200 OK\r\n 响应头 Server: nginx/1.18.0\r\n Content-Type: application/json; charset=utf-8\r\n Content-Length: 256\r\n Set-Cookie: sessionId=abc123; Path=/\r\n 空行(\r\n) 响应体 {"status": "success", "data": {"id": 1, "name": "test"}} 注意:空行是 \r\n,不是 \n。很多新手在这里踩坑。

4.7 实战:用 curl 看响应报文

光说不练假把式。咱们用 curl -v 命令,看看真实的 HTTP 响应长什么样:

$ curl -v https://api.example.com/data
*   Trying 93.184.216.34:443...
* Connected to api.example.com (93.184.216.34) port 443 (#0)
* SSL handshake completed
> GET /data HTTP/1.1
> Host: api.example.com
> User-Agent: curl/7.68.0
> Accept: */*
>
* HTTP 1.1, assume close after body
< HTTP/1.1 200 OK
< Server: nginx/1.18.0
< Content-Type: application/json; charset=utf-8
< Content-Length: 256
< Set-Cookie: sessionId=abc123; Path=/; HttpOnly
< Cache-Control: no-cache
< Date: Mon, 15 Jan 2024 12:00:00 GMT
<
{"status": "success", "data": {"id": 1, "name": "test"}}
* Closing connection 0

看到没?< 开头的行就是响应报文。状态行、响应头、空行、响应体,一目了然。

小技巧:用 curl -v 调试接口时,重点关注状态码和 Content-Type。如果返回 200 但 Content-Type 是 text/html,而你期望的是 JSON,那八成是服务器配置有问题。

4.8 常见问题与避坑

  • 状态码 200 但响应体为空:可能是服务器端逻辑错误,或者 Content-Length 为 0。用 curl 确认一下。
  • 响应头大小写:HTTP 规范规定头字段名不区分大小写,但建议统一用首字母大写格式(如 Content-Type)。
  • Set-Cookie 多个值:可以发送多个 Set-Cookie 头,每个设置一个 Cookie。
  • Content-Length 与实际不符:如果 Content-Length 小于实际响应体长度,浏览器会截断数据;如果大于,浏览器会一直等待。我遇到过因为 Gzip 压缩后忘记更新 Content-Length 导致页面白屏的问题。

避坑指南:我曾经在项目中遇到一个诡异的问题——客户端一直报 404,但接口路径明明是对的。查了半天才发现,是 Nginx 配置里把请求转发到了错误的 upstream。所以,看到 404 别急着改代码,先确认请求到底到了哪台机器。