使用Reactor线程模型构建的简易网络通信聊天程序

服务端程序启动一个服务线程负责监听客户端连接,接收新连接后,使用负载均衡从8个后台负责通信的线程中选一个将其注册到该线程,由该线程负责和该客户端通道的通信工作。每个线程一个selector。

服务端代码

class NioReactorChatServer {
    ExecutorService workPool; // 任务线程,业务使用

    private final static int WORKER_NUMBER = 8;

    private ReactorThread[] acceptThreads;
    private ReactorThread[] workThreads;

    public NioReactorChatServer() {
        this.workThreads = new ReactorThread[WORKER_NUMBER];
        this.acceptThreads = new ReactorThread[1]; // 也可设置一个
        workPool = Executors.newCachedThreadPool();
    }

    // 调用入口,监听服务开启
    public void startAccept() throws IOException {
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8001)); // 默认IP是0.0.0.0
        acceptThreads[0] = newAcceptWorker();
        ReactorThread acceptWorker = acceptThreads[0];
        acceptWorker.start(); // 先启动,因为下面语句基本都是先于run方法执行的
        acceptWorker.register(serverSocketChannel, SelectionKey.OP_ACCEPT);
    }

    // 创建服务端监听工作线程
    private ReactorThread newAcceptWorker() throws IOException {
        return new ReactorThread() {
            @Override
            public void handle(SelectableChannel channel) throws Exception {
                ServerSocketChannel server = (ServerSocketChannel) channel;
                SocketChannel client = server.accept();
                client.configureBlocking(false);
                // 将该client负载均衡到workThread中去
                ReactorThread worker = getWorker();
                worker.start(); // 启动worker,重复启动只是保证启动
                worker.register(client, SelectionKey.OP_READ);
            }
        };
    }

    // 选择一个工作线程负责该客户端通信,计算负载均衡,选择负载最小的那个,如果线程数没达到上限则创建新工作线程负责
    private ReactorThread getWorker() throws IOException {
        int leastLoad = Integer.MAX_VALUE;
        int selected = 0;
        for (int i = 0; i < WORKER_NUMBER; i++) {
            ReactorThread worker = workThreads[i];
            if (worker == null) {
                worker = newWorker();
                workThreads[i] = worker;
                return worker;
            }
            if (worker.getCount() < leastLoad) {
                leastLoad = worker.getCount();
                if (leastLoad == 0) {
                    return worker; // 如果有负载为0的直接使用,也就是工作线程负责的管道关闭后被清理后降为0直接使用免得创建新的
                }
                selected = i;
            }
        }
        return workThreads[selected];
    }

    // 创建一个新的工作线程
    private ReactorThread newWorker() throws IOException {
        return new ReactorThread() {
            @Override
            public void handle(SelectableChannel channel) throws Exception {
                SocketChannel client = (SocketChannel) channel;
                StringBuilder content = new StringBuilder();
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int readLength = client.read(buffer);
                while (client.isOpen() && readLength > 0) {
                    // 这里可以自定义协议接收,此时readLength>0改为!=-1,因为等于0通道可能继续发送,判断结束是协议遇到结束标识
                    content.append(new String(buffer.array(), 0, readLength));
                    buffer.clear();
                    readLength = client.read(buffer);
                }
                if (readLength == -1) {
                    client.close();
                }
                if (client.isOpen()) {
                    System.out.println(getName() + " receive: " + content.toString());
                    workPool.submit(() -> {
                        // todo 业务操作,数据库,接口等
                    });
                    // 读取完回应数据,如果client的isOpen为false则下面的写入会再次打开
                    buffer = ByteBuffer.wrap("response hello world".getBytes());
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                }
            }
        };
    }

    // 负责和客户端通讯
    abstract class ReactorThread extends Thread {
        // 新连接的注册任务放在selector工作线程中,降低争夺锁的可能性。
        // 虽然taskQueue是同步的,但因为register和select方法同步,select内部轮询方式进行,等待时更容易导致register争夺锁。
        private BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>(); // 分配到该工作线程的新连接任务

        private Selector selector;

        private final AtomicInteger count = new AtomicInteger(0); // 负载计数,指负责的channel数量,使用原子计数
        private int keyCount = 0; // select中的keys的大小

        private boolean running = false;

        @Override
        public synchronized void start() {
            // 拦截,重复调用只启动一个线程,running一旦设为true将不再设为false,该线程interrupt之后不能再启动,需重新创建
            if (!running) {
                super.start();
                running = true;
            }
        }

        /**
         * 创建一个Selector的工作线程负责注册到其上的通道通信工作,子类实现的读写数据处理工作。
         *
         * @throws IOException IO异常
         */
        public ReactorThread() throws IOException {
            this.selector = Selector.open();
        }

        @Override
        public void run() {
            while (!isInterrupted()) {
                try {
                    Runnable task;
                    while ((task = keyCount > 0 ? taskQueue.poll() : taskQueue.take()) != null) {
                        task.run();
                    }
                } catch (InterruptedException ex) {
                    continue;
                }
                try {
                    // 有通道时最长等待1秒,如果等待中有Ready事件不管有没到1秒都立即返回,到时限没有就绪事件返回0,没有通道时设最小时长。
                    selector.select(keyCount > 0 ? 1000 : 1); // keyCount为0,select参数为0仍然阻塞到有就绪事件
                    Set<SelectionKey> selectedKeys = selector.selectedKeys();
                    Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
                    while (keyIterator.hasNext()) {
                        SelectionKey key = keyIterator.next();
                        keyIterator.remove();
                        try {
                            handle(key.channel()); // 由使用者处理这个就绪事件
                            if (!key.channel().isOpen()) {
                                key.cancel(); // 关闭的管道取消订阅
                                decrementKeyCount();
                            }
                        } catch (Exception ex) {
                            key.cancel(); // 关闭的管道取消订阅
                            decrementKeyCount();
                        }

                    }
                } catch (IOException exc) {
                    // empty
                }
            }
        }

        /**
         * 处理具体的读写请求
         *
         * @param channel 连接通道,这里指客户端通道对象
         * @throws Exception 异常
         */
        public abstract void handle(SelectableChannel channel) throws Exception;

        /**
         * 将新的通道,一般是客户端通道,注册到该线程的选择器上
         *
         * @param channel 要注册的通道
         * @param ops     对该通道的关心事件,包括读写等,用于该通道接下来的通信
         */
        public void register(SelectableChannel channel, int ops) {
            this.taskQueue.add(() -> {
                try {
                    channel.register(this.selector, ops);
                    keyCount++;
                } catch (ClosedChannelException exc) {
                    // An empty catch block,客户端刚连接后中断
                    count.decrementAndGet();
                }
            });
            this.count.incrementAndGet();
        }

        // 更新keys中的数量同时更新负载数量
        private void decrementKeyCount() {
            keyCount--;
            count.decrementAndGet();
        }

        /**
         * 取得该线程的订阅的Key数量,即管道数量
         */
        public int getCount() {
            return count.get();
        }
    }
}

客户端代码

class NioChatClient {
    private Scanner scanner = new Scanner(System.in);

    void start() throws IOException {
        SocketChannel channel = SocketChannel.open();
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress("localhost", 8001));

        // 注册到选择器(多路复用器)
        Selector selector = Selector.open();
        channel.register(selector, SelectionKey.OP_CONNECT);

        do {
            selector.select();
            Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 取得就绪状态的SelectionKey
            Iterator<SelectionKey> keys = selectionKeys.iterator(); // 生成迭代器
            while (keys.hasNext()) {
                SelectionKey key = keys.next();
                if (key.isConnectable()) {
                    System.out.println("connect loop.");
                    channel.finishConnect();
                    channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                } else if (key.isReadable()) {
                    System.out.println("connect loop.");
                    read(key);
                } else if (key.isWritable()) {
                    System.out.println("connect loop.");
                    write(key);
                }
                keys.remove();
            }
            System.out.println("select one time.");
        } while (!getInput("exit? ").startsWith("y"));
        channel.close();
        this.scanner.close();
    }

    private void read(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int number = channel.read(buffer); // 读取一次一般要求一次发送的内容不超过buffer长度
        String message = new String(buffer.array(), 0, number);
        System.out.println("receive message: " + message);
        // channel.register(selector, SelectionKey.OP_WRITE); // 读取完,注册写操作,下次轮询直接进入写
    }

    private void write(SelectionKey key) throws IOException {
        SocketChannel channel = (SocketChannel) key.channel();
        String message = getInput("send message: ");
        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
        while (buffer.hasRemaining()) {
            channel.write(buffer);
        }
        // channel.register(selector, SelectionKey.OP_READ); // 写完,注册读操作,等待读
    }

    // 读取标准输入内容
    private String getInput(String prompt) {
        System.out.print(prompt);
        return this.scanner.nextLine();
        // scanner.close()将同时关闭System.in,调用后in关闭,下次再输入时将无法使用
    }
}

如果有什么建议欢迎提出。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值