网络编程¶
网络编程使不同进程能够交换数据。Java 提供 Socket、数据报和 HTTP 客户端等 API;使用时既要选择协议,也要约定消息格式、字符编码、超时和资源释放方式。
基本概念¶
| 概念 | 含义 |
|---|---|
| IP 地址 | 标识网络接口或通信目标 |
| 端口 | 在主机上区分不同的通信端点 |
| TCP | 提供面向连接、可靠、有序的字节流 |
| UDP | 以数据报为单位发送,不保证送达或顺序 |
| HTTP | 规定请求、响应、方法、状态码和头部等应用层语义 |
TCP 与 UDP 的主要区别:
| 比较项 | TCP | UDP |
|---|---|---|
| 连接 | 建立连接后通信 | 无连接,直接发送数据报 |
| 可靠性 | 保证按序送达,丢失自动重传 | 可能丢失、重复或乱序 |
| 边界 | 字节流,没有消息边界 | 保留数据报边界 |
| 适用场景 | 文件传输、HTTP、数据库连接 | 实时音视频、游戏状态、DNS 查询 |
TCP 不保留应用消息边界。一次写入不一定对应对端的一次读取,应用需要约定分隔符、固定长度或长度前缀等规则。
Java 网络 API¶
| API | 用途 |
|---|---|
Socket |
建立 TCP 客户端连接,读写字节流 |
ServerSocket |
监听并接受 TCP 连接 |
DatagramSocket |
发送和接收 UDP 数据报 |
URI |
表示和解析资源标识符 |
HttpClient |
发送 HTTP 请求,获取响应或异步结果 |
HTTP 业务通常可以先使用 java.net.http.HttpClient,只有需要直接控制传输过程时才下探到 Socket 等 API。
常见网络异常:
| 异常 | 常见触发 |
|---|---|
UnknownHostException |
主机名无法解析 |
ConnectException |
目标拒绝连接或不可达 |
SocketTimeoutException |
连接或读取超时 |
MalformedURLException |
地址格式不符合规范 |
示例:访问本地 HTTP 服务¶
为便于直接运行,下面的程序先用 JDK 自带的 HttpServer 启动一个本地服务,再用 HttpClient 发出请求。地址只绑定 127.0.0.1,端口由系统分配,不需要访问外部网站。
Main.java
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
public class Main {
public static void main(String[] args) throws Exception {
HttpServer server = HttpServer.create(
new InetSocketAddress("127.0.0.1", 0), 0);
server.createContext("/topic", exchange -> {
byte[] body = "Java networking".getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().set("Content-Type", "text/plain; charset=UTF-8");
exchange.sendResponseHeaders(200, body.length);
try (var output = exchange.getResponseBody()) {
output.write(body);
} finally {
exchange.close();
}
});
server.start();
try (HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(3))
.build()) {
URI uri = URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/topic");
HttpRequest request = HttpRequest.newBuilder(uri)
.timeout(Duration.ofSeconds(3))
.GET()
.build();
HttpResponse<String> response = client.send(
request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));
System.out.println("Status: " + response.statusCode());
System.out.println("Body: " + response.body());
} finally {
server.stop(0);
}
}
}
输出:
connectTimeout 约束新连接的建立过程,请求上的 timeout 约束该请求等待响应的时间。它们不是完全相同的概念。send 同步等待响应,sendAsync 则返回 CompletableFuture。
HttpServer 属于 JDK 的 jdk.httpserver 模块;使用自行裁剪的运行时时,需要包含该模块。本例用于解释请求与响应,完整 Web 应用可以进一步使用框架。
Socket 中的消息边界¶
若约定一行文本是一条消息,发送方需要写入换行并刷新缓冲,接收方才能用 readLine() 读取整行:
这段代码假定双方采用一致的编码和行分隔规则。接收方还应处理提前断开、超长消息和读取超时,不能把一次 read() 当作完整消息。
注意事项¶
- HTTP 的 4xx、5xx 状态不一定表现为 Java 异常,需要检查状态码。
- 阻塞式 Socket 应分别考虑连接超时和读取超时,资源使用完及时关闭。
- 对请求设置重试前,应判断操作是否可以安全重复执行。
- 公网请求使用 HTTPS 时应保持证书验证,并正确处理重定向和认证信息。
- UDP 不保证送达,重要数据需要应用层自行确认和重传。
练习¶
- 为本地服务增加
/missing路径并返回 404,修改客户端输出,让它根据状态码区分成功与失败。再把同步send改为sendAsync,比较两种调用方式。 - 为示例的 HTTP 响应增加
GET /topic?name=Java的查询参数处理,让服务端按参数返回不同的内容。