核心思想
单线程 Socket 服务器的模型是“一个客户端一个线程”,当服务器接收到一个客户端连接后,它会启动一个专门的线程来处理这个客户端的所有通信(包括读取数据、处理业务逻辑、发送响应),这样,当一个客户端线程因为等待网络数据而阻塞时,其他客户端的线程依然可以正常工作,从而实现了并发处理。

架构分解
一个基于多线程的 Socket 服务器通常包含以下几个部分:
-
服务器端:
- 主线程: 负责启动服务器,在指定端口上监听客户端的连接请求,它本身不处理具体的数据通信。
- 线程池: 为了避免为每个新连接都创建一个新线程(可能导致资源耗尽),我们会使用一个线程池来管理处理客户端连接的线程。
- 处理线程: 从线程池中获取一个线程,用于执行与单个客户端的通信任务,这个任务通常在一个循环中,不断读取客户端发送的数据,处理并发回响应,直到客户端关闭连接。
-
客户端:
客户端相对简单,它只需要创建一个 Socket 连接到服务器,然后通过输入流读取服务器的响应,通过输出流向服务器发送请求,通常一个客户端一个线程就足够了。
(图片来源网络,侵删)
完整代码示例
下面我将提供一个完整的服务端和客户端代码,并附上详细的解释。
服务器端代码 (MultiThreadedServer.java)
这个服务器会使用 ExecutorService(线程池)来管理客户端处理线程。
import java.io.*;
import java.net.*;
import java.util.concurrent.*;
public class MultiThreadedServer {
// 线程池,用于处理客户端连接
// 核心线程数4,最大线程数10,空闲线程60秒后回收
private static final ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
int port = 8888;
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);
threadPool.execute(clientHandler);
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
} finally {
// 优雅关闭线程池
System.out.println("服务器关闭,正在关闭线程池...");
threadPool.shutdown();
try {
if (!threadPool.awaitTermination(60, TimeUnit.SECONDS)) {
threadPool.shutdownNow();
}
} catch (InterruptedException e) {
threadPool.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}
/**
* 客户端处理任务
* 这个类实现了 Runnable 接口,每个实例代表一个客户端的通信任务
*/
class ClientHandler implements Runnable {
private final Socket clientSocket;
public ClientHandler(Socket socket) {
this.clientSocket = socket;
}
@Override
public void run() {
// 使用 try-with-resources 确保流和socket被正确关闭
try (InputStream input = clientSocket.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(input));
OutputStream output = clientSocket.getOutputStream();
PrintWriter writer = new PrintWriter(output, true)) { // autoFlush=true
String inputLine;
// 循环读取客户端发送的数据
while ((inputLine = reader.readLine()) != null) {
System.out.println("收到来自 " + clientSocket.getInetAddress() + " 的消息: " + inputLine);
// 简单的回显服务
String response = "服务器回应: " + inputLine;
writer.println(response);
}
} catch (IOException e) {
// 如果客户端正常断开连接,readLine()会返回null,此时不打印错误
if (e.getMessage() != null && !e.getMessage().equals("Connection reset")) {
System.err.println("处理客户端 " + clientSocket.getInetAddress() + " 时出错: " + e.getMessage());
}
} finally {
System.out.println("客户端 " + clientSocket.getInetAddress() + " 断开连接。");
try {
clientSocket.close();
} catch (IOException e) {
System.err.println("关闭客户端socket时出错: " + e.getMessage());
}
}
}
}
客户端代码 (Client.java)
客户端可以手动启动多个,以测试服务器的并发处理能力。
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class Client {
public static void main(String[] args) {
String hostname = "localhost";
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()));
Scanner scanner = new Scanner(System.in)) {
System.out.println("已连接到服务器。");
System.out.println("请输入消息 (输入 'exit' 退出):");
// 启动一个新线程来监听服务器消息
Thread listenerThread = new Thread(() -> {
try {
String serverResponse;
while ((serverResponse = in.readLine()) != null) {
System.out.println("服务器: " + serverResponse);
}
} catch (IOException e) {
System.err.println("读取服务器消息时出错: " + e.getMessage());
}
});
listenerThread.start();
// 主线程用于从用户输入读取并发送消息
while (true) {
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) {
break;
}
out.println(message);
}
} catch (UnknownHostException e) {
System.err.println("不知道的主机: " + hostname);
e.printStackTrace();
} catch (IOException e) {
System.err.println("I/O error: " + e.getMessage());
e.printStackTrace();
}
System.out.println("客户端关闭。");
}
}
代码详解
服务器端 (MultiThreadedServer.java)
-
ExecutorService threadPool:
(图片来源网络,侵删)- 我们使用
Executors.newFixedThreadPool(10)创建了一个固定大小的线程池,这意味着最多同时有 10 个线程在处理客户端请求。 - 为什么用线程池? 如果有成千上万的客户端同时连接,为每个连接都创建一个新线程会导致内存耗尽和性能下降(线程创建和销毁都有开销),线程池可以复用已创建的线程,提高了资源利用率。
- 我们使用
-
main方法中的while(true)循环:- 这是服务器的核心监听循环。
serverSocket.accept()是一个阻塞方法,它会一直等待,直到一个新的客户端连接到来。 - 当
accept()返回一个Socket对象后,说明一个新的客户端已经连接成功。
- 这是服务器的核心监听循环。
-
threadPool.execute(clientHandler):- 我们没有直接
new Thread(...).start(),而是将ClientHandler实例(它实现了Runnable)提交给线程池。 - 线程池会负责从池中取出一个空闲线程(或在需要时创建一个新线程,但不超过最大限制)来执行
ClientHandler的run()方法。
- 我们没有直接
-
ClientHandler类:- 它实现了
Runnable接口,是真正的客户端任务执行者。 try-with-resources: 这是 Java 7+ 的一个重要特性,可以自动关闭实现了AutoCloseable接口的对象(如Socket,InputStream,OutputStream等),即使在try块中发生异常也能确保资源被释放,避免了资源泄漏。BufferedReader.readLine(): 这个方法也是阻塞的,它会一直等待,直到客户端发送一行文本(以\n,\r, 或\r\n并返回该文本,如果客户端关闭了连接,readLine()会返回null,循环结束。PrintWriter.println(): 用于向客户端发送响应。true参数表示“自动刷新”,每次调用println后都会自动调用flush(),确保数据被立即发送出去。
- 它实现了
客户端 (Client.java)
-
双向通信:
- 为了实现客户端和服务器的双向通信,客户端也使用了多线程,主线程负责从
System.in读取用户输入并发送给服务器。 - 我们创建了一个新的
listenerThread,它专门负责从服务器的输入流in中读取消息并打印到控制台,这样就不会出现用户输入时无法接收服务器消息,或者反之的情况。
- 为了实现客户端和服务器的双向通信,客户端也使用了多线程,主线程负责从
-
Scanner.nextLine():用于读取用户在控制台输入的一整行内容。
关键点与最佳实践
-
资源管理: 始终使用
try-with-resources或在finally块中关闭Socket和相关的InputStream/OutputStream,忘记关闭资源是导致程序bug和资源泄漏的常见原因。 -
阻塞操作:
ServerSocket.accept(),InputStream.read(),BufferedReader.readLine()都是阻塞方法,理解这一点是编写网络程序的基础,多线程的核心目的就是为了处理这些阻塞,让程序能同时做其他事情。 -
线程池 vs. 每连接一线程:
- 每连接一线程: 简单直接,易于理解,但在高并发场景下性能差,资源消耗大。
- 线程池: 性能更好,资源利用率高,是生产环境中的标准做法,可以根据服务器负载和硬件配置调整线程池大小。
-
协议设计: 示例中使用的是简单的“行”协议(以换行符结束),在实际应用中,你需要设计更健壮的协议,
- 固定长度头: 消息前固定N个字节表示消息体的长度。
- 分隔符: 使用特殊字符串(如
\r\n\r\n)来分隔消息。 - 序列化框架: 使用 JSON、Protocol Buffers、XML 等格式来结构化你的数据。
-
异常处理: 网络环境不稳定,必须妥善处理各种
IOException,客户端突然断开连接(Connection reset)是一种常见情况,不应将其视为致命错误。
如何运行和测试
-
编译: 将两个
.java文件放在同一目录下,编译它们。javac MultiThreadedServer.java Client.java
-
启动服务器: 在一个终端窗口中运行服务器。
java MultiThreadedServer
你会看到 "服务器启动,监听端口: 8888"。
-
启动客户端: 打开两个或更多新的终端窗口,分别运行客户端。
java Client
-
测试:
- 在每个客户端窗口中输入消息,然后按回车。
- 你会看到该客户端收到了服务器的回显。
- 在服务器的控制台,你会看到它收到了来自不同 IP 地址的消息。
- 即使一个客户端在等待输入,其他客户端依然可以正常收发消息,证明了多线程的并发处理能力。
这个例子为你提供了一个坚实的基础,你可以在此基础上构建更复杂的网络应用,比如聊天室、文件传输服务、RPC 框架等。
