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. 混淆 CookieSet-CookieCookie 是请求头(浏览器带给服务器),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. 请求头 HostContent-TypeCookie 和响应头 Set-CookieLocation 的用途要分清。
  9. Content-Type 决定响应编码,中文乱码先检查是否写了 charset=utf-8
  10. HTTP 是无状态协议,靠 Cookie 携带"小纸条"让服务器认人。
  11. Keep-Alive 长连接让多个请求共用一条连接,减少握手开销。
  12. HTTPS = HTTP + TLS/SSL 加密,默认端口 443,需要 CA 证书。