NIO之Selector

Selector(选择器)是Java NIO中能够检测一到多个NIO通道,并能够知晓通道是否为诸如读写事件做好准备的组件。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接。

Selector的用途

  • 使用单个线程来处理多个通道。只需要更少的线程来处理通道。事实上,可以只用一个线程处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源(如内存)。因此,使用的线程越少越好。
    一个选择器来处理三个通道

Selector的创建

通过Selector.open()方法创建一个Selector,如:

Selector selector = Selector.open();

打开服务器套接字通道

ServerSocketChannel serverSocket = ServerSocketChannel.open();  
InetSocketAddress hostAddress = new InetSocketAddress("localhost", 8099);  
serverSocket.bind(hostAddress);

向Selector注册通道

为了将Channel和Selector配合使用,需要将Channel注册到Selector上。通过selectableChannel.register()方法来实现。

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);
  • 和Selector一起使用时,channel必须处于非阻塞模式下。这意味着不能将FileChannel与Selector一起使用,因为FileChannel不能切换到非阻塞模式。而套接字通道都可以。
  • 注意register()方法的第二个参数。这是一个“interest集合”,意思是在通过Selector监听Channel时对什么事件感兴趣。可以监听四种不同类型的事件:
  • 不同的Channel对象可以注册的事件是不一样的,可以通过public final int validOps()方法查看。
通道触发了一个事件意思是该事件已就绪。如下可以使用SelectionKey的四个常量来表示。
Connect:某个channel成功连接到另一个服务器称为“连接就绪”。可以用来表示 SelectionKey.OP_CONNECTS
Accept:一个server socket channel准备好接收新进入的连接称为“接收就绪”。 SelectionKey_ACCEPT
Read:一个有数据可读的通道可以说是“读就绪”。 SelectionKey.OP_READS
Write:等待写数据的通道可以说是“写就绪”。 SelectionKey.OP_WRITE
  • 如果你不止对一种事件感兴趣,可以使用位或操作符将SelectionKey常量连接 起来。如
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

SelectionKey

表示一个特定的通道和特定的选择器对象直接的注册关系。当向Selector中注册通道时,register()方法会返回一个SelectionKey对象。这个对象包含了一个你感兴趣的属性。

  • interest集合
  • ready集合
  • Channel
  • Selector
  • 附加的对象(可选)

interest集合

interest集合是你选择的感兴趣的事件集合。可以通过SelectionKey来读写interest集合,如下:

int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept  = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;

可以看到,用“位与”操作interest 集合和给定的SelectionKey常量,可以确定某个确定的事件是否在interest 集合中。

ready集合

ready 集合是通道已经准备就绪的操作的集合。在一次选择(Selection)之后,你会首先访问这个ready set。可以这样访问ready集合:

int readySet = selectionKey.readyOps();

可以用像检测interest集合那样的方法,来检测channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:

  • selectionKey.isAcceptable();
  • selectionKey.isConnectable();
  • selectionKey.isReadable();
  • selectionKey.isWritable();

Channel + Selector

从SelectorKey访问Channel和Selector很简单,如下

Channel channel = selectorKey.channel();
Selector selector = selectorKey.selector();

附加的对象

可以将一个对象或者更多信息附着到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:

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

还可以在用register()方法向Selector注册Channel的时候附加对象。如:

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

通过Selector选择通道

一旦向Selector注册了一或多个通道,就可以调用几个重载的select()方法。这些方法返回你所感兴趣的事件(如连接、接受、读或写)已经准备就绪的那些通道。换句话说,如果你对“读就绪”的通道感兴趣,select()方法会返回读事件已经就绪的那些通道。
下面是select()方法:

  • int select():阻塞到至少有一个通道在你注册的事件上就绪了。
  • int select(long timeout):和select()一样,但是只会阻塞timeout毫秒。
  • int selectNow():不会阻塞,不管什么通道就绪都立刻返回,如果没有通道就绪则返回0。
  • select()方法返回的int值表示有多少个通道已经就绪了。也就是从上次调用select()方法后有多少通道变成就绪状态。

selectedKeys()

一旦调用了select()方法,并且返回值表明有一个或更多的通道就绪了。就可以调用selector的selectedKeys()方法,访问这些已经就绪的通道了。

Set selectedKeys = selector.selectedKeys();

当像Selector注册Channel时,Channel.register()方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。可以通过SelectionKey的selectedKeySet()方法访问这些对象。

可以遍历这个已选择的键集合来访问就绪的通道,如下

Set selectedKeys = selectors.selectedKeys;
Set selectedKeys = selector.selectedKeys();
Iterator 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()调用。Selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道是手动移除。下次通道变成就绪是,Selector会再次将其放入选择集中。
  • SelectionKey.channel()方法返回的通道需要转型为我们直接需要的类型,如ServerSocketChannel或SocketChannel。

wakeup()

唤醒因为调用selector()方法阻塞的线程。如果当前没有线程阻塞在select()方法上,下个调用select()方法的线程会立刻醒来。

close()

用来Selector调用close()方法会将其关闭,且是注册到该Selector的所有SelectionKey实例失效。通道本身不会关闭。

实例

package com.bzb.java8.main;

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;

/**
 * @author bzb
 * @Description:
 * @date 2018/9/21 16:07
 */
public class ServerSelector {

    public static final int PORT = 9797;
    private static ByteBuffer buffer = ByteBuffer.allocate(1024);

    public static void main(String[] args) throws IOException {

        // 获取一个ServerSocket通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false); // 设置该通道为非阻塞的
        serverSocketChannel.socket().bind(new InetSocketAddress(PORT)); // 将该通道对应的socket绑定到9797端口

        // 将serversocketchannel注册到Selector中
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 注册通道和感兴趣的accept事件

        System.out.println("register channel, channel number is:" + selector.keys().size());


        while (true) {
            // 阻塞直到通道有事件就绪
            int i = selector.select();
            if (i == 0) { // 无通道就绪,直接返回
                continue;
            }

            // 获取事件准备就绪的通道
            Set<SelectionKey> selectedKeys = selector.selectedKeys();

            // 轮询SelectedKeys处理就绪事件
            Iterator<SelectionKey> iterator = selectedKeys.iterator();

            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();

                if (key.isAcceptable()) {  // 如果满足Acceptable条件,则必定是一个ServerSocketChannel
                    ServerSocketChannel ssc = (ServerSocketChannel) key.channel();

                    // 获取一个连接好的SocketChannel,并将它注册到Selector上,兴趣为READ
                    SocketChannel socketChannel = ssc.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector, SelectionKey.OP_READ);
                    System.out.println("register channel, channel number is:" + selector.keys().size());
                } else if (key.isReadable()) { // 读取客户端传过来的数据并显示
                    SocketChannel socketChannel = (SocketChannel) key.channel();

                    buffer.clear();
                    while (socketChannel.read(buffer) > 0) {
                        buffer.flip();
                        byte[] bytes = new byte[buffer.remaining()];
                        buffer.get(bytes);
                        System.out.println("from client:" + new String(bytes));
                    }
                }

                // 清除处理完的key
                iterator.remove();
            }
        }

    }
}

package com.bzb.java8.main;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * @author bzb
 * @Description:
 * @date 2018/9/21 16:37
 */
public class ClientSelector {
    public static void main(String[] args) throws IOException, InterruptedException {
        SocketChannel channel = SocketChannel.open();
        channel.configureBlocking(false);
        channel.connect(new InetSocketAddress(ServerSelector.PORT));
        while (!channel.finishConnect()) {
            TimeUnit.MILLISECONDS.sleep(100);
        }
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        for (int i = 0; i < 5; i++) {
            TimeUnit.MILLISECONDS.sleep(100 * new Random().nextInt(10));
            String str = "Message from client, number:" + i;
            buffer.put(str.getBytes());
            buffer.flip();
            while (buffer.hasRemaining()) {
                channel.write(buffer);
            }
            buffer.clear();
        }
        channel.close();
    }
}

参考:并发编程网

猜你喜欢

转载自blog.csdn.net/java852987/article/details/82799519