3. 连接管理与TCP握手代价

HTTP 请求报文,说白了就是客户端发给服务器的一封信。这封信有固定的格式,服务器拿到手就知道你想干嘛。我刚开始学 HTTP 的时候,总觉得报文结构很抽象,直到自己抓包看了几次,才真正理解——原来每一行都有它的道理。

一个完整的 HTTP 请求报文,由四部分组成:请求行、请求头、空行、请求体。缺一不可,顺序也不能乱。

核心要点:HTTP 请求报文 = 请求行 + 请求头 + 空行 + 请求体。空行是分隔符,告诉服务器“头部结束了,下面是正文”。

3.1 请求行:请求的第一句话

请求行只有一行,但信息量巨大。它包含三个要素:方法、URL、协议版本。格式如下:

GET /index.html HTTP/1.1
POST /api/login HTTP/2.0
PUT /user/profile HTTP/1.1

这三个部分用空格分隔,最后以回车换行(CRLF)结束。我见过不少新手在写自定义 HTTP 客户端时,忘了加空格或者用了 Tab,结果服务器直接返回 400 Bad Request。嗯,这个坑我踩过。

方法(Method)

方法就是你想对资源做什么操作。常用的有:

  • GET:获取资源,最常用。不修改服务器数据。
  • POST:提交数据,比如表单登录、上传文件。
  • PUT:更新资源,幂等的(多次调用结果一样)。
  • DELETE:删除资源。
  • PATCH:部分更新资源。
  • HEAD:只获取响应头,不获取响应体。常用于检查资源是否存在。
  • OPTIONS:询问服务器支持哪些方法。跨域请求时会用到。

我的经验:在实际项目中,很多人把 GET 和 POST 用混了。比如用 GET 提交登录表单,密码直接暴露在 URL 里,浏览器历史记录里一查就看到了。记住:GET 用于读取,POST 用于写入,这是基本原则。

URL(统一资源定位符)

URL 就是你要访问的资源地址。在请求行里,通常只写路径部分,不写协议和域名。比如:

GET /api/users?page=1&size=20 HTTP/1.1

这里 /api/users?page=1&size=20 就是 URL 路径加查询参数。完整的 URL 应该是 https://example.com/api/users?page=1&size=20,但协议和域名会放在请求头的 Host 字段里。

为什么会这样?因为 HTTP/1.1 支持虚拟主机,一台服务器可以托管多个域名。服务器需要根据 Host 字段来判断你要访问哪个网站。

协议版本

协议版本告诉服务器你用的是哪个 HTTP 版本。常见的有:

版本 特点 现状
HTTP/0.9 只有 GET 方法,没有请求头 已废弃
HTTP/1.0 支持多种方法,短连接 基本淘汰
HTTP/1.1 持久连接、管道化、Host 头 目前最常用
HTTP/2.0 多路复用、二进制分帧、头部压缩 逐渐普及
HTTP/3.0 基于 QUIC,使用 UDP 新兴标准

我个人习惯在开发新项目时优先使用 HTTP/2.0,性能提升很明显。但如果是维护老系统,HTTP/1.1 也完全够用。

3.2 请求头:告诉服务器你的“底细”

请求头是键值对,每行一个,格式是 字段名: 字段值。请求头提供了请求的元数据,比如客户端信息、内容类型、缓存策略等。

常见的请求头字段:

字段名 作用 示例
Host 指定目标服务器的主机名和端口 Host: www.example.com:8080
User-Agent 标识客户端软件信息 User-Agent: Mozilla/5.0 ...
Accept 告诉服务器客户端能接受的内容类型 Accept: text/html, application/json
Content-Type 请求体的媒体类型(POST/PUT 时使用) Content-Type: application/json
Content-Length 请求体的字节长度 Content-Length: 348
Authorization 身份认证信息 Authorization: Bearer <token>
Cookie 发送之前服务器设置的 Cookie Cookie: sessionId=abc123
Referer 当前请求的来源页面 URL Referer: https://www.google.com/

注意:我曾经在项目中遇到过一个诡异的问题——用户反馈某些页面加载不出来。排查了半天,发现是 User-Agent 字段被某个中间件修改了,导致后端无法正确识别客户端类型。所以,不要随意修改请求头,除非你清楚后果。

Host 字段:虚拟主机的关键

HTTP/1.1 强制要求必须携带 Host 字段。没有它,服务器不知道你要访问哪个网站。比如一台服务器上托管了 a.com 和 b.com,请求行里只有 GET /index.html,服务器怎么区分?靠的就是 Host 头。

Content-Type:告诉服务器你发的是什么

当你有请求体时(比如 POST 请求),Content-Type 是必须的。常见的值有:

  • application/json:JSON 数据
  • application/x-www-form-urlencoded:表单数据(键值对)
  • multipart/form-data:文件上传
  • text/plain:纯文本
  • application/xml:XML 数据

3.3 空行:不起眼但必不可少

空行就是一行空白,只有回车换行(CRLF)。它的作用很简单:分隔请求头和请求体。

服务器解析请求时,读到空行就知道头部结束了,接下来是请求体。如果没有这个空行,服务器会把请求体的内容也当成请求头来解析,结果就是解析失败。

你想想看,如果一个人写信,正文和落款之间没有空行,是不是容易看混?HTTP 也是这个道理。

3.4 请求体:GET 与 POST 的本质区别

请求体是可选部分,不是所有请求都有。GET 请求没有请求体,POST 请求有请求体。这是两者最直观的区别,但远不止于此。

GET 请求

  • 没有请求体,参数放在 URL 的查询字符串中。
  • 幂等的:多次请求结果一样,不会改变服务器状态。
  • 可缓存:浏览器和代理服务器可以缓存 GET 请求的结果。
  • 长度限制:URL 长度有限制(不同浏览器不同,通常 2048 字符左右)。
  • 安全性低:参数暴露在 URL 中,会被浏览器历史记录、服务器日志记录。

POST 请求

  • 有请求体,参数放在请求体中。
  • 非幂等的:多次请求可能产生不同结果(比如重复提交订单)。
  • 不可缓存:默认不缓存,除非响应头明确允许。
  • 无长度限制:理论上请求体可以很大(实际受服务器配置限制)。
  • 安全性高:参数不在 URL 中,不会出现在浏览器历史记录里。
对比维度 GET POST
请求体 无 有
参数位置 URL 查询字符串 请求体
幂等性 是 否
可缓存 是 否(默认)
长度限制 有(URL 长度限制) 无(理论上)
安全性 低(参数暴露) 高(参数在体内)
典型用途 获取数据、搜索、分页 提交表单、上传文件、创建资源

重要原则:GET 用于读取,POST 用于写入。这是 RESTful API 设计的基本规范。违反这个原则,不仅会让 API 难以理解,还可能带来安全问题。

3.5 完整请求报文示例

下面是一个完整的 POST 请求报文,我把它拆开来看:

POST /api/login HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: application/json, text/plain, */*
Content-Type: application/json
Content-Length: 45

{"username":"admin","password":"123456"}

注意看:

  • 第一行是请求行:方法 POST,路径 /api/login,版本 HTTP/1.1
  • 第 2-6 行是请求头:Host、User-Agent、Accept、Content-Type、Content-Length
  • 第 7 行是空行(这里看不到,但实际存在)
  • 第 8 行是请求体:JSON 格式的登录信息

如果是 GET 请求,报文会更短:

GET /api/users?page=1 HTTP/1.1
Host: example.com
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36
Accept: application/json

没有请求体,也没有 Content-Type 和 Content-Length。因为 GET 不需要这些。

3.6 知识体系总览

下面这张图把 HTTP 请求报文的结构和核心概念串起来了。你可以把它当作一个快速参考:

HTTP 请求报文结构图 HTTP 请求报文结构 请求行 方法 (GET/POST/PUT/DELETE...) + URL (/api/users?page=1) + 协议版本 (HTTP/1.1) 请求头(多个键值对) Host: example.com User-Agent: Mozilla/5.0 ... Accept: application/json, Content-Type: application/json ... 空行(CRLF)—— 分隔头部和正文 请求体(可选) GET:无请求体,参数在 URL 中 POST:有请求体,如 JSON: {"username":"admin","password":"123456"} 💡 空行是必须的!没有空行,服务器无法区分头部和正文,会解析失败。 💡 GET 没有请求体,POST 有请求体——这是两者最直观的区别。

3.7 避坑指南

最后,分享几个我在项目中踩过的坑:

  • 忘记空行:写自定义 HTTP 客户端时,忘了在请求头后面加空行,结果服务器一直等待请求体,超时返回 408。排查了半小时才发现少了一个 \r\n。
  • Content-Length 不匹配:请求体实际长度和 Content-Length 不一致,服务器要么截断数据,要么一直等待。我建议用框架自动计算,不要手动写。
  • GET 请求带请求体:虽然 HTTP 规范没有明确禁止,但很多服务器和代理会忽略 GET 的请求体。所以,别这么干。
  • Host 头缺失:HTTP/1.1 强制要求 Host 头,没有的话服务器可能返回 400。我见过有人用 HTTP/1.0 的客户端访问 HTTP/1.1 的服务器,结果请求失败。

我的建议:如果你刚开始学习 HTTP,最好的方法是用抓包工具(比如 Wireshark 或 Chrome 开发者工具)看真实的请求报文。看一次,比读十遍书都管用。