Java NIO之选择器Selector

Selector类

      Channel对象的多路复用器

      Selector本身为抽象类,AbstractSelector是Selector类的抽象实现类,具体的实现类更加底层(SelectorImpl,位于sun.nio.ch);Selector即为"选择器",支撑了NIO的多路复用。Selector不能直接创建,可以通过调用Selector类的静态方法open()来创建选择器,该方法将使用系统的默认SelectorProvider来创建新的选择器。 选择器也可以通过调用自定义selector provider的openSelector()方法来创建。 选择器保持打开状态,直到通过其close()方法关闭它为止。

      一个选择器selector可以同时来管理一个或多个通道(Channel),将Channel关注的事件注册到选择器上,通过选择器来帮我们监听事件是否完成,在此期间Channel可以做自己的事情。如此可以实现单线程管理多个通道channels。

      每个Channel向Selector注册时,都将会创建一个选择键(SelectionKey)。SelectionKey将Channel与Selector建立了关系,并维护了channel事件。可以通过cancel方法取消键,取消的键不会立即从selector中移除,而是添加到cancelledKeys中,在下一次select操作时移除它。所以在调用某个key时,需要使用isValid()进行校验

三种SelectionKey集合 

    选择器维护着注册过的通道Channel的集合,并且这些注册关系中的任意一个都是封装在SelectionKey 对象中的,每一个selector对象维护者三种SelectionKey类型的集合。

  • The key set contains the keys representing the current channel registrations of this selector. This set is returned by the keys method.

  •  The selected-key set is the set of keys such that each key's channel was detected to be ready for at least one of the operations identified in the key's interest set during a prior selection operation. This set is returned by the selectedKeys method. The selected-key set is always a subset of the key set.

  • The cancelled-key set is the set of keys that have been cancelled but whose channels have not yet been deregistered. This set is not directly accessible. The cancelled-key set is always a subset of the key set.

已注册键的集合(Registered key set):

与选择器selector关联的已经注册的键的集合(可能已经cancelled但尚未deregister)。这个集合通过keys() 方法返回,可是会是空的。已注册的键的集合不可以直接修改。

已选择键的集合(Selected key set):
已注册的键的集合的子集。前一次选择操作期间,被选择器判断为已经准备就绪的通道所对应的选择键的集合,这个集合通过selectedKeys()方法返回(可能为空)。
不要将已选择键集合和ready集合搞混。这是一个键的集合,每个键都关联一个已经准备好至少一种操作的通道(一个键关联一个channel)。每个键都有一个内嵌的ready集合,指示了所关联的通道已经准备好的操作。
当我们遍历这个集合时,如果对事件没有处理,那么将丢弃这个事件键,可以从这个集合中移除,但不能添加。添加将抛出异常。

已取消的键的集合(Cancelled key set):

已注册的键的集合的子集,这个集合包含了cancel()方法被调用过的键(这个键已经被取消,但是对应的channel还没有被注销。)这个集合是选择器对象的私有成员,无法直接访问 。
已取消键的集合在select() 操作期间将被清空。

三种SelectionKey集合的变化关系

  • 对于新创建的Selector实例,上述三个集合均为空。
  • 通过通道channel的register()方法,一个key就被添加到key set 集合中。
  • 通过关闭一个key的通道或者调用key的cancel()方法,那么此key将会被添加到cancelled-key set集合中。取消某个key将导致其通道channel在下一次选择操作(selection operation)时被注销,这时该key将从所有选择器的key集合删除。
  • 通过选择操作(selection operations),可以将key添加到selected-key set集合中。

Selection选择操作

在每个选择操作,可以将键key添加到选择器的selected key set集合中或者从selected key set集合中删除,也可能将其从key set集合和cancelled-key set集合中删除。选择是通过select()select(long) selectNow()方法执行的,涉及三步

      1、已取消键的集合将被从其他键集合中删除,相关通道会被注销

如果是非空的,每个已取消键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。这个步骤结束后,已取消键的集合将是空的。

      2、已注册键的集合中的键的interest集合将被遍历,并询问底层操作系统查看interest集合中关心的操作是否就绪

在这个步骤中的检查执行过后,对interest集合的改动不会影响剩余的检查过程。

一旦就绪条件被定写下来,底层操作系统将会进行查询,以确定每个通道所关心的操作的真实就绪状态。依赖于特定select()方法调用,如果没有通道准备好,线程可能会在这里阻塞。

直到系统调用完成为止,这个过程可能会使得调用线程睡眠一段事件,然后当前每个通道的就绪状态将确定下来。

这对于那些还没准备好的通道将不会执行任何操作;

对于至少准备好interest集合中的一种操作的通道,将执行下面两种操作的一种:

        a: 如果通道的键还没有处于已选择键的集合(Selected key set)中,那么该键之前的ready-operation集合将被清空,然后重新设置为当前通道已经准备好的就绪操作集合;然后将该键加入到已选择键的集合中。(通俗来说,就是将已准备好的事件如read,write等在ready集合中做一个标记)。

        b: 否则,也就是键在已选择键的集合中。键的ready-operation集合将被更新上当前通道已经准备好的操作。之前已经是就绪状态的操作不会被清除,会被保留。由操作系统决定的ready集合与之前的ready集合按位分离的,一旦被放置于选择器的已选择集合中,它的ready操作将是累积的。比特位只会被设置,不会被清理。

      3、在步骤2进行的过程中,被添加到已消键的集合中的键会重新执行步骤1

选择操作(selection operation)是否阻塞等待一个或多个通道准备就绪,如果阻塞,则等待多长时间,是这三种选择方法之间的唯一本质区别。

Concurrency并发

Selector本身在多线程环境中是线程安全的,但是其各种键集合并非是线程安全的。

选择操作按照在选择器本身,key set 和selected-key set上这样的顺序同步。 它们还会在上面的步骤(1)和(3)中在cancelled-key集合中同步。

在进行选择操作(selection operation)时,对选择器的键的兴趣集(interest set)所做的更改对该操作没有影响; 他们将在下一个选择操作中看到。

可以随时取消键,关闭通道。 因此,一个键在选择器的一个或多个键集中存在并不表示该键有效或其通道已打开。 其他线程有可能取消键或关闭通道,因此,应用程序代码应谨慎同步并在必要时检查这些条件。

在select()或select(long)方法中被阻塞的线程可能会被其他线程以下面三种方式之一中断:

  • 调用选择器的 wakeup()方法

  • 调用选择器的close()方法

  • 调用已被阻塞的线程的interrupt()方法,在这种情况下将设置其中断状态并调用选择器的唤醒方法 wakeup()

Selector方法列表

static Selector open() throws IOException
打开一个选择器,此操作将会导致系统默认的SelectorProvider对象的openSelector方法来创建选择器。

abstract boolean isOpen()
检测此selector是否处于打开状态,仅当selector创建成功却没有关闭,返回true.

abstract Selt<SelectionKey> keys() 线程不安全
获取此selector中已经注册(可能已经cancelled但尚未deregister)的所有选择键。此集合不能被外部修改。

abstract Set<SelectionKey> selectedKeys()
返回此选择器当前已选择键集合.此集合不能被add,但可以remove操作.

abstract int selectNow() throws IOException
选择一组键,该键相应的通道已为I/O操作就绪。此方法为非阻塞操作,立即返回已经就绪的的个数,可能为0。"非阻塞",意味着,在I/O通道就绪信息的检测上,是无阻塞的,即如果底层线程(底层实现为多线程轮询检测I/O操作,并将已就绪的key放在内部的队列中)所维持的"就绪通道"个数为任意数字都立即返回,当然包括0.因为同一个selector实例中,select(),selectNow(),select(long)底层的实现几乎一致,方法实体中都会对keys和selectedKeys进行同步操作,因此在多线程中同时进行"select"调用,也将会存在互相阻塞的影响.

abstract int select(long timeout) throws IOException
选择一组键,该键相应的通道已为I/O操作就绪。此方法为阻塞模式操作。
至少一个通道就绪/选择器的唤醒wakeup()方法被调用/当前线程被中断interrupted/超时(以先到者为准),它才返回

abstract int select() throws Exception
选择一组键,该键相应的通道已为I/O操作就绪。此方法为阻塞模式操作。
至少一个通道就绪/选择器的唤醒wakeup()方法被调用/当前线程被中断interrupted时返回(以先到者为准)。
注意Selector支持interrupt(即具有interruptable),它采取了和InterruptableChannel类似的设计思路.即具有begin()/end().

abstract Selector wakeup()
使尚未返回的第一个选择操作selection operation立即返回。

Selector使用步骤

1、获取选择器实例:

Selector.open();

2、将通道注册到选择器上,并确定关注的事件:

channel.configureBlocking(false);//通道设置为非阻塞。
channel.register(selector,SelectionKey.OP_XXXX);

3、选择器监听事件:

 selector.select();/selector.selectNow();/selector.select(long timeout);

4、遍历已就绪键集合,判断哪种事件完成:

Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();

5、关闭selector的资源。

SelectionKey类

类的方法

channel(): 返回此选择键所关联的通道.即使此key已经被取消,仍然会返回。
selector(): 返回此选择键所关联的选择器,即使此键已经被取消,仍然会返回。
boolean isValid(): 检测此key是否有效。当key被取消,或者通道被关闭,或者selector被关闭,都将导致此key无效.在AbstractSelector.removeKey(key)中,会导致selectionKey被置为无效。
cancel() : 请求将此键取消注册。一旦返回成功,那么该键就是无效的,被添加到selector的cancelledKeys中.cancel操作将key的valid属性置为false,并执行selector.cancel(key)(即将key加入cancelledkey集合)。
boolean isWritable(): 此key的通道是否已准备好进行写入。
boolean isReadable(): 此key的通道是否已准备好进行读取。
boolean isConnectable(): 此key的通道是否已完成或未能完成其套接字连接操作。
boolean isAcceptable(): 此key的通道是否已准备好接受新的套接字连接。


##下面是两个操作集,操作集为位运算值,每一位表示一种操作。
int interestOps():获取此键的interest集合,当前channel感兴趣的操作,此类操作将会在下一次选择器select操作时被交付,可以通过selectionKey.interestOps(int)进行修改。
SelectionKey interestOps(int ops):将此键的interst设置为指定值。此操作会对ops和channel.validOps进行校验。如果此ops不会当前channel支持,将抛出异常。
int readyOps():获取此键上ready-operation集合(即已经就绪的操作)。

类的静态变量,4种操作集位

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;//可接受事件的操作集位

示例

客户端发送加法计算公式,服务器端进行计算并返回计算结果给客户端

程序

package com.socket2;
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;

public class Server {
	public static void main(String args[]) {

		try {
			// 通过ServerSocketChannel 的open()方法创建一个ServerSocketChannel对象
			ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
			// 端口绑定
			serverSocketChannel.socket().bind(new InetSocketAddress(8080));

			// 打开一个选择器selector
			Selector selector = Selector.open();
			// 配置ServerSocketChannel为非阻塞模式
			serverSocketChannel.configureBlocking(false);
			// 将该channel注册到selector上并设置关注OP_ACCEPT监听事件
			serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
			System.out.println("等待客户端的消息……");

			// 进行选择操作
			while (selector.select() > 0) {
				System.out.println("有关注的事件发生");
				Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
				while (iterator.hasNext()) {
					SelectionKey selectionKey = iterator.next();
					// 删除已选择集合中的键
					iterator.remove();
					// 要判断SelectionKey是否有效
					if (selectionKey.isValid()) {
						if (selectionKey.isAcceptable()) {
							System.out.println("有可接收事件发生");
							// 得到该SelectionKey对应的channel
							ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel();
							// 接收该channel上的socket连接
							SocketChannel socketChannel = channel.accept();
							// 将该channel设置为非阻塞模式
							socketChannel.configureBlocking(false);
							// 将该channel注册到selector上并设置关注OP_READ事件
							socketChannel.register(selector, SelectionKey.OP_READ);
						} else if (selectionKey.isReadable()) {
							System.out.println("有可读事件发生");
							// 得到该SelectionKey对应的channel
							SocketChannel socketChannel = (SocketChannel) selectionKey.channel();

							// 创建数据的缓存区对象
							ByteBuffer byteBuffer = ByteBuffer.allocate(128);
							// 从通道中读取客户端传送的数据到缓冲区中
							byteBuffer.clear();
							socketChannel.read(byteBuffer);

							// 从缓冲区中get数据并进行解析计算
							byteBuffer.flip();
							byte[] bytes = new byte[byteBuffer.remaining()];
							byteBuffer.get(bytes);
							String formulaStr = new String(bytes);
							System.out.println(
									"客户端(" + socketChannel.getRemoteAddress().toString() + ")发送公式:" + formulaStr);
							if (formulaStr.equals("end")) {
								System.out.println("客户端(" + socketChannel.getRemoteAddress().toString() + ")结束通信");
								socketChannel.close();
								break;
							}
							if (!formulaStr.toString().contains("+")) {
								byteBuffer.clear();
								byteBuffer.put(("客户端(" + socketChannel.getRemoteAddress().toString() + ")发送不正确的加法公式")
										.getBytes());
								byteBuffer.flip();
								socketChannel.write(byteBuffer);
							} else {
								// 进行计算,并将结果放入到缓冲区中
								String stra = formulaStr.substring(0, formulaStr.indexOf("+"));
								String strb = formulaStr.substring(formulaStr.indexOf("+") + 1, formulaStr.length());
								int a = Integer.parseInt(stra);
								int b = Integer.parseInt(strb);
								int sum = a + b;
								byteBuffer.clear();
								System.out.println("客户端(" + socketChannel.getRemoteAddress().toString() + ")计算结果:"
										+ formulaStr + "=" + sum);
								byteBuffer.put(String.valueOf(formulaStr + "=" + sum).getBytes());
								// 从缓冲区中读取数据到通道发向客户端
								byteBuffer.flip();
								socketChannel.write(byteBuffer);
							}
						}
					}
					System.out.println("集合中剩余事件个数:  " + selector.selectedKeys().size());
				}

			} // while
			serverSocketChannel.close();
		} catch (Exception e) {
			System.out.println("异常:" + e);
		}
	}
}
package com.socket2;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class Client {
	public static void main(String[] args) {
		String str;

		try {
			// 通过SocketChannel的静态方法open()创建一个SocketChannel对象
			SocketChannel socketChannel = SocketChannel.open();
			// 创建一个InetSocketAddress对象
			InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 8080);
			// 配置该socket为非阻塞模式
			socketChannel.configureBlocking(false);
			// 打开一个选择器selector
			Selector selector = Selector.open();

			// SocketChannel连接服务器端
			// 因为前面设置的是非阻塞,所以调用这个方法会立即返回.
			// 因此如果返回true就不需要放到select中
			if (!socketChannel.connect(addr)) {
				// 向选择器selector注册该socketchannel并关注OP_CONNECT事件
				socketChannel.register(selector, SelectionKey.OP_CONNECT);
				// 进行选择
				selector.select();
				// 得到关注的事件集合
				Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
				while (iterator.hasNext()) {
					SelectionKey selectionKey = iterator.next();
					iterator.remove();
					if (selectionKey.isValid() && selectionKey.isConnectable()) {
						SocketChannel channel = (SocketChannel) selectionKey.channel();
						// 如果连接成功则完成连接
						if (channel.isConnectionPending()) {
							channel.finishConnect();
						}
						System.out.println("客户端连接成功");
						// 设置该socketChannel为非阻塞模式
						channel.configureBlocking(false);
						// 向选择器selector注册该socketchannel并关注OP_READ事件
						channel.register(selector, SelectionKey.OP_READ);
					}
				}
			}

			// 创建数据缓存区对象
			ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
			// in是System的一个"标准"输入流, 此流已打开并准备提供输入数据。 通常,此流对应于键盘输入或者由主机环境或用户指定的另一个输入源
			BufferedReader userin = new BufferedReader(new InputStreamReader(System.in));
			while (true) {
				System.out.println("发送加法公式:");
				// 获取用户从终端输入的字符串
				str = userin.readLine();
				if (str.equals("end")) {
					System.out.println("结束通信");
					break;
				}
				// 将从终端读取到的数据放到缓冲区
				byteBuffer.clear();
				byteBuffer.put(str.getBytes());
				// 读取缓冲区中的数据到通道中,发向服务端
				byteBuffer.flip();
				socketChannel.write(byteBuffer);

				System.out.println("等待服务器端消息……");
				// 监听读事件,没有则阻塞
				selector.select();
				// int keyNum = selector.select();
				// System.out.println(keyNum);
				//
				// Set<SelectionKey> readyKeys = selector.selectedKeys();
				// System.out.println("Selected channels: " + readyKeys.size());

				// 清除缓冲区
				byteBuffer.clear();
				// 读取通道中从服务器端来的数据到缓冲区中
				socketChannel.read(byteBuffer);

				byteBuffer.flip();
				// 通过缓冲区有效元素大小确定接收数组大小
				byte[] bytes = new byte[byteBuffer.remaining()];
				// 从缓冲区读取数据
				byteBuffer.get(bytes);
				String stringBuffer = new String(bytes);
				System.out.println("服务器端计算结果:" + stringBuffer);
			} // while
			socketChannel.close();
			selector.close();
		} catch (Exception e) {
			System.out.println("异常:" + e);
		}
	}
}

运行结果

          

参考

http://tutorials.jenkov.com/java-nio/channels.html

https://blog.csdn.net/fly_fly_zhang/article/details/90452791

https://www.iteye.com/blog/shift-alt-ctrl-1840411

https://blog.csdn.net/jeffleo/article/details/54695959

发布了243 篇原创文章 · 获赞 138 · 访问量 138万+

猜你喜欢

转载自blog.csdn.net/ystyaoshengting/article/details/104147150