十五、使用Selector(多路复用器)实现Netty中Reactor主从模型

18 篇文章 1 订阅

导论

前面几篇文章我们分别从

一、C10K问题经典问答
二、java.nio.ByteBuffer用法小结
三、Channel 通道
四、Selector选择器
五、Centos-Linux安装nc
六、windows环境下netcat的安装及使用
七、IDEA的maven项目的netty包的导入(其他jar同)
八、JAVA IO/NIO
九、网络IO原理-创建ServerSocket的过程
十、网络IO原理-彻底弄懂IO
十一、JAVA中ServerSocket调用Linux系统内核
十二、IO进化过程之BIO
十三、Java-IO进化过程之NIO
十四、使用Selector(多路复用器)实现Netty中Reactor单线程模型
等几个纬度对JavaIO和NIO体系做了详细介绍,并由简到深的根据IO体系的升级过程做了系统分析。
今天我们开始讲解NIO体系下的多路复用器(Selector),并用实例教你如何实现Netty中Reactor主从模型。这篇文章是与上篇文章【十四、使用Selector(多路复用器)实现Netty中Reactor单线程模型】紧紧结合的,因此要读懂这篇文章,请先以上篇文章做铺垫。

图示介绍

我们通过以下的一个图示先了解一下主从模型的概念,然后后面通过代码示例来彻底了解。
在这里插入图片描述

代码实例

代码示例

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Created by Bruce on 2020/9/18
 *
 * 多路复用器-selector多线程版本
 *  *  网络IO之SELECT-服务端
 *  *  *  * SELECT-写法
 *  *  *  ServerSocketChannel
 **/
public class SocketServerSelectorMultiplexingThreads {


    /**
     * 服务端通道
     */
    private ServerSocketChannel serverSocketChannel;

    /**
     * 服务端端口
     */
    private int serverPort;

    /**
     * 主选择器-主要用于客户端的的接入-OP_ACCEPT-事件
     */
    private Selector bossSelector;

    /**
     * 任务选择器-主要用于客户端数据的传入处理-OP_READ-事件
     */
    private Selector[] workerSelectors;

    /**
     * 任务选择器数量
     */
    private int workerNum;


    /**
     * @param serverPort  服务端端口
     * @param workerNum 任务选择器数量
     * @throws IOException
     */
    public SocketServerSelectorMultiplexingThreads(int serverPort ,int workerNum) throws IOException {
        this.serverPort = serverPort;
        this.workerNum = workerNum;
        /**
         * 创建并绑定端口号
         */
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.bind(new InetSocketAddress(serverPort));
        serverSocketChannel.configureBlocking(false);

        System.out.println("step1 : new ServerSocket(" + serverPort+ ") ");

        /**
         * 创建主(boss)选择器
         */
        bossSelector = Selector.open();
        /**
         * serverSocket注册到
         */
        serverSocketChannel.register(bossSelector, SelectionKey.OP_ACCEPT);

        /**
         * 初始化任务选择器-根据设定的任务选择器数量
         */
        initWorderSelector();

    }

    /**
     * 初始化 任务选择器
     * @throws IOException
     */
    private void initWorderSelector() throws IOException {
        workerSelectors = new Selector[workerNum];
        for(int i = 0; i < workerNum; i++){
            workerSelectors[i] = Selector.open();
        }
    }

    public Selector getBossSelector() {
        return bossSelector;
    }

    public Selector[] getWorkerSelectors() {
        return workerSelectors;
    }

    public int getWorkerNum() {
        return workerNum;
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        int serverPort = 8080;
        int workerNum = 3;
        System.out.println("准备启动服务端端口号:" + serverPort + "---准备启动worker数量:" + workerNum);
        SocketServerSelectorMultiplexingThreads server = new SocketServerSelectorMultiplexingThreads(serverPort,workerNum);
        NioThread bossThreadRunnable = new NioThread(server.getBossSelector(),workerNum);
        Thread bossThread = new Thread(bossThreadRunnable);
        bossThread.start();
        Thread.currentThread().sleep(2000);

        Thread workerThread = null;
        Selector[] workerSelectors = server.getWorkerSelectors();
        for(Selector selector : workerSelectors){
            NioThread workerNioThread = new NioThread(selector);
            workerThread = new Thread(workerNioThread);
            workerThread.start();
        }


    }
}

class NioThread implements Runnable{

    /**
     * 传入的  选择器-私有
     */
    private Selector selector;

    /**
     * 任务选择器数量-多线程可见
     */
    private static int workerSelectorNum;

    /**
     * 每个任务选择器对应一个 队列  -多线程可见
     */
    static BlockingQueue<SocketChannel>[] clientSocketChannelQueues;


    /**
     * 任务选择器下标的数值-用于需任务选择器队列中获取自己的阻塞队列
     * 每个worker生成自己的ID。
     * 
     * 这个workerID是与<p>BlockingQueue<SocketChannel>[] clientSocketChannelQueues</p>中阻塞队列进行间接绑定了的
     */
    int id = 0;

    /**
     * 是否为工作任务的选择器线程
     * 默认为工作任务的选择器线程
     * 如果为非工作任务的选择器线程则设置为false
     */
    private boolean workerSign = true;

    /**
     * 自增ID-多线程可见
     */
    static AtomicInteger idx = new AtomicInteger();

    /**
     * Boss  选择器专用构造器
     * @param selector  boss任务选择器
     * @param workerSelectorNum 任务选择器数量
     */
    public NioThread(Selector selector, int workerSelectorNum){
        this.selector = selector;
        this.workerSelectorNum = workerSelectorNum;
        this.workerSign = false;

        clientSocketChannelQueues = new LinkedBlockingQueue[workerSelectorNum];
        initWorderSocketChannelQueues();

        System.out.println("Boss 启动");
    }

    /**
     * 初始化任务选择器所需的阻塞队列数组
     * 一个阻塞队列对应一个任务选择器
     * @throws IOException
     */
    private void initWorderSocketChannelQueues() {
        for (int i = 0; i < workerSelectorNum; i++){
            clientSocketChannelQueues[i] = new LinkedBlockingQueue<>();
        }
    }

    /**
     * worker 任务选择器专用构造器
     * @param selector
     */
    public NioThread(Selector selector) {
        this.selector = selector;
        /**
         * 每个worker生成自己的ID。
         */
        id = idx.getAndIncrement() % workerSelectorNum;//任务选择器队列下标
        System.out.println("任务线程 【worker---" + id + "】启动");
    }

    @Override
    public void run() {
        try {
            while (true){
                if(selector.select(10) > 0){//10毫秒延迟获取  不完全阻塞
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()){
                        SelectionKey selectionKey = iterator.next();
                        iterator.remove();

                        if(selectionKey.isConnectable()){
                            System.out.println("---------------selectionKey.isConnectable()......");
                        }else if(selectionKey.isAcceptable()){//客户端请求连接事件---其时只有boss选择器才能走到这一步
                            acceptHandler(selectionKey);
                        }else if(selectionKey.isReadable()){//客户端数据到达事件
                            readHandler(selectionKey);
                        }else if(selectionKey.isValid()){
                            System.out.println("---------------selectionKey.isValid()......");
                        }else if(selectionKey.isWritable()){
                            System.out.println("---------------selectionKey.isWritable()......");
                        }
                    }
                }
                /**
                 * boss 不参与这个过程
                 * 只有工作任务的选择器线程
                 * 且
                 * 对应的阻塞队列不为空的时候,才会执行
                 */
                if(workerSign && !clientSocketChannelQueues[id].isEmpty()){
                    //默认创建一个8字节的缓冲区
                    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(8192);
                    //从阻塞队列中取出对应的客户端
                    SocketChannel clientSocketChannel = clientSocketChannelQueues[id].take();
                    clientSocketChannel.register(selector,SelectionKey.OP_READ,byteBuffer);
                    System.out.println("-----------------------------------------------------");
                    System.out.println("客户端连接进入:" + clientSocketChannel.socket().getPort() + "分配到workder ---" + id);
                    System.out.println("-----------------------------------------------------");
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    /**
     * 其时这一步 只有 boss选择器可以进入
     * @param selectionKey
     * @throws IOException
     */
    private void acceptHandler(SelectionKey selectionKey) throws IOException {
        /**
         * 从选择器中获取服务端注册时的通道
         */
        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
        /**
         * 接收传入的客户端SocketChannel
         */
        SocketChannel clientSocketChannel = serverSocketChannel.accept();
        clientSocketChannel.configureBlocking(false);//设置客户端类型为非阻塞

        /**
         * 由于boss选择器所在的线程并不会处理客户端的链接,
         * 他只是把接受到的选择器按照自增的规则取模后放到不同的阻塞队列当中
         * 每次来一个客户端  idx都会自增1 ,然后取模后放到不同的任务队列。
         */
        //轮询分配
        int index = idx.getAndIncrement() % workerSelectorNum;
        /**
         * 把接受到的客户端放入不同任务选择器归属的阻塞队列中
         */
        clientSocketChannelQueues[index].add(clientSocketChannel);
    }

    /**
     * 其时这一步只有worker
     * @param selectionKey
     * @throws IOException
     */
    private void readHandler(SelectionKey selectionKey) throws IOException {
        SocketChannel clientSocketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer byteBuffer = (ByteBuffer) selectionKey.attachment();
        byteBuffer.clear();
        int readNum = 0;
        try {
            while (true){
                readNum = clientSocketChannel.read(byteBuffer);
                if(readNum < 0){
                    System.out.println("client port --" + clientSocketChannel.socket().getPort() + "---offline---");
                    selectionKey.cancel();
                    clientSocketChannel.socket().close();
                    clientSocketChannel.close();
                }else if(readNum == 0){
                    break;
                }else {
                    byteBuffer.flip();//每次读取之前都要反转一次
                    byte[] bytes = new byte[readNum];
                    byteBuffer.get(bytes);
                    String clientStr = new String(bytes);
                    System.out.println("client port --" + clientSocketChannel.socket().getPort() + "---data---" + clientStr);

                    String returnStr = "server get client data" + clientStr;
                    byteBuffer.clear();//把数据返回
                    byteBuffer.put(returnStr.getBytes());
                    byteBuffer.flip();//每次写出之前都要反转一次
                    while (byteBuffer.hasRemaining()){//判断当前缓冲区中是否有数据
                        clientSocketChannel.write(byteBuffer);//把当前缓冲区中数据写回客户端。
                    }
                    byteBuffer.clear();//写完之后清空。
                }
            }
        }catch (IOException e) {
            e.printStackTrace();
            System.out.println("client port --" + clientSocketChannel.socket().getPort() + "---offline---");
            selectionKey.cancel();
            clientSocketChannel.socket().close();
            clientSocketChannel.close();
        }
    }
}

打印输出

在linux环境或者windows环境下使用nc命令链接服务端,查看服务端打印过程。
具体linux系统或者windows系统如何安装nc命令,请从网络搜索或查看目录文档 ‘网络IO涉及到的-linux指令.docx’。

1.nc客户端1打印(Windows-nc命令打印)

C:\Users\Administrator>nc 127.0.0.1 8080
nc111
server get client datanc111
client111
server get client dataclient111

2.nc客户端2打印(Windows-nc命令打印)

C:\Users\Administrator>nc 127.0.0.1 8080
nc222
server get client datanc222
client222
server get client dataclient222

3. nc客户端2打印(Windows-nc命令打印)

C:\Users\Administrator>nc 127.0.0.1 8080
nc333
server get client datanc333
chient333
server get client datachient333

4.nc客户端2打印(Windows-nc命令打印)

C:\Users\Administrator>nc 127.0.0.1 8080
nc444
server get client datanc444
client444
server get client dataclient444

5.服务端打印

准备启动服务端端口号:8080---准备启动worker数量:3
step1 : new ServerSocket(8080) 
Boss 启动
任务线程 【worker---0】启动
任务线程 【worker---1】启动
任务线程 【worker---2】启动
-----------------------------------------------------
客户端连接进入:8822分配到workder ---0
-----------------------------------------------------
-----------------------------------------------------
客户端连接进入:8825分配到workder ---1
-----------------------------------------------------
-----------------------------------------------------
客户端连接进入:8828分配到workder ---2
-----------------------------------------------------
-----------------------------------------------------
客户端连接进入:8830分配到workder ---0
-----------------------------------------------------
client port --8822---data---nc111

client port --8825---data---nc222

client port --8828---data---nc333

client port --8830---data---nc444

client port --8830---data---client444

client port --8828---data---chient333

client port --8825---data---client222

client port --8822---data---client111

Reactor模型的几种类型

单线程模型
混合模型
主从模型

在这里插入图片描述

往期JavaIO文章

一、C10K问题经典问答
二、java.nio.ByteBuffer用法小结
三、Channel 通道
四、Selector选择器
五、Centos-Linux安装nc
六、windows环境下netcat的安装及使用
七、IDEA的maven项目的netty包的导入(其他jar同)
八、JAVA IO/NIO
九、网络IO原理-创建ServerSocket的过程
十、网络IO原理-彻底弄懂IO
十一、JAVA中ServerSocket调用Linux系统内核
十二、IO进化过程之BIO
十三、Java-IO进化过程之NIO
十四、使用Selector(多路复用器)实现Netty中Reactor单线程模型

整体JavaIO体系文章概览

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值