2.Java NIO之Selector使用方法简介

前言

在之前文章1. Java NIO三大部件概述中简单提及Selector概念、优缺点以及Netty中对其的优化,在这篇文章将对Selector的使用方法进行简单介绍。之后还会有关于Selector如何实现进行概括。

1. Selector创建

Selector selector = Selector.open();

2. 注册Channel到Selector

Selector要能够管理Channel,需要将Channel注册到Selector中:

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

这里需注意,如果Channel要注册到Selector中,Channel必须是非阻塞模式。 所以channel.configureBlocking(false);
所以,FileChannel不适用于Selector,因为他是阻塞的。(FileChannel没有继承SelectableChannel)。
SelectableChannel抽象类有一个configureBlocking()方法用于使通道处于阻塞模式或者非阻塞模式。

abstract SelectableChannel configureBlocking(boolean block)

SelectableChannel抽象类的configureBlocking()方法是由AbstractSelectableChannel抽象类实现的,SocketChannel、ServerSocketChannel、DatagramChannel都是直接继承了AbstractSelectableChannel抽象类。

在 register(Selector selector, int interestSet)方法的第二个参数,表示一个“interest”集合,意思是通过Selector监听Channel时,对哪些(可以为多个)事件感兴趣,可以监听四种不同类型的事件:

  1. Connect:连接完成事件(TCP连接),仅适用于客户端,对应SelectionKey.OP_CONNECT。
  2. Accept:接受新连接事件,仅适用于服务端,对应SelectionKey.OP_ACCEPT.
  3. Read:读事件,适用于客户端、服务端,对应SelectionKey.OP_READ,表示Buffer可读。
  4. Write:写事件,适用于客户端、服务端,对应SelectionKey.OP_WRITE,表示Buffer可写。
    Channel触发了一个事件,意思是该事件已经就绪。
  • 一个Client Channel Channel成功连接到另一个服务器,称为“连接就绪”。
  • 一个Server Socket Channel准备好接收新进入的连接,称为“接受就绪”。
  • 一个有数据可读的Channel,可以说是“读就绪”。
  • 一个等待写数据的Channel,可以说是“写就绪”。
    这里由于Selector可以对Channel的多个事件感兴趣,所以当我们想要注册Channel的多个事件到Selector中时,可用|来组合多个事件:
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;

这里在实际操作时,Selector对Channel感兴趣的事件集合可通过调用register(Selector selector, int interestSet)方法进行更改:

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

3. SelectionKey类

当调用Channel的register(…)方法时,向Selector注册一个Channel后,会返回一个SelectionKey对象,SelectionKey在java.nio.channels包下,被定义成一个抽象类,表示一个Channel和一个Selector的注册关系,包含以下内容:

  • interest set : 感兴趣的事件集合
  • ready set: 就绪的事件集合
  • Channel
  • Selector
  • attachment:可选择的附加对象
key.interestOps(); //返回代表需要Selector监控的IO操作的bit mask;
key.readyOps(); //返回bit mask,代表在相应channel上可以进行的IO操作。
key.channel(); //返回该SelectionKey对应的channel;
key.selector(): //返回该SelectionKey对应的Selector。
key.attachment(); //返回SelectionKey的attachment,attachment可以在注册channel时被指定。

3.1 interest set

通过调用interestOps()方法,返回感兴趣的事件集合:

int interestSet = selectionKey.interestOps(); 
//判断对哪些事件感兴趣
boolean isInterestedInAccept = (interestSet & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;

其中每个事件Key在SelectionKey中枚举,通过bit表示:

//  SelectionKey.java

public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;

因此,上面的上代码段才可用&运算来判断是否对指定事件感兴趣。

3.2 ready set

通过调用readyOps()方法,返回就绪的事件集合:

//创建ready集合的方法
int readySet = selectionKey.readyOps();

// 判断哪些事件已就绪
//是否可接收,是返回 true
selectionKey.isAcceptable();
//是否可连接,是返回 true
selectionKey.isConnectable();
//是否可读,是返回 true
selectionKey.isReadable();
//是否可写,是返回 true
selectionKey.isWritable();

与interest set相比,ready set 内置了判断事件的方法:

// SelectionKey.java
public final boolean isReadable() {
    return (readyOps() & OP_READ) != 0;
}
public final boolean isWritable() {
    return (readyOps() & OP_WRITE) != 0;
}
public final boolean isConnectable() {
    return (readyOps() & OP_CONNECT) != 0;
}
public final boolean isAcceptable() {
    return (readyOps() & OP_ACCEPT) != 0;
}

从SelectionKey访问Channel和Selector的操作如下:

Channel channel = key.channel();
Selector selector = key.selector();
key.attachment();

可以在selectionKey附加一个Object对象,来标识channel对象以便找出你要的channel对象,或者附加一些其他的信息

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

也可用register()方法向Selector注册Channel时附加对象:

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

3.3 attachment

通过调用attach(Object ob)方法,可以向SelectionKey添加附加对象,调用attachment()方法,可以获得SelectionKey附加对象:

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

又获得在注册时,直接添加附加对象:

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

4. 通过Selector选择Channel

在Selector中,提供三种类型的Select方法,返回当前有感兴趣事件准备就绪的Channel数量:

// Selector.java

// 阻塞到至少有一个 Channel 在你注册的事件上就绪了。
public abstract int select() throws IOException;

// 在 `select()` 方法的基础上,增加超时机制。最长的阻塞时间为timeout毫秒
public abstract int select(long timeout) throws IOException;

// 和 `select()` 方法不同,立即返回数量,而不阻塞。只要有通道就绪就立刻返回
public abstract int selectNow() throws IOException;

select()方法返回的int值表示有多少通道已经就绪,是自上次调用select()方法后有多少通道变成就绪状态。之前在select()调用时进入就绪的通道不会在本次调用中被记入,而在前一次select()调用进入就绪但现在已经不在处于就绪的通道也不会被记入。但在每次 select 方法调用之间,只有一个 Channel 就绪了,所以才返回 1。

5. 获取可操作的Channel

一旦调用select()方法,并且返回值不为0时,则 可以通过调用Selector的selectedKeys()方法来访问已选择键集合

Set selectedKeys=selector.selectedKeys();

6. 唤醒Selector选择

若某个线程调用select()方法后,发生阻塞,没有通道已就绪,仍有办法让该线程从selector()方法返回:
只要让其他线程在第一个线程调用select()方法的那个 Selector 对象上,调用该 Selector 的 wakeup() 方法,进行唤醒该 Selector 即可。
然后,阻塞在select()方法上的线程会立马返回。(Selector 的 select(long timeout) 方法,若未超时的情况下,也可以满足上述方式。)
如果有其它线程调用了 wakeup() 方法,但当前没有线程阻塞在 select() 方法上,下个调用 select() 方法的线程会立即被唤醒。

7. 关闭Selector

调用Selector的close()方法
该方法使得任何一个在选择操作中阻塞的线程都被唤醒(类似wakeup()),同时使得注册到该Selector的所有Channel被注销,所有的键将被取消,但是Channel本身并不会关闭。(与Selector相关的所有SelectionKey全部失效,与其相关的Channel不会关闭。)

8. Selector示例

服务器端

package com.mec.Test;

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;

public class WebServer {
    public static void main(String[] args) {
        try {
            ServerSocketChannel ssc = ServerSocketChannel.open();
            ssc.socket().bind(new InetSocketAddress("127.0.0.1", 8000));
            ssc.configureBlocking(false);

            Selector selector = Selector.open();
            // 注册 channel,并且指定感兴趣的事件是 Accept
            ssc.register(selector, SelectionKey.OP_ACCEPT);

            ByteBuffer readBuff = ByteBuffer.allocate(1024);
            ByteBuffer writeBuff = ByteBuffer.allocate(128);
            writeBuff.put("received".getBytes());
            writeBuff.flip();

            while (true) {
                int nReady = selector.select();
                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();

                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    it.remove();

                    if (key.isAcceptable()) {
                        // 创建新的连接,并且把连接注册到selector上,而且,
                        // 声明这个channel只对读操作感兴趣。
                        SocketChannel socketChannel = ssc.accept();
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    }
                    else if (key.isReadable()) {
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        readBuff.clear();
                        socketChannel.read(readBuff);

                        readBuff.flip();
                        System.out.println("received : " + new String(readBuff.array()));
                        key.interestOps(SelectionKey.OP_WRITE);
                    }
                    else if (key.isWritable()) {
                        writeBuff.rewind();
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        socketChannel.write(writeBuff);
                        key.interestOps(SelectionKey.OP_READ);
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

客户端

package com.mec.Test;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class WebClient {
    public static void main(String[] args) throws IOException {
        try {
            SocketChannel socketChannel = SocketChannel.open();
            socketChannel.connect(new InetSocketAddress("127.0.0.1", 8000));

            ByteBuffer writeBuffer = ByteBuffer.allocate(32);
            ByteBuffer readBuffer = ByteBuffer.allocate(32);

            writeBuffer.put("hello".getBytes());
            writeBuffer.flip();

            while (true) {
                writeBuffer.rewind();
                socketChannel.write(writeBuffer);
                readBuffer.clear();
                socketChannel.read(readBuffer);
            }
        } catch (IOException e) {
        }
    }
}

运行服务端不断收到客户端消息:
在这里插入图片描述

发布了26 篇原创文章 · 获赞 4 · 访问量 2385

猜你喜欢

转载自blog.csdn.net/weixin_43257196/article/details/103789091