3. HTTP报文结构:请求与响应的骨架
说实话,我刚开始学HTTP的时候,总觉得报文结构就是个格式问题,背下来就行了。直到有一次在生产环境排查一个诡异的接口超时,抓包一看——请求头里少了个Content-Length,服务器一直傻等。从那以后我才真正明白:报文结构不是死记硬背的格式,它是客户端和服务器之间沟通的「语法」。语法错了,话就说不清楚。
这一章,咱们就把HTTP报文拆开来看。请求报文和响应报文,其实是对称的。你掌握了其中一个,另一个就很好理解。
3.1 请求报文结构
一个HTTP请求报文,由四部分组成:请求行、请求头、空行、请求体。我习惯把这四部分叫做「一行、多头、一空、一体」。
核心记忆点:请求行是「一句话说清楚我要干什么」,请求头是「详细交代我的能力和条件」,空行是「头结束了,下面是正文」,请求体是「我要发给服务器的数据」。
3.1.1 请求行
请求行只有一行,但信息量极大。它包含三个要素:方法、URI、HTTP版本。
GET /api/users HTTP/1.1
POST /login HTTP/2
PUT /profile/123 HTTP/1.0
你看,第一行就告诉服务器三件事:
- 方法:我要做什么(GET拿数据、POST提交、PUT更新、DELETE删除)
- URI:我要对哪个资源操作
- 版本:我用什么协议版本跟你说话
我个人习惯在抓包时先看请求行。为什么?因为很多问题一眼就能看出来。比如URI写错了、方法用错了、版本不匹配——这些都在请求行里暴露无遗。
小技巧:抓包时如果看到HTTP/1.0,大概率是老旧客户端或者配置有问题。现在主流已经是HTTP/2,HTTP/1.1都算「老将」了。
3.1.2 请求头
请求头是键值对,每行一个。它描述了请求的「上下文」。我遇到过很多新手,觉得请求头可有可无——这是大错特错。
常见的请求头我列个表:
| 请求头 | 含义 | 我踩过的坑 |
|---|---|---|
Host |
目标主机名和端口 | 缺少这个头,HTTP/1.1直接报400 |
User-Agent |
客户端标识 | 有些API会校验这个,伪造不对就拒绝 |
Accept |
客户端能接收的响应格式 | 没写application/json,服务器返回了XML |
Content-Type |
请求体的媒体类型 | POST提交表单时写错,服务器解析失败 |
Content-Length |
请求体字节长度 | 长度不对,服务器一直等,直到超时 |
Cookie |
会话状态 | Cookie过期了还在发,服务器返回401 |
嗯,这里要注意:Host头在HTTP/1.1里是强制要求的。为什么?因为一台服务器可以托管多个域名,没有Host头,服务器不知道你要访问哪个网站。这是虚拟主机技术的基石。
3.1.3 空行
空行就是\r\n,一个回车换行。别小看它——它是请求头和请求体的分界线。没有这个空行,服务器就不知道头在哪里结束、体从哪里开始。
我曾经踩过的坑:有一次写HTTP客户端,拼接报文时忘了加空行。结果服务器一直返回400 Bad Request。我抓包看了半天才发现——头后面直接跟了请求体,服务器解析失败。从那以后,我每次构造报文都会检查空行。
3.1.4 请求体
请求体不是必须的。GET请求就没有请求体,POST/PUT通常有。请求体的格式由Content-Type决定:
application/x-www-form-urlencoded:表单数据,key1=value1&key2=value2application/json:JSON格式,{"key":"value"}multipart/form-data:文件上传,有边界分隔text/plain:纯文本
我建议你在抓包时,重点关注Content-Type和请求体内容是否匹配。比如你声明了application/json,但请求体里写的是name=张三——服务器解析时大概率会报错。
3.2 响应报文结构
响应报文和请求报文是对称的。它由四部分组成:状态行、响应头、空行、响应体。
对称记忆法:请求行对应状态行,请求头对应响应头,空行都是空行,请求体对应响应体。你记住请求报文,响应报文就记住了80%。
3.2.1 状态行
状态行也包含三个要素:HTTP版本、状态码、原因短语。
HTTP/1.1 200 OK
HTTP/2 404 Not Found
HTTP/1.1 500 Internal Server Error
状态码是响应报文里最重要的信息。我把它分成五类:
| 分类 | 范围 | 含义 | 常见例子 |
|---|---|---|---|
| 1xx | 100-199 | 信息性,服务器还在处理 | 100 Continue |
| 2xx | 200-299 | 成功,请求已处理 | 200 OK, 201 Created |
| 3xx | 300-399 | 重定向,需要进一步操作 | 301 Moved, 302 Found |
| 4xx | 400-499 | 客户端错误,请求有问题 | 400 Bad, 404 Not Found |
| 5xx | 500-599 | 服务器错误,服务端出问题 | 500 Internal, 502 Bad Gateway |
我个人习惯:看到4xx先查客户端,看到5xx先查服务端。这个原则帮我节省了大量排查时间。
3.2.2 响应头
响应头告诉客户端「我返回了什么、怎么返回的」。常见的响应头:
Content-Type:响应体的媒体类型(比如text/html、application/json)Content-Length:响应体字节长度Set-Cookie:服务器要求客户端设置CookieCache-Control:缓存策略Location:重定向的目标URL
避坑指南:我曾经遇到一个接口,返回的Content-Type是text/html,但实际内容是JSON。前端解析时直接报错。所以抓包时一定要检查响应头的Content-Type和响应体是否一致。
3.2.3 空行与响应体
空行和请求报文一样,是\r\n,分隔响应头和响应体。响应体就是服务器返回的实际数据——HTML页面、JSON数据、图片二进制流等等。
3.3 报文示例深度解析
光说不练假把式。咱们看一个真实的抓包报文。这是我之前调试一个登录接口时抓到的:
3.3.1 请求报文示例
POST /api/login HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.9
Accept-Encoding: gzip, deflate, br
Content-Type: application/json
Content-Length: 45
Connection: keep-alive
{"username":"admin","password":"123456"}
咱们逐行分析:
- 请求行:POST方法,URI是
/api/login,HTTP/1.1版本 - Host:目标服务器是
example.com - User-Agent:客户端是Chrome浏览器
- Accept:客户端能接收JSON、纯文本、任意格式
- Content-Type:请求体是JSON格式
- Content-Length:请求体有45个字节
- 空行:头结束,体开始
- 请求体:用户名和密码的JSON
你想想看,如果Content-Length写的是40,但实际请求体有45个字节——服务器就会少读5个字节,解析失败。这就是为什么我强调Content-Length必须精确。
3.3.2 响应报文示例
HTTP/1.1 200 OK
Date: Mon, 15 Jan 2024 08:30:25 GMT
Server: nginx/1.24.0
Content-Type: application/json; charset=utf-8
Content-Length: 52
Connection: keep-alive
Set-Cookie: session_id=abc123; Path=/; HttpOnly
Cache-Control: no-store
{"status":"success","token":"eyJhbGciOiJIUzI1NiJ9"}
逐行分析:
- 状态行:200 OK,请求成功
- Date:服务器响应时间
- Server:服务器是nginx 1.24.0
- Content-Type:响应体是JSON,UTF-8编码
- Content-Length:响应体52个字节
- Set-Cookie:服务器要求设置session_id这个Cookie,且HttpOnly(JS不可读)
- Cache-Control:no-store,不允许缓存
- 响应体:登录成功,返回token
注意:响应头里的Set-Cookie带有HttpOnly标志。这意味着前端JavaScript无法通过document.cookie读取这个Cookie。这是安全设计——防止XSS攻击窃取会话。我在项目中见过不少因为没加HttpOnly导致的安全漏洞。
3.4 报文结构全景图
说了这么多,咱们用一张图把整个报文结构串起来。这张图我画了很多次,每次给团队培训都会用。
这张图把请求和响应放在一起对比。你看,结构几乎是对称的。左边是请求,右边是响应。唯一不同的是第一行——请求叫「请求行」,响应叫「状态行」。其他部分完全对应。
3.5 抓包实战建议
最后,分享几个我多年抓包总结的经验:
- 先看第一行:请求行或状态行能告诉你80%的信息。方法对不对?状态码是多少?一眼就能判断。
- 检查Content-Type:这是最常见的出错点。声明和实际不一致,客户端或服务器就会解析失败。
- 验证Content-Length:长度不对,报文就「断头断尾」。我遇到过因为代理服务器修改了请求体但没更新Content-Length导致的诡异问题。
- 别忽略空行:虽然它不可见,但它是报文结构的「分水岭」。没有它,头和体就混在一起了。
- 关注Cookie和Set-Cookie:会话状态全靠它们。Cookie丢了、过期了、域不对——都会导致认证失败。
我的习惯:每次抓包,我会把请求报文和响应报文「并排」看。先看请求行和状态行是否匹配,再看请求头和响应头是否对应。比如请求里带了Authorization头,响应里就应该有对应的认证结果。这种「对照法」能快速定位问题。
报文结构是HTTP协议的「语法」。语法对了,沟通才能顺畅。下一层,咱们要深入HTTP的「语义」——方法、状态码、头字段的具体含义。不过那是后面的事了。先把这一章的结构吃透,抓包时你就能一眼看穿报文里的门道。