答案:HTTP消息格式与报文结构
为什么 HTTP 报文要设计成"首部 + 空行 + 主体"的三段式?如果去掉空行直接拼接会有什么后果?
设计原因:
HTTP 报文采用"起始行 + 首部 + 空行 + 主体"的结构,本质上是为了让接收方能够明确区分"元数据"和"实际数据"。
首部和主体是两种完全不同的信息:
- 首部(Header):是航仔和凌叔之间的"控制指令",比如
Content-Type: application/json告诉对方主体是什么格式,Content-Length: 256告诉对方主体有多少字节。 - 主体(Body):是实际要传输的内容,比如波比提交的报名表单数据、星宇查询到的航班列表。
空行(\r\n\r\n,即两个 CRLF)是一个明确的边界标记。HTTP 解析器读到空行时,就知道"首部结束了,接下来全是主体"。这类似于快递单和包裹之间用胶带隔开,快递员一看就知道哪里是地址信息、哪里是货物。
去掉空行的后果:
如果直接拼接,解析器无法判断首部在哪里结束。例如:
POST /api/register HTTP/1.1\r\n
Host: feixiang.net\r\n
Content-Type: application/json\r\n
{"name":"波比","dept":"市场部"}
没有空行,Content-Type: application/json 后面的 {"name":"波比"...} 会被解析器误认为是又一个首部字段。但 {"name" 不是合法的首部格式(首部是 Key: Value 形式),解析器就会报错,请求直接失败。
更隐蔽的 bug 是:如果主体内容恰好包含类似 X-Custom: value 的字符串,没有空行做边界,这部分主体数据会被误解析为伪首部,导致波比的报名信息被篡改或丢弃。凌叔在调试代理服务器时,就遇到过这种因边界模糊导致的诡异问题。
# 用 telnet 手动发送 HTTP 请求,观察空行的作用
telnet feixiang.net 80
POST /api/test HTTP/1.1
Host: feixiang.net
Content-Length: 18
name=boy&age=25
# 上面的空行必不可少!
请求报文中的 Host 首部有什么作用?如果一台服务器托管了多个网站(虚拟主机),缺少 Host 会怎样?
Host 首部的作用:
Host 首部告诉服务器"我要访问的是哪个域名"。在 HTTP/1.1 中,Host 是唯一一个必须存在的请求首部。
飞翔公司的服务器资源由凌叔统一管理。为了节省成本,凌叔在一台物理服务器(IP 为 192.168.10.5)上托管了多个网站:
www.feixiang.net—— 飞翔公司官网(图妹维护)oa.feixiang.net—— 飞翔 OA 系统(星宇使用)hr.feixiang.net—— 人力资源系统(雁姐管理)
这三个域名都解析到同一个 IP。当浏览器向 192.168.10.5 发起连接时,服务器收到 TCP 连接后,必须靠 Host 首部来判断用户到底想访问哪个网站:
GET /index.html HTTP/1.1
Host: oa.feixiang.net
服务器一看 Host: oa.feixiang.net,就把请求路由到 OA 系统的代码;如果是 Host: www.feixiang.net,就返回官网页面。这就是基于域名的虚拟主机(Name-based Virtual Host)。
缺少 Host 的后果:
如果请求缺少 Host 首部,服务器无法判断用户目标。不同服务器行为不同:
- Nginx 会返回
400 Bad Request,直接拒绝。 - 某些老旧配置可能返回默认第一个虚拟主机的内容——波比想访问 OA 系统,却看到了官网首页,一脸懵。
- 如果默认站点恰好是内部系统,外部用户可能意外访问到敏感信息,造成安全隐患。
# 正确请求
curl -H "Host: oa.feixiang.net" http://192.168.10.5/login
# 缺少 Host —— Nginx 返回 400
curl http://192.168.10.5/login
# HTTP/1.1 400 Bad Request
凌叔在配置 Nginx 时,总会加一个默认 server 块返回 444(直接断开连接),防止无 Host 的请求落到任何实际业务上。
假设波比要提交一个活动报名表单,应该使用 GET 还是 POST?请求报文和响应报文分别长什么样?
应该使用 POST。
波比要报名参加飞翔公司年会,需要提交姓名、部门、是否带家属、饮食禁忌等信息。选择 POST 的理由:
- 数据量较大:GET 把参数放在 URL 中,有长度限制(通常 2KB~8KB),而波比的饮食禁忌可能写很长。
- 安全性:GET 参数暴露在 URL 中,会被浏览器历史、服务器日志、代理日志记录。波比的手机号如果放在 URL 里,凌叔查日志时都能看到,不合适。
- 语义正确:GET 表示"获取资源",POST 表示"提交数据创建/处理资源"。报名是"创建一条报名记录",用 POST 语义更准确。
请求报文示例:
POST /api/activity/register HTTP/1.1
Host: oa.feixiang.net
Content-Type: application/x-www-form-urlencoded
Content-Length: 78
Cookie: session_id=abc123; user=boy
name=%E6%B3%A2%E6%AF%94&dept=%E5%B8%82%E5%9C%BA%E9%83%A8&guest=1&diet=%E4%B8%8D%E5%90%83%E8%BE%A3
- 起始行:
POST /api/activity/register HTTP/1.1 - 首部:指定主机、内容类型(表单编码)、内容长度、Cookie(标识波比已登录)
- 空行:
\r\n\r\n - 主体:经过 URL 编码的表单数据,
%E6%B3%A2%E6%AF%94是"波比"的 UTF-8 编码
如果用 JSON 格式(更现代的做法):
POST /api/activity/register HTTP/1.1
Host: oa.feixiang.net
Content-Type: application/json
Content-Length: 95
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
{
"name": "波比",
"dept": "市场部",
"guest": true,
"diet": "不吃辣",
"submitter": "boy"
}
响应报文示例(报名成功):
HTTP/1.1 201 Created
Date: Mon, 15 Jan 2026 09:30:00 GMT
Content-Type: application/json
Content-Length: 120
Location: /api/activity/register/10086
{
"code": 0,
"message": "报名成功",
"data": {
"register_id": 10086,
"name": "波比",
"status": "已确认"
}
}
- 状态码
201 Created表示资源创建成功 Location首部指向新创建的报名记录地址- 波比收到响应后,前端页面可以展示"报名成功,您的报名号是 10086"
如果波比没填姓名就提交,响应可能是:
HTTP/1.1 400 Bad Request
Content-Type: application/json
{"code": 1001, "message": "姓名不能为空"}
# 用 curl 模拟波比提交报名
curl -X POST http://oa.feixiang.net/api/activity/register \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"name":"波比","dept":"市场部","guest":true,"diet":"不吃辣"}'