我们将从核心概念讲起,然后通过一个完整的、可运行的“多人聊天室”示例来演示其具体实现。

核心概念回顾
在开始编码前,我们先快速回顾一下相关的核心类和概念:
| 类/概念 | 描述 |
|---|---|
java.net.ServerSocket |
服务器端套接字,它的主要职责是在指定的端口上监听客户端的连接请求,一旦有客户端连接,它会返回一个代表该客户端连接的 Socket 对象。 |
java.net.Socket |
客户端套接字,也代表服务器端与一个客户端之间的连接通道,通过 Socket,我们可以获取输入流 (InputStream) 和输出流 (OutputStream) 来与客户端进行双向数据通信。 |
java.lang.Thread |
线程,是实现多编程的基础,在服务器端,每为一个新的客户端连接创建一个 Thread,是这个模型最简单的实现方式。 |
java.io.BufferedReader / java.io.InputStreamReader |
用于从输入流中高效地读取文本行(客户端发送的消息)。 |
java.io.PrintWriter |
用于将格式化的文本写入输出流,它有一个 println() 方法,非常方便。 |
多线程Socket编程的核心思想:
- 主线程(服务器线程):
ServerSocket在一个死循环中accept()客户端连接,这个线程不负责处理具体的数据,只负责“接待”新来的客户端。 - 子线程(客户端处理线程):每当
accept()方法成功返回一个Socket对象,就意味着一个新的客户端连接建立了,服务器会立即创建一个新的Thread,并将这个Socket对象传递给这个新线程。 - 并行处理:每个子线程独立地处理一个客户端的通信(读取消息、发送消息),由于线程是并行执行的,服务器就可以同时与多个客户端进行交互,而不会互相阻塞。
模型架构图
下面是这个模型的简单架构图,能帮助你更好地理解流程:
+----------------+ accept() +-----------------+
| Client 1 | <-----------------> | ServerSocket | (主线程: 监听端口)
+----------------+ +-----------------+
|
| 返回一个新的 Socket
v
+-----------------+
| Client 1 Socket |
+-----------------+
|
| 创建新线程 (Thread 1)
v
+-----------------+
| Thread 1 | (处理 Client 1 的所有通信)
| - run() { ... } |
+-----------------+
+----------------+ accept() +-----------------+
| Client 2 | <-----------------> | ServerSocket | (主线程: 继续监听)
+----------------+ +-----------------+
|
| 返回一个新的 Socket
v
+-----------------+
| Client 2 Socket |
+-----------------+
|
| 创建新线程 (Thread 2)
v
+-----------------+
| Thread 2 | (处理 Client 2 的所有通信)
| - run() { ... } |
+-----------------+
完整示例:多人在线聊天室
我们将创建两个Java文件:Server.java 和 Client.java。

1 服务器端代码 (Server.java)
这个服务器会监听指定端口,并为每个连接的客户端启动一个新的处理线程,它还会维护一个所有客户端输出流的列表,以便实现“广播”功能(即一个客户端发送的消息,所有其他客户端都能收到)。
// Server.java
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList; // 线程安全的ArrayList
public class Server {
// 使用线程安全的List来存储所有客户端的输出流
private static final List<PrintWriter> clientWriters = new CopyOnWriteArrayList<>();
public static void main(String[] args) {
System.out.println("服务器启动中...");
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 handler = new ClientHandler(clientSocket);
new Thread(handler).start();
}
} catch (IOException e) {
System.err.println("服务器异常: " + e.getMessage());
e.printStackTrace();
}
}
// 内部类:专门用于处理单个客户端通信的线程
private static class ClientHandler implements Runnable {
private Socket socket;
private PrintWriter out;
private BufferedReader in;
private String username;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 1. 获取输入输出流
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(socket.getOutputStream(), true); // autoFlush=true
// 2. 让新用户输入用户名
out.println("请输入您的用户名:");
username = in.readLine();
System.out.println(username + " 已加入聊天室。");
broadcastMessage("[系统] " + username + " 加入了聊天室。");
// 3. 将该客户端的输出流加入列表,以便广播消息
clientWriters.add(out);
// 4. 循环读取该客户端发送的消息
String inputLine;
while ((inputLine = in.readLine()) != null) {
if ("exit".equalsIgnoreCase(inputLine)) {
break; // 客户端退出
}
// 广播该用户的消息给所有人
broadcastMessage("[" + username + "] " + inputLine);
}
} catch (IOException e) {
// 如果客户端异常断开,这里会捕获到异常
System.out.println("与客户端 " + username + " 的连接出现问题。");
} finally {
// 5. 客户端断开连接后的清理工作
if (username != null) {
System.out.println(username + " 已离开聊天室。");
broadcastMessage("[系统] " + username + " 离开了聊天室。");
}
clientWriters.remove(out); // 从广播列表中移除
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 广播消息给所有客户端
private void broadcastMessage(String message) {
for (PrintWriter writer : clientWriters) {
writer.println(message);
}
}
}
}
2 客户端代码 (Client.java)
这个客户端连接到服务器,然后在一个线程中持续监听服务器发来的消息,同时在主线程中允许用户输入消息并发送给服务器。
// Client.java
import java.io.*;
import java.net.*;
import java.util.Scanner;
public class Client {
private static final String SERVER_ADDRESS = "localhost"; // 服务器地址
private static final int SERVER_PORT = 12345; // 服务器端口
public static void main(String[] args) {
try (
// 1. 创建Socket连接服务器
Socket socket = new Socket(SERVER_ADDRESS, SERVER_PORT);
// 2. 获取输入输出流
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
Scanner scanner = new Scanner(System.in)
) {
System.out.println("已连接到服务器。");
// 3. 启动一个独立的线程来监听服务器消息
// 这个线程只负责接收,不负责发送,避免阻塞用户输入
new Thread(new ServerListener(in)).start();
// 4. 主线程负责读取用户输入并发送给服务器
System.out.println("请输入消息 (输入 'exit' 退出):");
while (true) {
String message = scanner.nextLine();
if ("exit".equalsIgnoreCase(message)) {
break;
}
out.println(message);
}
} catch (UnknownHostException e) {
System.err.println("找不到服务器: " + e.getMessage());
} catch (IOException e) {
System.err.println("I/O Error: " + e.getMessage());
}
System.out.println("客户端已关闭。");
}
// 内部类:专门用于监听服务器消息的线程
private static class ServerListener implements Runnable {
private BufferedReader in;
public ServerListener(BufferedReader in) {
this.in = in;
}
@Override
public void run() {
try {
String serverMessage;
// 循环读取服务器转发的所有消息
while ((serverMessage = in.readLine()) != null) {
System.out.println(serverMessage);
}
} catch (IOException e) {
// 如果服务器关闭或连接断开,循环会结束
System.out.println("\n与服务器的连接已断开。");
}
}
}
}
如何运行和测试
-
编译代码:
javac Server.java Client.java
-
启动服务器:
java Server
你会看到控制台输出:
服务器启动中... 服务器已在端口 12345 上启动,等待客户端连接... -
启动第一个客户端: 打开一个新的终端窗口,运行:
java Client
客户端会提示:
已连接到服务器。 请输入您的用户名:输入一个用户名,
Alice。 -
启动第二个客户端: 再打开一个新的终端窗口,运行:
java Client
输入另一个用户名,
Bob。 -
测试聊天:
- 在
Alice的客户端输入大家好!,然后按回车。 - 在
Bob的客户端,你应该能看到[Alice] 大家好!。 - 同样,在
Bob的客户端输入你好,Alice!,Alice的客户端也应该能看到。 - 在任意一个客户端输入
exit并回车,该客户端会退出,另一个客户端会收到[系统] xxx 离开了聊天室。的消息。
- 在
代码优化与更高级的模型
上面的“一个连接一个线程”模型虽然简单易懂,但在面对海量客户端时(比如数万甚至数十万连接),它会创建大量的线程,这会消耗大量内存和CPU进行线程上下文切换,导致性能急剧下降。
在实际生产环境中,通常会使用更高级的I/O模型:
线程池模型
这是对简单模型最直接的优化,不再为每个连接都创建一个新线程,而是从一个预先创建好的线程池中获取线程。
如何实现:
在 Server.java 的主循环中,不要用 new Thread(handler).start(),而是:
// 创建一个固定大小的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(100); // 比如100个线程 // 在主循环中 threadPool.execute(handler); // 将任务提交给线程池执行
这样,线程数量得到了控制,避免了资源耗尽的风险。
NIO (New I/O) 模型
这是目前Java高性能网络编程的主流方案,它使用非阻塞I/O和多路复用机制(如 Selector),用一个或几个线程来管理成百上千个连接,极大地提高了并发性能。
核心概念:
Selector:一个可以同时监控多个Channel(通道,对应Socket)的I/O操作的“多路复用器”。Channel:双向的,可以读也可以写,替代了传统I/O中的InputStream和OutputStream。Buffer:数据被读取到或写入自缓冲区。
NIO模型非常复杂,通常不会从零开始实现,而是会使用基于NIO框架,如 Netty 或 Mina。
Netty 框架
Netty是一个异步事件驱动的网络应用框架,用于快速开发可维护、高性能和高扩展性的协议服务器和客户端,它内部封装了复杂的NIO细节,提供了简单易用的API,是目前Java网络编程领域的首选框架。
使用Netty的优势:
- 高性能:基于NIO,轻松处理高并发。
- 简单易用:API设计优雅,上手快。
- 功能丰富:内置了多种协议(HTTP, WebSocket等)的编解码器。
- 社区活跃:文档完善,问题容易解决。
| 模型 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 多线程模型 | 编码简单,逻辑直观,易于理解。 | 线程数量受限于系统资源,高并发下性能差,资源消耗大。 | 学习、小型应用、连接数非常少的场景。 |
| 线程池模型 | 控制了线程数量,资源消耗可控,性能优于简单模型。 | 线程切换开销依然存在,无法应对海量连接。 | 中小型应用,连接数在几百到几千的场景。 |
| NIO模型 | 高性能,高并发,资源利用率极高。 | 编码复杂,学习曲线陡峭,难以调试。 | 大型应用,如IM服务器、RPC框架、游戏服务器等。 |
| Netty框架 | 继承NIO优点,同时提供易用API、丰富功能、高可靠性。 | 引入第三方框架,需要学习其特定概念和模式。 | 绝大多数需要高性能网络通信的Java应用。 |
对于初学者来说,深刻理解“多线程Socket”模型是迈向高性能网络编程的第一步和最重要的一步,在这个基础上,再去学习和尝试更高级的模型和框架,就会事半功倍。
