4. HTTP请求报文结构:从请求行到请求体
抓包分析HTTP,说白了就是看客户端跟服务器之间到底说了什么悄悄话。我干了这么多年网络排障,见过太多人一上来就盯着响应码看,结果绕了一大圈才发现是请求本身写错了。今天咱们就把HTTP请求报文拆开揉碎了讲清楚。
一个完整的HTTP请求报文,由四部分组成:请求行、请求头、空行和请求体。嗯,这个结构其实挺像一封信——信封上写地址(请求行),信纸抬头写问候(请求头),空一行表示正文开始,最后才是你要说的内容(请求体)。
核心记忆点:请求行 + 请求头 + 空行 + 请求体 = 完整的HTTP请求。缺了空行,服务器就不知道头什么时候结束,这是新手最容易忽略的坑。
4.1 请求行:请求的第一张名片
请求行只有一行,但信息量巨大。它告诉服务器三件事:你要干什么、对哪个资源下手、用什么协议版本。格式就是:方法 空格 URI 空格 协议版本。
我在一次线上故障排查中,发现客户端一直报400错误。抓包一看,请求行里URI写了个中文路径没编码,服务器直接不认。你想想看,服务器看到乱码能不懵吗?
| 组成部分 | 示例 | 说明 |
|---|---|---|
| 方法 | GET / POST / PUT / DELETE | 表示操作类型,GET最常见 |
| URI | /api/v1/users?id=123 | 统一资源标识符,包含路径和查询参数 |
| 协议版本 | HTTP/1.1 或 HTTP/2 | 决定了连接行为和特性 |
我的小技巧:在Wireshark里过滤 http.request 可以直接看到所有请求行。我个人习惯先看请求行里的方法,如果是POST但没看到请求体,那八成是抓包没抓到完整数据包。
4.2 请求头:藏在细节里的魔鬼
请求头是键值对,一行一个,冒号分隔。别看它们不起眼,很多安全问题和兼容性问题都出在这里。我挑几个最常用的讲讲。
4.2.1 Host:虚拟主机的身份证
HTTP/1.1强制要求必须带Host头。为什么?因为一台服务器可能托管了十几个网站,Host告诉服务器你要访问的是哪个域名。我曾经遇到一个案例,客户说网站打不开,我抓包一看,Host头写的是IP地址而不是域名,服务器直接返回了默认站点——根本不是他要的那个。
4.2.2 User-Agent:浏览器的自我介绍
这个头告诉服务器你用的是Chrome还是Safari,是手机还是电脑。有些网站会根据User-Agent返回不同的页面。我记得有一次调试移动端接口,服务器死活不返回JSON数据,最后发现是User-Agent没带移动端标识,服务器以为你是PC浏览器,直接返回了HTML页面。
4.2.3 Accept:客户端想要什么格式
Accept头告诉服务器:我能接受哪些内容类型。比如 Accept: text/html, application/json 表示HTML和JSON都行。如果服务器返回了你不接受的内容类型,客户端就会报错。嗯,这个坑我踩过——写爬虫时忘了加Accept头,服务器返回了XML,解析器直接崩溃。
| 请求头 | 作用 | 常见值 |
|---|---|---|
| Host | 指定目标主机和端口 | www.example.com:8080 |
| User-Agent | 标识客户端软件信息 | Mozilla/5.0 (Windows NT 10.0; Win64; x64) |
| Accept | 告知可接受的响应类型 | text/html, application/json, */* |
| Accept-Language | 告知可接受的语言 | zh-CN, en-US;q=0.9 |
| Cookie | 携带会话状态信息 | sessionid=abc123 |
| Referer | 标识请求来源页面 | https://www.google.com/ |
避坑指南:我曾经在调试一个API时,发现服务器总是返回403。抓包看了半天,才发现是Referer头被浏览器自动带上了内网地址,服务器做了防盗链校验。所以,遇到权限问题时,先检查请求头里的Referer和Cookie。
4.3 空行:不起眼但绝对不能少
空行就是一对CRLF(回车换行),在Wireshark里看起来就是一行空白。它的作用很简单:分隔请求头和请求体。没有这个空行,服务器就不知道请求头什么时候结束,会把请求体的内容也当成请求头来解析。
你想想看,如果服务器把请求体里的数据当成请求头来解析,那结果会怎样?轻则解析失败返回400,重则引发安全漏洞。所以,写代码构造HTTP请求时,千万别忘了这个空行。
Wireshark观察技巧:在Packet Details面板里展开HTTP协议层,你会看到 [Request line]、[Request headers]、[Empty line] 和 [Request body] 四个部分。空行在十六进制视图里就是 0d 0a 0d 0a(两个CRLF)。
4.4 请求体:POST请求的灵魂
GET请求没有请求体,参数都写在URI里。POST、PUT、PATCH这些方法才有请求体。请求体里放的是你要提交的数据,比如表单内容、JSON字符串、文件二进制数据等。
请求体的格式由请求头里的 Content-Type 决定:
- application/x-www-form-urlencoded:表单数据,键值对形式,如
username=admin&password=123 - application/json:JSON格式,如
{"username":"admin","password":"123"} - multipart/form-data:文件上传,包含边界分隔符
- text/plain:纯文本
我记得有一次排查一个文件上传失败的问题,抓包发现请求体里的二进制数据被截断了,原因是Content-Length头写的是1000,但实际只发了500字节。服务器等不到剩下的数据,直接超时了。所以,Content-Length一定要和实际请求体长度一致。
我的经验:在Wireshark里看请求体,右键选择"Follow TCP Stream"就能看到完整的请求内容。如果请求体是二进制数据,Wireshark会显示成乱码,这时候可以看十六进制视图。我个人习惯先看Content-Type,再看Content-Length,最后看实际数据,三步确认请求体是否完整。
4.5 实战:在Wireshark里看一个完整的HTTP请求
光说不练假把式。咱们打开Wireshark,抓一个实际的HTTP请求看看。假设你访问了 http://www.example.com,过滤条件用 http.request,双击一个GET请求包,在Packet Details里你会看到:
Hypertext Transfer Protocol
GET / HTTP/1.1\r\n
Host: www.example.com\r\n
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36\r\n
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8\r\n
Accept-Encoding: gzip, deflate\r\n
Connection: keep-alive\r\n
\r\n
[Full request URI: http://www.example.com/]
[HTTP request 1/1]
[Response in frame: 12]
看到没?请求行、请求头、空行,清清楚楚。因为是GET请求,所以没有请求体。如果是POST请求,空行后面就会跟着数据。
核心总结:HTTP请求报文就是四层结构——请求行告诉服务器做什么,请求头告诉服务器怎么做,空行告诉服务器头结束了,请求体告诉服务器具体数据。每一层都有它的作用,少一个都不行。下次抓包时,你可以按这个结构去拆解,保证一眼就能看出问题出在哪。