02 · HTTP 协议详解(HTTP in Depth)

📅 预计 45 分钟 | ⭐ = 高频考点 | 📌 中英术语见文末
✍️ 配套练习:网页 quiz shengxia.dev/java?module=02


2.1 HTTP 报文结构

快递单:面单 + 内件

收快递时,包裹外面贴着一张面单(寄件人、收件人、物品名、订单号),里面才是真正的内件(你买的东西)。HTTP 报文的结构和快递完全一样:最前面是"面单"(起始行 + 头部),空一行之后是"内件"(主体)。起始行、头部、空行、主体四个部分缺一不可,任何一个部分出问题,对方都读不懂你的"信"。

一个原始 HTTP 请求报文长这样:

POST /user/login HTTP/1.1          ← 起始行:方法 + 路径 + 版本
Host: www.example.com               ← 请求头:键: 值
Content-Type: application/json
                                    ← 空行:头部到此为止
{"username": "tom", "password": "123"}   ← 请求体

一个原始 HTTP 响应报文长这样:

HTTP/1.1 200 OK                    ← 起始行:版本 + 状态码 + 原因短语
Content-Type: text/html; charset=utf-8   ← 响应头
                                    ← 空行
<html><body>登录成功</body></html>  ← 响应体

注意那个空行:它是头部和主体的分界线,绝对不能省。服务器解析报文时,先读到空行认为"头部读完了",空行之后的内容才当成主体。如果漏掉空行,整个报文会被当成头部解析,直接报错。

💡 记忆口诀:报文 = 面单 + 空行 + 内件。起始行是"谁给谁什么",头部是附带说明,空行是分界线,主体是真正的东西。

报文的四个部分

部分 请求里的角色 响应里的角色
起始行 方法 + URL + 版本 版本 + 状态码 + 原因短语
头部(Header) 说明请求的附加信息 说明响应的附加信息
空行 分隔头部与主体 分隔头部与主体
主体(Body) 提交的数据(POST 才有) 真正的网页 / 文件内容

注意请求和响应起始行的顺序正好相反:请求是"方法 + 地址 + 版本",响应是"版本 + 状态码 + 说明"。为什么?因为请求要告诉服务器"我想干嘛",响应要告诉浏览器"结果如何"——各自最关心的信息放在最前面,解析起来最快。

⚠️ 常见错误

  1. 漏掉空行:自己拼报文时忘了头部和主体间的空行,服务器报 400 Bad Request。为什么会这样?空行是头部的结束标志,没有它服务器不知道头部什么时候结束。怎么避免?写调试代码或抓包分析时,记住"头部之后必然有一个空行"。
  2. 头部写成中文冒号:HTTP 头必须是英文 键: 值(英文冒号 + 空格),用中文":"或漏掉空格解析都会失败。为什么?HTTP 协议规范里规定分隔符是 ASCII 冒号,中文冒号是两个字节,完全不匹配。怎么避免?所有请求头都用英文标点。
  3. 以为请求必须有主体:GET 请求一般没有主体,参数放 URL 里;只有 POST/PUT 才有主体。为什么容易错?因为表单提交 POST 时经常有主体,初学者就以为所有请求都有。怎么判断?看方法:GET 主体通常为空,POST/PUT 才有。

2.2 请求方法:GET / POST / PUT / DELETE ⭐

图书馆借阅:读、借、还、续期

把服务器上的资源想成图书馆的书,四种常用方法就是四种不同操作:GET 是翻书看内容(不借走)、POST 是把一本新书登记进馆、PUT 是把书架上某本书整个换掉、DELETE 是把某本书撤出图书馆。想清楚"这次操作是读、是加、是整体换、还是删",你自然就选对了方法。

方法 语义 生活类比 典型场景 是否有主体
GET 读取资源 翻书看 打开网页、搜索、看文章 通常无
POST 创建资源 登记新书 注册账号、发表评论、提交表单 有
PUT 整体替换 换掉整本书 更新整个用户资料 有
DELETE 删除资源 撤掉书 删除订单、注销账号 通常无

这四个方法对应着对资源的四种基本操作,业界称为 CRUD(Create 增、Read 读、Update 改、Delete 删)。写接口时把 CRUD 和这四个方法对应起来,接口风格就统一了,别人一看就懂。

GET 与 POST 的本质区别(常考)

GET 和 POST 是课程里最常被对比的一对,从四个维度记牢:

  1. 语义:GET 是"取",不应有副作用(多取几次结果一样);POST 是"送",让服务器干活、改数据。
  2. 参数位置:GET 参数放在 URL 的查询字符串里(?name=tom),会暴露在地址栏、留在历史记录里;POST 参数放在请求体里,不显示在地址栏。
  3. 数据大小:URL 长度有限制(浏览器和服务器一般约定几 KB),GET 传不了大内容;POST 用请求体,能传大文件、大文本。
  4. 安全性:两者本身都不加密(要加密得上 HTTPS)。但 GET 参数会进服务器日志和浏览器历史,敏感信息绝不能用 GET 传。

用 Java 伪代码体会服务器如何区分这两个方法:

// 伪代码:服务器根据方法决定走"读"还是"写"
void service(请求 req, 响应 resp) {
    String 方法 = req.getMethod();          // 拿到 "GET" 或 "POST"
    if ("GET".equals(方法)) {
        查数据库(req);                       // GET:只读,不修改数据
        resp.setStatus(200);
    } else if ("POST".equals(方法)) {
        写入数据库(req);                     // POST:带新数据去改动
        resp.setStatus(201);                 // 创建成功用 201 更贴切
    }
}

💡 记忆口诀:GET 是"看"、POST 是"干"。看的东西地址栏能看见,干的事藏进请求体。PUT 整体换、DELETE 整个删。

⚠️ 常见错误

  1. 用 GET 传密码:密码会暴露在地址栏、浏览器历史、服务器日志里,等于写在明处。为什么危险?日志一旦泄露,密码全跟着泄露。怎么避免?登录、支付等敏感操作一律用 POST。
  2. 以为 POST 比 GET 安全:POST 只是不显示在地址栏,抓包照样能看到明文。为什么容易误解?因为地址栏看不见,给人"隐藏"的错觉。怎么避免?真正的安全靠 HTTPS 加密,而不是换方法。
  3. GET 传超长内容:把一大段文章塞进 URL,超过长度限制直接被服务器拒绝或截断。为什么?URL 本身有长度上限,浏览器和服务器都会卡。怎么避免?大内容用 POST 放请求体。
  4. POST 提交后刷新重复提交:POST 提交后按 F5,浏览器会提示"要重新提交表单吗",点确认可能产生重复数据。为什么?浏览器重放上一次的请求,而 POST 是有副作用的。怎么避免?服务端做好幂等处理,或提交成功后重定向到结果页(PRG 模式)。

2.3 状态码:1xx - 5xx ⭐

考试成绩单:数字对号入座

状态码是服务器在响应里告诉浏览器"这单处理得怎么样",就像成绩单上的等级:A 优秀、C 及格、F 挂科。HTTP 把结果分成五大类,第一位数字决定类别:

分类 含义 一句话理解
1xx 信息 已收到,继续处理(很少见)
2xx 成功 搞定,这是结果
3xx 重定向 别停这,去别处
4xx 客户端错误 你(浏览器)那边有问题
5xx 服务器错误 我(服务器)这边出事了

常见状态码逐个记

状态码 含义 生活类比
200 成功,一切正常 取餐成功
301 / 302 永久 / 临时重定向 店铺永久搬家 / 临时搬走
304 资源没变,用本地缓存 东西没变,不用再拿一份
400 报文格式不对 你说话我没听懂
401 / 403 未认证(没登录)/ 无权限 没带证件 / 证件有了但没资格
404 资源不存在 店里没有这道菜
500 / 502 服务器异常 / 网关拿不到上游响应 后厨起火 / 外卖平台联系不上餐厅

3xx 值得单独说:301 是"永久搬家",浏览器会把书签里的地址自动更新成新地址;302 是"临时搬走",这次去新地址,下次还走老地址也没问题。304 和它们不一样,它是"内容没变",让浏览器直接用本地缓存,省流量、省时间,是性能优化的常用手段。

💡 记忆口诀:4 打头怪自己,5 打头怪服务器。看到 404 先检查 URL 和文件路径,看到 500 去翻后端日志。

⚠️ 常见错误

  1. 把 401 和 403 混了:401 是"没凭证"(没登录),403 是"有凭证但没权限"(登录了也不让看)。为什么容易混?两个都是"拒绝访问"。怎么区分?先问一句"对方知道我是谁吗"——不知道就是 401,知道但拒绝就是 403。
  2. 看到 500 只刷新浏览器:500 是服务器程序抛异常,刷新没用。为什么?异常发生在服务器内部,浏览器再怎么重试都一样。怎么避免?去服务器日志(Tomcat 的 logs/catalina.out)里找异常堆栈,定位是哪行代码抛的。
  3. 忘记 304 的含义:304 不是错误,是"用缓存"。为什么容易误判?看到不是 2xx 就紧张。怎么理解?响应里没有主体,浏览器用本地副本,这正是缓存命中的标志,是好事。

2.4 常见请求头与响应头

快递面单的"备注栏"

快递面单除了地址还有"备注栏":加急、易碎、到付。HTTP 头部就是报文的"备注栏",一行一个 键: 值,给对话双方补充说明,不参与正文。头部有成百上千种,但常考的就那么几个,逐个记牢。

常用请求头(浏览器发给服务器的说明):

请求头 作用 例子
Host 告诉服务器访问的是哪个域名(必须有) Host: www.example.com
User-Agent 我是哪种浏览器 / 设备 User-Agent: Mozilla/5.0
Content-Type 请求体的类型(POST 提交时尤其重要) Content-Type: application/json
Accept 我希望能收到什么类型的响应 Accept: text/html, application/json
Cookie 把我以前存的"小纸条"带给服务器 Cookie: sessionId=abc123

常用响应头(服务器给浏览器的说明):

响应头 作用 例子
Content-Type 响应体是什么类型、什么编码 Content-Type: text/html; charset=utf-8
Content-Length 响应体有多少字节 Content-Length: 58
Set-Cookie 让浏览器存一张"小纸条"(Cookie) Set-Cookie: sessionId=abc123; Max-Age=3600
Location 配合 3xx 重定向,告诉浏览器"去这个新地址" Location: /login.html

为什么 Content-Type 请求头和响应头都要看:请求里的告诉服务器"我发给你的是 JSON 还是表单";响应里的告诉浏览器"返回的是 HTML 还是图片、用什么编码"。中文乱码十有八九是响应头没写 charset=utf-8——服务器返回的中文是 UTF-8 编码,浏览器却用别的编码去解读,自然变乱码。

💡 记忆口诀:请求头是"我的请求说明",响应头是"我的响应说明";Content-Type 是双方报格式、报编码的窗口,中文乱码先看它。

⚠️ 常见错误

  1. 请求头漏写 Host:HTTP/1.1 规定 Host 必须存在,漏了会直接报 400。为什么?一台服务器上可能托管几十个网站,靠 Host 区分要访问哪一个。怎么避免?浏览器会自动带,但用 curl 或手写请求时别漏。
  2. Content-Type 和实际内容对不上:声明了 application/json 却发 a=1&b=2 这种表单格式,服务器按 JSON 解析直接报错。为什么?服务器按头部声明的类型解析主体。怎么避免?发什么格式就声明什么类型,前后保持一致。
  3. 响应头忘写 charset=utf-8:中文全部变成问号或乱码。为什么?浏览器不知道用什么编码解读,只能用默认编码(可能是 GBK 或 ISO-8859-1)。怎么避免?setContentType 里带上 ; charset=utf-8。

2.5 无状态协议、Keep-Alive、HTTPS

无状态:快餐店不记得你上次点过什么

HTTP 是无状态协议(stateless):服务器处理完一个请求就"失忆"了,不记得你上次来过、点过什么。就像快餐店排队——你上回点了什么,店员这次完全不知道,每次都是从头来。这个设计让服务器不用为每个用户维护记忆,架构简单、能扛并发。

但"失忆"带来了现实问题:购物车怎么记住?登录状态怎么保持?于是有了 Cookie:服务器通过 Set-Cookie 给你一张"小纸条",你下次带着它来,它就知道"哦,是你"。一来一回的过程:

第一次:浏览器 →(无 Cookie)→ 服务器
        服务器 → Set-Cookie: sessionId=abc → 浏览器存下纸条
第二次:浏览器 → Cookie: sessionId=abc → 服务器认出你了

注意两个头方向相反:Set-Cookie 是响应头(服务器给浏览器),Cookie 是请求头(浏览器还给服务器),一存一取,正好闭环。

💡 记忆口诀:无状态 = 答完就失忆;Cookie = 失忆后的"小纸条"。Set-Cookie 存、Cookie 取,方向别记反。

Keep-Alive:长连接省得反复握手

HTTP/1.1 默认开启 Keep-Alive(长连接)。一个网页要加载 HTML、CSS、图片、JS 十几个文件,如果每个请求都"新建连接 → 用完断开",光建立连接的三次握手开销就够受的。Keep-Alive 让一条连接保持一段时间,多个请求共用它,像打一个电话说好多件事,省去反复拨号的成本。

对比 短连接 Keep-Alive 长连接
连接数 每个请求都新开 一条连接复用
效率 低,握手开销大 高,省握手
服务器压力 连接频繁建立销毁 连接保持占用资源
适用场景 请求量极少的场景 网页这种多资源场景

HTTP 与 HTTPS:加了一把锁

HTTPS = HTTP + TLS/SSL 加密。HTTP 的报文是明文,在公共 Wi-Fi 上,中间人(同一个网络里的攻击者)能直接看到你发的密码;HTTPS 在 HTTP 外面套了一层加密,报文变成密文,中间人看到的是乱码,只有服务器能解开。

对比 HTTP HTTPS
默认端口 80 443
传输 明文 加密(TLS/SSL)
证书 不需要 需要 CA 签发的证书
适用场景 不敏感页面 登录、支付等敏感场景

除了加密,HTTPS 还有一层"身份验证":证书由 CA 机构签发,浏览器验证证书有效才继续。这就好比寄信时不仅要密封,还要确认收件人身份,双保险。

💡 记忆口诀:HTTP 是明信片(谁都能读),HTTPS 是密封信(只有收件人能拆)。默认端口一个 80 一个 443。

⚠️ 常见错误

  1. 把无状态理解成"服务器没内存":无状态指"服务器不主动记你的上次请求",不是服务器不能存数据,数据库、文件系统照常存。为什么容易错?"无状态"这个词太容易望文生义。怎么避免?记住"无状态"针对的是"请求与请求之间不自动关联",而不是"不能存数据"。
  2. 混淆 Cookie 和 Set-Cookie:Cookie 是请求头(浏览器带给服务器),Set-Cookie 是响应头(服务器让浏览器存)。为什么容易混?两个单词太像。怎么避免?记"服务器 Set(设置),浏览器 Cookie(携带)",方向是反的。
  3. 以为长连接永不关闭:Keep-Alive 有超时时间,闲置久了服务器也会断。为什么?连接长期占用不释放会耗尽服务器资源。怎么避免?把超时时间设置得合理,别设成无限大。
  4. 以为 HTTPS 会让网站慢很多:TLS 握手确实有开销,但现代硬件和会话复用机制下影响很小。为什么有人担心?早期 HTTPS 确实有明显开销,传言流传至今。怎么避免?按需求选型,敏感业务直接上 HTTPS,别为省这点时间牺牲安全。

📌 双语术语表(本讲)

中文 English 记忆点
报文 Message HTTP 对话的"信"
起始行 Start Line 报文的"标题"
头部 Header 报文的"备注栏"
请求体 / 响应体 Request / Response Body POST 送的数据 / 返回的内容
请求方法 Request Method 对资源的操作
状态码 Status Code 处理结果的三位数字
重定向 Redirect 3xx:去别处拿
客户端错误 Client Error 4xx:怪请求方
服务器错误 Server Error 5xx:怪服务器
无状态 Stateless 答完就失忆
长连接 Keep-Alive 一条连接多用几次
安全超文本传输协议 HTTPS HTTP + 加密
Cookie Cookie 服务器发的"小纸条"
明文 Plaintext 没加密的内容,谁都能读

⭐ 本讲考点清单

  1. HTTP 报文四部分:起始行、头部、空行、主体,空行是头与主体的分界线。
  2. GET 用于读取、参数放 URL、无主体;POST 用于提交、参数放请求体、能传大内容。
  3. 四个方法对应 CRUD:GET 读、POST 增、PUT 改、DELETE 删。
  4. GET 和 POST 都不加密,真正保安全的是 HTTPS,不是换方法。
  5. 状态码按首位分类:1xx 信息、2xx 成功、3xx 重定向、4xx 客户端错误、5xx 服务器错误。
  6. 高频状态码 200/301/302/304/400/401/403/404/500/502 的含义要能说出。
  7. 401 是"没登录",403 是"登录了没权限",两者不要混淆。
  8. 请求头 Host、Content-Type、Cookie 和响应头 Set-Cookie、Location 的用途要分清。
  9. Content-Type 决定响应编码,中文乱码先检查是否写了 charset=utf-8。
  10. HTTP 是无状态协议,靠 Cookie 携带"小纸条"让服务器认人。
  11. Keep-Alive 长连接让多个请求共用一条连接,减少握手开销。
  12. HTTPS = HTTP + TLS/SSL 加密,默认端口 443,需要 CA 证书。