Java中的NIO详解Day06-Selector

基本概念

  • 选择器Selector:
    • Java NIO中能够检测一个或者多个NIO通道,并且能够获取到通道是否为诸如读写事件做好准备的组件
    • 一个单独的线程可以管理多个Channel,从而管理多个网络连接
  • 使用单个线程来处理多个Channel的优点:
    • 只需要更少的线程来处理通道
      • 事实上,可以只用一个线程处理所有的通道
      • 对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程之间都要占用一些内存资源
      • 因此,使用的线程越少越好

Selector的创建

  • 通过调用Selector.open() 方法创建一个Selector:
Selector selector = Selector.open();

Selector注册通道

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

SelectionKey

  • 当向Selector注册Channel,register() 方法会返回一个SelectionKey对象,这个对象中包含了Selector感 兴趣的属性:
    • interest集合
    • ready集合
    • Channel
    • Selector
    • 附加的对象

interest集合

  • interest集合: Selector感兴趣的事件集合. 可以通过SelectionKey读写interest集合
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;
  • 使用 “位于&” 操作interest集合与给定的SelectionKey常量,可以确定某个确定的事件是否在interest集合中

ready集合

  • ready集合是通道已经准备就绪操作的集合.在一次选择Selection之后,会首先访问这个ready集合
  • 访问ready集合:
int readySet = selectionKey.readyOps();
  • 可以使用 “位或&” 操作ready集合与给定的SelectionKey常量,可以确定Channel中什么事件或者操作已经就绪
  • 也可以使用SelectionKey中的方法来检测Channel中什么事件或者操作已经就绪:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();

Channel和Selector

  • 根据SelectionKey访问ChannelSelector:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();

附加的对象

  • 为了更加方便地识别某个给定的通道,可以将一个对象或者更多信息附加到SelectionKey
    • 可以附加一个与通道一起使用的Buffer或者是包含聚集数据的某个对象:
    selectionKey.attach(theObject);
    Object theObject = selectionKey.attachment();
    
    • 可以在用register() 方法向Selector注册Channel时附加对象:
    SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
    

Selector选择通道

  • 如果向Selector注册了一个或多个通道,就可以调用几个重载的select() 方法
    • select() 方法返回所感兴趣事件已经准备就绪的通道:
      • 连接
      • 接受
  • select() 方法:
/*
 * select()阻塞到至少有一个通道在注册的事件上就绪
 * 
 * @return int 表示有多少个通道已经就绪 或者是 从上次调用select()方法后有多少通道变成就绪状态
 */
 int select();

/*
 * select(long timeout)阻塞到至少有一个通道在注册的事件上就绪,最长会阻塞timeout毫秒
 *  
 * @param timeout 最长会阻塞timeout毫秒
 * @return int 表示有多少个通道已经就绪 或者是 从上次调用select(long timeout)方法后有多少通道变成就绪状态
 */
 int select(long timeout);

/*
 * selectNow()不会阻塞,不管什么通道就绪都立刻返回
 *  
 * @return int 此方法执行非阻塞的选择操作,从前一次选择操作后,没有通道变成可选择的,直接返回0
 */
 int selectNow();

SelectedKeys

  • 如果调用了select() 方法,并且返回值表明有一个或更多个通道就绪了.然后可以通过调用selectorselectedKeys() 方法,访问 “已选择键集selected key set” 中的就绪通道
Set selectedKeys = selector.selectedKeys();
  • 当向Selector注册Channel,Channel.register() 方法会返回一个SelectionKey对象:
    • SelectionKey对象代表了注册到该Selector的通道
    • 可以通过SelectionKeyselectedKeySet() 方法访问这些对象
  • 可以通过遍历这个已选择的键集合来访问就绪的通道:
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
	SelectionKey key = keyIterator.next();
	if (key.isAcceptable()) {
		// a connection was acceptable by a ServerSocketChannel
	}
	if (key.isConnectable()) {
		// a connection was established with a remote server
	}
	if (key.isReadable()) {
		// a channel is ready for reading
	}
	if (key.isWritable()) {
		// a channel is ready for writing
	}
	keyIterator.remove();
}
  • 循环遍历已选择键集中的每个键,并检测各个键所对应的通道的就绪事件
  • 每次迭代末尾调用keyIterator.remove():
    • Selector本身不会从已选择键集中移除SelectionKey实例
    • 必须在处理完通道时自己移除
    • 下次该通道变成就绪时 ,Selector会再次将SelectionKey实例放入已选择键集中
  • SelectionKey.channel() 方法返回的通道需要转型成为需要处理的类型: 比如ServerSocketChannel或者SocketChannel

wakeup()

  • 某个线程调用select() 方法后阻塞,即使没有通道已经就绪,也可以从select() 方法返回:
    • 只要让其余线程在第一个线程调用select() 方法的对象上调用Selector.wakeup() 方法
    • 阻塞在select() 方法上的线程会立即返回
  • 如果有其余线程调用了wakeup() 方法,但是当前没有线程阻塞在select() 方法上,下一个调用select() 方法的线程会立即返回

close()

  • 使用完毕Selector之后调用close() 方法会关闭该Selector. 并且注册到该Selector上的所有SelectionKey实例无效
  • Channel本身并不会关闭

Selector示例

  • 打开一个Selector
  • 注册一个初始化完成的通道到这个Selector
  • 持续监控这个Selector的四种事件:接受,连接,读,写.是否就绪
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while (true) {
	int readyChannels = selector.select();
	if (readyChannels == 0) {
		continue;
	}
	Set selectedKeys = selector.selectedKeys();
	Iterator keyIterator = selectedKeys.iterator();
	while (keyIterator.hasNext()) {
		SelectionKey key = keyIterator.next();
		if (key.isAcceptable()) {
			// 一个连接被ServerSocketChannel接受
			...
		}
		if (key.isConnectable()) {
			// 一个连接被远程服务发布
			...
		}
		if (key.isReadable()) {
			// 一个准备去读的channel
			...
		}
		if (key.isWritable()) {
			// 一个准备去写的channel
		}
		keyIterator.remove();
	}
}

猜你喜欢

转载自blog.csdn.net/JewaveOxford/article/details/107609502