Java NIO之Selector

Java NIO主要包含三个概念,即缓冲区(Buffer)、通道(Channel)和选择器(Selector)。前面的文章已经介绍了缓冲区和通道,本文则讲述最复杂的选择器Selector。
本文是本系列的第三篇文章,关于缓冲区Buffer可以看第一篇:
Java NIO之Buffer(缓冲区)
关于通道Channel可以看第二篇:
Java NIO 之 Channel(通道)

  1. Selector涉及的三个概念
    在理解了Buffer和Channel之后,终于来到了最终的解决方案面前,那就是使用Selector来实现单线程控制多路非阻塞IO。Selector是如此重要,可以说它就是NIO异步IO的核心控制器。Selector需要其他两种对象配合使用,即SelectionKey和SelectableChannel,它们之间的关系如下图所示:


    image.png

SelectableChannel是一类可以与Selector进行配合的通道,例如Socket相关通道以及Pipe产生的通道都属于SelectableChannel。这类通道可以将自己感兴趣的操作(例如read、write、accept和connect)注册到一个Selector上,并在Selector的控制下进行IO相关操作。
Selector是一个控制器,它负责管理已注册的多个SelectableChannel,当这些通道的某些状态改变时,Selector会被唤醒(从select()方法的阻塞中),并对所有就绪的通道进行轮询操作。
SelectionKey是一个用来记录SelectableChannel和Selector之间关系的对象,它由SelectableChannel的register()方法返回,并存储在Selector的多个集合中。它不仅记录了两个对象的引用,还包含了SelectableChannel感兴趣的操作,即OP_READ,OP_WRITE,OP_ACCEPT和OP_CONNECT。

1.1 register方法
在展示例子代码之前,必须对一些概念和操作进行简要的介绍。首先是SelectableChannel的register方法,它的正式定义为:

SelectionKey register(Selector sel, int ops)

第一个参数指明要注册的Selector,第二个参数指明本通道感兴趣的操作,此参数的取值可以是SelectionKey.OP_ACCEPT等四个,以及它们的逻辑值,例如SelectionKey.OP_READ & SelectionKey.OP_WRITE。方法的返回值是一个SelectionKey,这个对象会被自动加入Selector的keys集合,因此不必特意保留这个SelectionKey的对象引用,需要时可以使用Selector的keys()方法得到所有的SelectionKey对象引用。
注册完成后,该通道就与Selector保持关联了。当通道的状态改变时,其改变会自动被Selector感知,并在Selector的三个集合中反应出来。

1.2 Selector的三个集合
如上图所示,Selector对象会维持三个SelectionKey集合,分别是keys集合,存储了所有与Selector关联的SelectionKey对象;selectedKeys集合,存储了在一次select()方法调用后,所有状态改变的通道关联的SelectionKey对象;cancelledKeys集合,存储了一轮select()方法调用过程中,所有被取消但还未从keys中删除的SelectionKey对象。
其中最值得关注的是selectedKeys集合,它使用Selector对象的selectedKeys()方法获得,并通常会进行轮询处理。

1.3 select方法
Selector类的select()方法是一个阻塞方法,它有两种形式:
int select()
int select(long timeout)
不带参数的方法会一直阻塞,直到至少有一个注册的通道状态改变,才会被唤醒;带有timeout参数的方法会一直阻塞,直到时间耗尽,或者有通道的状态改变。

1.4 轮询处理
在一次select()方法返回后,应对selectedKeys集合中的所有SelectionKey对象进行轮询操作,并在操作完成后手动将SelectionKey对象从selectedKeys集合中删除。

  1. Selector代码实例
    在展示具体的代码之前,先画一个从《Netty In Action》书上抄来的图:


    image.png

服务端代码:

public class SelectorServer {
    private static final int PORT = 1234;
    private static ByteBuffer buffer = ByteBuffer.allocate(1024);

    public static void main(String[] args) {
        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.bind(new InetSocketAddress(PORT));
            ssc.configureBlocking(false);
            //1.register()
            Selector selector = Selector.open();
            ssc.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("REGISTER CHANNEL , CHANNEL NUMBER IS:" + selector.keys().size());

            while (true) {
                //2.select()
                int n = selector.select();
                if (n == 0) {
                    continue;
                }
                //3.轮询SelectionKey
                Iterator<SelectionKey> iterator = (Iterator) selector.selectedKeys().iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    //如果满足Acceptable条件,则必定是一个ServerSocketChannel
                    if (key.isAcceptable()) {
                        ServerSocketChannel sscTemp = (ServerSocketChannel) key.channel();
                        //得到一个连接好的SocketChannel,并把它注册到Selector上,兴趣操作为READ
                        SocketChannel socketChannel = sscTemp.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                        System.out.println("REGISTER CHANNEL , CHANNEL NUMBER IS:" + selector.keys().size());
                    }
                    //如果满足Readable条件,则必定是一个SocketChannel
                    if (key.isReadable()) {
                        //读取通道中的数据
                        SocketChannel channel = (SocketChannel) key.channel();
                        readFromChannel(channel);
                    }
                    //4.remove SelectionKey
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private static void readFromChannel(SocketChannel channel) {
        buffer.clear();
        try {
            while (channel.read(buffer) > 0) {
                buffer.flip();
                byte[] bytes = new byte[buffer.remaining()];
                buffer.get(bytes);
                System.out.println("READ FROM CLIENT:" + new String(bytes));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

首先注册了一个ServerSocketChannel,它用来监听1234端口上的连接;当监听到连接时,把连接上的SocketChannel再注册到Selector上,这些SocketChannel注册的是SelectionKey.OP_READ事件;当这些SocketChannel状态变为可读时,读取数据并显示。
客户端代码:

public class SelectorClient {
    static class Client extends Thread {
        private String name;
        private Random random = new Random(47);

        Client(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            try {
                SocketChannel channel = SocketChannel.open();
                channel.configureBlocking(false);
                channel.connect(new InetSocketAddress(1234));
                while (!channel.finishConnect()) {
                    TimeUnit.MILLISECONDS.sleep(100);
                }
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                for (int i = 0; i < 5; i++) {
                    TimeUnit.MILLISECONDS.sleep(100 * random.nextInt(10));
                    String str = "Message from " + name + ", number:" + i;
                    buffer.put(str.getBytes());
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        channel.write(buffer);
                    }
                    buffer.clear();
                }
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(new Client("Client-1"));
        executorService.submit(new Client("Client-2"));
        executorService.submit(new Client("Client-3"));
        executorService.shutdown();
    }
}

客户端创建了三个线程,每个线程创建一个SocketChannel通道,并连接到服务器,并向服务器发送5条消息。

  1. 小结
    Selector是Java NIO的核心概念,以至于一些人直接将NIO称之为Selector-based IO。要学会Selector的使用首先是要明白其相关的多个概念,并多多动手去写。
    至此《Java NIO编程实例》系列的三篇就写完了,接下来应该好好介绍一下Netty了,毕竟它才是在具体的Java服务端编程用得最多的框架。Netty克服了NIO中一些概念和设计上的不足之处,提供了更加优雅的解决方案。但是,要学好用好Netty,学习NIO是必经之路,有了NIO的基础,才能真正学好Netty。

参考:
https://blog.csdn.net/LogicTeamLeader/article/details/69666274

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容

  • 1、选择器基础 1.1、选择器、可选择通道、选择键类 选择器(Selector): 选择器类管理着一个被注册的通道...
    桥头放牛娃阅读 1,147评论 0 6
  • 一 Selector(选择器)介绍 Selector 一般称 为选择器 ,当然你也可以翻译为 多路复用器 。它是J...
    小波同学阅读 293评论 0 3
  • # Java NIO # Java NIO属于非阻塞IO,这是与传统IO最本质的区别。传统IO包括socket和文...
    Teddy_b阅读 595评论 0 0
  • Java NIO(New IO)是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java I...
    zhisheng_blog阅读 1,120评论 0 7
  • Sennheiser 表示要针对 VR 直播市场发布一款新品——VR 麦克风。此外,公司正在寻找创新的 VR 内容...
    晓晓13号阅读 306评论 0 0