BIO实现多线程处理客户端socket的示例

54 阅读5分钟

BIO实现的服务端,多线程处理客户端socket


public class BioMutilThreadMain {
    
    private static final int PORT = 5279;
    private static final int THREAD_POOL_SIZE = 3;
    
    public static void main(String[] args) {
        // 创建自定义的ThreadFactory
        ThreadFactory threadFactory = new ThreadFactory() {
            private final AtomicInteger threadNumber = new AtomicInteger(1);
            
            @Override
            public Thread newThread(Runnable r) {
                Thread thread = new Thread(r, "SocketServer-Worker-" + threadNumber.getAndIncrement());
                // 设置为守护线程
                thread.setDaemon(true);
                return thread;
            }
        };
        
        // 使用自定义的ThreadFactory创建线程池
        ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE, threadFactory);
        
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("服务器启动,监听端口:" + PORT);
            
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("新客户端连接成功!IP: " + 
                    clientSocket.getInetAddress().getHostAddress() + 
                    ", 端口: " + clientSocket.getPort());
                threadPool.execute(new ClientHandler(clientSocket));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }
    }
}

class ClientHandler implements Runnable {
    private Socket clientSocket;
    
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }
    
    @Override
    public void run() {
        // 获取当前线程信息并打印
        Thread currentThread = Thread.currentThread();
        System.out.println(String.format("[%s] 开始处理客户端连接:%s:%d", 
            currentThread.getName(),
            clientSocket.getInetAddress().getHostAddress(),
            clientSocket.getPort()));
            
        try (
            InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()
        ) {
            byte[] buffer = new byte[1024];
            
            while (true) {
                /**
                 * 
                 * inputStream.available()
                 * 非阻塞操作,立即返回
                 * 返回0并不意味着连接断开,只是当前没有数据可读
                 * 不能用来可靠地检测连接是否断开
                 * 主要用于避免阻塞读取
                 * 
                 * 
                 * inputStream.read()
                 * 阻塞操作,会等待直到有数据可读或连接断开
                 * 返回实际读取到的字节数
                 * 返回-1明确表示流已经结束(连接断开)
                 * 可靠地检测连接断开状态
                 * 如果没有数据会阻塞线程
                 * 
                 */
                // if (inputStream.available() > 0) {
                    
                    
                // }
                int bytesRead = inputStream.read(buffer);
                if (bytesRead == -1) {// 客户端主动断开连接
                    System.out.println(String.format("[%s] 客户端主动断开连接:%s:%d", 
                        currentThread.getName(),
                        clientSocket.getInetAddress().getHostAddress(),
                        clientSocket.getPort()));
                    break;
                }
                
                byte[] actualData = new byte[bytesRead];
                System.arraycopy(buffer, 0, actualData, 0, bytesRead);
                String receivedData = new String(actualData).trim();
                
                // 在每次数据处理时打印线程信息
                if (isHexString(receivedData)) {
                    System.out.println(String.format("[%s] 收到HEX格式数据:%s", 
                        currentThread.getName(), receivedData));
                } else {
                    System.out.println(String.format("[%s] 收到ASCII格式数据:%s", 
                        currentThread.getName(), receivedData));
                }
                outputStream.flush();
                Thread.sleep(100);
            }
        } catch (IOException e) {
            System.out.println(String.format("[%s] 客户端断开连接:%s:%d", 
                currentThread.getName(),
                clientSocket.getInetAddress().getHostAddress(),
                clientSocket.getPort()));
            e.printStackTrace();
        } catch (InterruptedException e) {
            System.out.println(String.format("[%s] 处理线程被中断", 
                currentThread.getName()));
            Thread.currentThread().interrupt();
        } finally {
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    // 判断是否为HEX字符串
    private boolean isHexString(String str) {
        str = str.replaceAll("\\s", ""); // 移除所有空白字符
        // 检查是否符合HEX格式(允许空格分隔)
        return str.matches("^[0-9A-Fa-f]+$") && str.length() % 2 == 0;
    }
}


使用netAssist开启多个客户端进行测

[SocketServer-Worker-2] 收到ASCII格式数据:hello world 001
[SocketServer-Worker-1] 收到ASCII格式数据:hello wooo003
[SocketServer-Worker-3] 收到ASCII格式数据:hello world002
[SocketServer-Worker-2] 收到ASCII格式数据:hello world 001
[SocketServer-Worker-1] 收到ASCII格式数据:hello wooo003
[SocketServer-Worker-3] 收到ASCII格式数据:hello world002
[SocketServer-Worker-2] 收到ASCII格式数据:hello world 001
[SocketServer-Worker-1] 收到ASCII格式数据:hello wooo003
[SocketServer-Worker-3] 收到ASCII格式数据:hello world002
[SocketServer-Worker-2] 收到ASCII格式数据:hello world 001
[SocketServer-Worker-1] 收到ASCII格式数据:hello wooo003
[SocketServer-Worker-3] 收到ASCII格式数据:hello world002
[SocketServer-Worker-2] 收到ASCII格式数据:hello world 001
[SocketServer-Worker-1] 收到ASCII格式数据:hello wooo003
[SocketServer-Worker-3] 收到ASCII格式数据:hello world002
[SocketServer-Worker-2] 收到ASCII格式数据:hello world 001
[SocketServer-Worker-1] 收到ASCII格式数据:hello wooo003
[SocketServer-Worker-3] 收到ASCII格式数据:hello world002
[SocketServer-Worker-2] 收到ASCII格式数据:hello world 001
[SocketServer-Worker-1] 收到ASCII格式数据:hello wooo003
[SocketServer-Worker-3] 收到ASCII格式数据:hello world002
[SocketServer-Worker-2] 收到ASCII格式数据:hello world 001
[SocketServer-Worker-1] 收到ASCII格式数据:hello wooo003
[SocketServer-Worker-3] 收到ASCII格式数据:hello world002
[SocketServer-Worker-2] 收到ASCII格式数据:hello world 001
[SocketServer-Worker-1] 收到ASCII格式数据:hello wooo003

缺点:

  • 资源利用效率低,一个连接占用一个线程,即使客户端无数据传输,线程也处于阻塞状态,线程资源无法动态释放和复用

  • 该代码存在一个问题,一个客户端完全绑定一个线程,如果该客户端断开连接了,对应的线程仍然没有解绑,无法重新绑定其他客户端,处理其他客户端的数据

  • 扩展性限制,连接数受限于线程池大小,线程池过大会导致系统资源消耗过高

  • 不适合高并发场景,性能瓶颈,阻塞式I/O导致线程利用率低,频繁的上下文切换影响性能,不适合短连接、高频请求场景

优化下代码

  • 添加了 setSoTimeout 设置读取超时,避免永久阻塞
  • 使用 running 标志控制线程运行状态,在连接断开时及时跳出循环,释放线程回线程池
  • 当客户端断开连接时,线程会及时释放回线程池,超时机制确保即使在异常情况下也能释放线程,线程可以被重新分配给新的客户端连接

class ClientHandler implements Runnable {
    private Socket clientSocket;
    private volatile boolean running = true;  // 添加运行状态控制
    
    public ClientHandler(Socket socket) {
        this.clientSocket = socket;
    }
    
    @Override
    public void run() {
        Thread currentThread = Thread.currentThread();
        System.out.println(String.format("[%s] 开始处理客户端连接:%s:%d", 
            currentThread.getName(),
            clientSocket.getInetAddress().getHostAddress(),
            clientSocket.getPort()));
            
        try (
            InputStream inputStream = clientSocket.getInputStream();
            OutputStream outputStream = clientSocket.getOutputStream()
        ) {
            byte[] buffer = new byte[1024];
            
            while (running) {
                // 设置读取超时,避免永久阻塞
                clientSocket.setSoTimeout(30000); // 30秒超时
                
                int bytesRead = inputStream.read(buffer);
                if (bytesRead == -1) {
                    System.out.println(String.format("[%s] 客户端主动断开连接:%s:%d", 
                        currentThread.getName(),
                        clientSocket.getInetAddress().getHostAddress(),
                        clientSocket.getPort()));
                    break;  // 跳出循环,释放线程
                }
                
                byte[] actualData = new byte[bytesRead];
                System.arraycopy(buffer, 0, actualData, 0, bytesRead);
                String receivedData = new String(actualData).trim();
                
                // 在每次数据处理时打印线程信息
                if (isHexString(receivedData)) {
                    System.out.println(String.format("[%s] 收到HEX格式数据:%s", 
                        currentThread.getName(), receivedData));
                } else {
                    System.out.println(String.format("[%s] 收到ASCII格式数据:%s", 
                        currentThread.getName(), receivedData));
                }
                outputStream.flush();
                Thread.sleep(100);
            }
        } catch (Exception e) {
            System.out.println(String.format("[%s] 客户端异常断开连接:%s:%d", 
                currentThread.getName(),
                clientSocket.getInetAddress().getHostAddress(),
                clientSocket.getPort()));
        }finally {
            running = false;  // 确保停止运行
            try {
                clientSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            System.out.println(String.format("[%s] 线程释放完成", currentThread.getName()));
        }
    }