飞翔飞翔
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • 学习路径
  • HTTP 基础

    • 认识HTTP协议与应用层定位
    • HTTP消息格式与报文结构
    • HTTP请求方法与幂等性
    • HTTP状态码详解
  • 连接与缓存

    • HTTP持久连接与版本演进
    • HTTP缓存机制
  • 状态与协商

    • Cookie与Session状态管理
    • HTTP重定向与内容协商
    • HTTP条件请求与范围请求
  • 安全与加密

    • HTTP认证机制
    • HTTPS与TLS握手
  • 协议演进

    • HTTP2核心特性
    • HTTP3与QUIC
  • 架构与实战

    • HTTP代理服务器与Web缓存
    • HTTP常见攻击与防御
    • HTTP实践工具与抓包分析

答案: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 的理由:

  1. 数据量较大:GET 把参数放在 URL 中,有长度限制(通常 2KB~8KB),而波比的饮食禁忌可能写很长。
  2. 安全性:GET 参数暴露在 URL 中,会被浏览器历史、服务器日志、代理日志记录。波比的手机号如果放在 URL 里,凌叔查日志时都能看到,不合适。
  3. 语义正确: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":"不吃辣"}'