Skip to content

网络编程与 HTTP ​

学习目标 ​

  • 区分 URI、HTTP、TCP、UDP、Socket 和应用层消息。
  • 能根据请求协议选择 HttpClient、Socket 或 DatagramSocket。
  • 能识别连接超时、读取超时、HTTP 状态、业务失败和半包问题。

核心知识点 ​

专业术语 ​

  • URI(Uniform Resource Identifier):描述资源身份和定位信息的统一语法。
  • TCP:面向连接的可靠字节流,不保留应用消息边界。
  • UDP:面向数据报的无连接协议,保留数据报边界但不保证送达。
  • HTTP:运行在 TCP/TLS 之上的请求-响应协议,包含方法、头、状态码和主体。

白话解释与边界 ​

浏览器式接口优先使用 HttpClient,它已经处理请求构造、TLS、连接复用、同步/异步发送等 HTTP 语义;只有自定义文本协议、长连接或教学 TCP 场景才直接使用 Socket。TCP 的一次 read 可能只得到半条消息,必须由应用层设计长度、分隔符或帧格式。HTTP 状态码不是业务结果,2xx 也可能包含业务失败字段。

常用用法 ​

URI:表达 HTTP 资源地址 ​

URI 是结构化地址值,适合传给 HttpRequest;它本身不负责建立连接。

java
// 说明:URI:表达 HTTP 资源地址。
import java.net.URI;

public class UriDemo {
    public static void main(String[] args) {
        URI uri = URI.create("https://example.com/api?q=java");
        // 关键变化:URI uri = URI.create("https://example.com/api?q=java");;按 "https://example.com/api?q=java" 调用 create 创建值,结果写入 uri。
        System.out.println(uri.getScheme() + ":" + uri.getPath());
        // 输出:https:/api
    }
}

不要把用户输入字符串直接拼进 URL,至少先解析并校验 scheme、host 和允许的端口。URI、URL 和 URLConnection 的差别见 Socket 与网络边界。

HttpClient:构建可复用的 HTTP 客户端 ​

客户端可复用以共享连接池;connectTimeout 只限制建立连接,不等于完整请求的读取超时。

java
// 说明:HttpClient:构建可复用的 HTTP 客户端。
import java.net.http.HttpClient;
import java.time.Duration;

public class HttpClientBuilderDemo {
    public static void main(String[] args) {
        HttpClient client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(3))
                .followRedirects(HttpClient.Redirect.NORMAL)
                .build();
                // 关键变化:.connectTimeout(Duration.ofSeconds(3)) .followRedirects(HttpClient.Redirect.NORMAL) .build();;调用 build,实参为 无显式参数;按已配置的超时和重定向策略构建 HttpClient。
                // 关键变化:.followRedirects(HttpClient.Redirect.NORMAL) .build();;调用 build,实参为 无显式参数;按已配置的超时和重定向策略构建 HttpClient。
                // 关键变化:.build();;调用 build,实参为 无显式参数;完成前面的 builder 配置并返回构造对象。
        System.out.println(client.version());
        // 输出:HTTP_2
    }
}

完整请求和响应处理见 HTTP Client 常用 API。

Socket:自定义 TCP 字节流 ​

Socket 只提供连接和字节流,不替你定义消息边界、编码、认证和重试;要看完整回显服务和半关闭行为,阅读 Socket 与网络边界。

java
// 说明:Socket:自定义 TCP 字节流。
import java.net.InetSocketAddress;

public class SocketAddressDemo {
    public static void main(String[] args) {
        var address = new InetSocketAddress("127.0.0.1", 8080);
        System.out.println(address.getHostString() + ":" + address.getPort());
        // 输出:127.0.0.1:8080
    }
}

HTTP 请求的同步与异步选择 ​

少量调用用同步 send 最直观;需要并发请求且不想阻塞当前线程时用 sendAsync,但仍要处理完成、异常、取消和资源大小边界。

java
// 说明:HTTP 请求的同步与异步选择。
import java.net.URI;
import java.net.http.HttpRequest;

public class HttpRequestChoiceDemo {
    public static void main(String[] args) {
        var request = HttpRequest.newBuilder(URI.create("https://example.com"))
                .header("Accept", "text/plain")
                .GET()
                .build();
                // 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .header("Accept", "text/plain") .GET() .build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
                // 关键变化:.header("Accept", "text/plain") .GET() .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
                // 关键变化:.GET() .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
                // 关键变化:.build();;调用 build,实参为 无显式参数;完成前面的 builder 配置并返回构造对象。
        System.out.println(request.method() + " " + request.uri());
        // 输出:GET https://example.com
    }
}

不常用但需要知道 ​

URLConnection:旧式 URL 连接 API ​

旧代码中仍会看到 URLConnection/HttpURLConnection,但新 HTTP 客户端优先使用 JDK 11+ 的 HttpClient,避免在连接、异步和 HTTP/2 处理上重复造轮子。

java
import java.net.URI;

public class UrlConnectionBoundaryDemo {
    public static void main(String[] args) throws Exception {
        var connection = URI.create("https://example.com").toURL().openConnection();
        // 关键变化:var connection = URI.create("https://example.com").toURL().openConnection();;按 "https://example.com" 调用 create 创建值,结果写入 connection。
        System.out.println(connection.getClass().getSimpleName());
        // 输出:HttpURLConnection
    }
}

DatagramSocket:UDP 数据报 ​

UDP 适合能容忍丢包、重复或乱序的场景;需要可靠顺序、拥塞控制或复杂重试时不要把 UDP 当成“更快的 TCP”。

java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;

public class UdpPacketDemo {
    public static void main(String[] args) throws Exception {
        byte[] bytes = "ping".getBytes(StandardCharsets.UTF_8);
        var packet = new DatagramPacket(bytes, bytes.length);
        System.out.println(packet.getLength());
        // 输出:4
        try (var socket = new DatagramSocket()) {
        // 输入:try (var socket = new DatagramSocket()) {;资源变量 socket 接收 new DatagramSocket(),try 结束时自动关闭。
        // 关键变化:try (var socket = new DatagramSocket()) {;创建资源 socket,构造表达式为 new DatagramSocket();try 结束时关闭该资源。
            socket.connect("127.0.0.1", 9999);
            // 关键变化:socket.connect("127.0.0.1", 9999);;让 socket 连接到 127.0.0.1:9999;连接状态更新。
        }
    }
}

shutdownInput/shutdownOutput:TCP 半关闭 ​

半关闭表示“我不再发送”或“我不再接收”,不等于立即关闭整个连接;只有协议明确支持结束方向时才使用,否则对端可能把 EOF 误判为异常。

java
// 说明:这里只创建未连接的 socket;连接后 shutdownOutput() 会向对端发送 EOF,而输入方向仍可继续接收字节。
import java.net.Socket;

public class HalfCloseDemo {
    public static void main(String[] args) throws Exception {
        try (Socket socket = new Socket()) {
        // 输入:try (Socket socket = new Socket()) {;资源变量 socket 接收 new Socket(),try 结束时自动关闭。
        // 关键变化:try (Socket socket = new Socket()) {;创建资源 socket,构造表达式为 new Socket();try 结束时关闭该资源。
            System.out.println(socket.isClosed());
            // 输出:false
            // 连接建立后可分别调用 socket.shutdownOutput() 或 shutdownInput()
        }
    }
}

Proxy 与代理边界 ​

代理是连接路径的配置,不是业务重试或认证策略;生产环境还要明确代理凭证、TLS 终止位置和内网地址校验。

java
// 说明:proxy 把 HTTP 连接路径指向 127.0.0.1:8080;URI 仍表示目标 https 资源,不携带代理凭证。
import java.net.Proxy;
import java.net.URI;

public class ProxyBoundaryDemo {
    public static void main(String[] args) {
        var proxy = new Proxy(Proxy.Type.HTTP, new java.net.InetSocketAddress("127.0.0.1", 8080));
        System.out.println(proxy.type() + " " + URI.create("https://example.com").getScheme());
        // 输出:HTTP https
    }
}

简单案例 ​

java
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;

public class NetworkDemo {
    public static void main(String[] args) throws Exception {
        var client = HttpClient.newBuilder()
                .connectTimeout(Duration.ofSeconds(3))
                .build();
                // 关键变化:.connectTimeout(Duration.ofSeconds(3)) .build();;调用 build,实参为 无显式参数;按已配置的超时和重定向策略构建 HttpClient。
                // 关键变化:.build();;调用 build,实参为 无显式参数;完成前面的 builder 配置并返回构造对象。
        var request = HttpRequest.newBuilder(URI.create("https://example.com"))
                .timeout(Duration.ofSeconds(5))
                .header("Accept", "text/html")
                .GET()
                .build();
                // 关键变化:var request = HttpRequest.newBuilder(URI.create("https://example.com")) .timeout(Duration.ofSeconds(5)) .header("Accept", "text/html") .GET() .build();;按 "https://example.com" 调用 create 创建值,结果写入 request。
                // 关键变化:.timeout(Duration.ofSeconds(5)) .header("Accept", "text/html") .GET() .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
                // 关键变化:.header("Accept", "text/html") .GET() .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
                // 关键变化:.GET() .build();;调用 build,实参为 无显式参数;按已配置的方法、请求头和请求体构建 HttpRequest。
                // 关键变化:.build();;调用 build,实参为 无显式参数;完成前面的 builder 配置并返回构造对象。
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        System.out.println(response.statusCode());
        // 输出:200
    }
}

该案例需要网络可达;生产请求还应显式处理非 2xx、响应大小、字符集、TLS 证书、取消和幂等重试。

易混点 ​

  • URI 是地址值,URL/URLConnection 是旧式访问 API,HttpClient 才是现代 HTTP 请求客户端。
  • TCP 提供字节流,不提供消息边界;UDP 提供数据报边界,但不保证送达。
  • 连接超时、请求超时、读取超时和业务超时不是同一个概念,应分别设定。
  • HTTP 2xx 只表示协议层成功,响应体仍可能表示业务失败;网络异常也不等于 HTTP 状态码。

课后小问 ​

  1. 为什么不能把一次 Socket.read() 当成一条完整消息? 答案:TCP 只保证字节顺序,不保留发送方的消息边界。 解析:应用协议必须定义长度、分隔符或帧头,并循环读取到完整消息。

  2. HttpClient 的连接超时和 HttpRequest 的 timeout 分别解决什么问题? 答案:前者限制建立连接,后者限制一次请求操作的总等待范围。 解析:DNS、TLS、服务器处理和响应读取还可能各自耗时,不能只配置一个数字就覆盖所有故障。

本节小结 ​

  • HTTP 请求优先使用可复用的 HttpClient,自定义协议才直接操作 Socket。
  • URI、连接、协议状态和业务结果应分层处理。
  • TCP 需要应用层消息边界,UDP 需要接受丢包和乱序风险。
  • 超时、取消、响应体关闭和幂等性决定网络代码是否可靠。

快速回顾 ​

  • HttpRequest 描述请求,HttpClient 发送请求,HttpResponse 携带状态和主体。
  • Socket 是字节流连接,不是完整消息队列。
  • HTTP 重试必须考虑方法幂等性和退避。
  • 网络代码要区分连接失败、协议失败和业务失败。