概念

BIO

采用阻塞式 IO 模型:服务器端每接受一个客户端连接,就需要创建一个独立的线程处理该连接的读写;如果没有数据读写,线程会一直阻塞在 read()/write() 方法上。

NIO

一个线程管理多个连接,只有连接有实际数据时才处理,避免 BIO 的线程阻塞和资源浪费。
(1)Channel:双向的 “数据管道”
常用实现类:
ServerSocketChannel:服务器端监听连接(对应 BIO 的 ServerSocket);
SocketChannel:客户端 / 服务器端通信(对应 BIO 的 Socket);
FileChannel:文件读写(本文聚焦网络编程,暂不展开)。
核心特性:可设置为 configureBlocking(false)(非阻塞模式),这是 NIO 高性能的基础。
(2)Buffer:数据的 “中转站”
常用实现类:ByteBuffer(网络编程最常用)、CharBuffer、IntBuffer 等;
核心属性:
capacity:缓冲区总容量(不可变);
position:当前读写位置;
limit:读写的边界(最多能读 / 写多少数据);
核心方法:
flip():切换为 “读模式”(写完数据后,准备读);
clear():清空缓冲区(重置 position/limit,并非删除数据);
compact():压缩缓冲区(保留未读完的数据,方便后续继续读)。
(3)Selector:多连接的 “监听器”
核心作用:一个 Selector 可以监听多个 Channel 的事件,常见事件:
OP_ACCEPT:服务器端有新的客户端连接请求;
OP_READ:Channel 有数据可读;
OP_WRITE:Channel 可以写入数据;
核心流程:
Channel 注册到 Selector,并指定要监听的事件;
Selector 调用 select() 阻塞等待事件(无事件时线程休眠,有事件时唤醒);
处理就绪的事件(如接连接、读数据)。

代码实现

回声服务器案例

BIO

Server端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

/**
 * BIO回声服务器:接收客户端消息并原样返回
 */
public class BioEchoServer {
    // 监听端口
    private static final int PORT = 8080;

    public static void main(String[] args) {
        // 声明ServerSocket,使用try-with-resources自动关闭资源
        try (ServerSocket serverSocket = new ServerSocket(PORT)) {
            System.out.println("BIO服务器已启动,监听端口:" + PORT);

            // 无限循环监听客户端连接
            while (true) {
                // 阻塞等待客户端连接
                Socket clientSocket = serverSocket.accept();
                System.out.println("新客户端连接:" + clientSocket.getInetAddress() + ":" + clientSocket.getPort());

                // 为每个客户端创建独立线程处理
                new Thread(new ClientHandler(clientSocket)).start();
            }
        } catch (IOException e) {
            System.err.println("服务器启动失败:" + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 客户端处理器:处理单个客户端的读写操作
     */
    static class ClientHandler implements Runnable {
        private final Socket clientSocket;

        public ClientHandler(Socket clientSocket) {
            this.clientSocket = clientSocket;
        }

        @Override
        public void run() {
            // 使用try-with-resources自动关闭流和socket
            try (
                // 输入流:读取客户端发送的消息
                BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
                // 输出流:向客户端发送响应(PrintWriter更易用,自动刷新)
                PrintWriter out = new PrintWriter(new OutputStreamWriter(clientSocket.getOutputStream()), true)
            ) {
                String msg;
                // 阻塞读取客户端消息,直到客户端关闭连接(readLine返回null)
                while ((msg = in.readLine()) != null) {
                    System.out.println("收到客户端[" + clientSocket.getPort() + "]消息:" + msg);
                    // 回声响应:原样返回消息
                    out.println("服务器响应:" + msg);

                    // 特殊指令:客户端发送"exit"则关闭连接
                    if ("exit".equalsIgnoreCase(msg)) {
                        System.out.println("客户端[" + clientSocket.getPort() + "]主动断开连接");
                        break;
                    }
                }
            } catch (IOException e) {
                System.err.println("客户端[" + clientSocket.getPort() + "]处理异常:" + e.getMessage());
            } finally {
                // 确保关闭客户端连接
                try {
                    clientSocket.close();
                    System.out.println("客户端[" + clientSocket.getPort() + "]连接已关闭");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

为每个连接新增一个线程处理,这个步骤可使用线程池优化。

Client端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.UnknownHostException;

/**
 * BIO客户端:向服务器发送消息并接收响应
 */
public class BioEchoClient {
    public static void main(String[] args) {
        // 服务器地址和端口
        String serverIp = "127.0.0.1";
        int serverPort = 8080;

        // 建立连接并通信
        try (
            Socket socket = new Socket(serverIp, serverPort);
            // 读取用户输入
            BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
            // 向服务器发送消息
            PrintWriter out = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()), true);
            // 读取服务器响应
            BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))
        ) {
            System.out.println("已连接到BIO服务器:" + serverIp + ":" + serverPort);
            System.out.println("请输入消息(输入exit退出):");

            String inputMsg;
            // 循环读取用户输入并发送
            while ((inputMsg = consoleReader.readLine()) != null) {
                // 发送消息到服务器
                out.println(inputMsg);

                // 若输入exit,退出循环
                if ("exit".equalsIgnoreCase(inputMsg)) {
                    System.out.println("客户端主动退出");
                    break;
                }

                // 阻塞读取服务器响应
                String response = in.readLine();
                System.out.println("服务器返回:" + response);
            }
        } catch (UnknownHostException e) {
            System.err.println("无法连接服务器:" + serverIp + ":" + serverPort);
        } catch (IOException e) {
            System.err.println("客户端通信异常:" + e.getMessage());
        }
    }
}

代码关键说明
ServerSocket.accept():主线程阻塞等待客户端连接,是 BIO 第一个阻塞点;
BufferedReader.readLine():工作线程阻塞读取客户端数据,是 BIO 第二个阻塞点;
try-with-resources:自动关闭 Socket、流等资源,避免手动关闭遗漏;
ClientHandler:每个客户端连接对应一个独立线程,体现 “一连接一线程” 的核心设计。
ServerSocket和Socket的区别:ServerSocket服务器端专用的Socket,监听指定端口,阻塞等待客户端连接请求。而Socket专门用来处理数据交互,服务端对于每个客户端都会创建一个单独的Socket与客户端的Socket进行数据交互。

NIO

Server端

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * NIO回声服务器:单线程管理多连接,非阻塞IO + 多路复用
 */
public class NioEchoServer {
    // 监听端口
    private static final int PORT = 8080;
    // 缓冲区大小(1KB)
    private static final int BUFFER_SIZE = 1024;

    public static void main(String[] args) {
        // 1. 创建Selector(多路复用器)
        try (Selector selector = Selector.open()) {
            // 2. 创建ServerSocketChannel(服务器端监听通道)
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            // 绑定端口
            serverChannel.bind(new InetSocketAddress(PORT));
            // 设置为非阻塞模式(必须!否则Selector无法工作)
            serverChannel.configureBlocking(false);
            // 3. 将ServerSocketChannel注册到Selector,监听OP_ACCEPT事件
            // 第三个参数为附件(可传任意对象,这里暂传null)
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);

            System.out.println("NIO服务器已启动,监听端口:" + PORT);
            System.out.println("等待客户端连接...");

            // 4. 无限循环处理事件
            while (true) {
                // 阻塞等待事件就绪(返回就绪的事件数,可传超时时间如selector.select(1000))
                int readyChannels = selector.select();
                // 无就绪事件,继续循环
                if (readyChannels == 0) {
                    continue;
                }

                // 5. 获取所有就绪的事件(SelectionKey)
                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> keyIterator = selectedKeys.iterator();

                // 6. 遍历处理每个就绪事件
                while (keyIterator.hasNext()) {
                    SelectionKey key = keyIterator.next();
                    // 移除已处理的key(必须!否则会重复处理)
                    keyIterator.remove();

                    // ========== 处理连接事件(OP_ACCEPT) ==========
                    if (key.isAcceptable()) {
                        handleAccept(key);
                    }

                    // ========== 处理读事件(OP_READ) ==========
                    if (key.isReadable()) {
                        handleRead(key);
                    }
                }
            }
        } catch (IOException e) {
            System.err.println("服务器异常:" + e.getMessage());
            e.printStackTrace();
        }
    }

    /**
     * 处理客户端连接事件(OP_ACCEPT)
     */
    private static void handleAccept(SelectionKey key) throws IOException {
        // 1. 获取ServerSocketChannel
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        // 2. 接受客户端连接(非阻塞!因为ServerSocketChannel已设为非阻塞)
        SocketChannel clientChannel = serverChannel.accept();
        if (clientChannel == null) {
            return; // 无连接时返回null(非阻塞特性)
        }

        // 3. 设置客户端通道为非阻塞模式
        clientChannel.configureBlocking(false);
        System.out.println("新客户端连接:" + clientChannel.getRemoteAddress());

        // 4. 将客户端通道注册到Selector,监听OP_READ事件,并绑定一个ByteBuffer作为附件
        clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(BUFFER_SIZE));
    }

    /**
     * 处理读事件(OP_READ)
     */
    private static void handleRead(SelectionKey key) throws IOException {
        // 1. 获取客户端通道
        SocketChannel clientChannel = (SocketChannel) key.channel();
        // 2. 获取绑定的缓冲区(注册时传入的附件)
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        // 3. 非阻塞读取数据(返回读取的字节数,-1表示客户端关闭连接)
        int readBytes = clientChannel.read(buffer);

        // ========== 客户端关闭连接 ==========
        if (readBytes == -1) {
            System.out.println("客户端[" + clientChannel.getRemoteAddress() + "]断开连接");
            key.cancel(); // 取消注册
            clientChannel.close(); // 关闭通道
            return;
        }

        // ========== 读取并处理数据 ==========
        if (readBytes > 0) {
            buffer.flip(); // 切换为读模式(position归0,limit设为当前position)
            // 将缓冲区数据转为字符串
            byte[] bytes = new byte[buffer.remaining()];
            buffer.get(bytes);
            String msg = new String(bytes).trim();
            System.out.println("收到客户端[" + clientChannel.getRemoteAddress() + "]消息:" + msg);

            // 特殊指令:exit关闭连接
            if ("exit".equalsIgnoreCase(msg)) {
                System.out.println("客户端[" + clientChannel.getRemoteAddress() + "]主动退出");
                key.cancel();
                clientChannel.close();
                return;
            }

            // ========== 回声响应:将数据写回客户端 ==========
            ByteBuffer responseBuffer = ByteBuffer.wrap(("服务器响应:" + msg + "\n").getBytes());
            clientChannel.write(responseBuffer); // 非阻塞写(可能写不完,实际开发需处理半包)

            // 重置缓冲区,准备下次读取
            buffer.clear();
        }
    }
}

Client端

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * NIO客户端:向服务器发送消息并接收响应
 */
public class NioEchoClient {
    private static final String SERVER_IP = "127.0.0.1";
    private static final int SERVER_PORT = 8080;
    private static final int BUFFER_SIZE = 1024;

    public static void main(String[] args) {
        // 1. 创建SocketChannel(客户端通道)
        try (SocketChannel clientChannel = SocketChannel.open()) {
            // 2. 设置为非阻塞模式
            clientChannel.configureBlocking(false);
            // 3. 连接服务器(非阻塞,立即返回,需要检查连接状态)
            clientChannel.connect(new InetSocketAddress(SERVER_IP, SERVER_PORT));

            // 等待连接完成(非阻塞连接需要手动检查)
            while (!clientChannel.finishConnect()) {
                // 连接未完成时,可做其他事(这里简单休眠)
                Thread.sleep(10);
            }

            System.out.println("已连接到NIO服务器:" + SERVER_IP + ":" + SERVER_PORT);
            System.out.println("请输入消息(输入exit退出):");

            // 读取控制台输入
            BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));
            ByteBuffer sendBuffer = ByteBuffer.allocate(BUFFER_SIZE);
            ByteBuffer receiveBuffer = ByteBuffer.allocate(BUFFER_SIZE);

            String inputMsg;
            while ((inputMsg = consoleReader.readLine()) != null) {
                // ========== 发送消息到服务器 ==========
                sendBuffer.clear();
                sendBuffer.put(inputMsg.getBytes());
                sendBuffer.flip(); // 切换为读模式,准备发送
                clientChannel.write(sendBuffer);

                // 输入exit则退出
                if ("exit".equalsIgnoreCase(inputMsg)) {
                    System.out.println("客户端主动退出");
                    break;
                }

                // ========== 接收服务器响应 ==========
                receiveBuffer.clear();
                int readBytes = clientChannel.read(receiveBuffer);
                if (readBytes > 0) {
                    receiveBuffer.flip();
                    byte[] responseBytes = new byte[receiveBuffer.remaining()];
                    receiveBuffer.get(responseBytes);
                    System.out.println("服务器返回:" + new String(responseBytes).trim());
                }
            }
        } catch (IOException | InterruptedException e) {
            System.err.println("客户端异常:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

NIO需要处理的问题

「粘包 / 半包」问题
  1. 核心成因(网络传输的本质特性)
    粘包 / 半包不是 NIO 独有的问题(BIO 也存在),但 NIO 是非阻塞 + 缓冲区读写,会让这个问题更突出,根源是:
    TCP 是 “流式协议”:TCP 只保证数据有序、可靠传输,但不会保留 “消息边界”—— 发送方发 10 次 100 字节的消息,接收方可能一次收到 1000 字节(粘包),也可能一次只收到 50 字节(半包);
    NIO 基于 Buffer 读写:NIO 读取数据时,是从 Channel 读取到 ByteBuffer,而 ByteBuffer 有固定大小(比如 1024 字节),如果一次读取的字节数刚好截断了完整消息,就会产生半包;如果多个消息被一次性读入缓冲区,就会产生粘包。
  2. 通俗举例
    粘包:你发 3 条消息 [“hello”, “world”, “nio”],TCP 可能把它们拼成一个字节流发送,接收方一次读到所有字节,分不清哪里是消息边界;
    半包:你发一条 2000 字节的消息,接收方 ByteBuffer 只有 1024 字节,第一次只读到 1024 字节(半包),剩下的 976 字节需要下次读取。
  3. NIO 中更突出的原因
    BIO 是阻塞读取,开发者可以等完整消息读完再处理;但 NIO 是非阻塞读取,channel.read(buffer) 可能返回任意字节数(0 到 buffer 容量),必须手动处理 “没读完” 或 “读多了” 的情况,否则就会解析错误。
  4. 解决思路
    自定义协议:给消息加 “边界标识”,比如固定长度(前 4 字节表示消息长度)、分隔符(如 \n);
    缓冲区拼接:半包时把未读完的字节暂存,下次读取后拼接成完整消息;粘包时按协议拆分缓冲区中的字节流。
「Selector 空轮询」问题
  1. 核心成因(JDK 底层 Bug + NIO 设计缺陷)
    这是 NIO 特有的严重问题,根源是 JDK 对 Selector 的实现有漏洞:
    Selector.select () 空唤醒:正常情况下,selector.select() 会阻塞到有事件就绪,但 JDK 1.4~1.7 存在 Bug,导致 select() 会无理由 “空唤醒”(返回 0 个就绪事件);
    空轮询循环:如果发生空唤醒,线程会进入 “select() → 空唤醒 → 再次 select()” 的死循环,CPU 占用率直接飙升到 100%,服务器性能雪崩。
  2. 通俗举例
    你用 Selector 监听 1000 个连接,正常情况下 select() 会阻塞到有连接发数据;但空轮询时,select() 每隔几毫秒就 “假醒” 一次,线程不停循环,什么正事都没干,却把 CPU 跑满。
  3. 触发场景
    客户端连接后快速断开;
    网络波动导致 Channel 状态异常;
    多线程操作 Selector 时的竞态条件。
  4. 解决思路
    超时检测:记录 select() 空唤醒的次数,超过阈值就重建 Selector(关闭旧 Selector,重新注册所有 Channel);
    升级 JDK:JDK 1.8 对该 Bug 做了优化(但未完全修复);
    使用成熟框架:Netty 内置了空轮询的检测和修复机制(比如 NioEventLoop 中的 selectCnt 计数),不用开发者手动处理。

解决案例:
一、针对半包/粘包:自定义处理消息编解码

import java.nio.ByteBuffer;

/**
 * 消息编解码工具:解决粘包/半包问题(固定长度头协议)
 * 协议格式:[4字节消息长度][消息体字节数组]
 */
public class MessageCodec {
    // 消息头长度(4字节,存储int类型的消息长度)
    public static final int HEADER_LENGTH = 4;

    /**
     * 编码:将字符串消息转为符合协议的ByteBuffer
     */
    public static ByteBuffer encode(String msg) {
        byte[] bodyBytes = msg.getBytes();
        // 1. 创建缓冲区(头4字节 + 消息体长度)
        ByteBuffer buffer = ByteBuffer.allocate(HEADER_LENGTH + bodyBytes.length);
        // 2. 写入消息长度(消息头)
        buffer.putInt(bodyBytes.length);
        // 3. 写入消息体
        buffer.put(bodyBytes);
        // 4. 切换为读模式,准备发送
        buffer.flip();
        return buffer;
    }

    /**
     * 解码:从缓冲区中解析出完整的消息(处理半包/粘包)
     * @param buffer 待解析的缓冲区(包含未处理的字节)
     * @param tempBuffer 暂存半包数据的缓冲区
     * @return 完整的消息(无完整消息时返回null)
     */
    public static String decode(ByteBuffer buffer, ByteBuffer tempBuffer) {
        // 1. 将新读取的字节写入临时缓冲区
        tempBuffer.put(buffer);
        // 2. 切换为读模式,准备解析
        tempBuffer.flip();

        // 3. 检查是否至少有消息头(4字节)
        if (tempBuffer.remaining() < HEADER_LENGTH) {
            tempBuffer.compact(); // 压缩缓冲区(保留未读完的数据)
            return null; // 半包(连消息头都没读完)
        }

        // 4. 读取消息头(获取消息体长度)
        int bodyLength = tempBuffer.getInt();
        // 5. 检查是否有完整的消息体
        if (tempBuffer.remaining() < bodyLength) {
            // 半包(消息头读完,但消息体没读完):重置position,保留数据
            tempBuffer.position(tempBuffer.position() - HEADER_LENGTH);
            tempBuffer.compact();
            return null;
        }

        // 6. 读取完整消息体
        byte[] bodyBytes = new byte[bodyLength];
        tempBuffer.get(bodyBytes);
        String msg = new String(bodyBytes);

        // 7. 处理粘包:将剩余未解析的字节保留在临时缓冲区
        ByteBuffer remainingBuffer = tempBuffer.slice();
        tempBuffer.clear();
        tempBuffer.put(remainingBuffer);

        return msg;
    }
}

修复后的服务端代码:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;

/**
 * 解决粘包/半包的NIO服务器(固定长度头协议)
 */
public class NioFixStickyHalfServer {
    private static final int PORT = 8080;
    // 临时缓冲区大小(用于暂存半包数据)
    private static final int TEMP_BUFFER_SIZE = 4096;

    public static void main(String[] args) {
        try (Selector selector = Selector.open()) {
            // 初始化服务器通道
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            serverChannel.bind(new InetSocketAddress(PORT));
            serverChannel.configureBlocking(false);
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);

            System.out.println("修复粘包/半包的NIO服务器启动,端口:" + PORT);

            while (true) {
                int readyChannels = selector.select();
                if (readyChannels == 0) continue;

                Set<SelectionKey> selectedKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectedKeys.iterator();

                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    iterator.remove();

                    // 处理连接事件
                    if (key.isAcceptable()) {
                        handleAccept(key);
                    }

                    // 处理读事件(核心:用自定义协议解析)
                    if (key.isReadable()) {
                        handleRead(key);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 处理客户端连接:为每个客户端绑定临时缓冲区(存储半包数据)
     */
    private static void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept();
        if (clientChannel == null) return;

        clientChannel.configureBlocking(false);
        // 为客户端通道绑定临时缓冲区(用于暂存半包数据)
        ByteBuffer tempBuffer = ByteBuffer.allocate(TEMP_BUFFER_SIZE);
        clientChannel.register(key.selector(), SelectionKey.OP_READ, tempBuffer);

        System.out.println("客户端连接:" + clientChannel.getRemoteAddress());
    }

    /**
     * 处理读事件:用MessageCodec解析,解决粘包/半包
     */
    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        // 获取绑定的临时缓冲区
        ByteBuffer tempBuffer = (ByteBuffer) key.attachment();
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);

        int readBytes = clientChannel.read(readBuffer);
        // 客户端断开连接
        if (readBytes == -1) {
            System.out.println("客户端断开:" + clientChannel.getRemoteAddress());
            key.cancel();
            clientChannel.close();
            return;
        }

        if (readBytes > 0) {
            readBuffer.flip();
            // 核心:用自定义协议解码,处理粘包/半包
            String msg = MessageCodec.decode(readBuffer, tempBuffer);
            if (msg != null) {
                System.out.println("收到完整消息:" + msg);

                // 回声响应(编码后发送)
                ByteBuffer responseBuffer = MessageCodec.encode("服务器响应:" + msg);
                clientChannel.write(responseBuffer);

                // 特殊指令退出
                if ("exit".equalsIgnoreCase(msg)) {
                    System.out.println("客户端主动退出:" + clientChannel.getRemoteAddress());
                    key.cancel();
                    clientChannel.close();
                }
            }
        }
    }
}

(3)配套客户端代码

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

/**
 * 配套客户端(发送符合协议的消息)
 */
public class NioFixStickyHalfClient {
    private static final String SERVER_IP = "127.0.0.1";
    private static final int SERVER_PORT = 8080;

    public static void main(String[] args) {
        try (SocketChannel clientChannel = SocketChannel.open()) {
            clientChannel.configureBlocking(false);
            clientChannel.connect(new InetSocketAddress(SERVER_IP, SERVER_PORT));

            // 等待连接完成
            while (!clientChannel.finishConnect()) {
                Thread.sleep(10);
            }

            System.out.println("连接服务器成功,输入消息(exit退出):");
            BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in));

            String inputMsg;
            while ((inputMsg = consoleReader.readLine()) != null) {
                // 核心:编码后发送(符合固定长度头协议)
                ByteBuffer buffer = MessageCodec.encode(inputMsg);
                clientChannel.write(buffer);

                if ("exit".equalsIgnoreCase(inputMsg)) {
                    System.out.println("客户端退出");
                    break;
                }

                // 读取服务器响应
                ByteBuffer responseBuffer = ByteBuffer.allocate(4096);
                int readBytes = clientChannel.read(responseBuffer);
                if (readBytes > 0) {
                    responseBuffer.flip();
                    // 解码响应消息
                    String response = MessageCodec.decode(responseBuffer, ByteBuffer.allocate(4096));
                    if (response != null) {
                        System.out.println("服务器响应:" + response);
                    }
                }
            }
        } catch (IOException | InterruptedException e) {
            e.printStackTrace();
        }
    }
}

代码关键说明
MessageCodec.encode():给消息加 4 字节长度头,保证发送的消息符合协议;
MessageCodec.decode():先读长度头,再读完整消息体,半包时用 tempBuffer 暂存数据,粘包时拆分缓冲区;
tempBuffer.compact():压缩缓冲区,保留未读完的半包数据,避免丢失;
每个客户端绑定独立的 tempBuffer,确保多客户端数据不混淆。
二、解决 Selector 空轮询问题(核心:超时检测 + 重建 Selector)

  1. 解决思路
    JDK 空轮询 Bug 表现为 selector.select() 频繁空返回(返回 0),解决方案:
    记录 select() 空返回次数;
    超过阈值(如 5 次)判定为空轮询,重建 Selector(关闭旧 Selector,重新注册所有 Channel);
    核心:保证重建过程中不丢失客户端连接。
  2. 完整代码(修复空轮询的 NIO 服务器)
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;

/**
 * 解决Selector空轮询的NIO服务器
 * 核心:检测空轮询次数,超过阈值重建Selector
 */
public class NioFixEmptyPollServer {
    private static final int PORT = 8080;
    // 空轮询阈值(连续空返回5次判定为空轮询)
    private static final int EMPTY_POLL_THRESHOLD = 5;
    // 临时缓冲区大小
    private static final int TEMP_BUFFER_SIZE = 4096;

    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
        // 初始化服务器通道
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(PORT));
        serverChannel.configureBlocking(false);
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("修复空轮询的NIO服务器启动,端口:" + PORT);

        int emptyPollCount = 0; // 空轮询计数器
        while (true) {
            // 带超时的select(避免永久阻塞)
            int readyChannels = selector.select(1000);

            // 检测空轮询:select返回0表示无就绪事件
            if (readyChannels == 0) {
                emptyPollCount++;
                // 超过阈值,重建Selector
                if (emptyPollCount >= EMPTY_POLL_THRESHOLD) {
                    System.out.println("检测到Selector空轮询,开始重建...");
                    selector = rebuildSelector(selector);
                    emptyPollCount = 0; // 重置计数器
                    continue;
                }
            } else {
                emptyPollCount = 0; // 有就绪事件,重置计数器
            }

            // 处理就绪事件(逻辑和之前一致)
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectedKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();

                if (key.isAcceptable()) {
                    handleAccept(key);
                }
                if (key.isReadable()) {
                    handleRead(key);
                }
            }
        }
    }

    /**
     * 重建Selector:解决空轮询Bug
     */
    private static Selector rebuildSelector(Selector oldSelector) throws IOException {
        // 1. 创建新Selector
        Selector newSelector = Selector.open();
        // 2. 遍历旧Selector的所有Channel,重新注册到新Selector
        Set<SelectionKey> keys = oldSelector.keys();
        for (SelectionKey key : keys) {
            if (!key.isValid()) {
                continue; // 无效key跳过
            }

            SelectableChannel channel = key.channel();
            int interestOps = key.interestOps();
            Object attachment = key.attachment(); // 保留原附件(如临时缓冲区)

            // 取消旧注册
            key.cancel();
            // 重新注册到新Selector
            channel.register(newSelector, interestOps, attachment);
        }
        // 3. 关闭旧Selector
        oldSelector.close();
        System.out.println("Selector重建完成");
        return newSelector;
    }

    // 以下handleAccept/handleRead方法和上一个服务器完全一致,复制即可
    private static void handleAccept(SelectionKey key) throws IOException {
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        SocketChannel clientChannel = serverChannel.accept();
        if (clientChannel == null) return;

        clientChannel.configureBlocking(false);
        ByteBuffer tempBuffer = ByteBuffer.allocate(TEMP_BUFFER_SIZE);
        clientChannel.register(key.selector(), SelectionKey.OP_READ, tempBuffer);

        System.out.println("客户端连接:" + clientChannel.getRemoteAddress());
    }

    private static void handleRead(SelectionKey key) throws IOException {
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer tempBuffer = (ByteBuffer) key.attachment();
        ByteBuffer readBuffer = ByteBuffer.allocate(1024);

        int readBytes = clientChannel.read(readBuffer);
        if (readBytes == -1) {
            System.out.println("客户端断开:" + clientChannel.getRemoteAddress());
            key.cancel();
            clientChannel.close();
            return;
        }

        if (readBytes > 0) {
            readBuffer.flip();
            String msg = MessageCodec.decode(readBuffer, tempBuffer);
            if (msg != null) {
                System.out.println("收到完整消息:" + msg);
                ByteBuffer responseBuffer = MessageCodec.encode("服务器响应:" + msg);
                clientChannel.write(responseBuffer);

                if ("exit".equalsIgnoreCase(msg)) {
                    System.out.println("客户端主动退出:" + clientChannel.getRemoteAddress());
                    key.cancel();
                    clientChannel.close();
                }
            }
        }
    }
}
  1. 代码关键说明
    selector.select(1000):带 1 秒超时的 select,避免永久阻塞,便于检测空轮询;
    emptyPollCount:统计连续空返回次数,超过阈值触发重建;
    rebuildSelector():核心方法,将旧 Selector 的所有 Channel 重新注册到新 Selector,保留原附件(如半包临时缓冲区),保证业务不中断;
    重建后重置计数器,恢复正常监听。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐