肖哥弹架构 跟大家“弹弹” BIO/NIO/AIO设计与实战应用,需要代码关注
欢迎 关注,点赞,留言。
关注公号Solomon肖哥弹架构获取更多精彩内容
历史热点文章
- MyCat应用实战:分布式数据库中间件的实践与优化(篇幅一)
- 图解深度剖析:MyCat 架构设计与组件协同 (篇幅二)
- 一个项目代码讲清楚DO/PO/BO/AO/E/DTO/DAO/ POJO/VO
- 写代码总被Dis:5个项目案例带你掌握SOLID技巧,代码有架构风格
- 里氏替换原则在金融交易系统中的实践,再不懂你咬我
想彻底搞懂Java BIO模型?本文通过一个完整的多人在线聊天室项目,带你从零实现基于线程池优化的BIO服务端与客户端!包含:
- ✅ 完整代码实现:服务端广播机制 + 客户端消息收发
- ✅ 性能优化实战:线程池管理、ConcurrentHashMap应用、资源泄漏防护
- ✅ 架构设计图:服务端/客户端交互流程
- 🚀 扩展方向:对比NIO方案,揭示BIO的并发瓶颈与突破点
1. 项目概述
功能需求
- 支持多个客户端通过 TCP 连接服务器。
- 客户端发送的消息会被广播给所有其他客户端。
- 服务端记录客户端连接和断开日志。
技术栈
- BIO 模型:
ServerSocket
+Socket
。 - 线程池:
ExecutorService
管理客户端连接线程。 - 简单协议:消息以换行符
\n
分隔。
2. 系统架构图
2.1 服务端设计图
2.2 客户端设计图
2.3 交互时序图
3.普通版本
3.1 服务端完整代码
3.1.1 ChatServer.java
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
/**
* BIO 聊天服务端(线程池优化版)
*/
public class ChatServer {
private static final int PORT = 8888;
private static final Set<PrintWriter> clientWriters = new ConcurrentHashSet<>(); // 线程安全的客户端输出流集合
/**
*自动扩容**:根据需要创建新线程(无上限)
- *自动回收**:60秒空闲线程会被回收
**/
private static ExecutorService threadPool = Executors.newCachedThreadPool(); // 动态大小的线程池
public static void main(String[] args) {
System.out.println("BIO 聊天服务器启动,监听端口: " + PORT);
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true) {
// 1. 阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
System.out.println("客户端连接: " + clientSocket.getRemoteSocketAddress());
// 2. 为每个客户端分配一个线程处理
threadPool.execute(new ClientHandler(clientSocket));
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 客户端处理线程
*/
private static class ClientHandler implements Runnable {
private Socket socket;
private PrintWriter writer;
public ClientHandler(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
// 1. 获取输入输出流
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
writer = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()), true
);
// 2. 将当前客户端输出流加入集合
clientWriters.add(writer);
// 3. 循环读取客户端消息
String message;
while ((message = reader.readLine()) != null) { // 阻塞直到收到消息
System.out.println("收到消息: " + message);
broadcast(message); // 广播给所有客户端
}
} catch (IOException e) {
System.out.println("客户端断开: " + socket.getRemoteSocketAddress());
} finally {
// 4. 清理资源
if (writer != null) {
clientWriters.remove(writer);
}
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 广播消息给所有客户端
*/
private void broadcast(String message) {
for (PrintWriter writer : clientWriters) {
writer.println(message); // 自动刷新缓冲区
}
}
}
}
3.2 客户端完整代码
3.2.1 ChatClient.java
import java.io.*;
import java.net.*;
import java.util.Scanner;
/**
* BIO 聊天客户端
*/
public class ChatClient {
private static final String HOST = "localhost";
private static final int PORT = 8888;
public static void main(String[] args) {
try (
Socket socket = new Socket(HOST, PORT);
PrintWriter writer = new PrintWriter(socket.getOutputStream(), true);
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream())
);
Scanner scanner = new Scanner(System.in)
) {
System.out.println("已连接到聊天服务器,输入消息开始聊天(输入 'exit' 退出):");
// 1. 启动子线程接收服务器消息
new Thread(() -> {
try {
String serverMessage;
while ((serverMessage = reader.readLine()) != null) {
System.out.println("收到广播: " + serverMessage);
}
} catch (IOException e) {
System.out.println("与服务器断开连接");
}
}).start();
// 2. 主线程发送用户输入
while (true) {
String userInput = scanner.nextLine();
if ("exit".equalsIgnoreCase(userInput)) {
break;
}
writer.println(userInput); // 发送到服务器
}
} catch (IOException e) {
System.err.println("连接服务器失败: " + e.getMessage());
}
}
}
4.线程池优化版
4.1. 优化点说明
- 线程池管理:使用
FixedThreadPool
控制最大线程数,防止线程爆炸 - 线程安全集合:改用
ConcurrentHashMap
替代ConcurrentHashSet
(Java 无原生ConcurrentHashSet
) - 资源清理:添加更完善的连接断开处理逻辑
- 日志优化:添加更详细的连接状态日志
4.2. 服务端代码
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
/**
* 优化后的BIO聊天服务器(线程池 + 线程安全集合)
* 主要优化点:
* 1. 使用固定大小线程池控制资源
* 2. 使用ConcurrentHashMap管理客户端连接
* 3. 增强异常处理和资源释放
*/
public class OptimizedChatServer {
private static final int PORT = 8888;
private static final int MAX_THREADS = 100; // 控制最大线程数
// 使用ConcurrentHashMap管理客户端输出流(Key: 客户端ID,Value: PrintWriter)
private static final Map<String, PrintWriter> clientWriters = new ConcurrentHashMap<>();
// 使用固定大小的线程池(避免无限制创建线程)
private static final ExecutorService threadPool = Executors.newFixedThreadPool(MAX_THREADS);
public static void main(String[] args) {
System.out.println("[服务器] BIO聊天服务器启动,监听端口: " + PORT);
System.out.println("[服务器] 最大工作线程数: " + MAX_THREADS);
try (ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true) {
// 阻塞等待客户端连接
Socket clientSocket = serverSocket.accept();
// 为每个客户端生成唯一ID(IP+端口+时间戳)
String clientId = clientSocket.getInetAddress() + ":" +
clientSocket.getPort() + "-" +
System.currentTimeMillis();
System.out.printf("[服务器] 客户端连接: %s (ID: %s)\n",
clientSocket.getRemoteSocketAddress(),
clientId);
// 将新连接交给线程池处理
threadPool.execute(new ClientHandler(clientSocket, clientId));
}
} catch (IOException e) {
System.err.println("[服务器] 启动失败: " + e.getMessage());
} finally {
threadPool.shutdown(); // 优雅关闭线程池
}
}
/**
* 客户端处理线程(内部类)
*/
private static class ClientHandler implements Runnable {
private final Socket socket;
private final String clientId;
private PrintWriter writer;
public ClientHandler(Socket socket, String clientId) {
this.socket = socket;
this.clientId = clientId;
}
@Override
public void run() {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
// 初始化输出流(自动刷新缓冲区)
writer = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()), true);
// 将客户端添加到全局Map
clientWriters.put(clientId, writer);
// 通知所有客户端有新用户加入
broadcast("[系统] 用户 " + clientId + " 加入聊天室");
String message;
// 循环读取客户端消息(阻塞点)
while ((message = reader.readLine()) != null) {
System.out.printf("[消息] %s: %s\n", clientId, message);
// 广播消息给所有客户端(排除自己)
broadcast(clientId + ": " + message);
}
} catch (IOException e) {
System.out.printf("[服务器] 客户端 %s 异常断开: %s\n",
clientId, e.getMessage());
} finally {
// 清理资源
if (writer != null) {
clientWriters.remove(clientId); // 从Map中移除
}
try {
socket.close();
broadcast("[系统] 用户 " + clientId + " 已退出");
} catch (IOException e) {
System.err.println("[错误] 关闭socket失败: " + e.getMessage());
}
}
}
/**
* 广播消息给所有客户端(线程安全)
* @param message 要广播的消息
*/
private void broadcast(String message) {
// 遍历所有客户端输出流
for (Map.Entry<String, PrintWriter> entry : clientWriters.entrySet()) {
try {
PrintWriter writer = entry.getValue();
writer.println(message); // 自动刷新缓冲区
} catch (Exception e) {
// 如果发送失败,移除失效的客户端
System.err.printf("[错误] 广播消息到 %s 失败: %s\n",
entry.getKey(), e.getMessage());
clientWriters.remove(entry.getKey());
}
}
}
}
}
4.3. 客户端代码
import java.io.*;
import java.net.*;
import java.util.Scanner;
/**
* 优化后的BIO聊天客户端
* 主要优化点:
* 1. 使用try-with-resources自动关闭资源
* 2. 更友好的用户提示
* 3. 添加客户端昵称功能
*/
public class OptimizedChatClient {
private static final String HOST = "localhost";
private static final int PORT = 8888;
private static String nickname; // 客户端昵称
public static void main(String[] args) {
System.out.println("=== BIO聊天客户端 ===");
try (Scanner scanner = new Scanner(System.in)) {
// 获取用户昵称
System.out.print("请输入昵称: ");
nickname = scanner.nextLine();
// 建立连接
try (Socket socket = new Socket(HOST, PORT);
PrintWriter writer = new PrintWriter(
new OutputStreamWriter(socket.getOutputStream()), true);
BufferedReader reader = new BufferedReader(
new InputStreamReader(socket.getInputStream()))) {
System.out.println("已连接到服务器,开始聊天吧!(输入'exit'退出)");
// 启动消息接收线程
new Thread(() -> {
try {
String serverMessage;
while ((serverMessage = reader.readLine()) != null) {
System.out.println(serverMessage); // 显示服务器消息
}
} catch (IOException e) {
System.out.println("[错误] 与服务器断开连接");
}
}).start();
// 主线程处理用户输入
while (true) {
String userInput = scanner.nextLine();
if ("exit".equalsIgnoreCase(userInput)) {
writer.println(nickname + " 离开了聊天室"); // 通知服务器
break;
}
writer.println(nickname + ": " + userInput); // 发送格式化的消息
}
} catch (ConnectException e) {
System.err.println("连接服务器失败,请检查服务器是否运行");
} catch (IOException e) {
System.err.println("通信错误: " + e.getMessage());
}
}
System.out.println("客户端已退出");
}
}
4.4. 优化效果
指标 | 原始版本 | 优化版本 |
---|---|---|
最大并发 | 无限制(可能OOM) | 可控(MAX_THREADS) |
线程安全 | 使用非线程安全集合 | ConcurrentHashMap保证安全 |
资源泄漏风险 | 高 | 完善的finally块保证释放 |
功能扩展性 | 差 | 支持客户端唯一标识 |
5. 交互流程图
6. 关键设计说明
6.1 BIO 模型的核心问题
- 阻塞点:
serverSocket.accept()
:等待客户端连接。reader.readLine()
:等待客户端消息。
- 优化方案:
- 使用线程池避免为每个连接创建新线程(防止线程爆炸)。
ConcurrentHashSet
保证多线程安全。
6.2 性能瓶颈
- 线程数限制:
- 线程池大小需根据机器配置调整(用
CachedThreadPool
动态扩展)。 - 高并发时(如 10k 连接),线程切换开销仍无法避免。
- 线程池大小需根据机器配置调整(用
6.3 扩展性改进方向
- NIO 重构:改用
Selector
实现单线程管理多连接(如 Netty)。 - 协议优化:定义更复杂的消息格式(如 JSON 封装用户名)。
7. 运行方式
- 启动服务端:
javac ChatServer.java java ChatServer
- 启动多个客户端(新终端):
javac ChatClient.java java ChatClient
8. 总结
- BIO 的优势:代码简单直观,适合快速开发低并发应用。
- BIO 的缺陷:线程资源消耗大,无法支撑高并发。
- 生产建议:超过 1k 并发时,改用 NIO/Netty。
📌 思考题:如果客户端不发送
exit
直接关闭,服务端如何检测断开?
答案:捕获IOException
(如readLine()
返回null
)。