Selector

  1. Selector
    Selector允许一个单一的线程来操作多个Channel。如果我们的应用程序使用了多个Channel,那么使用Selector很方便的实现这样的目的,但是因为在一个线程中使用了多个Channel,因此也会造成每个Channel传输效率的降低。
    使用Selector的图解如下:
selector.png

为了使用Selector,我们首先需要将Channel注册到Selector中,随后调用Selector的select()方法,这个方法会阻塞,直到注册在Selector中的Channel发送可读写事件。当这个方法返回后,当前的这个线程就可以处理Channel的事件了。

  1. 创建选择器
    通过Selector.open()方法,我们可以创建一个选择器:

    Selector selector = Selector.open();
    
  2. 将Channel注册到选择器中
    为了使用选择器管理Channel,我们需要将Channel注册到选择器中:

    channel.configureBlocking(false);
    SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
    

注意:如果一个Channel要注册到Selector中,那么这个Channel必须是非阻塞的,即channel.configureBlocking(false);因此Channel必须要是非阻塞的,因此FileChannel是不能够使用选择器的,因此FileChannel都是阻塞的。
在使用Channel.register()方法时,第二个参数指定了我们队Channel的什么类型的事件感兴趣,这些事件有:

  • Connect,即连接事件(TCP连接),对应于SelectionKey.OP_CONNECT

  • Accept,即确认时间,对应于SelectionKey.OP_ACCEPT

  • Read,即读事件,对应于SelectionKey.OP_READ,表示buffer可读

  • Write,即写事件,对应于SelectionKey.OP_WRITE,表示buffer可写
    一个Channel发出的一个事件也可以称为对于某个时间,Channel准备好了。因此一个Channel成功连接到了另一个服务器也可以被成为connect ready.
    我们可以使用或运算符|来组合多个事件,例如

     int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
    

注意,一个Channel仅仅可以被注册到一个Selector一次,如果将Channel注册到Selector多次,那么其实就实现相当于更新SelectionKey的interest set,例如:

channel.register(selector, SelectionKey.OP_READ);
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

上面的channel注册到同一个Selector两次了,那么第二次的注册其实就是相当于更新这个Channel的interest set为SelectionKey.OP_READ | SelectionKey.OP_WRITE.

  1. 关于SelectionKey
    如上所示,当我们使用register注册一个Channel时,会返回一个SelectionKey对象,这个对象包含了如下内容:
  • interest set,即我们感兴趣的事件集,即在调用register注册channel时所设置的interest set
  • ready set
  • channel
  • selector
  • attached object,可选的附加对象
  1. interest set
    我们可以通过如下方式获取interest set
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead    = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite   = interestSet & SelectionKey.OP_WRITE;   
  1. ready set
    代表了Channel所准备好了的操作
    我们可以像判断interest set一样操作ready set,但是我们还尅使用如下方法进行判断

     int readySet = selectionKey.readyOps();
    
     selectionKey.isAcceptable();
     selectionKey.isConnectable();
     selectionKey.isReadable();
     selectionKey.isWritable();
    
  2. Channel和Selector
    我们可以通过SelectionKey获取相对应的Channel和Selector

     Channel  channel  = selectionKey.channel();
     Selector selector = selectionKey.selector();
    
  3. Attaching Object
    我们可以在selectionKey中附加一个对象

     selectionKey.attach(theObject);
     Object attachedObj = selectionKey.attachment();
    

或者在注册时直接附加:

    SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
  1. 通过Selector选择Channel
    我们可以通过Selector.select()方法获取对某件事件准备好了的Channel,即如果我们在注册Channel时,对其的可写事件感兴趣,那么当select()返回时,我们就可以获取Channel了
    注意:select()方法返回的值表示有多少个Channel可操作。

  2. 获取可操作的Channel
    如果select()方法返回值表示有多少个Channel准备好了,那么我们可以通过Selected key set 访问这个Channel

     Set<SelectionKey> selectedKeys = selector.selectedKeys();
    
     Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
    
     while(keyIterator.hasNext()) {
    
           SelectionKey key = keyIterator.next();
    
           if(key.isAcceptable()) {
           // a connection was accepted by a ServerSocketChannel.
    
           } else if (key.isConnectable()) {
             // a connection was established with a remote server.
    
           } else if (key.isReadable()) {
             // a channel is ready for reading
    
           } else if (key.isWritable()) {
               // a channel is ready for writing
           }
    
           keyIterator.remove();
     }
    

注意,在每次迭代时,我们都调用“keyIterator.remove()”将这个key从迭代器中删除,因为select()方法仅仅是简单的将就绪的IO操作放到selectedKeys集合中,因此如果我们从selectedKeys获取一个key,但是没有将他删除,那么下一次select时,这个key所对应的IO事件还在selectedKeys中。
例如此时我们收到OP_ACCEPT通知,然后我们进行相应处理,但是并没有将这个key从SelectedKeys中删除,那么下一次select()返回时,我们还可以在SelectedKeys中获取到OP_ACCEPT的key.
注意,我们还可以动态更改selectedKeys中的key 的interest set.例如在OP_ACCEPT中,我们可以降interest set更新为OP_READ,这样Selector就会将这个Channel的读IO就绪事件包含进来了。

  1. Selector的基本使用流程

  2. 通过Selector.open()打开一个Selector

  3. 将Channel注册到Selector中,并设置需要监听的事件(interest set)
    3.不断重复:

    • 调用select()方法
    • 调用selector.selectedKey()获取selected keys
    • 迭代每个selected key:
      • 从selected key中获取对应的Channel和附加信息(如果有的话)
      • 判断是那些IO事件已经就绪了,然后处理他们,如果是OP_ACCEPT事件,则调用“SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept()”获取SocketChannel,并将他们设置为非阻塞的,然后将这个Channel注册到Selector中
      • 根据需要更改selected key的监听事件
      • 将已经处理过的key从selected keys集合中删除
  4. 关闭Selector
    当调用了Selector.close()方法时,我们其实是关闭了Selector本身并且将所有的SelectionKey失效,但是并不会关闭Channel

  5. 完整的Selector例子

     public class NioEchoServer {
       private static final int BUF_SIZE = 256;
       private static final int TIMEOUT = 3000;
    
       public static void main(String[] args) throws IOException {
     //打开服务器Socket
     ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    
     //打开Selector
     Selector selector = Selector.open();
    
     //服务端socket监听8080端口,并配置为非阻塞模式
     serverSocketChannel.socket().bind(new InetSocketAddress(8080));
     serverSocketChannel.configureBlocking(false);
    
     //将channel注册到selector中
     //通常我们都是先注册一个OP_ACCEPT事件,然后在OP_ACCEPT到来时,再将这个Channel的OP_READ注册到Selector中
     serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    
     while (true) {
         //通过调用select方法,阻塞地等待Channel I/O可操作
         if (selector.select(TIMEOUT) == 0) {
             System.out.println(".");
             continue;
         }
    
         //获取I/O操作就绪的SelectionKey,通过SelectionKey可以知道哪些Channel在那类I/O操作已经就绪
         Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
         while (keyIterator.hasNext()) {
             SelectionKey key = keyIterator.next();
    
             if (key.isAcceptable()) {
                 //当OP_ACCEPT事件到来时,我们就有从ServerSocketChannel中获取一个SocketChannel,代表客户端的连接
                 //注意,在OP_ACCEPT事件中,从key.channel()返回的Channel是ServerSocketChannel,
                 //而在OP_WRITE 和OP_READ中,从key.channel()返回的是SocketChannel
                 SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
                 clientChannel.configureBlocking(false);
                 //在OP_ACCEPT到来时,再将这个Channel的OP_READ注册到Selector中
                 //注意,这里我们如果没有设置OP_READ的话,即interest set仍然是OP_CONNECT的话,那么select方法会一直直接返回
                 clientChannel.register(key.selector(), SelectionKey.OP_READ, ByteBuffer.allocate(BUF_SIZE));
             }
             if (key.isReadable()) {
                 SocketChannel clientChannel = (SocketChannel) key.channel();
                 ByteBuffer buf = (ByteBuffer) key.attachment();
                 long bytesRead = clientChannel.read(buf);
                 if (bytesRead == -1) {
                     clientChannel.close();
                 } else if (bytesRead > 0) {
                     key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
                     System.out.println("Get data length: " + bytesRead);
                 }
             }
    
             if (key.isValid() && key.isWritable()) {
                 ByteBuffer buf = (ByteBuffer) key.attachment();
                 buf.flip();
                 SocketChannel clientChannel = (SocketChannel) key.channel();
    
                 clientChannel.write(buf);
    
                 if (!buf.hasRemaining()) {
                     key.interestOps(SelectionKey.OP_READ);
                 }
                 buf.compact();
             }
    
         }
     }
     }
     }
    
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容