Socket 通信是网络编程的基础,它允许运行在不同计算机上的程序进行数据交换,Java 提供了非常简单易用的 java.net 包来实现 Socket 通信。

我们将分步进行,从最基础的概念到完整的代码示例,再到更高级的用法。
核心概念
在开始编码前,我们先理解几个核心概念:
- IP 地址: 网络上每台计算机的唯一标识,
0.0.1(本机地址) 或168.1.100(局域网内某台电脑的地址)。 - 端口号: 计算机上运行的应用程序(服务)的标识,一个 IP 地址可以提供多种服务,端口号用于区分这些服务,范围是 0 到 65535,Web 服务常用 80 端口,HTTPS 服务常用 443 端口,在客户端/服务器模型中,服务器需要在一个固定的、众所周知的端口上监听客户端的连接请求。
- 客户端: 主动发起连接请求的一方,它会知道服务器的 IP 地址和端口号,并尝试连接到服务器。
- 服务器: 在一个指定的 IP 地址和端口上监听,等待客户端连接的一方。
- Socket (套接字): 它是网络通信的端点,客户端和服务器建立连接后,都会得到一个 Socket 对象,通过这个对象,双方就可以进行 I/O 操作(输入/输出),就像读写文件一样。
客户端编程步骤
Java 客户端编程通常遵循以下步骤:
- 创建
Socket对象: 指定服务器的 IP 地址和端口号,向服务器发起连接请求,如果连接成功,客户端和服务器之间就建立了一条通信线路。 - 获取输入/输出流:
- 通过
socket.getOutputStream()获取一个OutputStream,用于向服务器发送数据。 - 通过
socket.getInputStream()获取一个InputStream,用于接收从服务器发来的数据。
- 通过
- 使用流进行数据收发:
- 我们会将字节流包装成更高级的流,如
OutputStreamWriter和InputStreamReader来处理字符,或者直接使用DataOutputStream和DataInputStream来处理基本数据类型。
- 我们会将字节流包装成更高级的流,如
- 关闭资源: 通信结束后,必须按照创建的相反顺序关闭所有打开的资源,即先关闭流,再关闭
Socket,以释放系统资源。
简单的客户端示例(字符串收发)
下面是一个最基础的客户端示例,它可以连接到服务器,向服务器发送一行字符串,然后接收服务器返回的一行字符串。

服务器代码 (为了演示,我们先写一个简单的服务器)
// SimpleServer.java
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class SimpleServer {
public static void main(String[] args) {
int port = 8888; // 服务器监听的端口
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器启动,正在监听端口 " + port + "...");
// accept() 方法会阻塞,直到有客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 获取输入流,用于接收客户端数据
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// 获取输出流,用于向客户端发送数据
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); // autoFlush=true
String inputLine;
// 读取客户端发送的数据
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 如果客户端发送 "exit",则关闭连接
if ("exit".equalsIgnoreCase(inputLine)) {
System.out.println("客户端请求断开连接。");
break;
}
// 向客户端发送响应
out.println("服务器收到了你的消息: " + inputLine);
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
System.out.println("服务器已关闭。");
}
}
客户端代码
// SimpleClient.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 SimpleClient {
public static void main(String[] args) {
String hostname = "127.0.0.1"; // 服务器的IP地址,这里用本机地址
int port = 8888; // 服务器的端口号
try (
// 1. 创建Socket对象,尝试连接服务器
Socket socket = new Socket(hostname, port);
// 2. 获取输出流,用于向服务器发送数据
// 使用 PrintWriter 包装 OutputStream,方便发送字符串
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 3. 获取输入流,用于接收服务器的响应
// 使用 BufferedReader 包装 InputStream,方便接收字符串
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 为了方便从控制台获取用户输入
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
) {
System.out.println("已连接到服务器 " + hostname + ":" + port);
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
String userInput;
// 4. 循环收发数据
while ((userInput = stdIn.readLine()) != null) {
// 向服务器发送消息
out.println(userInput);
// 如果用户输入 exit,则退出循环
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
// 从服务器接收响应
String response = in.readLine();
System.out.println("服务器响应: " + response);
}
} catch (UnknownHostException e) {
System.err.println("不知道的主机: " + hostname);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O 发生错误: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端已关闭。");
}
}
如何运行
- 编译: 将
SimpleServer.java和SimpleClient.java编译成.class文件。javac SimpleServer.java SimpleClient.java
- 运行服务器: 先在终端运行服务器。
java SimpleServer
你会看到输出:
服务器启动,正在监听端口 8888... - 运行客户端: 再打开另一个终端,运行客户端。
java SimpleClient
- 交互:
- 在客户端的控制台输入任意文本,
Hello Server,然后按回车。 - 客户端会立即收到服务器的响应:
服务器收到了你的消息: Hello Server。 - 在服务器终端,你会看到:
收到客户端消息: Hello Server。 - 在客户端输入
exit并按回车,客户端和服务器都会关闭连接并退出程序。
- 在客户端的控制台输入任意文本,
代码解析
try-with-resources 语句
try (Socket socket = ...) {
// ... 使用 socket
} // socket 和其他在 try() 中声明的资源会自动关闭
我们使用了 try-with-resources 语句,这是 Java 7 引入的一个非常方便的特性,只要实现了 AutoCloseable 接口的资源(如 Socket, InputStream, OutputStream 等)都可以在这里声明,当 try 块执行完毕后(无论正常结束还是发生异常),这些资源都会被自动 close(),从而避免了资源泄漏。
PrintWriter 和 BufferedReader
PrintWriter: 包装了OutputStream,提供了println(),print(),printf()等方便的方法来打印各种数据类型,它默认不自动刷新缓冲区,所以我们在构造时传入了true作为第二个参数 (autoFlush),这样每次调用println()后都会自动刷新缓冲区,确保数据被立即发送。BufferedReader: 包装了InputStreamReader,提供了readLine()方法,可以高效地按行读取文本数据。
更健壮的客户端(多线程处理)
上面的例子中,客户端在等待服务器响应时是阻塞的,无法同时发送和接收消息,在实际应用中,我们通常使用多线程来处理收发,使客户端可以同时进行输入和输出。
一个常见的模式是:

- 主线程: 负责从控制台读取用户输入并发送给服务器。
- 一个独立的接收线程: 负责持续监听并打印服务器的消息。
下面是一个改进后的多线程客户端示例。
多线程客户端代码
// MultiThreadClient.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 MultiThreadClient {
public static void main(String[] args) {
String hostname = "127.0.0.1";
int port = 8888;
try (
Socket socket = new Socket(hostname, port);
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))
) {
System.out.println("已连接到服务器 " + hostname + ":" + port);
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
// 1. 创建并启动一个接收消息的线程
ReceiverThread receiverThread = new ReceiverThread(in);
receiverThread.start();
// 2. 主线程负责发送消息
String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput);
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
}
// 等待接收线程结束
receiverThread.join();
} catch (UnknownHostException e) {
System.err.println("不知道的主机: " + hostname);
} catch (IOException e) {
System.err.println("I/O 发生错误: " + e.getMessage());
} catch (InterruptedException e) {
System.err.println("接收线程被中断: " + e.getMessage());
Thread.currentThread().interrupt(); // 恢复中断状态
}
System.out.println("客户端已关闭。"
}
}
// 专门用于接收服务器消息的线程
class ReceiverThread extends Thread {
private BufferedReader in;
public ReceiverThread(BufferedReader in) {
this.in = in;
}
@Override
public void run() {
try {
String response;
// 持续读取服务器的消息,直到连接关闭
while ((response = in.readLine()) != null) {
System.out.println("服务器响应: " + response);
}
} catch (IOException e) {
// 如果连接断开,readLine() 会返回 null,线程会自然结束
// System.err.println("接收消息时发生错误: " + e.getMessage());
} finally {
System.out.println("接收消息的线程已结束。");
}
}
}
这个版本的客户端可以让你在发送消息的同时,也能实时看到服务器发来的消息,体验更好。
总结与注意事项
- 异常处理: 网络编程充满了不确定性,必须妥善处理
IOException和UnknownHostException等异常。 - 资源管理: 务必关闭所有打开的流和
Socket,推荐使用try-with-resources。 - 阻塞行为:
Socket的connect(),InputStream的read(),ServerSocket的accept()等方法都是阻塞的,会一直等待直到有结果,多线程是解决阻塞问题的常用手段。 - 数据格式: 上述例子中我们使用换行符
\n作为消息的结束标记(readLine()),在更复杂的场景下,你可能需要定义自己的协议,例如使用长度前缀来标识消息的边界,以避免“粘包”或“半包”问题。 - 编码: 如果要传输非英文字符,务必在
InputStreamReader和OutputStreamWriter中指定字符编码,new InputStreamReader(socket.getInputStream(), "UTF-8"),以避免乱码。
