SelectionKey

SelectionKey,选择键,在每次通道注册到选择器上时都会创建一个SelectionKey储存在该选择器上,该SelectionKey保存了注册的通道、注册的选择器、通道事件类型操作符等信息。

SelectionKey是一个抽象类,它有俩个实现类了AbstractSelectionKey(抽象类)SelectionKeyImpl(最终实现类)。SelectionKey有6个属性:

//读操作符,左移位后的整型值为1
public static final int OP_READ = 1 << 0;
//写操作符,左移位后的整型值为4
public static final int OP_WRITE = 1 << 2;
//连接操作符,左移位后的整型值为8
public static final int OP_CONNECT = 1 << 3;
//接收操作符,左移位后的整型值为16
public static final int OP_ACCEPT = 1 << 4;
//附件
private volatile Object attachment = null;
//附件更新者,当要更新附件时需调用该对象的方法
private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
SelectionKey.class, Object.class, "attachment"
);

这些属性中较为重要的是4个操作符属性,需记住它们左移位后的整型值,在后面对选择通道的操作事件判断需使用到。

SelectionKey除开构造器方法,有13个方法:

public abstract SelectableChannel channel();//返回该SelectionKey对应通道
public abstract Selector selector();//返回该SelectionKey注册的选择器
public abstract boolean isValid();//判断该SelectionKey是否有效
public abstract void cancel();//撤销该SelectionKey
public abstract int interestOps();//返回SelectionKey的关注操作符
//设置该SelectionKey的关注键,返回更改后新的SelectionKey
public abstract SelectionKey interestOps(int ops);
public abstract int readyOps();//返回SelectionKey的预备操作符 //这里readyOps()方法返回的是该SelectionKey的预备操作符,至于什么是预备操作符在最终实现类SelectionKeyImpl中会讲解。
//判断该SelectionKey的预备操作符是否是OP_READ
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_WRITE
public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_CONNECT
public final boolean isConnectable() {
return (readyOps() & OP_CONNECT) != 0;
}
//判断该SelectionKey的预备操作符是否是OP_ACCEPT
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
} //设置SelectionKey的附件
public final Object attach(Object ob) {
return attachmentUpdater.getAndSet(this, ob);
}
//返回SelectionKey的附件
public final Object attachment() {
return attachment;
}

AbstractSelectionKey

AbstractSelectionKey继承了SelectionKey类,它也是一个抽象类,相比其他俩个类,它的代码就简洁多了,它只有一个属性:

//用于判断该SelectionKey是否有效,true为有效,false为无效,默认有效
private volatile boolean valid = true;

AbstractSelectionKey除开构造器方法,只要三个实现方法:

//判断该SelectionKey是否有效
public final boolean isValid() {
return valid;
} //将该SelectionKey设为无效
void invalidate() {
valid = false;
}
//将该SelectionKey从选择器中删除
//注意,删除的SelectionKey并不会马上从选择器上删除,而是会加入一个需删除键的集合中,等到下一次调用选择方法才会将它从选择器中删除,至于具体实现会在选择器源码分析章节中讲
public final void cancel() {
synchronized (this) {
if (valid) {
valid = false;
((AbstractSelector)selector()).cancel(this);
}
}
}

SelectionKeyImpl

SelectionKeyImpl是SelectionKey的最终实现类,它继承了AbstractSelectionKey类,在该类中不仅实现了SelectionKey和抽象类的方法,而且扩展了其他方法。

SelectionKeyImpl的属性

SelectionKeyImpl中有5个新的属性:

//该SelectionKey对应的通道
final SelChImpl channel;
//该SelectionKey注册的选择器
public final SelectorImpl selector;
//该SelectionKey在注册选择器中储存SelectionKey集合中的下标索引,当该SelectionKey被撤销时,index为-1
private int index;
//SelectionKey的关注操作符
private volatile int interestOps;
//SelectionKey的预备操作符
private int readyOps;

从上面属性中可以看到,SelectionKeyImpl有俩个操作符属性:关注操作符interestOps预备操作符readyOps

interestOps是储存通道的注册方法register(Selector sel, int ops)输入的ops参数,可以在register方法的最终实现中看出,代表程序需选择器对通道关注的操作事件。

public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException
{
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
if (blocking)
throw new IllegalBlockingModeException();
SelectionKey k = findKey(sel);
if (k != null) {
//将输入的参数ops储存在SelectionKey的interestOps属性中
k.interestOps(ops);
k.attach(att);
}
if (k == null) {
synchronized (keyLock) {
if (!isOpen())
throw new ClosedChannelException();
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
}
return k;
}
}

readyOps是通道实际发生的操作事件,当我们对选择器Selector.select()方法层层追溯,到达该方法的最终实现doSelect(long var1)方法,会发现doSelect方法调用了一个updateSelectedKeys()方法来更新选择器的SelectionKey集合,而updateSelectedKeys方法又调用了updateSelectedKeys(this.updateCount)方法来进行实际的更新操作,而updateSelectedKeys方法最终通过processFDSet方法来实现更新。在processFDSet方法里有两个方法调用实现了SelectionKey里readyOps属性的更新:translateAndSetReadyOps(用于设置readyOps)translateAndUpdateReadyOps(用于更新readyOps)

public boolean translateAndUpdateReadyOps(int var1, SelectionKeyImpl var2) {
return this.translateReadyOps(var1, var2.nioReadyOps(), var2);
}
public boolean translateAndSetReadyOps(int var1, SelectionKeyImpl var2) {
return this.translateReadyOps(var1, 0, var2);
}

通过观察两个方法,可以发现它们都调用了translateReadyOps方法:

/**
* @param var1 该通道发生的事件类型,为Net中的POLL类型属性,这里需注意的有3个POLL类型属性:POLLIN、
* POLLOUT、POLLCONN,分别对应SelectionKey的OP_READ、OP_WRITE、OP_CONNECT
* @param var2 translateAndUpdateReadyOps中该参数为var3的readyOps,在translateAndSetReadyOps * 中该参数为0
* @param var3 需修改的SelectionKey
*/
public boolean translateReadyOps(int var1, int var2, SelectionKeyImpl var3) {
int var4 = var3.nioInterestOps();
int var5 = var3.nioReadyOps();
int var6 = var2;
if ((var1 & Net.POLLNVAL) != 0) {
return false;
} else if ((var1 & (Net.POLLERR | Net.POLLHUP)) != 0) {
var3.nioReadyOps(var4);
this.readyToConnect = true;
return (var4 & ~var5) != 0;
} else {
//var1 & var2 !=0 类似于 var1 == var2,或var1=0或var2=0
//判断发生的事件是否是Net.POLLIN,如果是则判断该通道对应的SelectionKey的readyOps是否等于1(OP_READ的值)
if ((var1 & Net.POLLIN) != 0 && (var4 & 1) != 0 && this.state == 2) {
var6 = var2 | 1;//等同于:var2 | OP_READ
}
//判断发生的事件是否是Net.POLLCONN,如果是则判断该通道对应的SelectionKey的readyOps是否等于8(OP_CONNECT的值)
if ((var1 & Net.POLLCONN) != 0 && (var4 & 8) != 0 && (this.state == 0 || this.state == 1)) {
var6 |= 8;//等同于:var6 | OP_CONNECT
this.readyToConnect = true;
}
//判断发生的事件是否是Net.POLLOUT,如果是则这判断该通道对应的SelectionKey的readyOps是否等于4(OP_OP_WRITE的值)
if ((var1 & Net.POLLOUT) != 0 && (var4 & 4) != 0 && this.state == 2) {
var6 |= 4;//等同于:var6 | OP_READ
} var3.nioReadyOps(var6);
return (var6 & ~var5) != 0;
}
}

看完上面源码可以发现,在判断通道实际发生事件的POLL类型时,还需判断该POLL类型对应的SelectionKey的ops类型是否与该SelectionKey的关注键interestOps相同,当所有条件满足时再将var6与其ops类型按位或,最后将该SelectionKey即var3的预备键readyOps设为var6。总结起来就是:

当通道实际发生的操作事件类型不等于该通道对应的SelectionKey的关注键interestOps时, 预备键readyOps不会被修改,且该发生事件的通道对应的SelectionKey也不会被加入Selector的selectedKeys集合中(从下面代码中可看出),但readyOps不一定就等于interestOps,因为调用interestOps的修改或设置方法时并不会同时修改readyOps。为了防止在进行select操作时,有另一个线程修改了某一SelectionKey的interestOps属性,在interestOps前添加了volatile修饰符,保证其可见性。

//这里是processFDSet方法中的一段代码,var10为SelectionKey
/*可以看出在进行了translateAndSetReadyOps或translateAndUpdateReadyOps方法设置readyOps属性后,
* 为了防止这时有另一线程修改了interestOps或readyOps为0,还判断了SelectionKey的readyOps和interestOps是否相同,
*/相同才把该SelectionKey加入到选择器的selectedKeys属性中
if (var9.clearedCount != var1) {
var10.channel.translateAndSetReadyOps(var4, var10);
if ((var10.nioReadyOps() & var10.nioInterestOps()) != 0) {
WindowsSelectorImpl.this.selectedKeys.add(var10);
var9.updateCount = var1;
++var6;
}
} else {
var10.channel.translateAndUpdateReadyOps(var4, var10);
if ((var10.nioReadyOps() & var10.nioInterestOps()) != 0) {
WindowsSelectorImpl.this.selectedKeys.add(var10);
var9.updateCount = var1;
++var6;
}
}

SelectionKeyImpl的方法

在SelectionKeyImpl中除开实现了SelectionKey抽象方法的简单方法外,需要关注几个较为重要的方法:

//确保该SelectionKey是有效的,如无效则会之间抛出异常
//在几个关于SelectionKey的属性方法中都有调用,如interestOps() 、interestOps(int var1)、readyOps()
private void ensureValid() {
if (!this.isValid()) {
throw new CancelledKeyException();
}
}
//设置readyOps的方法,不会确保该SelectionKey的有效性
public void nioReadyOps(int var1) {
this.readyOps = var1;
}
//该方法是SelectionKeyImpl自定义的获取readyOps方法,不同于readyOps(),它不会确保SelectionKey的有效性
public int nioReadyOps() {
return this.readyOps;
}
//获取interestOps的方法,interestOps()会通过调用该方法进行获取,是最终实现获取nterestOps的方法
public int nioInterestOps() {
return this.interestOps;
}

在SelectionKeyImpl类里,设置interestOps相比设置readyOps较为复杂,因为他调用了两个通道的方法validOps()和translateAndSetInterestOps(int var1, SelectionKeyImpl var2):

//interestOps(int var1)中调用了该方法来设置interestOps,是最终实现设置interestOps的方法
public SelectionKey nioInterestOps(int var1) {
if ((var1 & ~this.channel().validOps()) != 0) {
throw new IllegalArgumentException();
} else {
this.channel.translateAndSetInterestOps(var1, this);
this.interestOps = var1;
return this;
}
}

首先先来看看nioInterestOps方法中判断语句内的代码块:

(var1 & ~this.channel().validOps()) != 0

这里的this.channel有5个可能的实现类:SeverSocketChannel、SocketChannel、SinkChannel、SourceChannel、DatagramChannel,因为不同的通道可能只会发生对应的操作事件,如SeverSocketChannel只会发生OP_ACCEPT操作,SocketChannel会发生OP_READ、OP_WRITE和OP_CONNECT,而validOps()就是返回通道对应可能发生的操作事件(有效操作事件):

//SeverSocketChannel
public final int validOps() {
return SelectionKey.OP_ACCEPT;
}
//SocketChannel
public final int validOps() {
return (SelectionKey.OP_READ
| SelectionKey.OP_WRITE
| SelectionKey.OP_CONNECT);
}

所以nioInterestOps方法的判断语句可以改为:

(var1 & ~有效的操作事件) != 0

而后又对有效的操作事件进行了取非,变成了:

(var1 & 无效的操作事件) != 0

在之前说过 var1 & var2 !=0 类似于 var1 == var2,所以当判断语句内的条件成立时,即说明设置的输入的参数不在该SelectionKey对应的通道的有效操作事件里,所以抛出 IllegalArgumentException() 异常。

当参数var1为该SelectionKey对应的通道的有效操作事件时,在调用通道的translateAndSetInterestOps方法将该关注类型对应的POLL类型写入到选择器的pollWrapper属性中:

//SocketChannel
public void translateAndSet InterestOps(int var1, SelectionKeyImpl var2) {
int var3 = 0;
//OP_READ
if ((var1 & 1) != 0) {
var3 |= Net.POLLIN;
}
//OP_WRITE
if ((var1 & 4) != 0) {
var3 |= Net.POLLOUT;
}
//OP_CONNECT
if ((var1 & 8) != 0) {
var3 |= Net.POLLCONN;
}
var2.selector.putEventOps(var2, var3);
}

最后再将该SelectionKey的interestOps设为参数值var1,并返回新的SelectionKey。

(以上为本人对源码的个人理解,如果有错误,欢迎各位前辈指出)

NIO源码分析:SelectionKey的更多相关文章

  1. NIO 源码分析(05) Channel 源码分析

    目录 一.Channel 类图 二.begin 和 close 是什么 2.1 AbstractInterruptibleChannel 中的 begin 和 close 2.2 Selector 中 ...

  2. NIO 源码分析(01) NIO 最简用法

    目录 一.服务端 二.客户端 NIO 源码分析(01) NIO 最简用法 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) J ...

  3. NIO 源码分析(04) 从 SelectorProvider 看 JDK SPI 机制

    目录 一.SelectorProvider SPI 二.SelectorProvider 加载过程 2.1 SelectorProvider 加载 2.2 Windows 下 DefaultSelec ...

  4. NIO 源码分析(03) 从 BIO 到 NIO

    目录 一.NIO 三大组件 Channels.Buffers.Selectors 1.1 Channel 和 Buffer 1.2 Selector 1.3 Linux IO 和 NIO 编程的区别 ...

  5. NIO 源码分析(02-2) BIO 源码分析 Socket

    目录 一.BIO 最简使用姿势 二.connect 方法 2.1 Socket.connect 方法 2.2 AbstractPlainSocketImpl.connect 方法 2.3 DualSt ...

  6. NIO 源码分析(02-1) BIO 源码分析

    目录 一.BIO 最简使用姿势 二.ServerSocket 源码分析 2.1 相关类图 2.2 主要属性 2.3 构造函数 2.4 bind 方法 2.5 accept 方法 2.6 总结 NIO ...

  7. NIO-EPollSelectorIpml源码分析

    目录 NIO-EPollSelectorIpml源码分析 目录 前言 初始化EPollSelectorProvider 创建EPollSelectorImpl EPollSelectorImpl结构 ...

  8. NIO - Selector源码分析

    1. 背景 SelectableChannel对象的多路复用器. 可以通过调用Selector.open()方法创建Selector对象.Selector.open()方法会利用系统默认的Select ...

  9. 【Java】NIO中Selector的创建源码分析

    在使用Selector时首先需要通过静态方法open创建Selector对象 public static Selector open() throws IOException { return Sel ...

随机推荐

  1. MySQL 日志详解

    一.MySQL 日志分类 MySQL 日志主要包含:错误日志.查询日志.慢查询日志.事务日志.二进制日志. 错误日志: -log-err (记录启动.运行.停止 MySQL 服务时出现的信息) 查询日 ...

  2. RHSA-2018:1200-重要: patch 安全更新(代码执行)

    [root@localhost ~]# cat /etc/redhat-release CentOS Linux release 7.2.1511 (Core) 修复命令: 使用root账号登陆She ...

  3. 【面试题】java一般

    1.取最大最小值 public static int maxOrMinTest(List<Integer> list){ Integer[] integers = list.toArray ...

  4. 学不动了!微信官方推出 Web 前端和小程序统一框架 Kbone

    听说最近微信官方推出了一个统一 Web 前端和小程序的框架 -- Kbone ,特意去看了下... 为什么微信要搞Kbone? 微信小程序的底层模型和 Web 端不同,开发者无法直接把 Web 端的代 ...

  5. Spark学习总结

    RDD及其特点 1.RDD是Spark的核心数据模型,但是个抽象类,全称为Resillient Distributed Dataset,即弹性分布式数据集. 2.RDD在抽象上来说是一种元素集合,包含 ...

  6. java axis调用带有soap头(soapheader)的.net webservice

    使用axis调用.net带soapheader的webservice是如何实现的,现在贴出代码 <?xml version="1.0" encoding="utf- ...

  7. 异常java.lang.IllegalArgumentException: An invalid character [32] was present in the Cookie value

    通过HttpServletResponse的addCookie(Cookie cookie)向客户端写cookie信息,这里使用的tomcat版本是8.5.31,出现如下报错: java.lang.I ...

  8. apktool的下载,安装,反编译和重新打包

    一.环境要求 安装java 1.8 以上 命令行运行 java -version 返回版本大于1.8 如果没有,请安装java 1.8 二.下载与安装 下载apktool_x.x.x.jar到本地 官 ...

  9. F2. Same Sum Blocks (Hard) 解析(思維、前綴和、貪心)

    Codeforce 1141 F2. Same Sum Blocks (Hard) 解析(思維.前綴和.貪心) 今天我們來看看CF1141F2(Hard) 題目連結 題目 給你一個數列\(a\),要你 ...

  10. Memcached 的惹祸,.NET 5.0 的背锅

    抱歉,拖到现在才写这篇为 .NET 5.0 洗白的博文(之前的博文),不好意思,又错了,不是洗白,是还 .NET 5.0 的清白. 抱歉,就在今天上午写这篇博客的过程中,由于一个bug被迫在访问高峰发 ...