手把手实现Java BIO聊天室:从基础到优化,掌握高并发核心技巧!

在这里插入图片描述

肖哥弹架构 跟大家“弹弹” BIO/NIO/AIO设计与实战应用,需要代码关注

欢迎 关注,点赞,留言。

关注公号Solomon肖哥弹架构获取更多精彩内容

历史热点文章

想彻底搞懂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. 优化点说明

  1. 线程池管理:使用 FixedThreadPool 控制最大线程数,防止线程爆炸
  2. 线程安全集合:改用 ConcurrentHashMap 替代 ConcurrentHashSet(Java 无原生 ConcurrentHashSet
  3. 资源清理:添加更完善的连接断开处理逻辑
  4. 日志优化:添加更详细的连接状态日志

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. 运行方式

  1. 启动服务端
    javac ChatServer.java
    java ChatServer
    
  2. 启动多个客户端(新终端):
    javac ChatClient.java
    java ChatClient
    

8. 总结

  • BIO 的优势:代码简单直观,适合快速开发低并发应用。
  • BIO 的缺陷:线程资源消耗大,无法支撑高并发。
  • 生产建议:超过 1k 并发时,改用 NIO/Netty

📌 思考题:如果客户端不发送 exit 直接关闭,服务端如何检测断开?
答案:捕获 IOException(如 readLine() 返回 null)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Solomon_肖哥弹架构

你的欣赏就是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值