主题
界面
主题
《TCP 与 UDP》讲了传输层;本节讲应用层最重要的协议:
HTTP: 明文文本协议,简单但"谁都能看" ✗
HTTPS:HTTP + TLS,加密 + 完整性 + 身份验证 ✓✓★ 本节用 JDK 自带的 HttpServer 起真实服务 ✓ ——并用自签名证书起一个真实的 HTTPS 服务 ✓✓
HttpClient 吗?(T2-005)✓先看一个"只记 200 和 404"的代价 ✗:
HttpResponse<String> response = client.send(request, BodyHandlers.ofString());
if (response.statusCode() == 200) {
return parse(response.body());
}
// 其他情况一律重试 —— 包括 400(参数错)、401(未认证)、403(无权限)
retry(); // ✗ 白重试三次,还放大故障★ 本节的实测会告诉你 4xx 与 5xx 的区别有多重要 ✓:
GET /api/orders/ORD-1 → 200
POST /api/orders → 201
GET /api/orders/ORD-999 → 404(RESOURCE_NOT_FOUND)
DELETE /api/orders/ORD-1 → 405(METHOD_NOT_ALLOWED)★ 关键结论 ✓✓:
4xx 是"客户端错",重试没用(参数错、没权限,重试一百次还是错) ✗
5xx 是"服务端错",可以重试(可能只是瞬时过载) ✓
混淆这两者,会让故障被重试放大 ✗✗
请求 ✓:
GET /api/orders/ORD-1 HTTP/1.1 ← 请求行:方法 + 路径 + 版本
Host: 127.0.0.1:1138 ← 头:HTTP/1.1 必需
User-Agent: demo
Accept: application/json
← 空行:头结束
{ "body": "..." } ← 体(GET 通常没有)响应 ✓:
HTTP/1.1 200 OK ← 状态行:版本 + 状态码 + 原因
Content-Type: application/json; charset=UTF-8
Content-Length: 31 ← ★ 体有多少字节(必需)
← 空行
{ "orderNo": "ORD-1" } ← 体★ 四个必需或高频的头 ✓✓:
| 头 | 作用 |
|---|---|
Host ★★★ | HTTP/1.1 必需 ——一个 IP 上跑多个站点靠它区分 ✓ |
Content-Type ★★★ | 这段字节是什么(json / 表单 / 图片)✓ |
Content-Length ★★★ | 体有多少字节 ——否则对方不知道读到哪算完 ✓ |
Connection ★ | keep-alive(复用连接)/ close(用完就断)✓ |
★ 另外四个"业务相关"的头 ✓:
| 头 | 用途 |
|---|---|
Authorization ★★ | 认证凭据(明文 HTTP 下会被直接看到 ✗) |
Cookie / Set-Cookie ★★ | 会话保持 ✓ |
Cache-Control ★ | 缓存策略(max-age / no-store)✓ |
X-Forwarded-For ★★ | 经过代理时携带真实客户端 IP(但可伪造 ⚠️) |
| 分组 | 含义 | 能重试吗 |
|---|---|---|
| 2xx ★ | 成功(200 / 201 创建 / 204 无内容)✓ | — |
| 3xx ★ | 重定向(301 永久 / 302 临时 / 304 未修改) | 看情况(客户端自动跟随) |
| 4xx ★★★ | 客户端错(400 参数 / 401 未认证 / 403 无权限 / 404 不存在 / 409 冲突 / 429 限流) | 不能重试 ✗✗ |
| 5xx ★★★ | 服务端错(500 内部错 / 502 网关错 / 503 不可用 / 504 超时) | 可以重试(带退避)✓ |
★ 两个容易被忽略的例外 ✓✓:
| 状态码 | 为什么可以重试 |
|---|---|
| 429 Too Many Requests ★★ | 它是"限流",等一会儿就能过 ——但必须按 Retry-After 头等待 ✓ |
| 408 Request Timeout | 客户端超时,可以重试 ✓ |
★ 重试必须遵守三条 ✓✓:
① 只重试"幂等"的操作(GET / PUT / DELETE;POST 通常不幂等)✗
② 必须带"退避"(1s → 2s → 4s),不能立即重试(否则形成重试风暴)✗✗
③ 必须有次数上限 + 熔断(避免一直重试一个已经挂掉的服务)✓★ 第三条的后果 ✓:重试风暴会把一个"有点慢"的服务直接压死 ✗✗(《Sentinel 限流与熔断》 会展开熔断)
本节的实测(自签名证书的真实握手):
GET https://127.0.0.1:1141/api/secure
→ 200 {"ssl":true,"msg":"这响应用 TLS 加密过"}
HTTPS 请求耗时(含 TLS 握手):434 ms★ HTTPS 只做三件事 ✓✓:
| # | 做的事 | 防的是 |
|---|---|---|
| ① 加密 ★★★ | 中间人看不到内容 ✓ | 窃听 ✓ |
| ② 完整性 ★★★ | 内容被改过能发现 ✓ | 篡改 ✓ |
| ③ 身份验证 ★★★ | 确认对端真的是它声称的站点(靠证书)✓ | 中间人冒充 ✓✓ |
★★ 第三条最容易被忽略 ✓✓:
没有身份验证,加密毫无意义 ✗
因为你可以和"中间人"建立一条完美的加密连接,而中间人再和真实站点建立另一条 ✓
你看到的锁头图标是真的,但对面不是你以为的那个人 ✗✗
★ TLS 握手做了什么(简化版) ✓:
① 客户端:我要连 example.com(并列出我支持的加密套件)
② 服务端:这是我的证书(含公钥,由 CA 签名)
③ 客户端:验签证书链 → 确认"这个公钥属于 example.com" ✓
④ 双方:用非对称算法协商出"本次会话的对称密钥"
⑤ 之后:全部数据用对称加密(AES)传输 ✓✓★ 为什么"先非对称、后对称" ✓✓:
非对称加密很慢,但能安全地交换密钥;对称加密很快,适合传数据 ✓
所以用前者"协商",用后者"传输" ——这叫混合加密 ✓✓(《网络安全基础》 有实测)
实测里的关键细节 ✓:
本节的 HTTPS 服务用的是"自签名证书",所以客户端必须显式信任它 ✓
而真实网站不需要你手动信任 ——因为它的证书由受信任的 CA 签发,而 JDK 内置了这些 CA 的根证书 ✓✓
★ 信任链的形态 ✓:
根 CA(JDK/浏览器内置)→ 中间 CA → 站点证书(example.com)★ 三个现实问题 ✓:
| 问题 | 说明 |
|---|---|
| 证书过期 ★★ | 最常见的线上事故之一 ✗ ——到期当天全站不可用 ✗✗ |
| 证书域名不匹配 ★★ | 用 IP 访问、或用错的域名 → 校验失败 ✓ |
| 自签证书在内网 ⚠️ | 必须把自签 CA 加进客户端的 truststore(否则要"信任所有",那是危险写法)✗ |
★★ 一条重要的安全提醒 ✓✓:
TrustManager写成"信任所有证书",等于关掉身份验证 ✗✗本节的代码里明确标注了"仅演示用,生产绝不要这么写" ✓
因为那会让中间人攻击变得毫无门槛 ✗
★ 证书排障的两个命令 ✓:
# 看证书链与有效期
openssl s_client -connect example.com:443 -showcerts
# 用 Java 自带工具看
keytool -printcert -sslserver example.com:443| 版本 | 关键改进 | 遗留问题 |
|---|---|---|
| HTTP/1.0 | 每次请求一个连接 ✗ | 建连开销大 |
| HTTP/1.1 ★★ | keep-alive 复用连接 ✓ | 队头阻塞(《TCP 与 UDP》)✗ |
| HTTP/2 ★★ | 多路复用(一个连接跑多个请求) ✓、头部压缩 | TCP 层队头阻塞仍在 ⚠️ |
| HTTP/3 ★★ | 基于 QUIC(UDP) ——缓解队头阻塞、建连更快 ✓✓ | 生态仍在演进 ⚠️ |
★ 服务端的现实 ✓:HTTP/1.1 仍是主流,HTTP/2 在网关层普遍开启,HTTP/3 多在 CDN 上 ✓
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
switch (response.statusCode() / 100) {
case 2 -> handleSuccess(response);
case 4 -> throw new ClientException(response.statusCode(), response.body()); // ✗ 不重试
case 5 -> { if (attempt < maxAttempts) retryWithBackoff(); } // ✓ 可重试
default -> throw new IllegalStateException("未预期状态码:" + response.statusCode());
}// 按 Retry-After 决定等多久
String retryAfter = response.headers().firstValue("Retry-After").orElse(null);
// 按 Content-Type 决定怎么解析
String contentType = response.headers().firstValue("Content-Type").orElse("");
if (contentType.contains("application/json")) { parseJson(response.body()); }== 200 见第 4 节 ✓
代码:labs/cs-basics/src/main/java/cn/kb/learn/cs/net/HttpLesson.java
运行:
cd labs
.\mvnw.cmd -pl cs-basics compile
java "-Dstdout.encoding=UTF-8" -cp cs-basics/target/classes cn.kb.learn.cs.net.HttpLesson实测输出(节选):
===== 1. HTTP 基础:方法、状态码、头 =====
GET /api/orders/ORD-1 → 200 {"orderNo":"ORD-1","product":"机械键盘"}
POST /api/orders → 201 {"created":true,...}
GET /api/orders/ORD-999 → 404 {"code":"RESOURCE_NOT_FOUND"}
DELETE /api/orders/ORD-1 → 405 {"code":"METHOD_NOT_ALLOWED"}
===== 3. HTTPS:自签名证书的真实握手 =====
GET https://127.0.0.1:1141/api/secure
→ 200 {"ssl":true,"msg":"这响应用 TLS 加密过"}
HTTPS 请求耗时(含 TLS 握手):434 ms★ 三条可落地的结论 ✓:
| # | 结论 | 依据 |
|---|---|---|
| ① | 按 4xx/5xx 分流重试策略 | 4xx 重试无用,5xx 才值得 ✓ |
| ② | 重试必须带退避与上限 | 否则放大故障 ✗ |
| ③ | 凭据必须走 HTTPS;不要信任所有证书 | TLS 的第三件事是身份验证 ✓ |
curl -v 观察完整报文:对比自己代码发出的头 ★;curl -w 拆分 HTTPS 各阶段耗时:观察 TLS 握手占多少 ★★;keytool -printcert -sslserver 看真实证书链:观察有效期与签发者 ✓;-validity 1)并等到过期:观察客户端报错 ⚠️;| # | 错误 | 说明 |
|---|---|---|
| 1 | 只判断 == 200 ✗✗ | 忽略 201/204 与错误分流(5.2)✗ |
| 2 | 对 4xx 重试 ✗✗ | 白重试并放大故障 ✓ |
| 3 | 重试不带退避 ✗✗ | 重试风暴 ✓ |
| 4 | 对非幂等的 POST 盲目重试 ✗✗ | 可能重复下单 ✓ |
| 5 | TrustManager 信任所有证书 ✗✗ | 等于关掉身份验证 ✓✓ |
| 6 | 忽略证书过期 ✗✗ | 到期当天全站不可用 ✓ |
| 7 | 把 X-Forwarded-For 当真 ✗ | 可伪造,必须只信任已知代理设置的值 ✓ |
目标:把"状态码分流"与"TLS 的三件事"变成实操。
步骤:
HttpLesson,记录 200/201/404/405 与 HTTPS 的耗时 ✓;keytool 生成一个新证书(不同 CN),观察客户端校验失败 ★;curl -w 对比 HTTP 与 HTTPS 的阶段耗时 ✓。验收方式:能说出"哪些状态码该重试、退避策略是什么"。
扩展任务:做一个"降级开关":
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[]{trustAllManager}, new SecureRandom());这段代码有什么风险? 5. (编程)实现一个带退避的重试器:只对 5xx 重试,最多 3 次,间隔 1s/2s/4s。
《DNS、IP、端口与 ARP》:请求发出之前,还要先"找到对方"。