摘要: 本文全面介绍Java网络编程的核心技术,从基础的Socket编程到现代的HttpClient API。详细讲解如何使用ServerSocket和Socket实现TCP通信,以及如何利用JDK 11+的HttpClient发送同步/异步HTTP请求、处理JSON数据和流式响应。结合虚拟线程技术,展示如何构建高并发的网络应用,并提供一个完整的天气查询程序示例,帮助开发者掌握Java网络编程的实践技能。
关键词: Java网络编程, Socket编程, HttpClient, 虚拟线程, HTTP客户端
在过去的文章中,我们写的程序大多在“单机”上运行——数据来自文件、来自用户输入、来自内存。但现代社会,很少有程序是完全独立的。你的程序可能需要调用一个远程 API、从互联网上抓取数据、或者作为一个服务器接收来自世界各地用户的请求。
网络编程,就是让你的 Java 程序能够“上网”的能力——它能通过计算机网络与其他程序通信。
今天,我们就来学习 Java 网络编程的两个层面:
- 底层 Socket 编程:用
Socket和ServerSocket实现 TCP 通信,这是所有网络通信的基础。 - 高层 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
服务器端需要做三件事:
- 创建
ServerSocket,绑定到某个端口。 - 调用
accept()方法,阻塞等待一个客户端的连接。 - 当客户端连接后,
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
客户端更简单:
- 创建
Socket,指定服务器的 IP(或主机名)和端口。 - 通过
Socket的输入输出流与服务器通信。 - 通信完成后关闭
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());
}
}
}
运行步骤:
- 先启动
EchoServer(它会一直运行)。 - 再启动
EchoClient,输入一些文字,你会看到服务器原样返回(加上 "Echo: " 前缀)。 - 输入
exit退出客户端。服务器会继续等待下一个客户端连接。
2.3 改进:使用虚拟线程处理并发连接
上面的 EchoServer 是单线程的——一次只能处理一个客户端,其他客户端必须排队等待。在实际生产中,我们需要并发处理多个客户端。
用传统的平台线程池可以,但我们有更优雅的方式——虚拟线程。
// 改进后的 EchoServer(使用虚拟线程)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
while (true) {
Socket clientSocket = serverSocket.accept();
// 每个客户端分配一个虚拟线程,轻量、高效
executor.submit(() -> handleClient(clientSocket));
}
}
这样,即使有成千上万个客户端同时连接,每个连接也只是一个极轻的虚拟线程,不会耗尽系统资源。这也是虚拟线程在 I/O 密集型网络服务中的经典应用。
3. URLConnection 和 HttpURLConnection —— 旧的 HTTP 客户端
在 Java 11 之前,如果要发送 HTTP 请求,常用的方式是 java.net.URL 和 HttpURLConnection。
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 请求
HttpClient 的 sendAsync() 方法返回一个 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.BodyPublisher 和 HttpResponse.BodyHandler)
你还可以自定义 BodyPublisher 和 BodyHandler 来实现更复杂的逻辑,比如自动压缩、分块上传等。
5. HttpClient 与虚拟线程的结合
HttpClient 的 sendAsync() 方法默认使用 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. 网络编程的最佳实践
- 总是设置超时:网络请求可能因为各种原因(DNS 解析慢、服务器无响应)而卡住。设置连接超时和读取超时是必须的。
- 使用
try-with-resources管理资源:Socket、输入流、输出流都应自动关闭。 - 处理异常:网络编程充满了各种异常——
UnknownHostException、ConnectException、SocketTimeoutException、IOException。务必妥善处理。 - 使用虚拟线程处理并发连接:在高并发场景下,虚拟线程是网络服务的首选。
- 复用
HttpClient:HttpClient是线程安全的,创建一次复用多次,比每次都创建新的更高效。 - 注意安全性:使用 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 爬虫、还是一个微服务架构中的服务,今天学的这些技能都会是你工具箱里的核心组件。
动手试试
-
运行上面的
EchoServer和EchoClient,体验 Socket 通信的过程。然后修改EchoServer,让它能并发处理多个客户端(使用虚拟线程或线程池)。 -
用
HttpClient调用一个公开的 API(比如 GitHub API、天气 API),打印响应结果。 -
写一个异步的
HttpClient程序,同时发起 10 个请求,等待所有请求完成后打印每个请求的响应状态码(提示:用CompletableFuture.allOf())。 -
改造天气查询程序,让用户能多次查询(循环),直到输入
exit退出。 -
(挑战)实现一个简单的“聊天服务器”,允许多个客户端连接,客户端发送的消息会被广播给所有其他客户端(提示:维护一个
List<Socket>或使用ConcurrentHashMap管理连接,广播消息时逐个发送)。
网络编程的内容非常丰富,今天我们打下了扎实的基础。下一篇文章,我们将进入第五阶段的最后一篇——阶段实战:高并发聊天室,用我们学到的网络编程、虚拟线程、并发集合等知识,构建一个可用的实时聊天系统。
我们下一篇见。😃
📌 获取本系列示例代码请访问 GitCode。

1071

被折叠的 条评论
为什么被折叠?



