核心概念
在开始之前,我们先快速回顾一下 Socket 编程的基本概念:

(图片来源网络,侵删)
-
服务器端:
ServerSocket: 服务器端使用它来“监听”某个特定的端口,等待客户端的连接请求。Socket: 当一个客户端连接成功后,ServerSocket会返回一个新的Socket实例,这个Socket代表了与那个特定客户端的通信链路。
-
客户端:
Socket: 客户端使用Socket来主动尝试连接服务器的 IP 地址和端口号。
-
通信流:
- 一旦连接建立,双方就可以通过
Socket获取输入流和输出流来进行数据的读写。 InputStream(用于读取数据) 和OutputStream(用于发送数据) 是字节流,通常我们会把它们包装成更高级的流,如InputStreamReader/OutputStreamWriter(处理字符) 和BufferedReader/BufferedWriter(提供缓冲功能,提高效率)。
- 一旦连接建立,双方就可以通过
简单的单线程回显服务器
这个例子非常基础,服务器一次只能处理一个客户端的连接,当它处理当前客户端时,其他客户端必须等待。

(图片来源网络,侵删)
服务器端代码 (EchoServer.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 EchoServer {
public static void main(String[] args) {
// 定义服务器要监听的端口号
int port = 12345;
try (// 创建一个 ServerSocket,并绑定到指定的端口
ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("服务器已启动,正在监听端口 " + port + "...");
// 使用 accept() 方法阻塞,等待客户端连接
// 当有客户端连接时,accept() 方法会返回一个 Socket 对象
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);
String inputLine;
// 循环读取客户端发送的数据
// readLine() 也会阻塞,直到收到一行数据或流关闭
while ((inputLine = in.readLine()) != null) {
System.out.println("收到客户端消息: " + inputLine);
// 将收到的消息回显给客户端
out.println("服务器回显: " + inputLine);
// 如果客户端发送 "exit",则关闭连接
if ("exit".equalsIgnoreCase(inputLine)) {
System.out.println("客户端请求关闭连接。");
break;
}
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
System.out.println("服务器已关闭。");
}
}
客户端代码 (EchoClient.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 EchoClient {
public static void main(String[] args) {
// 服务器的 IP 地址 (localhost 表示本机) 和端口号
String hostName = "localhost";
int port = 12345;
try (// 创建一个 Socket 连接到指定的服务器地址和端口
Socket socket = new Socket(hostName, port);
// 获取输入流,用于读取服务器返回的数据
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
// 获取输出流,用于向服务器发送数据
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
// 创建一个 BufferedReader 来读取用户从控制台输入的内容
BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in))) {
System.out.println("已连接到服务器。");
System.out.println("请输入要发送的消息 (输入 'exit' 退出):");
String userInput;
// 循环读取用户输入
while ((userInput = stdIn.readLine()) != null) {
// 将用户输入发送给服务器
out.println(userInput);
// 读取服务器返回的回显消息
String response = in.readLine();
System.out.println("服务器响应: " + response);
// 如果用户输入 "exit",则退出循环
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
}
} catch (UnknownHostException e) {
System.err.println("无法找到主机: " + hostName);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O 发生异常: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端已关闭。");
}
}
如何运行
- 编译代码:
javac EchoServer.java EchoClient.java
- 启动服务器:
java EchoServer
你会看到控制台输出:
服务器已启动,正在监听端口 12345... - 启动客户端:
打开一个新的终端窗口,运行:
java EchoClient
你会看到客户端输出:
已连接到服务器。 - 测试通信:
- 在客户端的终端输入任意文本,
Hello, Server!,然后按回车。 - 客户端会收到服务器的回显:
服务器响应: 服务器回显: Hello, Server! - 在服务器端,你会看到:
收到客户端消息: Hello, Server! - 输入
exit即可关闭连接。
- 在客户端的终端输入任意文本,
多线程服务器
上面的例子有一个很大的问题:服务器一次只能为一个客户端服务,当服务器与一个客户端通信时,其他客户端必须排队等待,为了解决这个问题,我们可以使用多线程。
服务器为每一个连接的客户端都创建一个新的线程来处理通信,主线程则继续等待新的客户端连接。
多线程服务器端代码 (MultiThreadEchoServer.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 MultiThreadEchoServer {
public static void main(String[] args) {
int port = 12345;
try (ServerSocket serverSocket = new ServerSocket(port)) {
System.out.println("多线程服务器已启动,正在监听端口 " + port + "...");
while (true) { // 无限循环,持续等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端已连接: " + clientSocket.getInetAddress().getHostAddress());
// 为每个客户端连接创建一个新的线程来处理
ClientHandler clientHandler = new ClientHandler(clientSocket);
new Thread(clientHandler).start();
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
}
// 定义一个内部类,用于处理单个客户端的通信逻辑
class ClientHandler implements Runnable {
private Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
// 使用 try-with-resources 确保流和 socket 被正确关闭
try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) {
String inputLine;
while ((inputLine = in.readLine()) != null) {
System.out.println(" [线程 " + Thread.currentThread().getId() + "] 收到客户端 " + clientSocket.getInetAddress().getHostAddress() + " 的消息: " + inputLine);
out.println("服务器回显: " + inputLine);
if ("exit".equalsIgnoreCase(inputLine)) {
System.out.println(" [线程 " + Thread.currentThread().getId() + "] 客户端请求关闭连接。");
break;
}
}
} catch (IOException e) {
// 如果客户端异常断开,会抛出 SocketException,这里可以捕获并打印信息
System.out.println(" [线程 " + Thread.currentThread().getId() + "] 与客户端 " + clientSocket.getInetAddress().getHostAddress() + " 的连接已断开。");
} finally {
try {
clientSocket.close();
System.out.println(" [线程 " + Thread.currentThread().getId() + "] 已关闭与客户端的连接。");
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
客户端代码
客户端代码与实例一完全相同,无需修改,你可以直接使用 EchoClient.java。
如何运行
- 编译代码:
javac MultiThreadEchoServer.java EchoClient.java
- 启动多线程服务器:
java MultiThreadEchoServer
- 启动多个客户端:
打开两个或多个新的终端窗口,在每个窗口中都运行:
java EchoClient
- 测试:
- 在第一个客户端窗口输入消息,服务器端会显示一个线程ID。
- 在第二个客户端窗口输入消息,服务器端会显示另一个线程ID。
- 你会发现,服务器可以同时响应来自不同客户端的消息,而不会互相阻塞,这就是多线程服务器的优势。
总结与最佳实践
| 特性 | 单线程服务器 | 多线程服务器 |
|---|---|---|
| 并发处理 | 不能,一次只处理一个客户端 | 能,每个客户端一个线程 |
| 资源消耗 | 低,只有一个线程 | 高,每个连接一个线程 |
| 实现复杂度 | 简单 | 稍复杂,需要管理线程 |
| 适用场景 | 简单应用、学习目的 | 高并发、生产环境应用 |
进阶建议:
- 线程池: 在实际生产环境中,直接为每个连接创建新线程是非常消耗资源的,更好的方式是使用线程池 (
ExecutorService) 来复用线程,避免频繁创建和销毁线程带来的开销。 - NIO (New I/O): 对于更高性能、更高并发的场景,可以使用 Java NIO (非阻塞 I/O) 模型,NIO 使用一个或少数几个线程来管理成千上万的连接,效率远高于传统的多线程模型,但这会显著增加代码的复杂性。
- 使用框架: 对于复杂的网络应用,直接使用原生 Socket API 开发会非常繁琐,可以考虑使用成熟的网络框架,如 Netty、Mina 或 Vert.x,它们提供了更高级的抽象和更强大的功能,能让你更专注于业务逻辑。
希望这两个实例能帮助你理解 Java Socket 通信的基本原理!
