NIO之多Selector多线程模式(四)

细节重点!

Selector.select()只对这个方法被调用前注册进来的事件才会被监听。如果在这个方法阻塞后,有其他线程往这个selector注册事件,不会被监听到!

这个带来的影响直接影响多线程多Selector的程序设计!!!

初步引入netty原理图,帮助理解上述细节

下图中,select->processSelectedKeys->runAllTasks在while(true)中不断循环(loop),select从阻塞返回,处理相应的keys后,调用runAllTasks方法,将select阻塞过程中新注册进来的事件添加到selector中,这样再次返回到select阻塞方法后,新注册的事件就可以被selector监听到了!!!
在这里插入图片描述

使用多selector多线程模式

多路复用器线程类,同时也是一个ThreadLocal在生成对象前已经完成对selector的初始化。主要负责接收并监听新的事件请求。

public class SelectorThread  extends  ThreadLocal<LinkedBlockingQueue<Channel>>  implements   Runnable{
    // 一个线程对应一个selector,
    // 每个客户端,只绑定到其中一个selector
    Selector  selector = null;
    LinkedBlockingQueue<Channel> lbq = get();  //获取initialValue的值
    SelectorThreadGroup stg;

    @Override
    protected LinkedBlockingQueue<Channel> initialValue() {
        return new LinkedBlockingQueue<>();//初始化与当前线程绑定的ThreadLocal的值,通过get可以获取
    }

    SelectorThread(SelectorThreadGroup stg){
        try {
            this.stg = stg;
            selector = Selector.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        //Loop
        while (true){
            try {
                int nums = selector.select();  //阻塞,wakeup()可以让select()立即返回
                //2,处理selectkeys
                if(nums>0){
                    Set<SelectionKey> keys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = keys.iterator();
                    while(iter.hasNext()){  //线程处理的过程
                        SelectionKey key = iter.next();
                        iter.remove();
                        if(key.isAcceptable()){  //复杂,接受客户端的过程(接收之后,要注册,多线程下,新的客户端,注册到那里呢?)
                            acceptHandler(key);
                        }else if(key.isReadable()){
                            readHander(key);
                        }else if(key.isWritable()){
                        }
                    }
                }
                //3,处理一些task :  listen  client
                if(!lbq.isEmpty()){   //如果队列不为空,表示有新的连接要注册到selector
                    Channel c = lbq.take();
                    if(c instanceof ServerSocketChannel){
                        ServerSocketChannel server = (ServerSocketChannel) c;
                        server.register(selector,SelectionKey.OP_ACCEPT);
                        System.out.println(Thread.currentThread().getName()+" register listen");
                    }else if(c instanceof  SocketChannel){
                        SocketChannel client = (SocketChannel) c;
                        ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
                        client.register(selector, SelectionKey.OP_READ, buffer);
                        System.out.println(Thread.currentThread().getName()+" register client: " + client.getRemoteAddress());
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    private void readHander(SelectionKey key) {
        System.out.println(Thread.currentThread().getName()+" read......");
        ByteBuffer buffer = (ByteBuffer)key.attachment();
        SocketChannel client = (SocketChannel)key.channel();
        buffer.clear();
        while(true){
            try {
                int num = client.read(buffer);
                if(num > 0){
                    buffer.flip();  //将读到的内容翻转,然后直接写出
                    while(buffer.hasRemaining()){
                        client.write(buffer);
                    }
                    buffer.clear();
                }else if(num == 0){
                    break;
                }else if(num < 0 ){
                    //客户端断开了
                    System.out.println("client: " + client.getRemoteAddress()+"closed......");
                    key.cancel();
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void acceptHandler(SelectionKey key) {
        System.out.println(Thread.currentThread().getName()+"   acceptHandler......");

        ServerSocketChannel server = (ServerSocketChannel)key.channel();
        try {
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            stg.nextSelectorV3(client);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

	// 给当前多路复用器设置工作者线程
    public void setWorker(SelectorThreadGroup stgWorker) {
        this.stg =  stgWorker;
    }
}

线程组:包含多个SelectorThread,使用多个多路复用器,可以同时接收多个事件连接。

public class SelectorThreadGroup {

    ServerSocketChannel server=null;
    SelectorThread[] sts;
    AtomicInteger xid = new AtomicInteger(0);

    SelectorThreadGroup  stg =  this;// 默认是boss

    // 可以选择设置worker,处理客户端读写请求
    public void setWorker(SelectorThreadGroup  stg){
        this.stg =  stg;
    }

    SelectorThreadGroup(int num){
        //num  线程数
        sts = new SelectorThread[num];
        for (int i = 0; i < num; i++) {
            sts[i] = new SelectorThread(this);
            new Thread(sts[i]).start();
        }
    }

    public void bind(int port) {
        try {
            server =  ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
            //注册到某个selector
            nextSelectorV3(server);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void nextSelectorV3(Channel c) {
        try {
            if(c instanceof  ServerSocketChannel){
                SelectorThread st = nextBossThread();  //listen 选择了 boss组中的一个线程后,要更新这个线程的work组
                st.lbq.put(c);
                st.setWorker(stg);
                st.selector.wakeup();// 使得这个事件在线程st的selector中生效
            }else {
                SelectorThread st = nextWorkerThread();  //在 main线程种,取到堆里的selectorThread对象
                //1,通过队列传递数据 消息
                st.lbq.add(c);
                //2,通过打断阻塞,让对应的线程去自己在打断后完成注册selector
                st.selector.wakeup();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 返回boss的一个线程
    private SelectorThread nextBossThread() {
        int index = xid.incrementAndGet() % sts.length;
        return sts[index];
    }

    // 返回worker的一个线程
    private SelectorThread nextWorkerThread() {
        int index = xid.incrementAndGet() % stg.sts.length;
        return stg.sts[index];
    }
}

启动

public class MainThread {

    public static void main(String[] args) {
        //1,创建 IO Thread  (一个或者多个)
        //boss有自己的线程组
        SelectorThreadGroup boss = new SelectorThreadGroup(3);  //创建slector线程组并启动线程,一个线程一个selector
        //worker有自己的线程组
        SelectorThreadGroup worker = new SelectorThreadGroup(3);
        // boss线程组拥有worker线程组,因为boss一旦accept得到client后得去worker中 next出一个线程分配
        boss.setWorker(worker);
     
        // boss线程组可以同时监听多个端口
        boss.bind(9999);
        boss.bind(8888);
        boss.bind(6666);
        boss.bind(7777);

    }
}

总结

以上实现的功能:
1. 同一个端口进来的连接交给同一个boss线程处理,不同的连接会轮询boss线程组,交由不同的boss线程处理。
2. 不同的boss线程处理accept请求后,剩下的其它事件都交由同一个worker线程组处理。

这样就完成了boss只需要负责监听accept连接,后续的read/write事件交由worker去执行。

  • 4
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
【项目介绍】 Java开发基于多线程NIO实现聊天室源码+项目说明(含服务端+客户端).zip 涉及到的技术点 - 线程池ThreadPoolExecutor - 阻塞队列BlockingQueue,生产者消费者模式 - Selector - Channel - ByteBuffer - ProtoStuff 高性能序列化 - HttpClient连接池 - Spring依赖注入 - lombok简化POJO开发 - 原子变量 - 内置锁 - CompletionService - log4j+slf4j日志 - 实现的功能 - 登录注销 - 单聊 - 群聊 - 客户端提交任务,下载图片并显示 - 上线下线公告 - 在线用户记录 - 批量下载豆瓣电影的图片,并打为压缩包传输给客户端 - 客户端使用方式: - 登录:默认用户名是user1-user5,密码分别是pwd1-pwd5 - 例:打开客户端后输入用户名为user1,密码为pwd1 - 注销:关闭客户端即可 - 单聊:@username:message - 例:@user2:hello - 群聊:message - 例:hello,everyone - 提交任务:task.file:图片的URL / task.crawl_image:豆瓣电影的id[?imageSize=n] 可以加请求参数 - 例1:task.file:https://img1.doubanio.com/view/movie_poster_cover/lpst/public/p2107289058.webp 下载完毕后会弹出一个框,输入想将其保存到的路径,比如E:/img.webp - 例2:task.crawl_image:1292371?imageSize=2 下载完毕后在弹出的框中输入E:/images.zip 【备注】 1.项目代码均经过功能验证ok,确保稳定可靠运行。欢迎下载食用体验! 2.主要针对各个计算机相关专业,包括计算机科学、信息安全、数据科学与大数据技术、人工智能、通信、物联网等领域的在校学生、专业教师、企业员工。 3.项目具有丰富的拓展空间,不仅可作为入门进阶,也可直接作为毕设、课程设计、大作业、初期项目立项演示等用途。 4.当然也鼓励大家基于此进行二次开发。在使用过程中,如有问题或建议,请及时沟通。 5.期待你能在项目中找到乐趣和灵感,也欢迎你的分享和反馈!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值