Skip to content

Java 网络编程

网络编程涉及编写通过网络连接的不同设备之间通信的应用程序。

java.net 包提供了 Java 网络通信的核心类和接口,处理底层细节,让您可以专注于应用程序逻辑。

java.net 支持的关键协议:

  • TCP (传输控制协议 - Transmission Control Protocol): 提供应用程序之间可靠、有序、面向连接的通信。常与 IP (互联网协议 - Internet Protocol) 结合使用,称为 TCP/IP。适用于需要保证数据传输的应用,如网页浏览 (HTTP)、文件传输 (FTP) 和电子邮件 (SMTP)。
  • UDP (用户数据报协议 - User Datagram Protocol): 提供使用数据报(datagram packets)的无连接、不可靠通信。比 TCP 快,但不保证传输或顺序。适用于速度至关重要且偶尔数据丢失可接受的应用,如流媒体或在线游戏。

本教程重点介绍:

  • 套接字编程 (Socket Programming): 基于 TCP 的客户端-服务器通信的基础。
  • URL 处理 (URL Processing): 使用统一资源定位符 (Uniform Resource Locators - URL) 访问 Web 资源(单独介绍)。

套接字(Socket)提供了使用 TCP 在两台机器之间通信的端点。客户端应用程序创建一个 Socket 并尝试连接到在特定 IP 地址和端口上监听的服务器的 ServerSocket。

连接一旦建立,服务器接受连接,创建自己的 Socket。客户端和服务器随后可以使用与各自套接字关联的输入/输出 (I/O) 流交换数据。

关键类:

  • java.net.Socket: 表示 TCP 连接的一端(连接后客户端和服务器都使用)。
  • java.net.ServerSocket: 由服务器用于监听特定端口上接收的客户端连接请求。
  • java.net.InetAddress: 表示一个 IP 地址。
  • java.net.InetSocketAddress: 表示一个 IP 地址和端口号。

建立 TCP 连接:

  • 服务器 (Server): 创建一个 ServerSocket 实例,将其绑定到特定的端口号。
  • 服务器 (Server): 调用 ServerSocket 的 accept() 方法。此方法会阻塞(等待)直到客户端请求连接。
  • 客户端 (Client): 创建一个 Socket 实例,指定服务器的主机名/IP 地址和端口号。
  • 客户端 (Client): Socket 构造函数尝试与服务器建立连接。如果成功,客户端就拥有一个准备好通信的 Socket。
  • 服务器 (Server): 当连接建立后,accept() 会返回一个新的 Socket 对象,代表服务器与该客户端特定连接的末端。服务器通常在一个单独的线程中处理此客户端连接,以便 ServerSocket 可以继续监听其他客户端。

连接后,每个 Socket 都提供一个 InputStream 用于接收数据,以及一个 OutputStream 用于发送数据。客户端的 OutputStream 连接到服务器的 InputStream,反之亦然,从而实现双向通信。

套接字及其关联的流是必须正确关闭以释放系统资源并避免资源泄漏的资源。try-with-resources 语句 (Java 7+) 确保在其中声明的资源即使发生异常也会自动关闭。

// 客户端套接字和流使用 try-with-resources 的示例
try (Socket clientSocket = new Socket(serverName, port);
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
// 使用 'out' 和 'in' 进行通信
out.println("Hello Server!");
String response = in.readLine();
System.out.println("Server says: " + response);
} catch (IOException e) {
System.err.println("Communication error: " + e.getMessage());
}
// 在此处 clientSocket, out, 和 in 会自动关闭

服务器用于监听客户端连接。

常用构造函数:

构造函数 (Constructor)描述 (Description)
public ServerSocket(int port)创建一个绑定到指定端口的服务器套接字。如果端口已被占用,则抛出 IOException。
public ServerSocket(int port, int backlog)类似于上面,但 backlog 建议等待接受的传入连接的队列长度。
public ServerSocket(int port, int backlog, InetAddress bindAddr)绑定到特定的本地 IP 地址 (bindAddr) 和端口。适用于多网卡服务器。

常用方法:

方法 (Method)描述 (Description)
public Socket accept()监听并接受连接请求。阻塞(等待)直到建立连接。返回一个 Socket 对象,用于与连接的客户端通信。至关重要的是,返回的此 Socket 应该被处理(例如,在一个新线程中),以便服务器可以再次调用 accept() 来处理其他客户端。
public void bind(SocketAddress endpoint)如果 ServerSocket 创建时未绑定,则将其绑定到特定地址(IP 和端口)。
public int getLocalPort()返回此服务器套接字正在监听的端口号。
public void setSoTimeout(int timeout)设置 accept() 方法的超时时间(以毫秒为单位)。如果在超时时间内没有连接发生,则抛出 SocketTimeoutException。
public void close()关闭服务器套接字。通常应由 try-with-resources 或 finally 块处理。

代表 TCP 连接的一个单一端点。

常用构造函数(客户端使用):

构造函数 (Constructor)描述 (Description)
public Socket(String host, int port)创建一个套接字并将其连接到指定的远程主机和端口。失败时抛出 UnknownHostException 或 IOException。
public Socket(InetAddress address, int port)连接到指定的 InetAddress 和端口。
public Socket()创建一个未连接的套接字。稍后使用 connect() 建立连接。

常用方法(客户端和服务器都使用):

方法 (Method)描述 (Description)
public void connect(SocketAddress endpoint, int timeout)将此套接字连接到 endpoint 指定的服务器,带有连接超时时间。
public InputStream getInputStream()返回一个 InputStream 用于读取来自连接另一端的数据。
public OutputStream getOutputStream()返回一个 OutputStream 用于向连接另一端写入数据。
public InetAddress getInetAddress()获取远程端点的 InetAddress。
public int getPort()获取远程端点的端口号。
public SocketAddress getRemoteSocketAddress()获取远程端点的 SocketAddress(IP 和端口)。
public SocketAddress getLocalSocketAddress()获取本地端点的 SocketAddress(IP 和端口)。
public boolean isConnected()如果套接字已连接,则返回 true。
public boolean isClosed()如果套接字已关闭,则返回 true。
public void close()关闭套接字及其流。通常应由 try-with-resources 处理。

代表一个 IP 地址(包括 IPv4 和 IPv6)。

方法 (Method)描述 (Description)
static InetAddress getByName(String host)给定主机名确定主机的 IP 地址(执行 DNS 查找)。
static InetAddress getLocalHost()返回代表本地主机的 InetAddress 对象。
static InetAddress[] getAllByName(String host)返回与主机名关联的所有 IP 地址数组。
String getHostName()获取此 IP 地址的主机名(可能触发反向 DNS 查找)。
String getHostAddress()以文本形式返回 IP 地址字符串。

现代客户端示例(使用 Try-with-Resources 和字符流)

Section titled “现代客户端示例(使用 Try-with-Resources 和字符流)”

此客户端连接,发送消息,并使用 PrintWriter 和 BufferedReader 进行基于文本的通信来读取响应。

// 文件名: SimpleClient.java
import java.net.*;
import java.io.*;
public class SimpleClient {
public static void main(String[] args) {
if (args.length < 2) {
System.out.println("Usage: java SimpleClient <server-host> <port>");
return;
}
String serverName = args[0];
int port = Integer.parseInt(args[1]);
System.out.println("Connecting to " + serverName + " on port " + port);
// 使用 try-with-resources 确保套接字和流自动关闭
try (Socket clientSocket = new Socket(serverName, port);
// 为 PrintWriter 启用自动刷新
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) {
System.out.println("Connected to " + clientSocket.getRemoteSocketAddress());
String message = "Hello from " + clientSocket.getLocalSocketAddress();
System.out.println("Sending: " + message);
out.println(message); // 将消息发送到服务器
String response = in.readLine(); // 从服务器读取响应
System.out.println("Server says: " + response);
} catch (UnknownHostException e) {
System.err.println("不知道主机 " + serverName);
} catch (IOException e) {
System.err.println("无法获取到 " +
serverName + " 的 I/O 连接: " + e.getMessage());
}
// Sockets 和 streams 会在此处由 try-with-resources 自动关闭
System.out.println("连接已关闭。");
}
}

现代服务器示例(使用 Try-with-Resources 和线程)

Section titled “现代服务器示例(使用 Try-with-Resources 和线程)”

此服务器监听连接。对于每个客户端,它创建一个新线程来处理通信,从而允许主线程继续监听新的客户端。它使用 BufferedReader 和 PrintWriter。

// 文件名: SimpleServer.java
import java.net.*;
import java.io.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SimpleServer {
public static void main(String[] args) {
if (args.length < 1) {
System.out.println("Usage: java SimpleServer <port>");
return;
}
int port = Integer.parseInt(args[0]);
// 使用固定大小的线程池处理客户端通常比创建无限制的线程更好
ExecutorService clientHandlerPool = Executors.newFixedThreadPool(10);
// 为 ServerSocket 使用 try-with-resources
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("Server listening on port " + port + "...");
while (true) { // 持续监听无限期
try {
Socket clientSocket = serverSocket.accept(); // 等待客户端连接
System.out.println("Accepted connection from " + clientSocket.getRemoteSocketAddress());
// 在线程池的一个独立线程中处理客户端连接
clientHandlerPool.submit(new ClientHandler(clientSocket));
} catch (IOException e) {
System.err.println("接受客户端连接时出错: " + e.getMessage());
// 根据错误,可能希望中断或继续
}
}
} catch (IOException e) {
System.err.println("无法在端口 " + port + " 监听: " + e.getMessage());
} finally {
clientHandlerPool.shutdown(); // 服务器退出时关闭线程池
}
}
}
// 处理单个客户端连接的可运行任务
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
// 为客户端套接字流使用 try-with-resources
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
String inputLine;
if ((inputLine = in.readLine()) != null) { // 从客户端读取消息
System.out.println("Received from " + clientSocket.getRemoteSocketAddress() + ": " + inputLine);
// 将响应发送回客户端
out.println("Thank you for connecting to " + clientSocket.getLocalSocketAddress() + " Goodbye!");
}
} catch (IOException e) {
System.err.println("处理客户端 " + clientSocket.getRemoteSocketAddress() + " 时出错: " + e.getMessage());
} finally {
try {
clientSocket.close(); // 确保客户端套接字被关闭
System.out.println("Closed connection from " + clientSocket.getRemoteSocketAddress());
} catch (IOException e) {
System.err.println("关闭客户端套接字时出错: " + e.getMessage());
}
}
}
}

运行示例:

  1. 编译 SimpleServer.java 和 SimpleClient.java。
  2. 启动服务器:java SimpleServer 6066
  3. 启动客户端:java SimpleClient localhost 6066

预期的服务器输出(大致):

Server listening on port 6066...
Accepted connection from /127.0.0.1:xxxxx
Received from /127.0.0.1:xxxxx: Hello from /127.0.0.1:yyyyy
Closed connection from /127.0.0.1:xxxxx

预期的客户端输出:

Connecting to localhost on port 6066
Connected to localhost/127.0.0.1:6066
Sending: Hello from /127.0.0.1:yyyyy
Server says: Thank you for connecting to /127.0.0.1:6066 Goodbye!
Connection closed.

注意:对于需要非阻塞 I/O 和更好可伸缩性的更复杂应用程序,请探索 java.nio(新 I/O)包,它提供了 Channels、Buffers 和 Selectors 等功能。