Channel的注册是在SelectableChannel中定义的:

 public abstract SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException; public final SelectionKey register(Selector sel, int ops)
throws ClosedChannelException {
return register(sel, ops, null);
}

而其具体实现是在AbstractSelectableChannel中:

 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) {
k.interestOps(ops);
k.attach(att);
}
if (k == null) {
// New registration
synchronized (keyLock) {
if (!isOpen())
throw new ClosedChannelException();
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
}
return k;
}
}

其中regLock和keyLock是两个对象,分别用来做注册锁和key集合锁

 // Lock for key set and count
private final Object keyLock = new Object(); // Lock for registration and configureBlocking operations
private final Object regLock = new Object();

isOpen判断Channel是否关闭,只有在Channel关闭后才会令isOpen返回false;接着检验传入进来的ops(SelectionKey的状态,包括OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT四种,还包括一个0的缺省状态)是否满足条件,validOps方法在不同的Channel子类中有不同的实现:
SocketChannel中:

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

那么ops只要是上面三种状态的任意一种或者一种以上(还包括一个0的缺省状态),再和validOps的结果运算都为0,若是其他值则抛出IllegalArgumentException异常;
ServerSocketChannel中:

 public final int validOps() {
return SelectionKey.OP_ACCEPT;
}

和上面同理ServerSocketChannel在注册时,只能传入OP_ACCEPT状态(还包括一个0的缺省状态)。

回到AbstractSelectableChannel的register方法,接下来是对blocking成员的判断,

 boolean blocking = true;

这是很重要的一步,因为NIO是既支持阻塞模式也支持非阻塞模式,但是若使用非阻塞模式,那么必然需要Selector的轮询,若是在注册Selector之前没有通过Channel调用configureBlocking方法设置为非阻塞模式,那么就会在此时注册时抛出IllegalBlockingModeException异常。

configureBlocking方法的实现也是在AbstractSelectableChannel中:

 public final SelectableChannel configureBlocking(boolean block)
throws IOException {
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if (blocking == block)
return this;
if (block && haveValidKeys())
throw new IllegalBlockingModeException();
implConfigureBlocking(block);
blocking = block;
}
return this;
}

前两个判断逻辑都很简单,在Channel打开的情况下根据参数block设置阻塞或者非阻塞模式,注意到第二个判断说明重复设置相同的阻塞模式直接返回,而第三个判断则表明block 和blocking不相等,那么就是在之前设置为了非阻塞模式,而haveValidKeys则间接表明已经完成了注册,并且已经拥有了自己的SelectionKey集合,此时再设置为非阻塞模式就会引起IllegalBlockingModeException异常。

haveValidKeys方法:

 private SelectionKey[] keys = null;
private int keyCount = 0; private boolean haveValidKeys() {
synchronized (keyLock) {
if (keyCount == 0)
return false;
for (int i = 0; i < keys.length; i++) {
if ((keys[i] != null) && keys[i].isValid())
return true;
}
return false;
}
}

逻辑比较简单,先检查keys的个数,为0直接返回没有可用的SelectionKey,接着遍历keys集合,找到一个可用的就返回true,其中isValid方法在AbstractSelectionKey中实现:

 private volatile boolean valid = true;

 public final boolean isValid() {
return valid;
}

可以看到在初始化时valid = true就代表自身是可用状态,当SelectionKey执行cancel方法撤销时或者在Channel关闭时的撤销都会改变:

 public final void cancel() {
// Synchronizing "this" to prevent this key from getting canceled
// multiple times by different threads, which might cause race
// condition between selector's select() and channel's close().
synchronized (this) {
if (valid) {
valid = false;
((AbstractSelector)selector()).cancel(this);
}
}
}

Channel关闭时的撤销在后续的博客给出,这里先不讨论。

在configureBlocking中的implConfigureBlocking是一个抽象方法,具体的实现和使用的Channel有关,ServerSocketChannel和SocketChannel的实现分别是在ServerSocketChannelImpl和
SocketChannelImpl中,这两个的实现方式也是完全一样:

 protected void implConfigureBlocking(boolean var1) throws IOException {
IOUtil.configureBlocking(this.fd, var1);
}

而IOUtil的configureBlocking方法是一个native方法,主要是对底层的操作,这里就不讨论了。

继续回到AbstractSelectableChannel的register方法,在对阻塞模式判断完毕后,调用findKey方法:

 private SelectionKey findKey(Selector sel) {
synchronized (keyLock) {
if (keys == null)
return null;
for (int i = 0; i < keys.length; i++)
if ((keys[i] != null) && (keys[i].selector() == sel))
return keys[i];
return null;
}
}

在同步块中,首先判断keys是否初始化过,如果是第一次注册,那么keys必定为null,直接就返回null结束;否则已经注册过,则遍历keys这个SelectionKey集合,找的传入的Selector 持有的SelectionKey后直接返回该SelectionKey对象,若没找到则返回null;

接着对findKey方法的返回值k判断
若k不为null,则说明注册过这个Selector ,先调用interestOps方法,该方法是在SelectionKeyImpl中实现的:

 public SelectionKey interestOps(int var1) {
this.ensureValid();
return this.nioInterestOps(var1);
}

首先通过ensureValid检验当前的SelectionKey是否可用(没有被撤销,调用cancel方法会撤销):

 private void ensureValid() {
if (!this.isValid()) {
throw new CancelledKeyException();
}
}

比较简单,使用之前说过的isValid方法,检查当前SelectionKey是否可用
nioInterestOps方法:

 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;
}
}

这个判断和一开始的register中的检查ops状态是否合法一样,若是合法需要调用Channel的translateAndSetInterestOps方法,同样不同的Channel有不同的实现:

SocketChannel是在SocketChannelImpl中实现的:

 public void translateAndSetInterestOps(int var1, SelectionKeyImpl var2) {
int var3 = 0;
if ((var1 & 1) != 0) {
var3 |= Net.POLLIN;
} if ((var1 & 4) != 0) {
var3 |= Net.POLLOUT;
} if ((var1 & 8) != 0) {
var3 |= Net.POLLCONN;
} var2.selector.putEventOps(var2, var3);
}

之前说过SelectionKey有四种状态(还包括一个0的缺省状态):

 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; //

正如之前所说的SocketChannel只允许存在OP_READ、OP_WRITE 、OP_CONNECT 这三种状态(还包括一个0的缺省状态),所以上面就根据这三种状态得到对应的POLL事件,最后给SelectionKey绑定的Selector设置POLL事件响应。

putEventOps的实现是在WindowsSelectorImpl中:

 public void putEventOps(SelectionKeyImpl var1, int var2) {
Object var3 = this.closeLock;
synchronized(this.closeLock) {
if (this.pollWrapper == null) {
throw new ClosedSelectorException();
} else {
int var4 = var1.getIndex();
if (var4 == -1) {
throw new CancelledKeyException();
} else {
this.pollWrapper.putEventOps(var4, var2);
}
}
}
}

还是一样若是Selector关闭则抛出异常,否则得到SelectionKey的index(在Selector中存储的SelectionKey数组的下标),判断下标的合法性,然后给pollWrapper设置事件响应,而pollWrapper的putEventOps方法是一个native方法,这里就不仔细讨论了。

pollWrapper是存放socket句柄fdVal和事件响应events的,用八个位来存储一对。

而ServerSocketChannel的translateAndSetInterestOps实现和上面一样,只不过只负责OP_ACCEPT 状态(还包括一个0的缺省状态):

 public void translateAndSetInterestOps(int var1, SelectionKeyImpl var2) {
int var3 = 0;
if ((var1 & 16) != 0) {
var3 |= Net.POLLIN;
} var2.selector.putEventOps(var2, var3);
}

还是回到AbstractSelectableChannel的register方法中,interestOps调用结束后调用SelectionKey的attach方法:

 private volatile Object attachment = null;

 private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
SelectionKey.class, Object.class, "attachment"
); public final Object attach(Object ob) {
return attachmentUpdater.getAndSet(this, ob);
}

这里直接使用了原子更新器对象来更新attachment 。

k不为null的情况解决了,接下来就是解决k为null的情况,即第一次注册,或者是再次注册没有找到和Selector对应的的SelectionKey。
首先在同步块内还是先检查Channel是否关闭,若没有关闭,调用AbstractSelector的register方法完成Selector对SelectionKey的注册:
而这个register方法的实现是在SelectorImpl中:

 protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
if (!(var1 instanceof SelChImpl)) {
throw new IllegalSelectorException();
} else {
SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
var4.attach(var3);
Set var5 = this.publicKeys;
synchronized(this.publicKeys) {
this.implRegister(var4);
} var4.interestOps(var2);
return var4;
}
}

检查Channel类型是否符合,然后直接创建一个SelectionKeyImpl对象:

 final SelChImpl channel;
public final SelectorImpl selector; SelectionKeyImpl(SelChImpl var1, SelectorImpl var2) {
this.channel = var1;
this.selector = var2;
}

SelectionKeyImpl构造很简单,直接给两个成员赋值;然后调用SelectionKeyImpl对象的attach方法更新附件,接着在同步块中调用抽象方法implRegister
implRegister方法是在WindowsSelectorImpl中实现的:

 protected void implRegister(SelectionKeyImpl var1) {
Object var2 = this.closeLock;
synchronized(this.closeLock) {
if (this.pollWrapper == null) {
throw new ClosedSelectorException();
} else {
this.growIfNeeded();
this.channelArray[this.totalChannels] = var1;
var1.setIndex(this.totalChannels);
this.fdMap.put(var1);
this.keys.add(var1);
this.pollWrapper.addEntry(this.totalChannels, var1);
++this.totalChannels;
}
}
}

首先调用growIfNeeded方法,因为Selector选择器解决非阻塞,就是使用轮询的方式,它存储了一个SelectionKeyImpl数组,而SelectionKeyImpl记录了channel以及SelectionKey的状态,那么就是根据SelectionKey的状态和channel来完成通信。由于在服务端的时候需要和多个客户端连接,那么这个数组必定是动态维持的,所以就考虑到扩容。

 private SelectionKeyImpl[] channelArray = new SelectionKeyImpl[8];
private int totalChannels = 1;

可以看到这个channelArray一开始固定初始化大小是8,而totalChannels 一开始就是1,这是为了方便后面的操作,channelArray 中下标为0的元素没用使用,直接从下标为1开始。

growIfNeeded方法:

 private void growIfNeeded() {
if (this.channelArray.length == this.totalChannels) {
int var1 = this.totalChannels * 2;
SelectionKeyImpl[] var2 = new SelectionKeyImpl[var1];
System.arraycopy(this.channelArray, 1, var2, 1, this.totalChannels - 1);
this.channelArray = var2;
this.pollWrapper.grow(var1);
} if (this.totalChannels % 1024 == 0) {
this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, this.totalChannels);
++this.totalChannels;
++this.threadsCount;
} }

因为totalChannels 是从1开始,所以直接判断totalChannels是否达到了数组长度,若已达到就需要扩容,可以看到每次扩容都是原来两倍,从原数组下标为1的地方开始一直到最后一个元素,拷贝到新数组下标为1的位置上,再更新channelArray,同时还要给pollWrapper扩容。

pollWrapper的grow方法:

 void grow(int var1) {
PollArrayWrapper var2 = new PollArrayWrapper(var1); for(int var3 = 0; var3 < this.size; ++var3) {
this.replaceEntry(this, var3, var2, var3);
} this.pollArray.free();
this.pollArray = var2.pollArray;
this.size = var2.size;
this.pollArrayAddress = this.pollArray.address();
} void replaceEntry(PollArrayWrapper var1, int var2, PollArrayWrapper var3, int var4) {
var3.putDescriptor(var4, var1.getDescriptor(var2));
var3.putEventOps(var4, var1.getEventOps(var2));
}

逻辑很简单,就是把原来的socket句柄fdVal和事件响应events复制到新的PollArrayWrapper对象中,且位置不变。

再回到growIfNeeded,可以看到第二个判断是检查totalChannels是否达到了1024的整数次方(totalChannels初始是1,排除0),若是则需要pollWrapper.addWakeupSocket(this.wakeupSourceFd, this.totalChannels)这个操作在WindowsSelectorImpl构造方法时也被调用:

 WindowsSelectorImpl(SelectorProvider var1) throws IOException {
super(var1);
this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal();
SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink();
var2.sc.socket().setTcpNoDelay(true);
this.wakeupSinkFd = var2.getFDVal();
this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
}

addWakeupSocket方法:

 void addWakeupSocket(int var1, int var2) {
this.putDescriptor(var2, var1);
this.putEventOps(var2, Net.POLLIN);
}

可以看到设置的事件响应是Net.POLLIN,其实就对应OP_READ,而这个wakeupSourceFd是初始化时就设置的wakeupPipe的source的描述符fdVal,即一开始建立的ServerSocketChannel端的SocketChannel(SourceChannel)的描述符fdVal,之前说过Selector的select方法是一个阻塞的操作,调用select方法时只有注册在Selector上的Channel有事件就绪时才会被唤醒;如果说有很多Channel注册了,但是只有一个Channel事件就绪,那么岂不是要做很多无用的轮询,而fdVal就是解决这个问题,用于之后的唤醒。
可以看到在growIfNeeded后面有一个++this.threadsCount操作,实际上Channel事件的轮询是交给线程来做的,WindowsSelectorImpl中有如下成员:

 private int threadsCount = 0;
private final List<WindowsSelectorImpl.SelectThread> threads = new ArrayList();SelectThread是Thread的子类,threadsCount记录轮询线程个数。

那么就有这种关系,在pollWrapper中,总是以wakeupSourceFd描述符开头,后面跟着1024个Channel的描述,再往后就又是这种形式;所以在totalChannels达到1024的整数次方时,需要增加新的轮询线程。

而这个wakeupSourceFd的起始标志,是用于进行轮询时的唤醒,当有一个线程轮询到了描述符就绪,而其他线程还一直在轮询,总不能让这个线程一直等其它线程轮询完,若是一直没轮循到,岂不是一直要等,明明已经轮循到描述符就绪,所以此时使用一开始建立的双向通道,向wakeupSourceFd发送一字节的消息,那么wakeupSourceFd上就有描述符就绪,所有线程都能够感知到,轮询就能结束。

growIfNeeded方法结束,channelArray中增添新的SelectionKeyImpl,并且设置下标(呼应前面获取下标的操作),然后将SelectionKeyImpl存放在fdMap
fdMap保存的时Channel的描述符和SelectionKeyImpl的映射关系:

 private static final class MapEntry {
SelectionKeyImpl ski;
long updateCount = 0L;
long clearedCount = 0L; MapEntry(SelectionKeyImpl var1) {
this.ski = var1;
}
} private static final class FdMap extends HashMap<Integer, WindowsSelectorImpl.MapEntry> {
static final long serialVersionUID = 0L; private FdMap() {
} private WindowsSelectorImpl.MapEntry get(int var1) {
return (WindowsSelectorImpl.MapEntry)this.get(new Integer(var1));
} private WindowsSelectorImpl.MapEntry put(SelectionKeyImpl var1) {
return (WindowsSelectorImpl.MapEntry)this.put(new Integer(var1.channel.getFDVal()), new WindowsSelectorImpl.MapEntry(var1));
} private WindowsSelectorImpl.MapEntry remove(SelectionKeyImpl var1) {
Integer var2 = new Integer(var1.channel.getFDVal());
WindowsSelectorImpl.MapEntry var3 = (WindowsSelectorImpl.MapEntry)this.get(var2);
return var3 != null && var3.ski.channel == var1.channel ? (WindowsSelectorImpl.MapEntry)this.remove(var2) : null;
}
}

代码逻辑都很简单,就不详细介绍了。

接着调用keys的add方法,keys是父类SelectorImpl的成员:

 protected HashSet<SelectionKey> keys = new HashSet();

接着调用pollWrapper的addEntry方法:

 void addEntry(int var1, SelectionKeyImpl var2) {
this.putDescriptor(var1, var2.channel.getFDVal());
}

可以看到仅仅是添加了channel的描述符fdVal,还没有设置事件响应,最后totalChannels自增implRegister方法结束。

回到SelectorImpl的register方法,在implRegister方法结束后,调用SelectionKeyImpl的interestOps方法,前面说过的,在此时设置了事件响应,最后返回SelectionKeyImpl对象赋给AbstractSelectableChannel方法中的k,之后调用addKey方法,返回k,register方法调用全部结束。

addKey方法:

 private void addKey(SelectionKey k) {
assert Thread.holdsLock(keyLock);
int i = 0;
if ((keys != null) && (keyCount < keys.length)) {
// Find empty element of key array
for (i = 0; i < keys.length; i++)
if (keys[i] == null)
break;
} else if (keys == null) {
keys = new SelectionKey[3];
} else {
// Grow key array
int n = keys.length * 2;
SelectionKey[] ks = new SelectionKey[n];
for (i = 0; i < keys.length; i++)
ks[i] = keys[i];
keys = ks;
i = keyCount;
}
keys[i] = k;
keyCount++;
}

逻辑很清晰,首先检查有没有没有使用的key,若存在,直接用k覆盖结束;若keys没有初始化大小为3的数组,先初始化keys,再将k放在下标为0的位置结束;若是keys已经初始化且keyCount == keys.length,就需要给keys扩容,并将原来的元素拷贝,最后将k放在新keys下标为keyCount的位置。

Channel的注册到此全部结束。

【Java】NIO中Channel的注册源码分析的更多相关文章

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

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

  2. Netty中NioEventLoopGroup的创建源码分析

    NioEventLoopGroup的无参构造: public NioEventLoopGroup() { this(0); } 调用了单参的构造: public NioEventLoopGroup(i ...

  3. RocketMQ中Broker的启动源码分析(二)

    接着上一篇博客  [RocketMQ中Broker的启动源码分析(一)] 在完成准备工作后,调用start方法: public static BrokerController start(Broker ...

  4. Spring Cloud Eureka服务注册源码分析

    Eureka是怎么work的 那eureka client如何将本地服务的注册信息发送到远端的注册服务器eureka server上.通过下面的源码分析,看出Eureka Client的定时任务调用E ...

  5. RocketMQ中Broker的启动源码分析(一)

    在RocketMQ中,使用BrokerStartup作为启动类,相较于NameServer的启动,Broker作为RocketMQ的核心可复杂得多 [RocketMQ中NameServer的启动源码分 ...

  6. RocketMQ中PullConsumer的启动源码分析

    通过DefaultMQPullConsumer作为默认实现,这里的启动过程和Producer很相似,但相比复杂一些 [RocketMQ中Producer的启动源码分析] DefaultMQPullCo ...

  7. RocketMQ中Broker的消息存储源码分析

    Broker和前面分析过的NameServer类似,需要在Pipeline责任链上通过NettyServerHandler来处理消息 [RocketMQ中NameServer的启动源码分析] 实际上就 ...

  8. JDK中String类的源码分析(二)

    1.startsWith(String prefix, int toffset)方法 包括startsWith(*),endsWith(*)方法,都是调用上述一个方法 public boolean s ...

  9. Springboot中mybatis执行逻辑源码分析

    Springboot中mybatis执行逻辑源码分析 在上一篇springboot整合mybatis源码分析已经讲了我们的Mapper接口,userMapper是通过MapperProxy实现的一个动 ...

随机推荐

  1. file标签样式修改

    1. 这是默认的file样式,无法修改,在网页中用它感觉非常不合群,大部分修改的办法就是把它隐藏,绝对定位一个文本框和一个按钮 这是修改后的样式,之后修改样式就是分别修改文本框和按钮样式了,就非常简单 ...

  2. php.ini的几个关键配置

    safe_mode = On safe_mode_gid = Off disable_functions = system,passthru,exec,shell_exec,popen,phpinfo ...

  3. Javascript模块化编程-require.js[3]

    很多情况下,JS都是放到一个或者多个文件里,只要加载这些文件就可以了. 但是对于一些小型项目而言,这种写法是没有任何问题的. 但是对于某些大型网站,JS的量是很大的,如果还采用这种方式,网站时常在加载 ...

  4. html乱码怎问题

    大家会不会常常遇到中文乱码的情况?html中文乱码问题该怎么调? <标签名 lang=lang> - 指定语言种类 lang 属性能够指定标签范围内的元素的语言种类. 英语lang=&qu ...

  5. 高性能 Socket 组件 HP-Socket v3.2.1-RC1 公布

    HP-Socket 是一套通用的高性能 TCP/UDP Socket 组件.包括服务端组件.client组件和 Agent 组件.广泛适用于各种不同应用场景的 TCP/UDP 通信系统.提供 C/C+ ...

  6. Latex 4: WinEdt 10试用时间限制的破解+注册码激活

    方法1:我发现这个方法1,现在(2018.06.05)在winedt 10.2上已经不能用了,在低版本(10.1及以下版本)上还可以用,所以如果方法1不行,请看方法2. WinEdt 是目前我发现最好 ...

  7. SAP FI 科目代码

    资产类 现金 银行存款 其他货币资金 短期投资 短期投资跌价准备 应收票据 应收股利 应收利息 应收账款 其他应收款 坏账准备 预付账款 应收补贴款 物料采购 原材料 包装物 低值易耗品 材料成本差异 ...

  8. java集合讲解干货集

    文章都来自网络,收集后便于查阅. 1.Java 集合系列01之 总体框架 2.Java 集合系列02之 Collection架构 3.Java 集合系列03之 ArrayList详细介绍(源码解析)和 ...

  9. debian下烧写stm32f429I discovery裸机程序

    需要安装openocd软件.如果已安装默认的openocd,需要先卸载系统默认的openocd(默认版本是0.5.0,版本太低),然后再安装. 在安装前需要安装libusb库文件: -dev libu ...

  10. 大数据之路- Hadoop环境搭建(Linux)

    前期部署 1.JDK 2.上传HADOOP安装包 2.1官网:http://hadoop.apache.org/ 2.2下载hadoop-2.6.1的这个tar.gz文件,官网: https://ar ...