53. 【Java】网络编程:Socket 与 HTTP 客户端

摘要: 本文全面介绍Java网络编程的核心技术,从基础的Socket编程到现代的HttpClient API。详细讲解如何使用ServerSocket和Socket实现TCP通信,以及如何利用JDK 11+的HttpClient发送同步/异步HTTP请求、处理JSON数据和流式响应。结合虚拟线程技术,展示如何构建高并发的网络应用,并提供一个完整的天气查询程序示例,帮助开发者掌握Java网络编程的实践技能。

关键词: Java网络编程, Socket编程, HttpClient, 虚拟线程, HTTP客户端


在过去的文章中,我们写的程序大多在“单机”上运行——数据来自文件、来自用户输入、来自内存。但现代社会,很少有程序是完全独立的。你的程序可能需要调用一个远程 API、从互联网上抓取数据、或者作为一个服务器接收来自世界各地用户的请求。

网络编程,就是让你的 Java 程序能够“上网”的能力——它能通过计算机网络与其他程序通信。

今天,我们就来学习 Java 网络编程的两个层面:

  1. 底层 Socket 编程:用 SocketServerSocket 实现 TCP 通信,这是所有网络通信的基础。
  2. 高层 HTTP 客户端:用 HttpClient(JDK 11+)发送 HTTP 请求,调用 REST API。

不管你是要写一个 Web 服务器、一个微服务调用者、还是一个网络爬虫,今天的内容都会给你打下坚实的基础。


1. 网络编程基础 —— IP 地址、端口、协议

在写代码之前,我们先快速回顾一下三个基本概念。

IP 地址:就像你家的门牌号,用来在网络上标识一台计算机。IPv4 是 192.168.1.1 这种格式,IPv6 更长一些。在 Java 中,我们用 InetAddress 类来表示 IP 地址。

端口号(Port):就像一栋大楼里的不同房间号。一台计算机可以有多个网络服务,每个服务监听不同的端口。HTTP 默认是 80,HTTPS 是 443,MySQL 是 3306。我们自己写程序时,通常用 1024 以上的端口。

协议(Protocol):通信的“语言规则”。最常见的两个协议是:

  • TCP(传输控制协议):面向连接、可靠的、基于字节流的协议。就像打电话——先拨号建立连接,然后通话,最后挂断。保证数据按顺序到达,不会丢失。
  • UDP(用户数据报协议):无连接、不可靠的、基于数据包的协议。就像寄信——不需要建立连接,直接扔进邮筒,但不保证能收到,也不保证顺序。

今天我们的重点是基于 TCP 的 Socket 编程,以及基于 HTTP 协议(它建立在 TCP 之上)的客户端调用。


2. Socket 编程 —— 通信的“管道”

Socket(套接字) 是 TCP/IP 网络编程的抽象概念。你可以把它想象成一根“管道”的两端:

  • 客户端创建一个 Socket,连接到服务器的 IP 和端口。
  • 服务器创建一个 ServerSocket,在某个端口上“监听”客户端的连接请求。

一旦连接建立,客户端和服务器就可以通过 Socket 的输入输出流互相发送数据了——就像两个程序之间打通了一条隧道。

2.1 服务器端 —— ServerSocket

服务器端需要做三件事:

  1. 创建 ServerSocket,绑定到某个端口。
  2. 调用 accept() 方法,阻塞等待一个客户端的连接。
  3. 当客户端连接后,accept() 返回一个 Socket 对象,通过这个 Socket 的输入输出流和客户端通信。
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * 一个简单的 Echo 服务器:接收客户端发来的消息,原样返回。
 */
public class EchoServer {
    public static void main(String[] args) {
        int port = 8888;
        System.out.println("Echo 服务器启动,监听端口:" + port);

        try (ServerSocket serverSocket = new ServerSocket(port)) {
            // 不断接受客户端连接(主循环)
            while (true) {
                System.out.println("等待客户端连接...");
                // accept() 会阻塞,直到有客户端连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("客户端连接:" + clientSocket.getRemoteSocketAddress());

                // 为每个客户端创建一个独立的线程处理(为了演示简单,我们直接在主线程处理)
                // 实际生产环境中,应该用线程池或虚拟线程
                handleClient(clientSocket);
            }
        } catch (IOException e) {
            System.err.println("服务器异常:" + e.getMessage());
        }
    }

    private static void handleClient(Socket socket) {
        try (
            BufferedReader reader = new BufferedReader(
                new InputStreamReader(socket.getInputStream()));
            PrintWriter writer = new PrintWriter(
                socket.getOutputStream(), true)   // autoFlush = true
        ) {
            String line;
            // 循环读取客户端发来的每一行,直到客户端关闭连接
            while ((line = reader.readLine()) != null) {
                System.out.println("收到:" + line);
                // 原样返回(加上 "Echo: " 前缀)
                writer.println("Echo: " + line);
            }
            System.out.println("客户端断开连接");
        } catch (IOException e) {
            System.err.println("处理客户端时出错:" + e.getMessage());
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                // 忽略
            }
        }
    }
}
2.2 客户端 —— Socket

客户端更简单:

  1. 创建 Socket,指定服务器的 IP(或主机名)和端口。
  2. 通过 Socket 的输入输出流与服务器通信。
  3. 通信完成后关闭 Socket
import java.io.*;
import java.net.Socket;

/**
 * Echo 服务器的一个简单客户端。
 */
public class EchoClient {
    public static void main(String[] args) {
        String host = "localhost";
        int port = 8888;

        try (Socket socket = new Socket(host, port);
             BufferedReader reader = new BufferedReader(
                 new InputStreamReader(socket.getInputStream()));
             PrintWriter writer = new PrintWriter(
                 socket.getOutputStream(), true);
             BufferedReader consoleReader = new BufferedReader(
                 new InputStreamReader(System.in))) {

            System.out.println("已连接到 Echo 服务器,输入消息(输入 'exit' 退出):");

            String userInput;
            while ((userInput = consoleReader.readLine()) != null) {
                if ("exit".equalsIgnoreCase(userInput)) {
                    break;
                }
                // 发送消息到服务器
                writer.println(userInput);
                // 读取服务器的响应
                String response = reader.readLine();
                System.out.println("服务器响应:" + response);
            }

        } catch (IOException e) {
            System.err.println("客户端异常:" + e.getMessage());
        }
    }
}

运行步骤

  1. 先启动 EchoServer(它会一直运行)。
  2. 再启动 EchoClient,输入一些文字,你会看到服务器原样返回(加上 "Echo: " 前缀)。
  3. 输入 exit 退出客户端。服务器会继续等待下一个客户端连接。
2.3 改进:使用虚拟线程处理并发连接

上面的 EchoServer 是单线程的——一次只能处理一个客户端,其他客户端必须排队等待。在实际生产中,我们需要并发处理多个客户端。

用传统的平台线程池可以,但我们有更优雅的方式——虚拟线程

// 改进后的 EchoServer(使用虚拟线程)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    while (true) {
        Socket clientSocket = serverSocket.accept();
        // 每个客户端分配一个虚拟线程,轻量、高效
        executor.submit(() -> handleClient(clientSocket));
    }
}

这样,即使有成千上万个客户端同时连接,每个连接也只是一个极轻的虚拟线程,不会耗尽系统资源。这也是虚拟线程在 I/O 密集型网络服务中的经典应用。


3. URLConnectionHttpURLConnection —— 旧的 HTTP 客户端

在 Java 11 之前,如果要发送 HTTP 请求,常用的方式是 java.net.URLHttpURLConnection

import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;

public class OldHttpClient {
    public static void main(String[] args) throws IOException {
        // 1. 创建 URL 对象
        URL url = new URL("https://api.github.com/repos/openjdk/jdk");
        // 2. 打开连接
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        // 3. 设置请求方法
        conn.setRequestMethod("GET");
        // 4. 设置请求头
        conn.setRequestProperty("User-Agent", "Java-Client");
        // 5. 获取响应码
        int responseCode = conn.getResponseCode();
        System.out.println("响应码:" + responseCode);

        // 6. 读取响应体
        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream()))) {
            String line;
            StringBuilder response = new StringBuilder();
            while ((line = reader.readLine()) != null) {
                response.append(line);
            }
            System.out.println("响应体:" + response.toString().substring(0, 100) + "...");
        }
        // 7. 断开连接
        conn.disconnect();
    }
}

痛点

  • API 设计不够直观,需要手动处理输入流、响应码、连接管理等。
  • 不支持 HTTP/2。
  • 异步请求需要自己用多线程实现。
  • 代码冗长,容易出错。

所以,Java 11 引入了一个全新的 HTTP 客户端 API。


4. JDK 11+ 的 HttpClient —— 现代 HTTP 客户端

java.net.http.HttpClient 是 Java 11 引入的现代化 HTTP 客户端 API,它支持 HTTP/1.1 和 HTTP/2,同步和异步请求,WebSocket,以及响应式流。

它设计得很优雅,用起来就像在“构建”一个请求。

4.1 创建 HttpClient
import java.net.http.HttpClient;
import java.time.Duration;

HttpClient client = HttpClient.newBuilder()
    .version(HttpClient.Version.HTTP_2)          // 优先使用 HTTP/2
    .connectTimeout(Duration.ofSeconds(10))      // 连接超时
    .followRedirects(HttpClient.Redirect.NORMAL) // 自动重定向
    .build();

HttpClient 是线程安全的,你可以创建一次,在整个应用中复用。

4.2 发送同步 GET 请求
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class ModernHttpClient {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        // 1. 构造请求
        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.github.com/repos/openjdk/jdk"))
            .header("User-Agent", "Java-Client")
            .timeout(Duration.ofSeconds(10))
            .GET()   // 默认就是 GET,可以省略
            .build();

        // 2. 发送请求(同步)
        HttpResponse<String> response = client.send(
            request,
            HttpResponse.BodyHandlers.ofString()   // 把响应体转成 String
        );

        // 3. 输出结果
        System.out.println("状态码:" + response.statusCode());
        System.out.println("响应头:" + response.headers().map());
        System.out.println("响应体:" + response.body().substring(0, 100) + "...");
    }
}

如果你需要把响应体转成字节数组或文件,也可以:

// 响应体转成字节数组
HttpResponse<byte[]> byteResponse = client.send(request, HttpResponse.BodyHandlers.ofByteArray());

// 响应体转成文件
HttpResponse<Path> fileResponse = client.send(request, HttpResponse.BodyHandlers.ofFile(Paths.get("output.json")));
4.3 发送异步 GET 请求

HttpClientsendAsync() 方法返回一个 CompletableFuture<HttpResponse<T>>,让你可以非阻塞地发送请求,并在响应到达时处理。

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;

public class AsyncHttpClient {
    public static void main(String[] args) {
        HttpClient client = HttpClient.newHttpClient();

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://api.github.com/repos/openjdk/jdk"))
            .build();

        // 异步发送
        CompletableFuture<HttpResponse<String>> future = client.sendAsync(
            request,
            HttpResponse.BodyHandlers.ofString()
        );

        // 注册回调:当响应到达时,打印结果
        future.thenApply(HttpResponse::body)
              .thenAccept(body -> System.out.println("异步响应:" + body.substring(0, 100) + "..."))
              .exceptionally(e -> {
                  System.err.println("请求失败:" + e.getMessage());
                  return null;
              });

        // 主线程继续做其他事情...
        System.out.println("请求已发送,主线程继续执行...");

        // 为了演示,让主线程等待一会(否则程序可能提前退出)
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
4.4 POST 请求 —— 发送 JSON 数据
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;

public class PostJson {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newHttpClient();

        // JSON 字符串
        String json = """
            {"title": "Java 21", "body": "JDK 21 发布啦!", "userId": 1}
            """;

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create("https://jsonplaceholder.typicode.com/posts"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8))
            .build();

        HttpResponse<String> response = client.send(
            request,
            HttpResponse.BodyHandlers.ofString()
        );

        System.out.println("状态码:" + response.statusCode());
        System.out.println("响应:" + response.body());
    }
}

HttpRequest.BodyPublishers 提供了多种方式来构造请求体:

  • ofString(String):从字符串
  • ofByteArray(byte[]):从字节数组
  • ofFile(Path):从文件
  • ofInputStream(Supplier<InputStream>):从输入流
4.5 处理响应体为流(大文件下载)

如果你要下载一个大文件,直接把整个响应体读入内存可能会导致 OOM。HttpClient 支持将响应体作为 InputStream 处理,你可以流式地读取。

HttpResponse<InputStream> response = client.send(
    request,
    HttpResponse.BodyHandlers.ofInputStream()
);

try (InputStream is = response.body()) {
    // 逐块读取并写入文件
    Files.copy(is, Paths.get("downloaded_file.zip"));
}
4.6 请求和响应拦截(使用 HttpRequest.BodyPublisherHttpResponse.BodyHandler

你还可以自定义 BodyPublisherBodyHandler 来实现更复杂的逻辑,比如自动压缩、分块上传等。


5. HttpClient 与虚拟线程的结合

HttpClientsendAsync() 方法默认使用 ForkJoinPool.commonPool()(平台线程池)。但你可以给它指定一个自定义的 Executor——比如虚拟线程执行器,让异步回调也运行在虚拟线程中。

// 使用虚拟线程执行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    HttpClient client = HttpClient.newBuilder()
        .executor(executor)   // 异步任务在虚拟线程中执行
        .build();

    // 并发发送 100 个异步请求
    List<CompletableFuture<HttpResponse<String>>> futures = IntStream.range(0, 100)
        .mapToObj(i -> client.sendAsync(
            HttpRequest.newBuilder()
                .uri(URI.create("https://httpbin.org/delay/1"))  // 每个请求延迟1秒
                .build(),
            HttpResponse.BodyHandlers.ofString()
        ))
        .toList();

    // 等待所有请求完成
    CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]))
        .join();

    // 输出结果
    futures.forEach(f -> {
        try {
            System.out.println("状态:" + f.get().statusCode());
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}

虚拟线程 + HttpClient 的组合,让你可以轻松地同时发起成千上万个 HTTP 请求,而不用担心线程耗尽。


6. 综合示例 —— 一个简单的天气查询程序

我们来写一个实用的小程序:通过 HttpClient 调用一个公开的天气 API(我们使用 wttr.in,一个免费的天气服务),查询某个城市的天气。

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

public class WeatherClient {
    private static final HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(5))
        .build();

    public static void main(String[] args) throws Exception {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入城市名(拼音,如 beijing):");
        String city = scanner.nextLine().trim();

        String url = "https://wttr.in/" + city + "?format=%25l:+%25c+%25t";
        // 示例:https://wttr.in/beijing?format=%l:+%c+%t
        // 返回格式:"Beijing: ☀️ +28°C"

        HttpRequest request = HttpRequest.newBuilder()
            .uri(URI.create(url))
            .timeout(Duration.ofSeconds(10))
            .GET()
            .build();

        try {
            HttpResponse<String> response = client.send(
                request,
                HttpResponse.BodyHandlers.ofString()
            );

            if (response.statusCode() == 200) {
                System.out.println("🌤️ 天气信息:" + response.body());
            } else {
                System.out.println("❌ 查询失败,状态码:" + response.statusCode());
                System.out.println("响应:" + response.body());
            }
        } catch (Exception e) {
            System.err.println("❌ 请求异常:" + e.getMessage());
        }
    }
}

这个程序虽然简单,但包含了构建 HTTP 请求、发送、处理响应的完整流程。你可以把它扩展成更复杂的应用,比如加上缓存、支持异步查询多个城市、解析 JSON 响应等。


7. 网络编程的最佳实践

  1. 总是设置超时:网络请求可能因为各种原因(DNS 解析慢、服务器无响应)而卡住。设置连接超时和读取超时是必须的。
  2. 使用 try-with-resources 管理资源:Socket、输入流、输出流都应自动关闭。
  3. 处理异常:网络编程充满了各种异常——UnknownHostExceptionConnectExceptionSocketTimeoutExceptionIOException。务必妥善处理。
  4. 使用虚拟线程处理并发连接:在高并发场景下,虚拟线程是网络服务的首选。
  5. 复用 HttpClientHttpClient 是线程安全的,创建一次复用多次,比每次都创建新的更高效。
  6. 注意安全性:使用 HTTPS,不要发送明文密码或敏感数据。

8. 今天的总结

今天我们进入了 Java 网络编程的世界:

  • 网络基础:IP 地址、端口、TCP 协议。
  • Socket 编程
    • 服务器端:ServerSocket + accept() + Socket
    • 客户端:Socket + 输入输出流。
    • 用虚拟线程来处理多个并发连接。
  • HttpURLConnection:旧式 HTTP 客户端,可用但较笨重。
  • HttpClient(JDK 11+):现代化的 HTTP 客户端,支持同步和异步请求、HTTP/2、流式处理。
    • 同步请求:client.send(request, handler)
    • 异步请求:client.sendAsync(request, handler) 返回 CompletableFuture
  • HttpClient + 虚拟线程:高并发 HTTP 调用的理想组合。

网络编程让 Java 程序不再孤立,它能够与全世界的服务进行通信。不管你是写一个 REST API 的客户端、一个 Web 爬虫、还是一个微服务架构中的服务,今天学的这些技能都会是你工具箱里的核心组件。


动手试试

  1. 运行上面的 EchoServerEchoClient,体验 Socket 通信的过程。然后修改 EchoServer,让它能并发处理多个客户端(使用虚拟线程或线程池)。

  2. HttpClient 调用一个公开的 API(比如 GitHub API、天气 API),打印响应结果。

  3. 写一个异步的 HttpClient 程序,同时发起 10 个请求,等待所有请求完成后打印每个请求的响应状态码(提示:用 CompletableFuture.allOf())。

  4. 改造天气查询程序,让用户能多次查询(循环),直到输入 exit 退出。

  5. (挑战)实现一个简单的“聊天服务器”,允许多个客户端连接,客户端发送的消息会被广播给所有其他客户端(提示:维护一个 List<Socket> 或使用 ConcurrentHashMap 管理连接,广播消息时逐个发送)。

网络编程的内容非常丰富,今天我们打下了扎实的基础。下一篇文章,我们将进入第五阶段的最后一篇——阶段实战:高并发聊天室,用我们学到的网络编程、虚拟线程、并发集合等知识,构建一个可用的实时聊天系统。

我们下一篇见。😃

📌 获取本系列示例代码请访问 GitCode

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值