摘要: 本文详细介绍了如何使用Java虚拟线程构建高并发实时聊天室。通过整合虚拟线程、ConcurrentHashMap、Socket网络编程等核心技术,实现了一个支持广播、私聊、在线用户列表等功能的命令行聊天室。项目采用传统IO+虚拟线程的简洁架构,既能享受阻塞式编程的直观性,又能轻松支持成千上万的并发连接,是学习Java并发网络编程的绝佳实践。
关键词: Java虚拟线程, 高并发聊天室, Socket网络编程, ConcurrentHashMap, 实时通信
恭喜你走到了这里。第五阶段“精通篇”已经接近尾声,我们已经学习了虚拟线程、并发工具、网络编程等重要的知识。今天,我们要把这一阶段学到的所有内容整合在一起,构建一个真正的、可用的、高并发的实时聊天室。
这个项目会用到:
- 虚拟线程:让每个客户端连接都能被轻量地处理,轻松支持成千上万的并发用户。
ConcurrentHashMap:安全地管理所有在线用户和他们的连接。HttpClient:作为客户端发送消息(也可以改用 Socket 客户端,我们会用 Socket 实现客户端,更贴近网络编程本质,但也会提到 HttpClient 方式)。- 阻塞队列 / 线程安全容器:用于消息的缓冲和广播。
- 异常处理:让服务器在遇到错误时保持稳定运行。
- NIO 或传统 IO:我们采用传统 IO + 虚拟线程,简单且高效。
这个聊天室的功能包括:
- 用户连接到服务器,设置昵称。
- 用户可以发送消息给所有在线用户(广播)。
- 用户可以私聊另一个用户(扩展功能,我们会实现)。
- 用户可以查看在线用户列表。
- 用户可以退出聊天室。
这个项目虽然不大,但它模拟了一个真实世界中的网络服务——多个客户端同时连接、实时通信、消息广播。完成它之后,你会对“并发网络服务”这个主题有非常直观的深刻理解。
1. 需求分析 —— 我们要建一个什么样的聊天室?
我们做一个命令行版的多人聊天室:
服务器端:
- 监听一个端口(如 8888),接受客户端连接。
- 为每个客户端分配一个虚拟线程来处理。
- 维护一个在线用户列表:
Map<String, ClientHandler>(用户名 → 客户端的处理器)。 - 当一个客户端发送消息时,服务器把消息广播给所有其他在线客户端。
- 支持私聊(
@用户名 消息格式)。 - 支持查看在线用户列表(
/list命令)。 - 支持用户退出(
/quit或断开连接)。
客户端:
- 连接到服务器。
- 提示用户输入昵称,发送给服务器注册。
- 启动一个线程(或虚拟线程)持续接收服务器发来的消息并打印。
- 主线程读取用户输入,发送到服务器。
协议设计(简化版):
我们使用基于文本行的协议——每条消息以换行符 \n 结尾。消息格式:
- 普通广播消息:直接发送文本。
- 私聊消息:
@用户名 消息内容。 - 命令:
/list、/quit。
客户端和服务器约定:服务器发送的消息以 [系统]、[消息]、[私聊] 等前缀区分。
2. 架构设计 —— 分层清晰
┌─────────────────────────────────────────────────────────────┐
│ ChatServer │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 1. 启动 ServerSocket,监听端口 │ │
│ │ 2. 主循环:accept() 新连接,用虚拟线程处理 │ │
│ │ 3. 每个 ClientHandler 管理一个客户端 │ │
│ │ 4. 用 ConcurrentHashMap 存储所有在线用户 │ │
│ │ 5. 广播/私聊消息的路由 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
▲
│ 网络 Socket
▼
┌─────────────────────────────────────────────────────────────┐
│ ChatClient │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 1. 连接到服务器 │ │
│ │ 2. 发送昵称注册 │ │
│ │ 3. 接收线程:持续接收并打印服务器消息 │ │
│ │ 4. 发送线程(主线程):读取用户输入并发送 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
3. 实现服务器端
3.1 核心数据结构
// 用户映射:用户名 → 客户端处理器
private final Map<String, ClientHandler> clients = new ConcurrentHashMap<>();
// 可选:消息队列(这里我们直接在 ClientHandler 中处理,因为广播是即时进行的)
ConcurrentHashMap 保证了线程安全,多个虚拟线程可以并发地添加、删除、遍历用户列表,而不会产生数据竞争。
3.2 ClientHandler —— 管理每个客户端
每个客户端连接对应一个 ClientHandler,它负责:
- 读取客户端发送的消息。
- 处理消息(广播、私聊、命令)。
- 在客户端断开时清理资源。
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.Map;
/**
* 处理单个客户端连接的处理器
* 每个实例运行在一个虚拟线程中
*/
public class ClientHandler implements Runnable {
private final Socket socket;
private final Map<String, ClientHandler> clients;
private final String welcomeMessage;
private String username;
private BufferedReader reader;
private PrintWriter writer;
public ClientHandler(Socket socket, Map<String, ClientHandler> clients, String welcomeMessage) {
this.socket = socket;
this.clients = clients;
this.welcomeMessage = welcomeMessage;
}
@Override
public void run() {
try {
// 初始化输入输出流
reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
writer = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
// 1. 注册昵称
if (!registerUsername()) {
return; // 注册失败,关闭连接
}
// 2. 广播用户加入消息
broadcastMessage("[系统] " + username + " 加入了聊天室", null);
// 3. 发送欢迎消息
sendMessage("[系统] 欢迎 " + username + " 来到聊天室!");
sendMessage("[系统] 输入 /list 查看在线用户,@用户名 私聊,/quit 退出");
// 4. 主循环:读取并处理消息
String line;
while ((line = reader.readLine()) != null) {
if (line.trim().isEmpty()) continue;
processMessage(line);
}
} catch (IOException e) {
System.out.println("客户端 " + (username != null ? username : "未知") + " 连接异常:" + e.getMessage());
} finally {
// 清理资源
disconnect();
}
}
/**
* 注册用户名,确保唯一性
* @return 是否注册成功
*/
private boolean registerUsername() throws IOException {
while (true) {
sendMessage("请输入你的昵称:");
String name = reader.readLine();
if (name == null) {
return false; // 客户端关闭
}
name = name.trim();
if (name.isEmpty()) {
sendMessage("昵称不能为空,请重新输入");
continue;
}
// 检查是否已被占用
if (clients.containsKey(name)) {
sendMessage("昵称已被占用,请重新输入");
continue;
}
// 昵称可用,注册
this.username = name;
clients.put(name, this);
return true;
}
}
/**
* 处理一条消息
*/
private void processMessage(String line) {
// 1. 私聊检测:以 @ 开头
if (line.startsWith("@")) {
handlePrivateMessage(line);
return;
}
// 2. 命令检测
if (line.startsWith("/")) {
handleCommand(line);
return;
}
// 3. 普通广播消息
broadcastMessage("[" + username + "] " + line, this);
}
/**
* 处理私聊消息
*/
private void handlePrivateMessage(String line) {
// 格式: @target 消息内容
int spaceIndex = line.indexOf(' ');
if (spaceIndex == -1) {
sendMessage("[系统] 私聊格式错误,请使用: @用户名 消息");
return;
}
String targetName = line.substring(1, spaceIndex).trim();
String content = line.substring(spaceIndex + 1).trim();
if (content.isEmpty()) {
sendMessage("[系统] 消息内容不能为空");
return;
}
ClientHandler target = clients.get(targetName);
if (target == null) {
sendMessage("[系统] 用户 " + targetName + " 不在线");
return;
}
// 发送私聊消息
target.sendMessage("[私聊] " + username + " 对你说: " + content);
sendMessage("[私聊] 你对 " + targetName + " 说: " + content);
}
/**
* 处理命令
*/
private void handleCommand(String line) {
if (line.equalsIgnoreCase("/list")) {
// 显示在线用户列表
if (clients.isEmpty()) {
sendMessage("[系统] 当前没有在线用户");
} else {
StringBuilder sb = new StringBuilder("[系统] 在线用户: ");
String names = String.join(", ", clients.keySet());
sb.append(names);
sendMessage(sb.toString());
}
} else if (line.equalsIgnoreCase("/quit")) {
sendMessage("[系统] 正在退出...");
disconnect();
} else {
sendMessage("[系统] 未知命令,可用命令: /list, /quit");
}
}
/**
* 广播消息给所有在线用户(除指定用户外)
*/
private void broadcastMessage(String message, ClientHandler exclude) {
for (ClientHandler client : clients.values()) {
if (client != exclude) {
client.sendMessage(message);
}
}
}
/**
* 发送消息给当前客户端
*/
public void sendMessage(String message) {
if (writer != null) {
writer.println(message);
}
}
/**
* 断开连接,清理资源
*/
private void disconnect() {
try {
// 从在线列表中移除
if (username != null) {
clients.remove(username, this);
// 广播用户离开
broadcastMessage("[系统] " + username + " 离开了聊天室", this);
}
// 关闭资源
if (reader != null) try { reader.close(); } catch (IOException ignored) {}
if (writer != null) writer.close();
if (socket != null && !socket.isClosed()) try { socket.close(); } catch (IOException ignored) {}
} catch (Exception e) {
// 忽略清理时的异常
}
}
}
3.3 ChatServer —— 主服务器类
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
public class ChatServer {
private static final int PORT = 8888;
private final Map<String, ClientHandler> clients = new ConcurrentHashMap<>();
public void start() {
System.out.println("高并发聊天室服务器启动,监听端口:" + PORT);
System.out.println("使用虚拟线程处理每个客户端连接...");
// 使用虚拟线程执行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor();
ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true) {
// 接受客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("新客户端连接:" + clientSocket.getRemoteSocketAddress());
// 每个客户端分配一个虚拟线程
ClientHandler handler = new ClientHandler(clientSocket, clients,
"欢迎来到高并发聊天室!");
executor.submit(handler);
}
} catch (IOException e) {
System.err.println("服务器异常:" + e.getMessage());
e.printStackTrace();
}
}
public static void main(String[] args) {
new ChatServer().start();
}
}
就这么简单!服务器的主循环只是一个 accept() + submit(handler)。虚拟线程让每个客户端的处理成本极低,你可以轻松支持成千上万的并发连接。
4. 实现客户端
客户端要稍微复杂一些,因为它需要同时做两件事:
- 接收消息:持续从服务器读取消息并打印到控制台(需要独立线程)。
- 发送消息:读取用户输入并发送到服务器(主线程)。
我们仍然使用虚拟线程来处理接收任务,让主线程专注于用户输入。
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class ChatClient {
private static final String SERVER_HOST = "localhost";
private static final int SERVER_PORT = 8888;
private Socket socket;
private BufferedReader reader;
private PrintWriter writer;
private volatile boolean running = true;
public void start() {
try {
socket = new Socket(SERVER_HOST, SERVER_PORT);
reader = new BufferedReader(
new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
writer = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8), true);
System.out.println("已连接到服务器 " + SERVER_HOST + ":" + SERVER_PORT);
// 1. 启动接收线程(使用虚拟线程)
Thread receiver = Thread.ofVirtual().start(this::receiveMessages);
// 2. 主线程读取用户输入并发送
BufferedReader consoleReader = new BufferedReader(
new InputStreamReader(System.in, StandardCharsets.UTF_8));
String line;
while (running && (line = consoleReader.readLine()) != null) {
if (!running) break;
writer.println(line);
if (line.trim().equalsIgnoreCase("/quit")) {
break;
}
}
running = false;
// 等待接收线程结束
receiver.join();
System.out.println("客户端已关闭");
} catch (IOException e) {
System.err.println("连接服务器失败:" + e.getMessage());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
close();
}
}
/**
* 接收线程:持续从服务器读取消息并打印
*/
private void receiveMessages() {
try {
String line;
while (running && (line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
if (running) {
System.err.println("接收消息异常:" + e.getMessage());
}
} finally {
running = false;
}
}
/**
* 清理资源
*/
private void close() {
try {
if (writer != null) writer.close();
if (reader != null) try { reader.close(); } catch (IOException ignored) {}
if (socket != null && !socket.isClosed()) {
try { socket.close(); } catch (IOException ignored) {}
}
} catch (Exception ignored) {}
}
public static void main(String[] args) {
new ChatClient().start();
}
}
5. 运行与测试
-
启动服务器:运行
ChatServer的main方法。你会看到:高并发聊天室服务器启动,监听端口:8888 使用虚拟线程处理每个客户端连接... -
启动客户端:运行多个
ChatClient(你可以开多个终端或使用 IDEA 的多个 Run 窗口)。- 每个客户端会提示输入昵称。
- 输入一个独一无二的昵称,注册成功后会看到欢迎消息。
-
体验聊天:
- 在任意客户端输入消息,所有其他在线客户端都会收到广播。
- 输入
/list查看在线用户列表。 - 输入
@另一个用户 你好发送私聊消息。 - 输入
/quit退出聊天室。
-
测试高并发:
- 可以写一个简单的测试脚本,循环创建大量客户端连接到服务器,观察服务器是否能稳定运行。或者,你可以在本地启动数十个客户端,手动测试。
6. 用虚拟线程带来的好处
如果把 ClientHandler 改成继承 Thread 的传统实现,每个客户端就是一个平台线程。在 1000 个并发连接下,光线程栈就占用 1GB 内存,可能还会触发操作系统的线程数限制。
而使用虚拟线程:
- 每个客户端只占用几百字节到几 KB 的内存(具体取决于 JVM 实现)。
- 可以轻松支持 10,000+ 并发连接而不会耗尽内存。
- 线程的创建和切换开销极小,服务器可以快速响应每个客户端的消息。
这就是虚拟线程在 I/O 密集型网络服务中的典型应用——它让你用最简单的“阻塞式”编程模型,实现了过去需要复杂的 NIO 和异步框架才能达到的并发性能。
7. 扩展方向
这个聊天室已经可以工作,但还有很多可以改进和扩展的地方:
- 消息持久化:把聊天记录保存到文件或数据库中。
- 房间/频道:支持不同的聊天房间,用户可以加入/退出房间。
- 更丰富的命令:如
/help、/whois 用户名查看用户信息。 - 图形化客户端:用 JavaFX 或 Swing 实现一个图形化聊天界面。
- 使用
HttpClient替代 Socket 客户端:如果采用 RESTful API,可以把消息通过 HTTP POST 发送,但实时性会差一些(需要轮询)。更好的方式是用 WebSocket(JDK 也支持),但超出了本章范围。 - 认证与授权:加入密码验证,让用户登录更安全。
- 心跳机制:检测客户端是否意外断开,自动清理僵尸连接。
你可以选择其中一个方向继续拓展,把聊天室变成真正可用的产品。
8. 知识点回顾
这个项目用到了第五阶段的大部分核心知识:
| 知识点 | 应用位置 |
|---|---|
| 虚拟线程 | Executors.newVirtualThreadPerTaskExecutor() 处理每个客户端连接 |
ConcurrentHashMap | 安全地存储在线用户列表,支持并发读写 |
Socket / ServerSocket | 底层 TCP 网络通信 |
BufferedReader / PrintWriter | 字符流读写,处理文本行 |
| 异常处理 | try-catch-finally,IOException 处理 |
| 线程协作 | 客户端用两个线程(接收和发送)协同工作 |
| 进程隔离 | 服务器和客户端在不同进程中运行 |
| 优雅关闭 | 使用 volatile boolean running 控制线程退出 |
| 编码规范 | 使用 StandardCharsets.UTF_8 统一编码 |
try-with-resources | 自动关闭 ServerSocket 等资源 |
9. 写在最后
恭喜你完成了这个实战项目!你不再是只会写单机程序的开发者,而是能够构建高并发网络服务的程序员了。
这个聊天室,虽然只有几百行代码,但它背后承载了 Java 并发编程、网络编程、数据结构、异常处理等多个领域的知识积累。你走到这里,已经比大多数 Java 初学者走得更远、更扎实。
从下一篇开始,我们将进入这个系列的第六阶段——实战篇。我们会学习如何用 Maven 管理项目、如何写单元测试、如何集成 Spring Boot,最终构建一个真正的 Web 应用。
休息一下,喝杯水,然后继续前进吧。😃
📌 获取本系列示例代码请访问 GitCode。

6864

被折叠的 条评论
为什么被折叠?



