Skip to content

第41课:网络编程 - HTTP

🎯 学习目标

  • 掌握 HttpURLConnection(旧 API,JDK 自带)
  • 掌握 HttpClient(JDK 11+,现代推荐)
  • 实现 RESTful 调用(GET/POST/JSON)
  • 理解 HTTP 请求/响应结构、状态码、头部
  • 知道 OkHttp/Apache HttpClient 等第三方库

📖 一、概念讲解:HTTP 协议回顾

1. HTTP 请求/响应

请求:
  GET /api/users?id=1 HTTP/1.1     ← 请求行(方法 路径 版本)
  Host: example.com                 ← 请求头
  Accept: application/json
  <空行>
  <body>(GET 一般无)

响应:
  HTTP/1.1 200 OK                   ← 状态行(版本 状态码 原因)
  Content-Type: application/json    ← 响应头
  Content-Length: 123
  <空行>
  <body>

2. 常用方法与状态码

  • 方法:GET(查)/POST(增)/PUT(改)/DELETE(删)/PATCH
  • 状态码:2xx 成功(200)、3xx 重定向(301/302)、4xx 客户端错误(400/404/401)、5xx 服务端错误(500/502/503)

3. RESTful 风格

用 HTTP 方法表达操作,URL 表达资源:

  • GET /users/1 查询用户1
  • POST /users 创建用户
  • PUT /users/1 更新用户1
  • DELETE /users/1 删除用户1

📖 二、HttpURLConnection(旧 API)

java
URL url = new URL("https://api.github.com/users/octocat");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("Accept", "application/json");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);

int code = conn.getResponseCode();
InputStream is = (code < 400) ? conn.getInputStream() : conn.getErrorStream();
String body = new String(is.readAllBytes(), StandardCharsets.UTF_8);
conn.disconnect();

局限:API 笨重(URL.openConnection)、手动管理连接、无连接池、同步阻塞。JDK 11+ 推荐用 HttpClient 替代。


📖 三、HttpClient(JDK 11+,推荐)

现代、支持 HTTP/2、同步与异步、连接池:

java
import java.net.http.*;
import java.net.URI;

HttpClient client = HttpClient.newBuilder()
        .version(HttpClient.Version.HTTP_2)
        .connectTimeout(Duration.ofSeconds(5))
        .build();

// GET
HttpRequest getReq = HttpRequest.newBuilder()
        .uri(URI.create("https://api.github.com/users/octocat"))
        .header("Accept", "application/json")
        .timeout(Duration.ofSeconds(5))
        .GET()
        .build();
HttpResponse<String> resp = client.send(getReq, HttpResponse.BodyHandlers.ofString());
System.out.println(resp.statusCode());
System.out.println(resp.body());

// POST JSON
String json = "{\"name\":\"Alice\"}";
HttpRequest postReq = HttpRequest.newBuilder()
        .uri(URI.create("https://example.com/api/users"))
        .header("Content-Type", "application/json")
        .POST(HttpRequest.BodyPublishers.ofString(json))
        .build();

// 异步
client.sendAsync(getReq, HttpResponse.BodyHandlers.ofString())
      .thenApply(HttpResponse::body)
      .thenAccept(System.out::println);

优势:构建器模式、HTTP/2、异步(返回 CompletableFuture)、连接池复用、API 清晰。


📖 四、第三方库

  • OkHttp:Android 常用,连接池、拦截器、简洁 API。
  • Apache HttpClient:成熟、功能全,企业项目常见。
  • Spring WebClient / RestTemplate:Spring 生态内集成。

实际项目多用第三方库或 Spring 的客户端,它们封装更好(连接池、拦截器、JSON 自动转换)。


⚠️ 五、常见陷阱

陷阱1:不设超时

不设 connect/read timeout,服务器不响应时永久阻塞。务必设超时。

陷阱2:不处理非 2xx 响应

4xx/5xx 时 getInputStream 抛异常或返回 errorStream,需检查状态码。

陷阱3:连接不关闭

HttpURLConnection.disconnect、HttpClient 资源未释放。用 try-with-resources 或复用 HttpClient。

陷阱4:编码问题

body 用 UTF-8 解码,别用平台默认。JSON Content-Type 指定 charset。

陷阱5:HTTPS 证书

自签名证书需配置 SSLContext 信任,否则 SSLHandshakeException。


🆚 六、Java vs C / 库选择

特性C(libcurl)Java
HTTP 客户端libcurlHttpClient/OkHttp
异步回调CompletableFuture
HTTP/2libcurl 支持HttpClient 支持

对 C 程序员:Java 的 HttpClient 对应 C 的 libcurl,但面向对象、支持异步(CompletableFuture)。HTTP 客户端是分布式调用的基础(调外部 API、微服务间调用)。


💡 七、最佳实践

  1. JDK 11+ 用 HttpClient,弃用 HttpURLConnection。
  2. 必设超时(connect + read),防永久阻塞。
  3. 检查状态码,处理 4xx/5xx。
  4. 复用 HttpClient(连接池),别每次新建。
  5. JSON 用 Jackson/Gson 序列化,别手拼字符串。
  6. 异步用 sendAsync + CompletableFuture,非阻塞。
  7. HTTPS 证书/代理 按需配置。

📝 练习预告

完成 练习/Ex41_HTTP.java 中的 6 道题:

  1. HttpClient GET 请求
  2. HttpClient POST JSON
  3. 状态码与响应头
  4. 异步请求(sendAsync)
  5. HttpURLConnection(旧 API 对比)
  6. 综合:调用公开 API 并解析 JSON

完成后对比 答案/Sol41.java,查看逐行讲解与多解法。


📖 八、HTTP 客户端生命周期

HttpClient 应该复用,而不是每次请求都创建。

推荐写法:

java
public final class HttpClients {
    public static final HttpClient DEFAULT = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(3))
        .version(HttpClient.Version.HTTP_2)
        .build();

    private HttpClients() {
    }
}

原因:

text
复用连接池。
减少 TLS 握手成本。
减少对象创建。
统一超时和代理配置。
便于统一加日志、指标和重试策略。

请求对象 HttpRequest 可以每次创建,因为它描述单次请求;客户端对象 HttpClient 应尽量复用。


📖 九、超时、重试与幂等

HTTP 调用必须设置超时:

java
HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://example.com/api/users"))
    .timeout(Duration.ofSeconds(5))
    .GET()
    .build();

超时分类:

text
connect timeout:连接建立超时。
request timeout:整个请求等待超时。
read timeout:第三方库中常见,JDK HttpClient 用 request timeout 表达。

重试前必须考虑幂等性:

方法是否通常幂等说明
GET重试通常安全
PUT同一资源重复更新通常安全
DELETE通常是重复删除结果应一致
POST通常不是可能重复创建或扣款

POST 如果要重试,应使用业务幂等键:

text
Idempotency-Key: 7f4d0c2e-....

没有幂等设计的重试可能比失败更危险。


📖 十、状态码处理策略

不要只判断是否等于 200。

常见策略:

text
2xx:成功。
3xx:重定向,确认客户端是否自动跟随。
400:请求参数错误,不应盲目重试。
401/403:认证或授权失败。
404:资源不存在,可能是正常业务结果。
409:冲突,常见于并发更新。
429:限流,应退避重试。
500:服务端错误,可按策略重试。
502/503/504:网关或上游不可用,可短暂重试。

示例:

java
int code = response.statusCode();
if (code >= 200 && code < 300) {
    return response.body();
}
if (code == 404) {
    throw new NotFoundException("resource not found");
}
if (code == 429 || code >= 500) {
    throw new RetryableHttpException(code, response.body());
}
throw new HttpException(code, response.body());

错误响应体也要读取并记录,否则排查时只剩状态码。


🧪 十一、封装简易 API Client

不要在业务代码里到处散落 HttpClient.send

可以封装一个小客户端:

java
public class ApiClient {
    private final HttpClient client;
    private final URI baseUri;

    public ApiClient(HttpClient client, URI baseUri) {
        this.client = client;
        this.baseUri = baseUri;
    }

    public String get(String path) {
        HttpRequest request = HttpRequest.newBuilder()
            .uri(baseUri.resolve(path))
            .timeout(Duration.ofSeconds(5))
            .header("Accept", "application/json")
            .GET()
            .build();
        return send(request);
    }

    private String send(HttpRequest request) {
        try {
            HttpResponse<String> response = client.send(
                request,
                HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
            );
            return handle(response);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new IllegalStateException("request interrupted", e);
        }
    }

    private String handle(HttpResponse<String> response) {
        int code = response.statusCode();
        if (code >= 200 && code < 300) {
            return response.body();
        }
        throw new IllegalStateException("http " + code + ": " + response.body());
    }
}

封装层可以统一处理:

text
基础 URL。
公共请求头。
超时。
状态码。
日志。
指标。
异常转换。
JSON 序列化和反序列化。

🔐 十二、HTTPS/TLS 注意事项

HTTPS 在 HTTP 之下增加 TLS。

常见问题:

text
证书过期。
域名和证书不匹配。
自签名证书不被信任。
公司代理替换证书。
TLS 协议版本不兼容。
本机时间错误导致证书校验失败。

生产原则:

text
不要为了省事关闭证书校验。
内网自签名证书应导入信任库。
记录 TLS 握手失败的异常原因。
关注证书过期监控。

如果需要自定义信任库,应该集中配置,而不是在业务代码里散落 SSL 逻辑。


🛠 十三、HTTP 调用排查清单

请求失败时按下面顺序定位:

text
URL 是否正确。
DNS 是否能解析。
网络是否能连通。
代理是否配置。
TLS 证书是否可信。
请求方法是否正确。
请求头是否缺失。
Content-Type 和 Accept 是否正确。
请求体 JSON 是否合法。
状态码和错误响应体是什么。
是否触发限流。
是否超时。
是否有重试放大问题。

日志建议:

text
记录方法、URL 路径、状态码、耗时。
记录 traceId/requestId。
错误时记录响应体摘要。
不要记录敏感 header。
不要完整记录大响应体。

📊 十四、客户端库选择

场景推荐
JDK 11+ 基础调用java.net.http.HttpClient
Android 或需要拦截器生态OkHttp
复杂连接池和企业历史项目Apache HttpClient
Spring MVC 同步调用RestTemplate 或 RestClient
响应式非阻塞调用WebClient

选择原则:

text
能用 JDK HttpClient 解决的简单调用,不必额外引库。
团队已有统一客户端时,优先遵守团队封装。
微服务内部调用要统一超时、重试、熔断、指标。
高并发异步场景要关注线程模型和连接池。

🔍 十五、自测问题

text
为什么 HttpClient 应该复用?
connect timeout 和 request timeout 有什么区别?
为什么 POST 重试需要幂等键?
404、429、503 应该如何分别处理?
为什么不能简单关闭 HTTPS 证书校验?
HTTP 调用日志应该记录哪些信息?
什么时候选择 WebClient 而不是 JDK HttpClient?
InterruptedException 为什么要重新设置中断标记?

✅ 十六、掌握标准

学完本课后,应能做到:

text
能用 JDK HttpClient 发送 GET 和 POST JSON。
能设置连接超时和请求超时。
能处理状态码和错误响应体。
能解释同步 send 与异步 sendAsync 的差异。
能复用 HttpClient 并封装 API Client。
能说明重试和幂等的关系。
能排查常见 HTTPS 证书问题。
能根据场景选择 HttpClient、OkHttp、Apache HttpClient 或 WebClient。

HTTP 编程的重点不是“能发请求”,而是让失败、超时、重试、日志和安全都可控。


🎓 下一步

  • 第42课:正则表达式 — Pattern/Matcher、常用正则