Socket 编程是网络编程的基础,它允许两个不同设备上的程序进行通信,Java 提供了非常成熟的 java.net 包来支持 Socket 编程。

我们将分步进行,从最基础的例子开始,然后逐步完善,加入更健壮的特性,如异常处理和资源释放。
核心概念
在开始编码前,先理解几个核心概念:
- Socket (套接字):可以看作是网络通信的“端点”,每个 Socket 都有一个 IP 地址和一个端口号,通过这个组合,网络中的其他程序可以找到它并发送数据。
- 客户端:主动发起连接请求的一方,它需要知道服务器的 IP 地址和端口号。
- 服务器:被动等待连接的一方,它在一个固定的 IP 地址和端口上监听客户端的连接请求。
- IP 地址:网络中设备的唯一标识,
0.0.1(本机地址)或168.1.100。 - 端口号:设备上应用程序的标识,范围是 0-65535,一个 IP 地址上的不同端口可以对应不同的服务(Web 服务通常在 80 端口)。
Java 中,客户端主要使用 Socket 类。
基础 Socket 客户端示例
这是一个最简单的客户端,它连接到服务器,发送一条消息,然后接收服务器的响应。

服务器端代码(用于测试)
为了测试我们的客户端,我们需要一个简单的服务器,这里提供一个使用 Python 写的简单回显服务器,因为它非常简洁。
Python 服务器代码 (server.py):
# server.py
import socket
# 创建一个 TCP socket
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定到本机的 8888 端口
server_socket.bind(('127.0.0.1', 8888))
# 开始监听,允许 1 个客户端排队等待连接
server_socket.listen(1)
print("服务器启动,等待客户端连接...")
# 接受一个客户端连接
client_socket, addr = server_socket.accept()
print(f"客户端 {addr} 已连接")
# 接收客户端数据
data = client_socket.recv(1024).decode('utf-8')
print(f"收到来自客户端的消息: {data}")
# 将接收到的消息再发送回去
client_socket.sendall("服务器已收到你的消息!".encode('utf-8'))
# 关闭连接
client_socket.close()
server_socket.close()
如何运行服务器:
- 将上述代码保存为
server.py。 - 打开终端,运行
python server.py。 - 你会看到 "服务器启动,等待客户端连接..." 的提示。
Java 客户端代码
我们编写 Java 客户端来连接这个 Python 服务器。
Java 客户端代码 (SimpleClient.java):
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.net.UnknownHostException;
public class SimpleClient {
public static void main(String[] args) {
// 服务器的 IP 地址和端口号
String serverHost = "127.0.0.1";
int serverPort = 8888;
// try-with-resources 语句,确保资源在使用后被自动关闭
try (Socket socket = new Socket(serverHost, serverPort);
OutputStream out = socket.getOutputStream();
InputStream in = socket.getInputStream()) {
System.out.println("已连接到服务器 " + serverHost + ":" + serverPort);
// 1. 发送数据到服务器
String messageToSend = "你好,服务器!";
out.write(messageToSend.getBytes());
System.out.println("已发送消息: " + messageToSend);
// 2. 从服务器接收数据
byte[] buffer = new byte[1024];
int bytesRead = in.read(buffer);
String receivedMessage = new String(buffer, 0, bytesRead);
System.out.println("收到服务器响应: " + receivedMessage);
} catch (UnknownHostException e) {
System.err.println("找不到主机: " + serverHost);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O 错误: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端连接已关闭。");
}
}
代码解析
-
new Socket(serverHost, serverPort):- 这是客户端的核心,这行代码会尝试创建一个
Socket对象并连接到指定的服务器地址和端口。 - 如果连接成功,程序才会继续执行,如果服务器未启动或地址/端口错误,会抛出
IOException。 - 注意:
0.0.1是一个特殊的 IP 地址,代表“本机”,如果你的客户端和服务器在同一台电脑上运行,就使用这个地址。
- 这是客户端的核心,这行代码会尝试创建一个
-
OutputStream out = socket.getOutputStream():- 通过
Socket获取输出流,用于向服务器发送数据。
- 通过
-
out.write(messageToSend.getBytes()):String.getBytes()将字符串转换为字节数组。write()方法将字节数组通过 Socket 发送到服务器。
-
InputStream in = socket.getInputStream():- 通过
Socket获取输入流,用于从服务器读取数据。
- 通过
-
in.read(buffer):read()方法会阻塞,直到从输入流中读取到数据。- 它读取的数据被存入
buffer字节数组中,并返回实际读取的字节数。 - 如果服务器关闭了连接,
read()会返回-1。
-
try-with-resources:try (Socket socket = ...; OutputStream out = ...; InputStream in = ...)这种写法非常推荐。- 它会自动在
try块执行完毕后(无论是否发生异常)调用socket.close(),out.close(),in.close(),确保网络资源被正确释放,避免资源泄漏。
进阶:使用 BufferedReader 和 PrintWriter
直接使用字节流 (InputStream/OutputStream) 虽然灵活,但处理文本数据比较麻烦,更常见的做法是使用字符流包装器,以便直接读写字符串。
PrintWriter: 可以方便地将字符串写入输出流,并自动处理换行符。BufferedReader: 可以高效地从输入流中读取一行文本。
改进后的 Java 客户端 (AdvancedClient.java):
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class AdvancedClient {
public static void main(String[] args) {
String serverHost = "127.0.0.1";
int serverPort = 8888;
try (Socket socket = new Socket(serverHost, serverPort);
// 使用 PrintWriter 包装 OutputStream,方便写入字符串
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 使用 BufferedReader 包装 InputStream,方便读取一行
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {
System.out.println("已连接到服务器 " + serverHost + ":" + serverPort);
// 发送消息
String messageToSend = "你好,服务器!这次用 PrintWriter 发送。";
out.println(messageToSend); // println 会自动添加换行符
System.out.println("已发送消息: " + messageToSend);
// 接收服务器返回的一行
String serverResponse = in.readLine();
System.out.println("收到服务器响应: " + serverResponse);
} catch (UnknownHostException e) {
System.err.println("找不到主机: " + serverHost);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O 错误: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端连接已关闭。");
}
}
改进点
-
PrintWriter(socket.getOutputStream(), true):- 我们将
OutputStream包装成PrintWriter。 - 第二个参数
true表示启用“自动刷新”模式,这样,当我们调用println()或printf()时,输出缓冲区会自动刷新,确保数据立即被发送出去。
- 我们将
-
BufferedReader(new InputStreamReader(socket.getInputStream())):- 我们将
InputStream包装成InputStreamReader(字节流到字符流的桥梁),再包装成BufferedReader。 - 这样就可以使用
in.readLine()方法,它会读取一行直到遇到换行符,非常方便。
- 我们将
完整示例:交互式客户端
下面是一个更完整的客户端,它可以循环地与服务器进行交互,直到用户输入 "exit" 为止。
交互式 Java 客户端 (InteractiveClient.java):
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;
public class InteractiveClient {
public static void main(String[] args) {
String serverHost = "127.0.0.1";
int serverPort = 8888;
try (
Socket socket = new Socket(serverHost, serverPort);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 用于从控制台读取用户输入
stdIn = new BufferedReader(new InputStreamReader(System.in))
) {
System.out.println("已连接到服务器 " + serverHost + ":" + serverPort);
System.out.println("输入消息发送给服务器,输入 'exit' 退出。");
String userInput;
// 循环读取用户输入
while (true) {
System.out.print("请输入消息: ");
userInput = stdIn.readLine();
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
// 发送消息到服务器
out.println(userInput);
// 从服务器接收响应
String serverResponse = in.readLine();
if (serverResponse == null) {
// 如果服务器关闭了连接,readLine() 会返回 null
System.out.println("服务器已关闭连接。");
break;
}
System.out.println("服务器响应: " + serverResponse);
}
} catch (UnknownHostException e) {
System.err.println("找不到主机: " + serverHost);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O 错误: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端程序结束。");
}
}
这个客户端展示了如何构建一个可以持续与服务器对话的应用,是实际应用开发中更常见的模式。
总结与关键点
- 连接是第一步:客户端的核心是
new Socket(ip, port),这是建立通信的入口。 - 流是数据通道:
getOutputStream()用于发送数据。getInputStream()用于接收数据。
- 字符流更方便:使用
PrintWriter和BufferedReader可以简化文本数据的读写。 - 资源管理至关重要:务必关闭 Socket 和相关的流。
try-with-resources是现代 Java 中最推荐的方式。 - 异常处理:网络操作不可靠,必须妥善处理
IOException和UnknownHostException等异常。 - 阻塞行为:
read()和readLine()是阻塞方法,程序会一直等待,直到有数据到达或连接关闭,在交互式应用中,这通常是期望的行为。
