NIO源码分析:SelectionKey
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的更多相关文章
- NIO 源码分析(05) Channel 源码分析
目录 一.Channel 类图 二.begin 和 close 是什么 2.1 AbstractInterruptibleChannel 中的 begin 和 close 2.2 Selector 中 ...
- NIO 源码分析(01) NIO 最简用法
目录 一.服务端 二.客户端 NIO 源码分析(01) NIO 最简用法 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) J ...
- NIO 源码分析(04) 从 SelectorProvider 看 JDK SPI 机制
目录 一.SelectorProvider SPI 二.SelectorProvider 加载过程 2.1 SelectorProvider 加载 2.2 Windows 下 DefaultSelec ...
- NIO 源码分析(03) 从 BIO 到 NIO
目录 一.NIO 三大组件 Channels.Buffers.Selectors 1.1 Channel 和 Buffer 1.2 Selector 1.3 Linux IO 和 NIO 编程的区别 ...
- NIO 源码分析(02-2) BIO 源码分析 Socket
目录 一.BIO 最简使用姿势 二.connect 方法 2.1 Socket.connect 方法 2.2 AbstractPlainSocketImpl.connect 方法 2.3 DualSt ...
- NIO 源码分析(02-1) BIO 源码分析
目录 一.BIO 最简使用姿势 二.ServerSocket 源码分析 2.1 相关类图 2.2 主要属性 2.3 构造函数 2.4 bind 方法 2.5 accept 方法 2.6 总结 NIO ...
- NIO-EPollSelectorIpml源码分析
目录 NIO-EPollSelectorIpml源码分析 目录 前言 初始化EPollSelectorProvider 创建EPollSelectorImpl EPollSelectorImpl结构 ...
- NIO - Selector源码分析
1. 背景 SelectableChannel对象的多路复用器. 可以通过调用Selector.open()方法创建Selector对象.Selector.open()方法会利用系统默认的Select ...
- 【Java】NIO中Selector的创建源码分析
在使用Selector时首先需要通过静态方法open创建Selector对象 public static Selector open() throws IOException { return Sel ...
随机推荐
- git仓库之gitlab搭建使用
一.简介 GitLab 是一个用于仓库管理系统的开源项目,使用git作为代码管理工具,并在此基础上搭建起来的web服务.类似github,常用在企业内部做git私有仓库使用: 二.gitlab安装 系 ...
- Rust之路(3)——数据类型 下篇
[未经书面同意,严禁转载] -- 2020-10-14 -- 架构是道,数据是术.道可道,非常道:术不名,不成术!道无常形,术却可循规. 学习与分析数据类型,最基本的方法就是搞清楚其存储原理,变量和对 ...
- SQl编程存储过程
过程化存储 存储过程,一组为完成特定功能.经过编译后存储在数据库中的SQL语序集 灵活性:存储过程中可以进行流程控制和循环操作来完成复杂的判断和运算 一致性:通过存储过程可以使一些关联的操作一起发生, ...
- 不是计算机专业的,可以转专业甚至转行学IT吗?答案揭晓~
相信有这样疑惑的同学不在少数,随着互联网的快速发展,越来越多的人想要转行到IT行业,可又担心自己的专业不对口,影响将来的发展,那么究竟不是计算机专业的可以转行IT吗? 当然是可以的,其实很多的IT大佬 ...
- VitualBox CentOS增强功能的安装使用 - Linux操作系统
本人因为电脑配置原因,安装的是CentOS 6.6 minimal版本,虚拟环境为VirtualBox 4.3.18. 当我使用的时候,想从本机(WindowXP)电脑将文件共享到虚拟(Cen ...
- php使用xpath爬取内容
<?php $html = file_get_contents('https://tieba.baidu.com/f?kw=%C9%EE%BB%A7&fr=ala0&loc=re ...
- scrapy Request方法
# -*- coding: utf-8 -*- import scrapy class TestSpider(scrapy.Spider): name = 'test' allowed_domains ...
- deepin vue安装步骤
deepin安装node.js sudo wget https://nodejs.org/dist/v9.2.0/node-v9.2.0-linux-x64.tar.xz tar xJf node-v ...
- 第十二章 配置vlan
一.vlan技术简介 1.广播风暴 广播风暴(broadcast storm)简单的讲是指当广播数据充斥网络无法处理,并占用大量网络带宽,导致正常业务不能运行,甚至彻底瘫痪,这就发生了"广播 ...
- 彩贝网app破解登入参数(涉及app脱壳,反编译java层,so层动态注册,反编译so层)
一.涉及知识点 app脱壳 java层 so层动态注册 二.抓包信息 POST /user/login.html HTTP/1.1 x-app-session: 1603177116420 x-app ...