NIO-Acceptor+Reader 结合Selector实现简单多路复用

完整代码见最后

1. 主线程(AcceptorThread)

职责:专门处理客户端连接请求

java

- 创建主Selector,监听ACCEPT事件
- 绑定8080端口,等待客户端连接
- 使用轮询算法分配连接到读工作线程
- 维持ServerSocketChannel的生命周期

2. 读工作线程(ReadWorker)

职责:专门处理数据读写业务

java

- 每个ReadWorker拥有独立的Selector
- 处理注册到本线程的客户端Channel的READ/WRITE事件
- 管理数据缓冲区,处理半包、粘包问题
- 执行具体的业务逻辑处理

3. 线程选择器(ReadWorkerSelector)

职责:实现负载均衡

java

- 维护4个ReadWorker实例的数组
- 采用轮询(Round-Robin)算法分配连接
- 确保各工作线程负载相对均衡

核心执行流程

阶段一:服务启动与监听

  1. 初始化主Selector,注册ServerSocketChannel监听ACCEPT事件

  2. 创建工作线程组,初始化4个ReadWorker(但尚未启动)

  3. 进入事件循环,主线程在selector.select()阻塞等待事件

阶段二:连接建立与分配

java

客户端连接 → 触发ACCEPT事件 → 主线程处理 → 轮询选择ReadWorker → 注册到工作线程

关键步骤

  • 主线程调用serverSocketChannel.accept()获取客户端Channel

  • 通过ReadWorkerSelector.getReadWorker()获取下一个工作线程

  • 调用readWorker.register(clientChannel)注册客户端连接

阶段三:工作线程注册机制

采用任务队列 + wakeup的线程安全注册模式:

java

// 1. 主线程提交注册任务
queue.add(() -> {
    clientChannel.register(workerSelector, OP_READ, buffer);
    // 初始数据发送等操作
});

// 2. 唤醒工作线程
workerSelector.wakeup();

// 3. 工作线程处理注册
Processor task = queue.poll();
if(task != null) {
    task.process(); // 在工作线程中执行注册
}

设计优势

  • 所有Selector操作都在同一线程执行,避免线程安全问题

  • 通过wakeup确保新连接及时被处理

  • 任务队列保证注册操作的顺序性

阶段四:数据读写处理

读事件处理流程

java

触发READ事件 → 读取数据到ByteBuffer → 处理消息边界 → 业务处理 → 缓冲区管理

关键技术点

  1. 动态缓冲区扩容

java

if(byteBuffer.position() == byteBuffer.limit()) {
    // 缓冲区已满,扩容2倍
    ByteBuffer newBuffer = ByteBuffer.allocate(byteBuffer.capacity() * 2);
    byteBuffer.flip();
    newBuffer.put(byteBuffer);
    key.attach(newBuffer);
}
  1. 消息边界处理

java

// 按行解析消息,处理TCP粘包/半包问题
while(source.hasRemaining()) {
    // 查找换行符作为消息边界
    if(source.get(i) == '\n') {
        // 提取完整消息行
        String message = new String(lineBytes, StandardCharsets.UTF_8);
    }
}
  1. 写事件处理

java

// 大消息分批次发送
if(sentBuffer.hasRemaining()) {
    // 注册WRITE事件,继续发送剩余数据
    key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
    key.attach(sentBuffer);
}

Server端:

public class Server {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(Server.class);
    public static void main(String[] args) throws IOException {
        Thread.currentThread().setName("AcceptorThread");
        // 基于 selector 的非阻塞 IO 服务器端  单线程 配合 selector 实现对多个channel的可读写事件的管理 实现多路复用
        Selector selector = Selector.open();
        // 创建服务端 Socket
        ServerSocketChannel serversocketChannel = ServerSocketChannel.open();
        serversocketChannel.configureBlocking(false); // 设置为非阻塞模式  影响在 accept() 方法上  如果没有链接建立,返回的是none值
        // 将 channel 注册到 selector 上,监听 ACCEPT 事件
        // SelectionKey 是用来记录发生的事件 和 事件的channel 的对应关系  这里只会登记一些channel 和 事件的关系 到集合中  不可变的
        SelectionKey sscKey = serversocketChannel.register(selector, SelectionKey.OP_ACCEPT, null);
        // sscKey.interestOps(SelectionKey.OP_ACCEPT); // 也可以这么写   让 selector 监听 ACCEPT 事件
        // 客户端会发生的事件: connect
        // 可读、可写事件  我们的ssc只需要关心 ACCEPT 事件
        // 绑定端口
        serversocketChannel.bind(new InetSocketAddress(8080));
        // 采用轮询选择readWorker线程处理readable事件
//        ReadWorker readWorker = new ReadWorker("readWorer0");
        // 监听连接
        while (true) {
            // 没有事件发生 select会阻塞在这里
            // 发生事件后,selector 会将对应的 channel 放入 selectedKeys 集合中 如果事件没被处理,那下次调用 select 不会再次阻塞
            log.info("Listening on port 8080");
            selector.select();  //每次会新增事件 到集合当中 SelectedKeys Set  维护触发事件的key  不会自己删除,需要手动取消
            Iterator<SelectionKey> selectionKeyIterator = selector.selectedKeys().iterator();
            while (selectionKeyIterator.hasNext()) {
                SelectionKey selectionKey = selectionKeyIterator.next();
                log.info("Selected key: {}", selectionKey);  // 打印出发生的事件
                // 从selectedKeys集合中移除该事件
                selectionKeyIterator.remove();
                // 处理 ACCEPT 事件
                if (selectionKey.isAcceptable()) {
                    SelectableChannel channel = selectionKey.channel();// 获取事件对应的 channel
                    // 从集合中移除该事件
                    // selectionKey.cancel();
                    if (channel instanceof ServerSocketChannel) {
                        try {
                            SocketChannel clientChannel = ((ServerSocketChannel) channel).accept();
                            log.info("Accepted connection from {}", clientChannel.getRemoteAddress());
                            clientChannel.configureBlocking(false); // 设置为非阻塞模式
                            // 将 channel 注册到 selector 上,监听读写事件
                            // 给每一个客户端的socketChannel 绑定一个缓冲区  附件为ByteBuffer
//                            ByteBuffer byteBuffer = ByteBuffer.allocate(16);
//                            SelectionKey clientKey = clientChannel.register(selector, SelectionKey.OP_READ, byteBuffer);
                            // 让客户端的读事件绑定到另一个线程的selector上  处理读事件
                            log.info("注册读事件到读线程");
                            ReadWorkerSelector.getReadWorker().register(clientChannel);
                            log.info("注册读事件到读线程完成");
                            // 如果向客户端发送大量消息 返回的是发送的字节数
//                            ByteBuffer sentbuffer = StandardCharsets.UTF_8.encode("a".repeat(3000));
//                            // 可能一次不会发送完  所以需要循环发送 如果用while就会阻塞在这里
//                            int written = clientChannel.write(sentbuffer);
//                            log.info("此次发送的字节数 :{} byte", written);
//                            if (sentbuffer.hasRemaining()) {
//                                // 缓冲区没有写完, 不需要死等, 可以给这个channel 的key关注写事件,下次有写事件时,会调用对应的处理方法
//                                clientKey.interestOps(clientKey.interestOps() | SelectionKey.OP_WRITE);
//                                // 往哪儿写? 此次大消息的buffer
//                                clientKey.attach(sentbuffer);
//                            }
                        } catch (IOException e) {
                            log.error(e.getMessage(), e);
                        }
                    }
                }
                else if (selectionKey.isReadable()) {
                    SocketChannel clientChannel = (SocketChannel) selectionKey.channel(); // 获取事件对应的 channel
                    try {
                        // 获取缓冲区
                        ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
                        if (byteBuffer == null) {
                            byteBuffer = ByteBuffer.allocate(16);
                            selectionKey.attach(byteBuffer);
                            log.info("重新创建读缓冲区");
                        }
                        int read = clientChannel.read(byteBuffer); // 如果数据量大于缓冲区大小,会再次发送读事件 直到读完
                        if (read == -1) {
                            // 这里我们分配的是heap ByteBuffer  所以不需要手动释放
                            selectionKey.cancel(); // 客户端正常断开 反回-1  取消该事件
                        }
                        else {
//                            byteBuffer.flip();
//                            log.info("读取到数据:{}", StandardCharsets.UTF_8.decode(byteBuffer));
//                            byteBuffer.clear();
                            handleMessage(byteBuffer);
                            // 消息量超过了缓冲区大小,需要扩容处理,然后拷贝到新的缓冲区中
                            if (byteBuffer.position() == byteBuffer.limit()) {
                                byteBuffer.flip();  // 让position=0  让缓冲区可读
                                ByteBuffer newBuffer = ByteBuffer.allocate(byteBuffer.capacity() * 2);
                                newBuffer.put(byteBuffer);
                                selectionKey.attach(newBuffer);
                            }
                        }
                    } catch (IOException e) {
                        log.error(e.getMessage(), e);
                        // 异常断开
                        selectionKey.cancel(); // 会将映射关系 先添加到cancelledKeys集合中  下一次select() 会从keys 和 selectedKeys集合中移除该事件
                    }
                    // 不能在这里移除该事件,因为对于正常读完数据之后,也会将原先的映射关系key删除
//                    finally {
//                        // 移除该事件
//                        selectionKey.cancel();
//                    }
                } else if (selectionKey.isWritable()) {
                    // 拿到sentbuffer
                    ByteBuffer sentBuffer = (ByteBuffer) selectionKey.attachment();
                    SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
                    int written = clientChannel.write(sentBuffer);
                    if (written == -1) {
                        selectionKey.cancel(); // 客户端异常断开  取消该事件
                    }
                    log.info("此次发送的字节数 :{} byte", written);
                    // 清理sentbuffer
                    if (!sentBuffer.hasRemaining()) {
                        selectionKey.attach(null); // 发送完毕  取消大buffer  注意处理写事件的时候 要回填缓冲区
                        selectionKey.interestOps(selectionKey.interestOps()&~SelectionKey.OP_WRITE);
                        log.info("发送完毕, 取消关注写事件: {}", selectionKey);
                    }
                }
            }
        }
    }
    private static void handleMessage(ByteBuffer source) throws IOException {
        // 如果消息大小超出了缓冲区大小 ,需要扩容处理,然后拷贝到新的缓冲区中
        source.flip();
        try {
            while (source.hasRemaining()) {
                // 查找换行符
                int originalPosition = source.position();
                int lineEnd = -1;

                for (int i = originalPosition; i < source.limit(); i++) {
                    if (source.get(i) == '\n') {
                        lineEnd = i;
                        break;
                    }
                }

                if (lineEnd == -1) {
                    // 没有完整行,重置位置等待更多数据
                    source.position(originalPosition);
                    break;
                }

                // 提取一行消息
                int length = lineEnd - originalPosition;
                byte[] lineBytes = new byte[length];
                source.get(lineBytes);
                source.get(); // 跳过换行符

                // 处理消息
                String message = new String(lineBytes, StandardCharsets.UTF_8);
                System.out.println("收到消息: " + message);

                // 可以在这里添加业务逻辑处理
            }
        } finally {
            source.compact();
        }
    }

    // 使用多线程优化 acceptable事件主线程处理 readable 另一个线程处理
    static class ReadWorker implements Runnable {
        private Thread thread;
        private Selector selector;
        private String name;
        private volatile boolean running = false;
        private ConcurrentLinkedQueue<Processor> queue = new ConcurrentLinkedQueue<>();

        public ReadWorker(String name) {
            this.name = name;
        }

        // 注册到selector上,监听readable事件
        public void register(SocketChannel clientChannel) throws IOException {
            if (!running) {
                this.selector = Selector.open();
                this.thread = new Thread(this, name);
                thread.start();
                running = true;
            }
            queue.add(() -> {
                try {
                    SelectionKey clientKey = clientChannel.register(this.selector, SelectionKey.OP_READ, null);
                    ByteBuffer sentbuffer = StandardCharsets.UTF_8.encode("a".repeat(3000));
                    // 可能一次不会发送完  所以需要循环发送 如果用while就会阻塞在这里
                    int written = clientChannel.write(sentbuffer);
                    log.info("此次发送的字节数 :{} byte", written);
                    if (sentbuffer.hasRemaining()) {
                        // 缓冲区没有写完, 不需要死等, 可以给这个channel 的key关注写事件,下次有写事件时,会调用对应的处理方法
                        clientKey.interestOps(clientKey.interestOps() | SelectionKey.OP_WRITE);
                        // 往哪儿写? 此次大消息的buffer
                        clientKey.attach(sentbuffer);
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            });
            // 由 acceptor 线程 唤醒 worker线程
            selector.wakeup();
        }

        @Override
        public void run() {
            while (true) {
                try {
                    // 在主线程注册之前 就已经阻塞住了,新注册的事件不会唤醒当前worker线程
                    this.selector.select();
                    // 一定要由reader线程自己处理channel的注册
                    Processor polled = queue.poll();
                    if (polled!= null) {
                        polled.process();
                    }
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        iterator.remove();
                        if (key.isReadable()) {
                            log.info("read worker 处理readable事件");
                            SocketChannel channel = (SocketChannel) key.channel();
                            ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
                            if (byteBuffer == null) {
                                byteBuffer = ByteBuffer.allocate(16);
                                key.attach(byteBuffer);
                            }
                            int read = channel.read(byteBuffer);
                            if (read == -1) {
                                key.cancel(); // 客户端断开  取消该事件
                            } else {
                                handleMessage(byteBuffer);
                                // 消息量超过了缓冲区大小,需要扩容处理,然后拷贝到新的缓冲区中
                                if (byteBuffer.position() == byteBuffer.limit()) {
                                    byteBuffer.flip();  // 让position=0  让缓冲区可读
                                    ByteBuffer newBuffer = ByteBuffer.allocate(byteBuffer.capacity() * 2);
                                    newBuffer.put(byteBuffer);
                                    key.attach(newBuffer);
                                }
                            }
                        }
                        //TODO: 处理写事件
                    }
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }
}
@FunctionalInterface
interface Processor {
    void process();
}

class ReadWorkerSelector {
    private static Server.ReadWorker[] readWorkers = new Server.ReadWorker[]{
            new Server.ReadWorker("readWorker0"),
            new Server.ReadWorker("readWorker1"),
            new Server.ReadWorker("readWorker2"),
            new Server.ReadWorker("readWorker3"),
    };
    private static AtomicInteger index = new AtomicInteger(0);


    public static Server.ReadWorker getReadWorker() {
        if (index.get() == readWorkers.length) {
            index.set(0);
        }
        System.out.println("获取读线程:" + index.get());
        return readWorkers[index.getAndIncrement()];
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值