S07-02 网络编程-TCP与UDP编程
[TOC]
TCP 编程
Java 中的 TCP 编程主要基于 java.net 包提供的 Socket API 实现。TCP 是一种面向连接、可靠的、基于字节流的传输层通信协议。
为了便于系统梳理,本文首先介绍 TCP 编程的核心概念与基础通信实现,后续可以根据实际需求延伸至多线程并发处理与 NIO 高性能架构。
核心概念
在 Java 传统阻塞式 I/O(BIO)模型下,TCP 通信依赖两个关键类:
ServerSocket:运行在服务端,负责监听指定端口并接收客户端的连接请求。Socket:代表通信节点。客户端使用它向服务端发起连接,服务端在建立连接后也会获得一个Socket对象,双方通过该对象进行双向数据读写。
通信流程
TCP 套接字的通信建立在输入流(InputStream)与输出流(OutputStream)的交叉对接上:
服务端工作流程:
ServerSocket 的整个生命周期可以划分为四个阶段:
绑定端口:创建
ServerSocket实例并将其绑定到特定的网络端口(如 8080)。阻塞等待:调用
accept()进入阻塞状态,挂起当前线程直到客户端发起 TCP 连接请求。完成建立:底层完成 TCP 三次握手后,
accept()方法返回一个代表该连接的Socket对象。循环与关闭:服务端通常在循环中持续调用
accept()处理多连接,服务停止时调用close()释放端口。
客户端工作流程:
Socket 在客户端的使用通常遵循以下生命周期:
建立连接:指定目标服务器的 IP 地址与端口,发起 TCP 三次握手。
获取数据流:通过
getInputStream()获取输入流读取数据,通过getOutputStream()获取输出流发送数据。数据传输:借由字节流或包装后的字符流/缓冲流进行双向通信。
半关闭与全关闭:可选择使用
shutdownOutput()/shutdownInput()仅关闭单向数据通道;传输完毕后调用close()关闭套接字,释放底层资源。

代码示例
下面展示一个基础的单次交互示例:客户端发送一条消息,服务端接收并做出响应。
服务端实现
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class TCPServer {
public static void main(String[] args) {
int port = 8080;
// 1. 绑定端口并监听连接
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务端已启动,等待客户端连接...");
// 2. 阻塞等待客户端建立连接
try (Socket socket = serverSocket.accept();
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
PrintWriter out = new PrintWriter(socket.getOutputStream(), true)) {
System.out.println("客户端已连接,IP: " + socket.getInetAddress());
// 3. 读取客户端消息
String clientMsg = in.readLine();
System.out.println("收到客户端消息: " + clientMsg);
// 4. 返回响应消息
out.println("服务端已收到你的消息: " + clientMsg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}客户端实现
import java.io.*;
import java.net.Socket;
public class TCPClient {
public static void main(String[] args) {
String host = "127.0.0.1";
int port = 8080;
// 1. 发起连接并创建输入输出流
try (Socket socket = new Socket(host, port);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
System.out.println("已成功连接到服务端。");
// 2. 向服务端发送数据
out.println("Hello, Java TCP Server!");
// 3. 读取服务端的响应
String response = in.readLine();
System.out.println("服务端响应: " + response);
} catch (IOException e) {
e.printStackTrace();
}
}
}关键细节
阻塞特性:
accept()、read()/readLine()方法均为阻塞式操作。如果没有连接请求或数据未到达,线程会卡在当前调用处。资源管理:使用
try-with-resources可以确保在抛出异常或通信结束后自动关闭流和套接字,防止端口占用和内存泄漏。缓冲区刷盘:在利用
PrintWriter或BufferedWriter发送数据时,建议开启自动刷盘(如new PrintWriter(out, true))或手动调用flush(),否则数据可能会滞留在内存缓冲区中未及时发出。
InetAddress~
是 Java 标准库中用于表示 IP 地址(包含 IPv4 和 IPv6)的核心类。它封装了主机名与 IP 地址之间的映射关系,是所有底层网络通信(如 Socket、DatagramSocket、URL 连接等)的基础。java.net.InetAddress
类定位
在 Java 网络编程中,InetAddress 充当着网络节点标识符的角色:
- 无公有构造方法:不能通过
new InetAddress()实例化,必须通过其提供的静态工厂方法来获取实例。 - 不可变对象(Immutable):实例一旦创建,其包含的 IP 地址和主机名数据不可更改,天然具备线程安全性。
- 继承体系:
InetAddress(抽象父类,定义通用接口)Inet4Address(IPv4 地址实现类,如192.168.1.1)Inet6Address(IPv6 地址实现类,如fe80::1)
解析原理
当通过域名或主机名来获取 InetAddress 实例时,Java 会触发域名解析(DNS Lookup)过程:
JVM 缓存检索:首先检查 JVM 内部是否已存在该域名的解析缓存。
操作系统解析:若 JVM 未命中,调用操作系统的解析接口(如
getaddrinfo),检查本地hosts文件和系统 DNS 缓存。网络 DNS 查询:若本地均无记录,向配置的 DNS 服务器发送网络查询请求。
封装对象:获取到 IP 地址字节后,JVM 会根据 IP 版本创建对应的
Inet4Address或Inet6Address实例返回。
API:InetAddress
工厂方法
InetAddressInetAddress.getByName():(String host),根据主机名/IP 字符串获取地址对象。将传入的主机名(如"example.com")解析为 IP 地址,或将 IP 文本(如"192.168.1.1")直接转化为地址对象。InetAddressInetAddress.getLocalHost():(),获取本机地址对象。通过查找操作系统的本机主机名解析出本机的 IP 地址。InetAddress[]InetAddress.getAllByName():(String host),根据主机名获取所有关联 IP 地址。当一个域名绑定了多个 IP(如 CDN 或负载均衡)时,返回包含所有对应 IP 地址的数组。InetAddressInetAddress.getByAddress():(byte[] addr),根据原始 IP 字节数组创建对象。依据传入的 4 字节(IPv4)或 16 字节(IPv6)网络字节序(Big-Endians)数组直接构造地址对象,不触发 DNS 解析。InetAddressInetAddress.getByAddress():(String host, byte[] addr),根据主机名与原始 IP 字节数组创建对象。同时指定主机名与字节数组 IP,避免触发网络 DNS 校验。InetAddressInetAddress.getLoopbackAddress():(),获取本地环回地址。返回代表 IPv4 环回(127.0.0.1)或 IPv6 环回(::1)的地址对象。
注意事项:
InetAddress.getByName(String host)和InetAddress.getAllByName(String host)会触发网络 DNS 查询。如果域名不存在、网络断开或 DNS 服务器无响应,会抛出java.net.UnknownHostException(继承自IOException),且可能导致当前线程长时间阻塞。InetAddress.getByAddress(byte[] addr)必须严格保证传入的字节数组长度为 4 或 16。若传入其他长度的数组,将抛出UnknownHostException。由于 Java 中byte为有符号类型(-128 至 127),表示大于 127 的 IP 字节(如192)时需显式强转,例如(byte) 192。InetAddress.getLocalHost()在多网卡、VPN 或/etc/hosts主机名配置不当的环境下,可能抛出UnknownHostException或返回非预期的内网/虚拟网卡 IP。
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
public class InetAddressFactoryDemo {
public static void main(String[] args) {
try {
// 1. 根据域名解析单个 IP 地址
InetAddress singleIp = InetAddress.getByName("example.com");
System.out.println("example.com 解析结果: " + singleIp);
// 2. 根据域名解析所有关联 IP 地址(多 IP/负载均衡场景)
InetAddress[] allIps = InetAddress.getAllByName("baidu.com");
System.out.println("baidu.com 绑定的 IP 列表: " + Arrays.toString(allIps));
// 3. 根据原生字节数组构造 IP(无需 DNS 解析,注意 byte 强转)
byte[] ipv4Bytes = new byte[]{(byte) 192, (byte) 168, 1, 100};
InetAddress customIp = InetAddress.getByAddress(ipv4Bytes);
System.out.println("字节数组直接构造结果: " + customIp.getHostAddress());
// 4. 获取本机 IP 地址
InetAddress localHost = InetAddress.getLocalHost();
System.out.println("本机地址: " + localHost);
// 5. 获取本地环回地址(Loopback)
InetAddress loopback = InetAddress.getLoopbackAddress();
System.out.println("环回地址: " + loopback);
} catch (UnknownHostException e) {
System.err.println("域名解析失败或网络异常: " + e.getMessage());
}
}
}地址信息获取
StringgetHostName():(),获取主机名。返回此 IP 地址的主机名。如果该对象是通过主机名创建的则直接返回;否则会触发反向 DNS 查找(Reverse DNS Lookup)。StringgetHostAddress():(),获取文本格式 IP 地址。返回标准的 IP 地址文本表示(如 IPv4 点分十进制"192.168.1.1"或 IPv6 冒分十六进制)。StringgetCanonicalHostName():(),获取全限定域名(FQDN)。强行触发反向 DNS 查询,获取该 IP 地址在系统网络中完全合格的域名。byte[]getAddress():(),获取原始 IP 字节数组。返回网络字节序(高位字节在前,Big-Endian)的 IP 原始字节数组。
注意事项:
- 调用
getHostName()或getCanonicalHostName()时,若对象初始只包含 IP 地址,系统会尝试向 DNS 发起反向域名解析(PTR 记录查询)。如果目标 IP 没有配置 PTR 记录或网络超时,会导致严重的方法调用卡顿,最终超时后回退返回 IP 地址字符串本身。getAddress()返回的byte[]数组是对象内部字节数组的浅拷贝(Clone),修改该数组不会影响InetAddress内部状态,但读取字节数值时需特别注意无符号转换:int unsignedByte = byteValue & 0xFF。
import java.net.InetAddress;
import java.net.UnknownHostException;
public class InetAddressInfoDemo {
public static void main(String[] args) {
try {
// 通过 IP 文本构建对象
InetAddress address = InetAddress.getByName("8.8.8.8");
// 1. 获取字符串格式 IP 地址(不触发网络反查)
String ipString = address.getHostAddress();
System.out.println("IP 文本字符串: " + ipString);
// 2. 获取原始字节数组并转为无符号整数展示
byte[] rawBytes = address.getAddress();
System.out.print("原始字节数组(无符号转换): ");
for (int i = 0; i < rawBytes.length; i++) {
int unsignedByte = rawBytes[i] & 0xFF;
System.out.print(unsignedByte + (i < rawBytes.length - 1 ? "." : ""));
}
System.out.println();
// 3. 获取主机名(若无主机名可能触发反向 DNS 查找)
System.out.println("获取主机名 (getHostName): " + address.getHostName());
// 4. 获取规范全限定域名(强行反查)
System.out.println("获取 FQDN (getCanonicalHostName): " + address.getCanonicalHostName());
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
}状态判断与检测
booleanisReachable():(int timeout),测试地址可达性(Ping)。在指定的超时毫秒数内,尝试检测当前地址是否可达。booleanisReachable():(NetworkInterface netif, int ttl, int timeout),通过指定网卡和 TTL 测试可达性。通过指定的网络接口、生存时间(TTL)与超时毫秒数测试地址是否可达。booleanisLoopbackAddress():(),检测是否为环回地址。检查地址是否属于 IPv4(127.0.0.0/8)或 IPv6(::1)环回地址。booleanisAnyLocalAddress():(),检测是否为通配符地址。检查地址是否为通配地址(Wildcard Address,即0.0.0.0或::)。booleanisSiteLocalAddress():(),检测是否为私有/局域网地址。检查地址是否属于专用网络/局域网地址(如 IPv4 的10.0.0.0/8、172.16.0.0/12、192.168.0.0/16)。booleanisMulticastAddress():(),检测是否为组播地址。检查地址是否属于 IP 组播地址(IPv4 的 D 类地址224.0.0.0~239.255.255.255或 IPv6 的FF00::/8)。
注意事项:
isReachable(int timeout)的底层实现取决于运行环境权限:如果 JVM 具有根权限(Root/Administrator),它会尝试发送原生 ICMP ECHO REQUEST 包(标准 Ping);若缺乏根权限,它会退而求其次,尝试连接目标主机的 TCP Port 7 (Echo 端口)。由于现代防火墙普遍屏蔽 Port 7,无 Root 权限下isReachable()极易返回false,不可完全替代系统原生ping指令。
import java.io.IOException;
import java.net.InetAddress;
public class InetAddressStatusDemo {
public static void main(String[] args) {
try {
InetAddress local = InetAddress.getByName("127.0.0.1");
InetAddress privateIp = InetAddress.getByName("192.168.1.1");
InetAddress publicIp = InetAddress.getByName("1.1.1.1");
// 1. 检查环回地址
System.out.println("127.0.0.1 是否为环回地址: " + local.isLoopbackAddress());
// 2. 检查局域网私有地址
System.out.println("192.168.1.1 是否为局域网私有地址: " + privateIp.isSiteLocalAddress());
System.out.println("1.1.1.1 是否为局域网私有地址: " + publicIp.isSiteLocalAddress());
// 3. 检查通配符地址
InetAddress wildcard = InetAddress.getByName("0.0.0.0");
System.out.println("0.0.0.0 是否为通配符地址: " + wildcard.isAnyLocalAddress());
// 4. 检测目标可达性(设置 2000 毫秒超时)
System.out.println("尝试探测 1.1.1.1 可达性 (超时 2000ms)...");
boolean reachable = publicIp.isReachable(2000);
System.out.println("1.1.1.1 可达性测试结果: " + reachable);
} catch (IOException e) {
e.printStackTrace();
}
}
}DNS 缓存
为提升性能,JVM 默认会缓存 DNS 解析结果。缓存时长由两个关键系统参数决定:
networkaddress.cache.ttl:解析成功记录的缓存有效时间(单位为秒)。- 若设为
-1:表示永久缓存(非安全管理器模式下,不同 JVM 默认值可能不同)。 - 若设为
0:禁用缓存。
- 若设为
networkaddress.cache.negative.ttl:解析失败记录的缓存有效时间(默认 10 秒)。
微服务与动态 IP 注意事项:
在 Kubernetes 或云服务(如 AWS ALB)环境下,由于服务节点的 IP 经常动态变动,如果 JVM 将 DNS 解析永久缓存(TTL = -1),当后端节点更新 IP 后,Java 应用将无法感知,导致持续请求旧 IP 而报错。生产环境中通常建议设置一个较短的 TTL 值(如
-Dnetworkaddress.cache.ttl=10)。
注意事项
同步阻塞特性:
getByName()和getAllByName()需要发起网络 DNS 查询,这属于同步阻塞操作。若 DNS 服务器响应慢,会直接卡住当前调用线程。isReachable()的实现缺陷:isReachable()会优先尝试发送 ICMP Echo 报文(需要操作系统 root/admin 权限)。若缺乏权限,它会自动退化为尝试连接目标的 TCP 7 端口(Echo 端口)。如果目标服务器禁用了 7 端口,即使网络通畅,该方法也会返回false。与
InetSocketAddress的区别:InetAddress:仅代表 IP 地址(或 IP + 主机名),不包含端口信息。InetSocketAddress:代表 IP 地址 + 端口号(如127.0.0.1:8080),通常直接传入Socket.connect()或ServerSocket的绑定方法中。
API:ServerSocket
构造方法
ServerSocketServerSocket():(),构造方法。创建一个未绑定本地端口和地址的空套接字对象。常用于在绑定端口前需要提前配置套接字参数(如SO_REUSEADDR)的场景。ServerSocketServerSocket():(int port),构造方法。创建套接字并将其自动绑定到指定的本地端口。采用默认的全连接队列长度(Backlog)。ServerSocketServerSocket():(int port, int backlog),构造方法。创建套接字并绑定到指定端口,同时显式指定内核全连接队列的最大容量(Backlog)。ServerSocketServerSocket():(int port, int backlog, InetAddress bindAddr),构造方法。创建套接字,绑定到指定的端口及指定的本地 IP 地址(适用于具备多网卡/多 IP 的服务器)。
注意事项:
- 参数
port传入0时,系统会自动从动态/私有端口范围(Ephemeral Ports)中随机分配一个可用的空闲端口。可通过getLocalPort()获取实际分配的端口。backlog参数提示操作系统内核全连接队列的最大长度。当高并发场景下队列满载时,新的 TCP 连接请求可能会被拒(收到 RST)或被忽略(导致客户端超时重试)。若传入的值小于等于0,则系统将使用平台缺省默认值(JDK 内部默认通常为 50)。
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
public class ServerSocketConstructorDemo {
public static void main(String[] args) {
try {
// 1. 无参构造方法:手动绑定,可灵活配置选项
ServerSocket unboundServer = new ServerSocket();
unboundServer.bind(new InetSocketAddress("127.0.0.1", 0), 100);
System.out.println("绑定随机端口: " + unboundServer.getLocalPort());
unboundServer.close();
// 2. 指定端口与 Backlog 队列长度
ServerSocket portServer = new ServerSocket(8081, 200);
System.out.println("8081 端口服务端已就绪,Backlog=200");
portServer.close();
// 3. 绑定到特定网络接口(IP)
InetAddress localAddr = InetAddress.getByName("127.0.0.1");
ServerSocket specificIpServer = new ServerSocket(8082, 50, localAddr);
System.out.println("绑定特定 IP: " + specificIpServer.getInetAddress().getHostAddress());
specificIpServer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}连接监听与生命周期
Socketaccept():(),监听并接受客户端连接。阻塞当前线程,直到建立一个新的 TCP 连接,并返回与客户端通信的Socket对象。voidbind():(SocketAddress endpoint),绑定本地端点。将未绑定的套接字绑定到指定的 IP 地址和端口号。voidbind():(SocketAddress endpoint, int backlog),绑定本地端点并设置队列。绑定端点同时设置 TCP 连接挂起队列的最大长度。booleanisBound():(),检查绑定状态。返回套接字当前是否已成功绑定到本地端口。booleanisClosed():(),检查关闭状态。返回套接字当前是否已被关闭。voidclose():(),关闭套接字。释放该套接字绑定的端口与操作系统资源,同时中断处于阻塞状态的accept()调用。
注意事项:
accept()为阻塞式方法(Blocking I/O)。若没有任何客户端尝试连接,当前线程会持续挂起,不占用 CPU 资源。- 当线程被阻塞在
accept()时,若在另一个线程中调用该ServerSocket的close()方法,accept()会立刻抛出SocketException并终止阻塞。isBound()方法仅标识该套接字历史上是否成功绑定过端口。即便调用了close()方法,isBound()仍会返回true。判断套接字是否处于正常可用状态,应综合判定:serverSocket.isBound() && !serverSocket.isClosed()。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
public class ServerSocketLifecycleDemo {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try {
serverSocket = new ServerSocket();
System.out.println("绑定前状态 -> isBound: " + serverSocket.isBound() + ", isClosed: " + serverSocket.isClosed());
serverSocket.bind(new InetSocketAddress(8083));
System.out.println("绑定后状态 -> isBound: " + serverSocket.isBound() + ", isClosed: " + serverSocket.isClosed());
final ServerSocket targetServer = serverSocket;
// 创建子线程模拟异步关闭 ServerSocket 以解除 accept 阻塞
new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("异步关闭 ServerSocket...");
targetServer.close();
} catch (Exception e) {
e.printStackTrace();
}
}).start();
System.out.println("开始阻塞在 accept()...");
Socket socket = serverSocket.accept(); // 将抛出 SocketException
} catch (SocketException e) {
System.out.println("捕获到套接字关闭异常(已安全解除 accept 阻塞): " + e.getMessage());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (serverSocket != null) {
System.out.println("最终状态 -> isBound: " + serverSocket.isBound() + ", isClosed: " + serverSocket.isClosed());
}
}
}
}套接字参数配置
voidsetSoTimeout():(int timeout),设置接收超时时间。设置accept()方法阻塞等待的最大毫秒数(设置SO_TIMEOUT选项)。超时后抛出SocketTimeoutException,且ServerSocket保持有效状态。参数为0表示无限等待。intgetSoTimeout():(),获取超时设置。返回当前SO_TIMEOUT选项的毫秒值。voidsetReuseAddress():(boolean on),设置地址复用。启用或禁用SO_REUSEADDR套接字选项。处于TIME_WAIT状态的本地地址是否允许被快速重新绑定。booleangetReuseAddress():(),获取地址复用状态。返回当前SO_REUSEADDR选项的状态。voidsetReceiveBufferSize():(int size),设置默认接收缓冲区大小。为由此ServerSocket接受(accept)建立的所有Socket设定默认的 TCP 接收缓冲区字节数大小(TCP Window Size)。intgetReceiveBufferSize():(),获取接收缓冲区大小。返回当前设置的 TCP 接收缓冲区字节数大小。
注意事项:
setReuseAddress(boolean on)必须在套接字绑定端口之前(即bind()调用前)配置,否则配置将不会对底层 Socket 产生任何效果。setReceiveBufferSize(int size)也建议在bind()之前调用。这样设置的值才能通过 TCP SYN 报文中的 Window Scale 选项向对端通告初始滑动窗口大小(RFC 1323)。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketTimeoutException;
public class ServerSocketOptionsDemo {
public static void main(String[] args) {
try {
ServerSocket serverSocket = new ServerSocket();
// 1. 设置地址复用(必须在 bind 之前设置)
serverSocket.setReuseAddress(true);
System.out.println("SO_REUSEADDR 状态: " + serverSocket.getReuseAddress());
// 2. 设置接收缓冲区大小(建议在 bind 之前设置)
serverSocket.setReceiveBufferSize(64 * 1024); // 64 KB
System.out.println("套接字接收缓冲区大小: " + serverSocket.getReceiveBufferSize() + " 字节");
// 3. 绑定端口
serverSocket.bind(new InetSocketAddress(8084));
// 4. 设置 accept 阻塞超时时间为 1500 毫秒
serverSocket.setSoTimeout(1500);
System.out.println("SO_TIMEOUT 毫秒数: " + serverSocket.getSoTimeout());
try {
System.out.println("进入 accept 阻塞,等待客户端连接(将在 1.5 秒后超时)...");
serverSocket.accept();
} catch (SocketTimeoutException e) {
System.err.println("accept 触发超时,无需终止服务端,可继续重试:" + e.getMessage());
}
serverSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}API:Socket
构造方法
SocketSocket():(),构造方法。创建一个未连接的套接字对象。常用于在连接前配置套接字参数(如SO_TIMEOUT、TCP_NODELAY)。SocketSocket():(String host, int port),构造方法。创建套接字并自动连接到指定的主机名和端口号。使用默认的系统超时配置。SocketSocket():(InetAddress address, int port),构造方法。创建套接字并自动连接到指定的 IP 地址和端口号。SocketSocket():(String host, int port, InetAddress localAddr, int localPort),构造方法。创建套接字,连接到远程主机和端口,同时显式绑定本地特定的 IP 地址和本地端口号。SocketSocket():(Proxy proxy),构造方法。使用指定的代理服务器(如 SOCKS 或 HTTP 代理)创建一个未连接的套接字对象。
注意事项:
- 直接使用
Socket(String host, int port)构造方法会立即触发阻塞式的 TCP 连接请求。如果网络不可达,该方法将持续阻塞直到底层操作系统超时(Linux 默认通常需要数十秒到 2 分钟)。在生产环境中,应推荐使用空参构造方法Socket()配合connect(SocketAddress endpoint, int timeout)来精准控制建连超时。- 指定
localPort为0时,系统会自动分配任意可用的本地空闲端口。
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Proxy;
import java.net.Socket;
public class SocketConstructorDemo {
public static void main(String[] args) {
try {
// 1. 无参构造方法:方便配置参数后再发起连接
Socket socket1 = new Socket();
System.out.println("socket1 创建完成,当前连接状态: " + socket1.isConnected());
socket1.close();
// 2. 绑定特定本地接口和本地端口的构造方法
InetAddress localHost = InetAddress.getByName("127.0.0.1");
Socket socket2 = new Socket("example.com", 80, localHost, 0);
System.out.println("socket2 已连接,系统分配的本地端口: " + socket2.getLocalPort());
socket2.close();
// 3. 通过 Proxy 代理对象创建 Socket
Proxy socksProxy = new Proxy(Proxy.Type.SOCKS, new InetSocketAddress("127.0.0.1", 1080));
Socket socket3 = new Socket(socksProxy);
System.out.println("socket3 代理套接字创建成功(未连接)");
socket3.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}连接建立与状态控制
voidconnect():(SocketAddress endpoint),建立套接字连接。将套接字连接到指定的远端端点。采用系统无限等待或默认超时设置。voidconnect():(SocketAddress endpoint, int timeout),指定超时的连接建立。将套接字连接到远端端点,并设置连接建立的最大超时时间(单位:毫秒)。booleanisConnected():(),获取连接绑定状态。返回套接字历史上是否成功建立过 TCP 连接。booleanisBound():(),获取本地绑定状态。返回套接字是否已成功绑定到本地端口。booleanisClosed():(),获取关闭状态。返回套接字是否已被主动关闭。voidclose():(),关闭套接字。释放底层操作系统套接字句柄及与之关联的所有输入输出流。
注意事项:
isConnected()返回true仅仅代表该 Socket 过去曾经成功与服务端建立过连接,并不代表当前网络链路上 TCP 连接依然健康通畅。套接字关闭后isConnected()依然会返回true。要判断 Socket 当前的有效状态,应使用:socket.isConnected() && !socket.isClosed()。connect()方法中的timeout为0时被视为无限期等待。若超时触发,将抛出java.net.SocketTimeoutException。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
public class SocketConnectionDemo {
public static void main(String[] args) {
Socket socket = new Socket();
try {
System.out.println("连接前 -> isConnected: " + socket.isConnected() + ", isClosed: " + socket.isClosed());
// 设置 2000 毫秒连接超时
InetSocketAddress address = new InetSocketAddress("10.255.255.1", 80); // 假定不可达 IP
socket.connect(address, 2000);
} catch (SocketTimeoutException e) {
System.err.println("连接指定超时,触发 SocketTimeoutException: " + e.getMessage());
} catch (IOException e) {
System.err.println("网络连接发生异常: " + e.getMessage());
} finally {
System.out.println("连接尝试后 -> isBound: " + socket.isBound() + ", isClosed: " + socket.isClosed());
try {
if (!socket.isClosed()) {
socket.close();
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println("关闭套接字后 -> isConnected: " + socket.isConnected() + ", isClosed: " + socket.isClosed());
}
}
}数据传输与半关闭控制
InputStreamgetInputStream():(),获取套接字输入流。返回用于从该套接字读取字节数据的输入流。OutputStreamgetOutputStream():(),获取套接字输出流。返回用于向该套接字写入字节数据的输出流。voidshutdownInput():(),半关闭输入通道。终止当前套接字的输入流,后续试图从该流读取数据都将直接返回-1(EOF)。voidshutdownOutput():(),半关闭输出通道。终止当前套接字的输出流,并向对端发送 TCPFIN包,告知对方本端已无数据发送。booleanisInputShutdown():(),获取输入流半关闭状态。返回该套接字的输入通道是否已单向关闭。booleanisOutputShutdown():(),获取输出流半关闭状态。返回该套接字的输出通道是否已单向关闭。
注意事项:
- 切勿直接关闭包装套接字的流(如调用
inputStream.close()或outputStream.close())。这会导致整个底层 Socket 被直接关闭,从而破坏全双工通信通道。shutdownOutput()是实现单向单工通知(如 HTTP 协议或请求应答模型中显式通知对方“请求报文已发送完毕”)的关键机制,它允许在关闭写通道的同时继续保持读通道可用。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class SocketDataStreamDemo {
public static void main(String[] args) {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress("httpbin.org", 80), 3000);
// 获取输出流写入数据
PrintWriter writer = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
writer.print("GET /get HTTP/1.0\r\nHost: httpbin.org\r\n\r\n");
writer.flush();
// 发送 FIN 告知服务端写入完毕(半关闭输出流)
socket.shutdownOutput();
System.out.println("输出通道已关闭: isOutputShutdown = " + socket.isOutputShutdown());
System.out.println("输入通道状态: isInputShutdown = " + socket.isInputShutdown());
// 输入流依然可用,正常读取服务端返回数据
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
String line;
System.out.println("--- 开始接收服务端数据 ---");
while ((line = reader.readLine()) != null) {
System.out.println(line);
break; // 仅读取第一行示例
}
} catch (Exception e) {
e.printStackTrace();
}
}
}套接字选项配置
voidsetSoTimeout():(int timeout),设置读取超时时间。设置InputStream.read()操作的最大阻塞毫秒数(SO_TIMEOUT)。超过该时长抛出SocketTimeoutException。intgetSoTimeout():(),获取读取超时时间。返回当前的SO_TIMEOUT参数毫秒值。voidsetTcpNoDelay():(boolean on),配置 Nagle 算法。启用或禁用TCP_NODELAY选项。设置为true时禁用 Nagle 算法,强制小数据包立即发送,大幅降低高频小包的延时。booleangetTcpNoDelay():(),获取 TCP_NODELAY 状态。返回当前 Nagle 算法是否已禁用。voidsetSoLinger():(boolean on, int linger),配置关闭挂起时间。设置SO_LINGER选项。控制调用close()时,底层缓冲区未发送数据时的阻塞滞留秒数。intgetSoLinger():(),获取 SO_LINGER 配置。返回当前的SO_LINGER滞留时间(秒)。未开启则返回-1。voidsetKeepAlive():(boolean on),设置 TCP 保持连接探针。启用或禁用SO_KEEPALIVE选项。长时间无数据交互时由内核发送心跳包检测链路存活。booleangetKeepAlive():(),获取 SO_KEEPALIVE 状态。返回当前保持连接探针的状态。voidsetSendBufferSize():(int size),设置发送缓冲区容量。设置内核 TCP 发送缓冲区(SO_SNDBUF)的字节数大小。voidsetReceiveBufferSize():(int size),设置接收缓冲区容量。设置内核 TCP 接收缓冲区(SO_RCVBUF)的字节数大小。
注意事项:
setTcpNoDelay(true)适合游戏、RPC、实时通讯等对延时极度敏感的场景;但对于高吞吐量写密集型应用,禁用 Nagle 算法可能会增加网络小包数量,降低网络整体利用率。setSoLinger(true, 0)会在调用close()时直接丢弃内核发送缓冲区中的所有数据,并向对端强行发送 TCPRST包(重置连接),跳过正常四次挥手流程。需慎用。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
public class SocketOptionsDemo {
public static void main(String[] args) {
try (Socket socket = new Socket()) {
// 1. 配置读取超时 SO_TIMEOUT = 5000ms
socket.setSoTimeout(5000);
System.out.println("SO_TIMEOUT: " + socket.getSoTimeout() + " ms");
// 2. 禁用 Nagle 算法,降低实时交互延迟
socket.setTcpNoDelay(true);
System.out.println("TCP_NODELAY (Nagle 禁用): " + socket.getTcpNoDelay());
// 3. 启用 TCP 存活探针
socket.setKeepAlive(true);
System.out.println("SO_KEEPALIVE: " + socket.getKeepAlive());
// 4. 配置 SO_LINGER,在 close 时最多逗留等待 3 秒以确保缓冲区数据发送完毕
socket.setSoLinger(true, 3);
System.out.println("SO_LINGER 滞留秒数: " + socket.getSoLinger());
// 5. 连接服务端
socket.connect(new InetSocketAddress("example.com", 80), 3000);
} catch (IOException e) {
e.printStackTrace();
}
}
}UDP 编程
Java 中的 UDP 编程基于 java.net 包提供的 DatagramSocket 和 DatagramPacket 两个类实现。UDP(用户数据报协议) 是一种无连接、不可靠但传输效率极高的传输层协议。
与 TCP 的字节流不同,UDP 以数据报(Datagram) 为独立单位进行传输,每个数据报中都包含了完整的目的地址和端口信息。
核心概念
Java 将 UDP 通信解耦为“传输通道”与“数据载体”两个部分:
DatagramSocket:代表通信的套接字端点,负责在网络中发送和接收数据报。可以将其比作邮筒或信箱。DatagramPacket:代表具体的数据报文。包含待传输的字节数组,以及目标的 IP 地址和端口号(发送时)或发送方的 IP 地址和端口号(接收时)。可以将其比作装有内容和地址的信封。
通信流程
由于 UDP 无需建立连接,发送端和接收端的地位是相对平等的:
接收端准备:创建
DatagramSocket并绑定指定端口;准备一个空字节数组和DatagramPacket容器,调用receive()阻塞等待。发送端打包:创建
DatagramSocket;将要发送的数据转为字节数组,连同目标的 IP 地址与端口封装进DatagramPacket。数据传输:发送端调用
send()方法将数据包投递出去。接收与解析:接收端的
receive()方法收到数据包后返回,随后从DatagramPacket中提取数据内容及发送者信息。

示例:UDP 客户端与服务端通信
以下演示一个完整的 UDP 客户端与服务端通信过程。
接收端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.charset.StandardCharsets;
public class UDPServer {
public static void main(String[] args) {
int port = 8088;
// 1. 绑定 8088 端口监听
try (DatagramSocket socket = new DatagramSocket(port)) {
System.out.println("UDP 服务端已启动,等待接收数据包...");
// 2. 准备 1KB 的缓冲区来接收数据
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
// 3. 阻塞等待客户端发送的数据
socket.receive(packet);
// 4. 解析接收到的字节数据
String receivedMsg = new String(packet.getData(), 0, packet.getLength(), StandardCharsets.UTF_8);
System.out.println("收到来自 [" + packet.getAddress() + ":" + packet.getPort() + "] 的消息: " + receivedMsg);
// 5. 给客户端发送回执
String response = "服务端已成功收到数据!";
byte[] respBytes = response.getBytes(StandardCharsets.UTF_8); // 字符串转字节
DatagramPacket respPacket = new DatagramPacket(
respBytes, respBytes.length, packet.getAddress(), packet.getPort()
);
socket.send(respPacket);
} catch (IOException e) {
e.printStackTrace();
}
}
}发送端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
public class UDPClient {
public static void main(String[] args) {
String serverHost = "127.0.0.1";
int serverPort = 8088;
// 1. 随机端口初始化 Socket
try (DatagramSocket socket = new DatagramSocket()) {
socket.setSoTimeout(3000); // 设置接收超时 3 秒
// 2. 构建要发送的数据包
String message = "Hello, Java UDP Server!";
byte[] data = message.getBytes(StandardCharsets.UTF_8);
InetAddress serverAddr = InetAddress.getByName(serverHost);
DatagramPacket packet = new DatagramPacket(data, data.length, serverAddr, serverPort);
// 3. 发送数据包
System.out.println("正在向服务端发送 UDP 数据包...");
socket.send(packet);
// 4. 接收服务端的响应
byte[] buffer = new byte[1024];
DatagramPacket respPacket = new DatagramPacket(buffer, buffer.length);
socket.receive(respPacket);
String response = new String(respPacket.getData(), 0, respPacket.getLength(), StandardCharsets.UTF_8);
System.out.println("收到服务端响应: " + response);
} catch (IOException e) {
System.err.println("通信失败或超时: " + e.getMessage());
}
}
}API:DatagramSocket
构造方法
DatagramSocketDatagramSocket():(),构造方法。创建一个未绑定到任何本地地址和端口的数据报套接字。需配合bind()方法使用,或在首次send()时由系统自动分配随机空闲端口。DatagramSocketDatagramSocket():(int port),构造方法。创建数据报套接字并将其绑定到指定的本地端口。通常用于 UDP 服务端绑定固定端口。DatagramSocketDatagramSocket():(int port, InetAddress laddr),构造方法。创建数据报套接字,绑定到指定的本地端口和特定的本地 IP 地址(适用于多网卡服务器环境)。DatagramSocketDatagramSocket():(SocketAddress bindaddr),构造方法。创建数据报套接字并绑定到指定的SocketAddress(封装 IP 地址与端口)。
注意事项:
- 参数
port传入0时,操作系统会自动分配一个未使用的动态/私有端口(Ephemeral Port)。可通过getLocalPort()获取实际绑定的端口。- 若指定的端口已经被其他进程(或套接字)占用,构造方法将抛出
java.net.SocketException: Address already in use。
import java.io.IOException;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
public class DatagramSocketConstructorDemo {
public static void main(String[] args) {
try {
// 1. 无参构造方法:未绑定状态
DatagramSocket socket1 = new DatagramSocket(null); // 明确传入 null 表示不绑定
socket1.bind(new InetSocketAddress("127.0.0.1", 0)); // 手动绑定随机端口
System.out.println("socket1 绑定成功,自动分配端口: " + socket1.getLocalPort());
socket1.close();
// 2. 指定端口构造方法
DatagramSocket socket2 = new DatagramSocket(9091);
System.out.println("socket2 成功绑定端口: " + socket2.getLocalPort());
socket2.close();
// 3. 绑定特定网卡 IP 与端口
InetAddress localAddr = InetAddress.getByName("127.0.0.1");
DatagramSocket socket3 = new DatagramSocket(9092, localAddr);
System.out.println("socket3 绑定特定 IP: " + socket3.getLocalAddress());
socket3.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}数据报发送与接收
voidsend():(DatagramPacket p),发送数据包。从给定的DatagramPacket中提取数据、目标 IP 和目标端口,通过底层 UDP 套接字直接投递到网络。voidreceive():(DatagramPacket p),接收数据包。阻塞当前线程,直到接收到一个 UDP 数据包并将数据填入传入的DatagramPacket缓冲区中。
注意事项:
receive(DatagramPacket p)是阻塞式方法。若网络上没有数据包到达,当前线程会持续挂起,除非设置了SO_TIMEOUT超时或在其他线程中显式调用了close()。- 数据截断问题:传入
receive()的DatagramPacket内部缓冲区长度若小于实际收到的 UDP 报文长度,多余的字节会被操作系统内核直接静默丢弃(Truncated),不会抛出异常。因此,接收缓冲区建议预留足够容量(例如 UDP 最大有效载荷为 65507 字节,或根据应用层协议约定设置大小)。
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
public class DatagramSocketSendReceiveDemo {
public static void main(String[] args) {
try (DatagramSocket receiver = new DatagramSocket(9093);
DatagramSocket sender = new DatagramSocket()) {
// 线程:异步发送数据
new Thread(() -> {
try {
Thread.sleep(500); // 模拟延迟
byte[] data = "UDP 通信数据载荷".getBytes(StandardCharsets.UTF_8);
DatagramPacket packet = new DatagramPacket(
data, data.length, InetAddress.getByName("127.0.0.1"), 9093);
sender.send(packet);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
// 主线程:接收数据
byte[] buffer = new byte[512];
DatagramPacket receivePacket = new DatagramPacket(buffer, buffer.length);
System.out.println("等待接收数据包...");
receiver.receive(receivePacket); // 阻塞挂起
String msg = new String(receivePacket.getData(), 0, receivePacket.getLength(), StandardCharsets.UTF_8);
System.out.println("成功接收数据: " + msg + " (字节长度: " + receivePacket.getLength() + ")");
} catch (Exception e) {
e.printStackTrace();
}
}
}虚拟连接与地址绑定
voidconnect():(InetAddress address, int port),创建虚拟连接。将套接字锁定到远程特定主机的指定端口。连接后,此套接字只能向该远端发送数据,也只能接收来自该远端的数据包。voidconnect():(SocketAddress addr),创建虚拟连接。使用SocketAddress锁定远端目标端点。voiddisconnect():(),断开虚拟连接。解除对特定远端地址的锁定,恢复为可向任意地址发送/接收数据包的自由模式。voidbind():(SocketAddress addr),绑定本地端点。将未绑定的套接字绑定到指定的本地 IP 和端口。booleanisConnected():(),获取虚拟连接状态。返回套接字当前是否已锁定指定的远程主机与端口。booleanisBound():(),获取绑定状态。返回套接字是否已成功绑定到本地端口。booleanisClosed():(),获取关闭状态。返回套接字是否已被主动关闭。voidclose():(),关闭套接字。释放底层套接字句柄及端口占用,中断处于阻塞状态的receive()调用。
注意事项:
- UDP 的
connect()方法完全不涉及网络握手或传输层的逻辑连接,它仅仅是一个操作系统内核级别的过滤限制(Kernel Filter)。- 调用
connect()后,内核会自动过滤掉非连接目标发来的 UDP 数据包,并且调用send()时无须在DatagramPacket中再次提供目标 IP 和端口。此外,若发送的目标 ICMP 不可达,内核能够将错误信息反馈给 Java 层(抛出PortUnreachableException)。
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetSocketAddress;
public class DatagramSocketConnectDemo {
public static void main(String[] args) {
try {
DatagramSocket socket = new DatagramSocket();
socket.bind(new InetSocketAddress("127.0.0.1", 0));
System.out.println("绑定状态 (isBound): " + socket.isBound());
System.out.println("虚拟连接状态 (isConnected): " + socket.isConnected());
// 建立 UDP 虚拟连接(锁定远端 127.0.0.1:9094)
InetSocketAddress remoteAddr = new InetSocketAddress("127.0.0.1", 9094);
socket.connect(remoteAddr);
System.out.println("执行 connect 后 (isConnected): " + socket.isConnected());
// 试图发送数据包(此时 DatagramPacket 无需显式指定远端地址)
byte[] data = "Connected UDP Packet".getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length);
socket.send(packet);
// 解除虚拟连接限制
socket.disconnect();
System.out.println("执行 disconnect 后 (isConnected): " + socket.isConnected());
socket.close();
System.out.println("关闭套接字后 (isClosed): " + socket.isClosed());
} catch (IOException e) {
e.printStackTrace();
}
}
}套接字参数与控制
voidsetSoTimeout():(int timeout),设置读取超时时间。配置receive()方法阻塞等待的最大毫秒数(SO_TIMEOUT选项)。超过该时长将抛出SocketTimeoutException。0表示无限等待。intgetSoTimeout():(),获取读取超时时间。返回当前SO_TIMEOUT的毫秒值。voidsetBroadcast():(boolean broadcast),设置广播选项。启用或禁用SO_BROADCAST选项,控制是否允许发送 UDP 广播数据包(如发往255.255.255.255)。booleangetBroadcast():(),获取广播选项状态。返回当前是否允许发送广播包。voidsetReuseAddress():(boolean on),设置地址复用。启用或禁用SO_REUSEADDR选项。允许多个套接字绑定到完全相同的本地地址与端口(常用于组播/广播监听)。booleangetReuseAddress():(),获取地址复用状态。返回当前SO_REUSEADDR选项的状态。voidsetSendBufferSize():(int size),设置发送缓冲区容量。设置操作系统内核为此套接字分配的 UDP 发送缓冲区(SO_SNDBUF)字节数。voidsetReceiveBufferSize():(int size),设置接收缓冲区容量。设置操作系统内核为此套接字分配的 UDP 接收缓冲区(SO_RCVBUF)字节数。
注意事项:
- 发送局域网广播包(针对
255.255.255.255或网段广播地址)前,必须显式调用setBroadcast(true),否则在某些操作系统上会直接抛出Permission denied异常。setReuseAddress(true)必须在套接字显式调用bind()之前执行方可生效。
import java.net.DatagramSocket;
import java.net.DatagramPacket
import java.net.InetSocketAddress;
import java.net.SocketTimeoutException;
public class DatagramSocketOptionsDemo {
public static void main(String[] args) {
try {
DatagramSocket socket = new DatagramSocket(null);
// 1. 设置地址复用(必须在 bind 前调用)
socket.setReuseAddress(true);
System.out.println("SO_REUSEADDR 配置状态: " + socket.getReuseAddress());
// 2. 绑定本地端口
socket.bind(new InetSocketAddress(9095));
// 3. 启用广播支持
socket.setBroadcast(true);
System.out.println("SO_BROADCAST 配置状态: " + socket.getBroadcast());
// 4. 调整内核收发缓冲区
socket.setSendBufferSize(64 * 1024);
socket.setReceiveBufferSize(64 * 1024);
// 5. 设置 1000ms 超时
socket.setSoTimeout(1000);
System.out.println("SO_TIMEOUT: " + socket.getSoTimeout() + " ms");
try {
byte[] buf = new byte[256];
DatagramPacket p = new DatagramPacket(buf, buf.length);
System.out.println("进入 receive() 阻塞,等待 1 秒超时...");
socket.receive(p);
} catch (SocketTimeoutException e) {
System.err.println("成功捕获读取超时异常: " + e.getMessage());
}
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}API:DatagramPacket
构造方法
DatagramPacketDatagramPacket():(byte[] buf, int length),接收端构造方法。创建一个用于接收数据包的DatagramPacket,指定接收缓冲区和最大接收字节数length。DatagramPacketDatagramPacket():(byte[] buf, int offset, int length),带偏移量的接收端构造方法。创建一个用于接收数据包的DatagramPacket,指定接收缓冲区、起始写入偏移量offset及最大接收字节数length。DatagramPacketDatagramPacket():(byte[] buf, int length, InetAddress address, int port),发送端构造方法。创建一个用于发送数据包的DatagramPacket,将缓冲区前length个字节发送给指定 IP 地址和端口号的主机。DatagramPacketDatagramPacket():(byte[] buf, int offset, int length, InetAddress address, int port),带偏移量的发送端构造方法。创建一个用于发送数据包的DatagramPacket,将缓冲区从offset开始的length个字节发送给指定 IP 地址和端口号的主机。DatagramPacketDatagramPacket():(byte[] buf, int length, SocketAddress address),基于 SocketAddress 的发送端构造方法。使用包含 IP 和端口的SocketAddress端点创建发送数据包。DatagramPacketDatagramPacket():(byte[] buf, int offset, int length, SocketAddress address),带偏移量与 SocketAddress 的发送端构造方法。使用SocketAddress端点及指定数据偏移量创建发送数据包。
注意事项:
- 构造方法中的
length不能大于buf.length - offset,否则会抛出IllegalArgumentException异常。buf字节数组不会被复制,DatagramPacket内部直接持有传入数组的引用。对原数组的修改会直接影响数据包,反之亦然。
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
public class DatagramPacketConstructorDemo {
public static void main(String[] args) throws Exception {
byte[] buffer = new byte[1024];
// 1. 用于接收数据包的构造方法
DatagramPacket recvPacket1 = new DatagramPacket(buffer, buffer.length);
DatagramPacket recvPacket2 = new DatagramPacket(buffer, 128, 512);
// 2. 用于发送数据包的构造方法(指定 InetAddress + 端口)
InetAddress destination = InetAddress.getByName("127.0.0.1");
DatagramPacket sendPacket1 = new DatagramPacket(buffer, 256, destination, 9000);
DatagramPacket sendPacket2 = new DatagramPacket(buffer, 64, 128, destination, 9000);
// 3. 用于发送数据包的构造方法(指定 SocketAddress)
SocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 9000);
DatagramPacket sendPacket3 = new DatagramPacket(buffer, buffer.length, socketAddress);
DatagramPacket sendPacket4 = new DatagramPacket(buffer, 0, 100, socketAddress);
System.out.println("各类构造方法实例化完成。");
}
}缓冲区与数据载荷控制
byte[]getData():(),获取数据缓冲区。返回数据包内部关联的原始字节数组(注意:返回的是整个缓冲区,并非仅包含有效数据的切片)。intgetOffset():(),获取数据偏移量。返回有效数据在缓冲区中的起始字节索引。intgetLength():(),获取有效数据长度。返回发送或接收到的数据有效字节数。voidsetData():(byte[] buf),重置数据缓冲区。重新设置数据包的缓冲区,并将offset重置为0,length设置为buf.length。voidsetData():(byte[] buf, int offset, int length),重置数据缓冲区及范围。显式重新指定缓冲区、数据起始偏移量以及有效字节长度。voidsetLength():(int length),重置有效数据长度。重新设置数据包的有效字节长度。
注意事项:
- 循环复用陷阱(最常见 Bug):当把一个
DatagramPacket对象反复传入DatagramSocket.receive(packet)循环接收数据时,底层内核每次接收数据后都会将packet的length属性修改为本次实际接收到的字节数。如果不显式调用packet.setLength(buffer.length)还原最大接收容量,下一次接收时length就会变成上一次接收到的数值,导致后续接收的数据包被不断截断!- 从
getData()获取数组转为字符串时,必须结合getOffset()和getLength(),严禁直接使用new String(packet.getData()),否则会将缓冲区尾部的无用零字节(0x00)一并输出。
import java.net.DatagramPacket;
import java.nio.charset.StandardCharsets;
public class DatagramPacketBufferDemo {
public static void main(String[] args) {
byte[] buffer = new byte[64];
String message = "Java Network Programming";
byte[] msgBytes = message.getBytes(StandardCharsets.UTF_8);
// 将数据写入缓冲区特定位置
System.arraycopy(msgBytes, 0, buffer, 10, msgBytes.length);
// 创建带偏移量的包(offset = 10, length = msgBytes.length)
DatagramPacket packet = new DatagramPacket(buffer, 10, msgBytes.length);
// 1. 正确提取有效数据
byte[] rawBuf = packet.getData();
int offset = packet.getOffset();
int length = packet.getLength();
String extracted = new String(rawBuf, offset, length, StandardCharsets.UTF_8);
System.out.println("解包提取的字符串: " + extracted);
// 2. 重置缓冲区长度(常用于循环接收前重置状态)
packet.setLength(buffer.length);
System.out.println("重置后的有效长度容量: " + packet.getLength());
// 3. 重新分配新的数据缓冲区
byte[] newBuf = "New Buffer Content".getBytes(StandardCharsets.UTF_8);
packet.setData(newBuf, 0, newBuf.length);
System.out.println("重新 setData 后提取字符串: " + new String(packet.getData(), packet.getOffset(), packet.getLength()));
}
}地址与网络端点控制
InetAddressgetAddress():(),获取目标/源 IP 地址。若为待发送数据包,返回目标 IP 地址;若为已接收数据包,返回发送方的源 IP 地址。intgetPort():(),获取目标/源端口号。若为待发送数据包,返回目标端口号;若为已接收数据包,返回发送方的源端口号。SocketAddressgetSocketAddress():(),获取套接字端点地址。返回封装了 IP 地址和端口号的SocketAddress(通常为InetSocketAddress)对象。voidsetAddress():(InetAddress iaddr),设置目标 IP 地址。更改或指定数据包发送的目标 IP 地址。voidsetPort():(int iport),设置目标端口号。更改或指定数据包发送的目标端口号。voidsetSocketAddress():(SocketAddress address),设置目标套接字端点。通过SocketAddress对象一次性配置目标的 IP 地址与端口号。
注意事项:
- 在服务端接收到来自客户端的
DatagramPacket后,若需向该客户端发送响应(ECHO 或数据回复),可直接复用该数据包的SocketAddress,通过setSocketAddress(packet.getSocketAddress())快速定位客户端。
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
public class DatagramPacketAddressDemo {
public static void main(String[] args) throws Exception {
byte[] data = "Address Test".getBytes();
// 1. 获取目标 IP 和 端口
DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("127.0.0.1"), 8080); // 初始配置目标为 127.0.0.1:8080
System.out.println("原始 IP 地址: " + packet.getAddress().getHostAddress());
System.out.println("原始端口号: " + packet.getPort());
// 2. 重新设置端口号与 IP 地址
packet.setPort(9090);
packet.setAddress(InetAddress.getByName("192.168.1.1"));
System.out.println("更新后端口号: " + packet.getPort());
// 3. 使用 SocketAddress 进行整体替换
SocketAddress newDestination = new InetSocketAddress("10.0.0.1", 9999);
packet.setSocketAddress(newDestination);
SocketAddress currentAddress = packet.getSocketAddress();
System.out.println("基于 SocketAddress 的端点信息: " + currentAddress);
}
}注意事项
数据包大小限制:网络标准的以太网 MTU 一般为 1500 字节。除去 IP 头(20 字节)和 UDP 头(8 字节)后,单包推荐的有效载荷(Payload)控制在 1472 字节以内,以避免 IP 分片带来的丢包风险。
丢包与乱序:UDP 不保证数据包的到达顺序,也不保证数据包一定能送达。如果业务需要高可靠性,必须在应用层自主实现确认重传(ACK)与序号控制机制。
设置超时:调用
socket.receive()是阻塞操作。如果对端未发送数据或数据包在网络中丢失,线程会无限期等待。必须使用setSoTimeout()设置合理的超时机制。
URL 编程
Java 中的 URL 编程主要基于 java.net 包提供的 URL、URI、URLConnection 和 HttpURLConnection 类实现。它允许 Java 程序通过标准的 URL 地址访问 HTTP、HTTPS、FTP 和本地文件等网络资源。
核心概念
在 Java 传统网络 API 中,URL 通信体系由以下核心类协作组成:
java.net.URI:代表统一资源标识符(Uniform Resource Identifier),仅负责资源的语法结构校验与解析,不涉及网络连接。java.net.URL:代表统一资源定位符(Uniform Resource Locator),既包含资源位置标识,又具备打开网络流的能力。java.net.URLConnection:代表与 URL 所指向资源的通信连接抽象类。java.net.HttpURLConnection:URLConnection的子类,增加了对 HTTP/HTTPS 协议特有功能(如请求方法 GET/POST、响应头、状态码等)的支持。
URL 组成
一个完整的标准 URL 遵循以下语法格式:
protocol://host:port/path?query#fragment
- Protocol(协议):如
http、https、ftp、file。 - Host(主机名):服务器域名或 IP 地址。
- Port(端口):可选,默认 HTTP 为 80,HTTPS 为 443。
- Path(资源路径):服务器上的文件或 Endpoint 路径。
- Query(查询参数):以
key=value形式传递,多个参数用&分隔。 - Fragment(锚点):客户端定位标记,不随 HTTP 请求发送给服务器。

API:URL
构造方法
URLURL():(String spec),构造方法。根据传入的完整 URL 字符串创建对象。URLURL():(String protocol, String host, int port, String file),构造方法。显式指定协议、主机名、端口号与文件路径创建 URL 对象。URLURL():(String protocol, String host, String file),构造方法。显式指定协议、主机名与文件路径(使用该协议缺省端口)创建 URL 对象。URLURL():(URL context, String spec),构造方法。基于基准 URL(Context)与相对路径(Spec)解析并构造绝对 URL 对象。URLURL():(String protocol, String host, int port, String file, URLStreamHandler handler),构造方法。使用指定的自定义URLStreamHandler策略对象创建 URL。
注意事项:
- 弃用告警与替代方案(Java 20+):自 Java 20 起,
URL的所有构造方法已被标记为@Deprecated(since = "20")。直接使用new URL(...)会触发编译期警告。生产环境标准规范是优先使用URI.create("...").toURL()或new URI(...).toURL()。URI能更加严密且符合 RFC 规范地对非法字符(如未转义的空格、中文)进行编码与语法校验。
import java.net.URI;
import java.net.URL;
public class UrlConstructorDemo {
public static void main(String[] args) throws Exception {
// 1. Java 20+ 推荐的标准构造方式(通过 URI 转换)
URI baseUri = URI.create("https://example.com:8080/api/v1/");
URL urlFromUri = baseUri.toURL();
System.out.println("通过 URI 构造绝对 URL: " + urlFromUri);
// 2. 解析相对路径构造新的 URL
URI resolvedUri = baseUri.resolve("users/list?page=1");
URL relativeUrl = resolvedUri.toURL();
System.out.println("相对路径解析后的 URL: " + relativeUrl);
}
}元素解析与组件获取
StringgetProtocol():(),获取协议名称。返回 URL 所使用的通信协议(如"https"、"http"、"file")。StringgetHost():(),获取主机名。返回 URL 中的域名或 IP 地址字符串。intgetPort():(),获取显式端口号。返回 URL 中显式指定的端口号;若未显式指定则返回-1。intgetDefaultPort():(),获取协议默认端口。返回当前协议的标准默认端口号(例如 HTTP 为80,HTTPS 为443)。StringgetPath():(),获取资源路径。返回 URL 的路径部分(不包含 Query 查询参数)。StringgetFile():(),获取文件路径与查询串。返回路径与 Query 参数的组合(即path + "?" + query)。StringgetQuery():(),获取查询参数。返回?后面的请求键值对字符串。StringgetRef():(),获取锚点标识。返回#后面的片段标识符(Fragment)。StringgetAuthority():(),获取授权部分。返回包含用户信息、主机名与端口号的完整授权字符串。StringgetUserInfo():(),获取用户信息。返回 URL 中包含的身份验证凭据(如"user:password")。
注意事项:
- 致命性能陷阱:
URL.equals()与URL.hashCode()的阻塞式网络解析:URL类的equals()和hashCode()实现存在严重的历史设计缺陷——为了判断两个 URL 是否指向同一物理资源,它们会触发底层的 DNS 反向解析(通过InetAddress.getByName())。若网络卡顿或 DNS 响应缓慢,会导致当前线程严重阻塞。因此,严禁将URL对象存入HashMap、HashSet等依赖哈希码的集合中,应始终使用URI作为 Key 或集合元素。
import java.net.URI;
import java.net.URL;
public class UrlParsingDemo {
public static void main(String[] args) throws Exception {
URL url = URI.create("https://admin:secret123@example.com:8443/docs/index.html?lang=zh_CN&sort=asc#top").toURL();
System.out.println("协议 (Protocol) : " + url.getProtocol());
System.out.println("主机名 (Host) : " + url.getHost());
System.out.println("显式端口 (Port) : " + url.getPort());
System.out.println("协议默认端口 : " + url.getDefaultPort());
System.out.println("资源路径 (Path) : " + url.getPath());
System.out.println("文件整体 (File) : " + url.getFile());
System.out.println("查询参数 (Query) : " + url.getQuery());
System.out.println("锚点 (Ref) : " + url.getRef());
System.out.println("授权信息(Authority): " + url.getAuthority());
System.out.println("用户信息(UserInfo) : " + url.getUserInfo());
}
}网络连接与数据读取
InputStreamopenStream():(),打开资源输入流。直接建立与远程资源的连接,并返回一个InputStream用于读取数据。URLConnectionopenConnection():(),打开网络连接对象。返回代表该 URL 所指向远程对象连接的URLConnection(或其子类如HttpURLConnection)。URLConnectionopenConnection():(Proxy proxy),通过代理打开连接。使用指定的网络代理(如 SOCKS 或 HTTP 代理)建立网络连接。ObjectgetContent():(),获取 URL 资源内容。由系统匹配的ContentHandler自动将资源解析并转换为对应的 Java 对象。
注意事项:
url.openStream()仅仅是url.openConnection().getInputStream()的简写。使用openStream()无法配置超时时间(Timeout)、请求头(Headers)以及 HTTP 方法。网络不稳定时极易导致线程永久卡死在read()调用上。生产环境强烈建议通过openConnection()强转为HttpURLConnection后显式配置连接超时与读取超时。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
public class UrlConnectionDemo {
public static void main(String[] args) {
try {
URL url = URI.create("https://httpbin.org/get").toURL();
// 1. 获取 URLConnection 并强转为 HTTP 专门实现
URLConnection connection = url.openConnection();
if (connection instanceof HttpURLConnection httpConn) {
// 2. 显式设置连接超时与读取超时
httpConn.setConnectTimeout(3000); // 3 秒建连超时
httpConn.setReadTimeout(5000); // 5 秒读取超时
httpConn.setRequestMethod("GET");
httpConn.setRequestProperty("User-Agent", "Java-URL-Client/1.0");
// 3. 校验 HTTP 响应码
int responseCode = httpConn.getResponseCode();
System.out.println("HTTP 状态码: " + responseCode);
if (responseCode == HttpURLConnection.HTTP_OK) {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(httpConn.getInputStream(), StandardCharsets.UTF_8))) {
String line;
System.out.println("--- 响应体内容 ---");
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
// 4. 断开底层连接
httpConn.disconnect();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}API:URLConnection
建立连接与生命周期控制
voidconnect():(),显式建立网络连接。发起与远程 URL 资源的真实 Socket 通信连接。如果尚未配置完请求属性,调用此方法后将锁定配置。voidsetConnectTimeout():(int timeout),设置建连超时时间。配置建立网络连接(TCP 三路握手)的最大等待毫秒数。设置0代表无限等待。intgetConnectTimeout():(),获取建连超时时间。返回设定的连接超时毫秒值。voidsetReadTimeout():(int timeout),设置读取超时时间。配置建立连接后,从输入流读取数据时的最大等待毫秒数。超长未读到数据将抛出SocketTimeoutException。intgetReadTimeout():(),获取读取超时时间。返回设定的数据读取超时毫秒值。URLgetURL():(),获取关联的 URL 对象。返回当前套接字连接所指向的URL资源引用。
注意事项:
URLConnection实例是**一次性(Single-use)**的。当调用输入输出流的close()方法关闭连接后,无法复用同一个URLConnection实例重新发起connect()。- 所有的连接参数(如超时设置)必须在调用
connect()方法或获取输入/输出流之前调用设置,否则抛出IllegalStateException。
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
public class UrlConnectionLifecycleDemo {
public static void main(String[] args) throws Exception {
URL url = URI.create("https://httpbin.org/delay/1").toURL();
URLConnection connection = url.openConnection();
// 1. 配置超时参数(必须在建连前执行)
connection.setConnectTimeout(3000); // 3 秒建连超时
connection.setReadTimeout(5000); // 5 秒读取超时
System.out.println("目标 URL 地址: " + connection.getURL());
System.out.println("建连超时配置 (ms): " + connection.getConnectTimeout());
System.out.println("读取超时配置 (ms): " + connection.getReadTimeout());
// 2. 显式建立网络链路
connection.connect();
System.out.println("已完成网络链路连接。");
}
}请求报头与响应元数据配置
voidsetRequestProperty():(String key, String value),设置单值请求头。设置指定键的 HTTP 请求标头属性。如果已有同名键,原有属性值将被覆盖。voidaddRequestProperty():(String key, String value),添加多值请求头。向指定名称的请求标头追加属性值,不会覆盖已有同名键。StringgetRequestProperty():(String key),获取指定请求头。根据属性键名称返回配置的请求头字符串。Map<String, List<String>>getRequestProperties():(),获取全部请求头集合。返回包含当前连接配置的所有请求标头 Map 映射表。StringgetHeaderField():(String name),获取指定响应头字段。根据属性名称获取服务端返回的对应响应标头值。Map<String, List<String>>getHeaderFields():(),获取全部响应头映射。返回服务端响应报文首部的所有属性映射多值 Map。intgetContentLength():(),获取响应内容字节长度。读取响应标头中的Content-Length字段值;未知或未返回时返回-1。StringgetContentType():(),获取响应媒体类型。读取响应标头中的Content-Type(MIME 类型与字符集)。
注意事项:
- 调用
getHeaderField()、getContentType()等获取响应首部的方法会**隐式触发connect()**。如果此时尚未建立连接,底层会自动发起网络连接。setRequestProperty与addRequestProperty区别在于多值处理:对Accept或Cookie等允许传多个值的报头,需使用addRequestProperty追加。
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.List;
import java.util.Map;
public class UrlConnectionHeadersDemo {
public static void main(String[] args) throws Exception {
URL url = URI.create("https://httpbin.org/headers").toURL();
URLConnection connection = url.openConnection();
// 1. 设置与添加请求头
connection.setRequestProperty("User-Agent", "Java-URLConnection-Client/1.0");
connection.addRequestProperty("Accept", "application/json");
connection.addRequestProperty("Accept", "text/plain");
System.out.println("User-Agent 标头: " + connection.getRequestProperty("User-Agent"));
// 2. 输出发送给服务端的全部请求头
Map<String, List<String>> reqHeaders = connection.getRequestProperties();
System.out.println("已配置请求头映射: " + reqHeaders);
// 3. 隐式建连并读取响应标头
System.out.println("\n--- 服务端响应首部 ---");
System.out.println("Content-Type: " + connection.getContentType());
System.out.println("Content-Length: " + connection.getContentLength());
System.out.println("Server 头信息: " + connection.getHeaderField("Server"));
Map<String, List<String>> respHeaders = connection.getHeaderFields();
System.out.println("所有响应头 Key 列表: " + respHeaders.keySet());
}
}I/O 数据流与缓存控制
voidsetDoInput():(boolean doinput),设置输入流读取模式。指定当前连接是否允许从远程获取输入数据。默认值为true。booleangetDoInput():(),获取输入流模式状态。返回当前连接是否配置为允许输入模式。voidsetDoOutput():(boolean dooutput),设置输出流写入模式。指定当前连接是否允许向远程提交数据。默认值为false。booleangetDoOutput():(),获取输出流模式状态。返回当前连接是否配置为允许输出模式。InputStreamgetInputStream():(),获取输入字节流。返回从该套接字连接读取响应数据的InputStream。OutputStreamgetOutputStream():(),获取输出字节流。返回向该套接字连接写入请求数据的OutputStream。voidsetUseCaches():(boolean usecaches),设置缓存机制。配置当前连接是否允许使用系统协议缓存(如 HTTP 缓存)。默认值为true。booleangetUseCaches():(),获取缓存机制状态。返回当前连接是否启用了缓存。
注意事项:
- HTTP 请求方法转换规则:对
HttpURLConnection而言,一旦调用了setDoOutput(true),HTTP 的请求方法(Request Method)会被**隐式自动更改为POST**。- 流的写入顺序:必须先获取
getOutputStream()写入完整的请求体并调用flush()/close(),然后再获取getInputStream()读取响应。如果颠倒顺序直接调用getInputStream(),请求体将无法正确发送至服务端。
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
public class UrlConnectionStreamDemo {
public static void main(String[] args) throws Exception {
URL url = URI.create("https://httpbin.org/post").toURL();
URLConnection connection = url.openConnection();
// 1. 开启输出模式以发送 POST 报文体,同时关闭缓存
connection.setDoInput(true);
connection.setDoOutput(true); // 隐式转换为 POST 请求
connection.setUseCaches(false);
System.out.println("DoInput 状态: " + connection.getDoInput());
System.out.println("DoOutput 状态: " + connection.getDoOutput());
System.out.println("UseCaches 状态: " + connection.getUseCaches());
// 2. 写入 JSON 数据至请求输出流
String jsonPayload = "{\"name\":\"Java\",\"module\":\"URLConnection\"}";
connection.setRequestProperty("Content-Type", "application/json; utf-8");
try (OutputStream os = connection.getOutputStream()) {
byte[] input = jsonPayload.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
os.flush();
}
// 3. 从输入流读取服务端响应体
try (BufferedReader br = new BufferedReader(
new InputStreamReader(connection.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String responseLine;
while ((responseLine = br.readLine()) != null) {
response.append(responseLine.trim());
}
System.out.println("服务端返回结果: " + response);
}
}
}代码示例
使用 URL 读取网络资源
如果只是简单下载图片或获取文本页面,可直接使用 URL.openStream():
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class SimpleURLExample {
public static void main(String[] args) {
String urlString = "https://httpbin.org/get";
try {
URL url = new URL(urlString);
// 读取并打印页面内容
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(url.openStream(), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}使用 HttpURLConnection 发送 POST 请求
如果需要配置请求头、设置超时时间或提交 JSON 请求体,需使用 HttpURLConnection:
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
public class HttpPostExample {
public static void main(String[] args) {
String urlString = "https://httpbin.org/post";
try {
URL url = new URL(urlString);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
// 1. 配置 HTTP 请求选项
conn.setRequestMethod("POST");
conn.setConnectTimeout(5000); // 5秒超时
conn.setReadTimeout(5000);
conn.setRequestProperty("Content-Type", "application/json; utf-8");
conn.setRequestProperty("Accept", "application/json");
// 开启输出流以发送 Body 数据
conn.setDoOutput(true);
// 2. 写入 POST JSON 请求体
String jsonInputString = "{\"name\": \"Java\", \"type\": \"URL Programming\"}";
try (OutputStream os = conn.getOutputStream()) {
byte[] input = jsonInputString.getBytes(StandardCharsets.UTF_8);
os.write(input, 0, input.length);
}
// 3. 获取响应状态码
int responseCode = conn.getResponseCode();
System.out.println("HTTP 状态码: " + responseCode);
// 4. 根据状态码选择读取正常响应流还是错误流
InputStream is = (responseCode >= 400) ? conn.getErrorStream() : conn.getInputStream();
try (BufferedReader br = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
StringBuilder response = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
response.append(line.trim());
}
System.out.println("服务端响应: " + response.toString());
}
// 5. 断开连接释放底层 Socket
conn.disconnect();
} catch (IOException e) {
e.printStackTrace();
}
}
}注意事项
构造函数弃用警告:从 Java 20 开始,直接使用
new URL(String)构造方法已被标记为@Deprecated。官方推荐使用URI.create("https://...").toURL()的方式构建URL对象,以提供更严格的 RFC 语法检验。URL.equals()阻塞隐患:URL类的equals()和hashCode()方法在比较时会触发同步网络 DNS 解析,以判断两个域名是否映射到同一 IP 地址。因此**绝对不要将URL实例作为HashMap的 Key 或存入HashSet**,应统一改用URI或String。错误流处理:当服务器返回 4xx 或 5xx 状态码时,调用
conn.getInputStream()会抛出IOException。此时必须使用conn.getErrorStream()读取服务端返回的错误信息。现代替代方案:
HttpURLConnection的 API 设计较为古老且不支持 HTTP/2 和异步调用。在 Java 11 及更高版本中,推荐使用全新的java.net.http.HttpClient,它支持链式调用、异步 Future、响应式流以及 HTTP/2 和 WebSocket。