自身尚学艺不精,文章写的一塌糊涂,希望海涵,有错误感谢指正。
FD : 文件描述符
ops : 兴趣事件
下面的代码是NIO聊天室启动服务的一段代码
public void startServer() throws IOException {
// 开启一个 ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 设置成非阻塞形式
serverSocketChannel.configureBlocking(false);
// 绑定端口号
serverSocketChannel.bind(new InetSocketAddress(PORT));
// 将通道注册到 Selector 上。该Selector会关心 serverSocketChannel 上的 accept事件
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("## 服务器启动成功");
while (true) {
// 如果就绪事件数量大于1。即说明有关心事件就绪
if (selector.select() > 0) {
// 获取遍历事件进行处理
Set<SelectionKey> selectionKeys = selector.selectedKeys();
selectionKeys.stream().forEach(Lambda.forEach(key -> hanlder(key)));
selectionKeys.clear();
}
}
}
下面通过四个关键方法来讲解整个NIO流程
Selector#open()
:创建了一个 AbstractSelector 的实现类(不同平台上不同的实现类,这里以windows平台为例是 WindowsSelectorImpl)。SelectableChannel#register(java.nio.channels.Selector, int)
:将 channel注册到Selector上。Selector#select()
:阻塞当前线程,直到有注册在Selector上的channel就绪事件发生。Selector#wakeup()
: 上述代码并未体现,用户唤醒阻塞的select 线程。额外的介绍一个类 PollArrayWrapper
。PollArrayWrapper 类中有一个 数组 pollArray
(轮询数据) 。是通过 unsafe.allocateMemory((long)var1)
分配的内存。这个数组里面保存了所有的注册channel的文件描述符和兴趣事件。
整个流程简单,简化、简而又简来说:
PollArrayWrapper
对象 pollWrapper
。这个对象非常重要,可以简单理解为一个数组,用来保存注册的channel的文件描述符fd 和 channel 对应的兴趣事件(Read, Write, Accept, Connect)。SelectableChannel#register(java.nio.channels.Selector, int)
)方法。即是将 channel 的文件描述符FD 和 兴趣事件ops 保存到 pollWrapper 数组中。Selector#select()
方法时。select()
方法 会调用本地方法 poll0(…)。并将 pollWrapper 地址传递过去,poll0方法根据地址获取到 pollWrapper 的内容后,会不断遍历( 阻塞当前线程 )。当有channel对应的fd 有兴趣事件发生时,则会返回。则select()方法的阻塞结束。Selector#wakeup()
则是唤醒阻塞的select()线程。唤醒的原理是通过第一步中建立的Pipe。根据上面说的原理,pollWrapper 中保存的第一个channel其实是Pipe的Source端。当Sink端有数据准备就绪时,Source端的FD就会处于就绪状态。当用户调用wakeup() 方法时,实际上是调用本地方法 setWakeupSocket0(int var1)
向Sink端写入一个字节,这样Source端的FD就会处于就绪状态,这时第三步中的poll0方法中就会发现有就绪事件,则会直接返回。上面简述了整个Selector 的工作流程,下面根据上面贴出的NIO聊天室服务端代码进行具体分析。
Selector.open() 的关键代码在 WindowsSelectorImpl 中(不同平台实现不同,这里是windows平台,所以分析WindowsSelectorImpl)。
整个调用流程如下:
Selector#open()
-> WindowsSelectorProvider#openSelector()
-> SelectorImpl#WindowsSelectorImpl()
具体代码
SelectorProvider.provider()
会根据平台的不同返回不同实现类,这里以window平台为例,返回 WindowsSelectorProvider
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
}
这里去看 WindowsSelectorImpl
类的构造方法
public class WindowsSelectorProvider extends SelectorProviderImpl {
public WindowsSelectorProvider() {
}
public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}
WindowsSelectorImpl 构造函数中做了下面几件事:
Pipe.open()
创建一个管道。(虽然不是在构造函数中写的,姑且算上在这一步完成的)wakeupSourceFd
、wakeupSinkFd
两个文件操作符,并将唤醒端的 wakeupSourceFd
放入 pollWrapper
中。当Sink端写入数据时,Source端的FD就会处于就绪状态。(这里将 wakeupSourceFd
、wakeupSinkFd
放入 pollWrapper
中并不影响用户操作,是为了 wakeup
唤醒操作使用。)WindowsSelectorImpl
构造方法如下,省略了一些不必要的参数。
// 创建一个管道pipe
private final Pipe wakeupPipe = Pipe.open();
// source 端的 fd
private final int wakeupSourceFd;
// sink 端的 fd
private final int wakeupSinkFd;
// 轮询数组的包装类
private PollArrayWrapper pollWrapper = new PollArrayWrapper(8);
// 构造函数
WindowsSelectorImpl(SelectorProvider var1) throws IOException {
super(var1);
// 获取source端的FD
this.wakeupSourceFd = ((SelChImpl)this.wakeupPipe.source()).getFDVal();
// 获取到Sink端的channel
SinkChannelImpl var2 = (SinkChannelImpl)this.wakeupPipe.sink();
var2.sc.socket().setTcpNoDelay(true);
// 获取Sink 端的 FD
this.wakeupSinkFd = var2.getFDVal();
// 将source端的 fd 保存到 pollWrapper 中,默认兴趣事件ops 是 POLLIN, 即输入时间
this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
}
下面进行进一步的拆解
按照如下流程 Pipe#open()
-> SelectorProviderImpl#openPipe
-> PipeImpl#PipeImpl
。
AccessController.doPrivileged
会调用 PipeImpl.Initializer#run
方法,紧接着调用 PipeImpl.Initializer.LoopbackConnector#run
方法。
PipeImpl(SelectorProvider var1) throws IOException {
try {
AccessController.doPrivileged(new PipeImpl.Initializer(var1));
} catch (PrivilegedActionException var3) {
throw (IOException)var3.getCause();
}
}
PipeImpl.Initializer.LoopbackConnector#run
如下 :
可以看到 windows下的实现是通过创建两个本地 SocketChannel
,连接后形成。两个socketChannel分别实现了管道的source与sink端, 并发送了。并且source端由前面提到的WindowsSelectorImpl
放到了pollWrapper
中( this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
)
public void run() {
ServerSocketChannel var1 = null;
// 创建两个 SocketChannel 一个作为Pipe的Source端,一个作为sink端
SocketChannel var2 = null;
SocketChannel var3 = null;
try {
ByteBuffer var4 = ByteBuffer.allocate(16);
ByteBuffer var5 = ByteBuffer.allocate(16);
InetAddress var6 = InetAddress.getByName("127.0.0.1");
assert var6.isLoopbackAddress();
InetSocketAddress var7 = null;
while(true) {
if (var1 == null || !var1.isOpen()) {
// 开启一个 ServerSocketChannel 服务端
var1 = ServerSocketChannel.open();
var1.socket().bind(new InetSocketAddress(var6, 0));
var7 = new InetSocketAddress(var6, var1.socket().getLocalPort());
}
var2 = SocketChannel.open(var7);
PipeImpl.RANDOM_NUMBER_GENERATOR.nextBytes(var4.array());
// 进行链接校验
do {
var2.write(var4);
} while(var4.hasRemaining());
var4.rewind();
var3 = var1.accept();
do {
var3.read(var5);
} while(var5.hasRemaining());
var5.rewind();
if (var5.equals(var4)) {
// 指定Pipe的 Source 端和 sink 端
PipeImpl.this.source = new SourceChannelImpl(Initializer.this.sp, var2);
PipeImpl.this.sink = new SinkChannelImpl(Initializer.this.sp, var3);
break;
}
var3.close();
var2.close();
}
} catch (IOException var18) {
try {
if (var2 != null) {
var2.close();
}
if (var3 != null) {
var3.close();
}
} catch (IOException var17) {
}
Initializer.this.ioe = var18;
} finally {
try {
if (var1 != null) {
var1.close();
}
} catch (IOException var16) {
}
}
}
}
在取得 Source 和Sink 的FD后,后面还有一步将 Source 端的FD 保存到pollWrapper中。
// 将source端的 fd 保存到 pollWrapper 中,默认兴趣事件ops 是 POLLIN, 即输入时间
this.pollWrapper.addWakeupSocket(this.wakeupSourceFd, 0);
PollArrayWrapper#addWakeupSocket方法代码如下, 省略一些不必要的参数信息 :
可以看到 addWakeupSocket
方法调用了 putDescriptor
保存fd 和 putEventOps
保存 兴趣事件ops。经过这一步,即将 Source端的 FD 保存到 PollArray中。当Sink端写入数据时, Source端的FD就会处于就绪状态,Selector.select()
方法就会被唤醒,不再阻塞,这一点后面再讲。
void addWakeupSocket(int var1, int var2) {
this.putDescriptor(var2, var1);
this.putEventOps(var2, Net.POLLIN);
}
// 保存文件描述符fd
void putDescriptor(int var1, int var2) {
this.pollArray.putInt(SIZE_POLLFD * var1 + 0, var2);
}
// 保存兴趣事件ops
void putEventOps(int var1, int var2) {
this.pollArray.putShort(SIZE_POLLFD * var1 + 4, (short)var2);
}
Selector.open 方法中保存的Source 和Sink 的FD 和用户操作无关联,仅仅是为了实现wakeup的唤醒操作,通过 SelectableChannel#register(java.nio.channels.Selector, int) 注册的事件才是真正和用户相关的事件。
调用链如下:
SelectableChannel#register(java.nio.channels.Selector, int)
-> AbstractSelectableChannel#register
从下面代码可以看到,流程如下
SelectionKey k = findKey(sel);
来判断当前channel是否已经注册在 Selector 上(可以看到是否注册是通过一个keys
是否包含来判断的。在后面的 implRegister
方法调用中会将注册的Channel 添加进去)。k.interestOps(ops);
)。AbstractSelector#register
方法进行注册下面从 AbstractSelectableChannel#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();
// 查找是否已经注册过该 channel。
SelectionKey k = findKey(sel);
if (k != null) {
// 如果已经保存了,则在SelectionKey上绑定新的兴趣事件和附件对象。
k.interestOps(ops);
k.attach(att);
}
// 如果该channel 没有注册过,则进行注册
if (k == null) {
synchronized (keyLock) {
if (!isOpen())
throw new ClosedChannelException();
// 注册一个新的 SelectionKey
k = ((AbstractSelector)sel).register(this, ops, att);
// 并添加到SelectionKey[] keys 集合中汇总。
addKey(k);
}
}
return k;
}
}
...
// 添加SelectionKey到 set集合中。
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++;
}
...
//
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;
}
}
上面说到如果channel没有注册到当前选择器则调用 AbstractSelector#register
方法进行注册。那么继续分析AbstractSelector#register
方法。这里来看 AbstractSelector#register
的实现方法 SelectorImpl#register
可以看到 register方法流程如下:
implRegister(SelectionKeyImpl var1)
来进行channel的注册。这个后面详解 protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
if (!(var1 instanceof SelChImpl)) {
throw new IllegalSelectorException();
} else {
// 创建一个新的SelectionKeyImpl。绑定附件对象。
SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
var4.attach(var3);
synchronized(this.publicKeys) {
// 注册文件描述符
this.implRegister(var4);
}
// 注册兴趣事件
var4.interestOps(var2);
return var4;
}
}
核心方法在: WindowsSelectorImpl#implRegister
下面重点分析implRegister 方法:
代码如下:
protected void implRegister(SelectionKeyImpl var1) {
synchronized(this.closeLock) {
if (this.pollWrapper == null) {
throw new ClosedSelectorException();
} else {
// 如果需要,扩容pollWrapper
this.growIfNeeded();
// channelArray 保存当前 SelectionKeyImpl
this.channelArray[this.totalChannels] = var1;
var1.setIndex(this.totalChannels);
this.fdMap.put(var1);
// 添加将要注册的Channel信息,用来上面判断上面是否已经注册
this.keys.add(var1);
// 将新的SelectorKeyImpl 保存到 pollWrapper 上。
this.pollWrapper.addEntry(this.totalChannels, var1);
++this.totalChannels;
}
}
}
...
// growIfNeeded 做了两件事
// 1. 扩容 channelArray 和 pollWrapper 数组大小
// 2. 每当 totalChannels 数量达到1024个时,增加一个线程。windows上select系统调用有最大文件描述符限制,一次只能轮询1024个文件描述符,如果多于1024个,需要多线程进行轮询。
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;
}
}
implRegister 重点的一句就是 this.pollWrapper.addEntry(this.totalChannels, var1);
。addEntry方法具体如下。我们可以看到,他将当前 SelectionKeyImpl 中保存的channel的FD保存到了 pollWrapper 中。这时候pollWrapper中现在就保存了Source 端的 FD 和当前用户 channel 的FD。
void addEntry(int var1, SelectionKeyImpl var2) {
this.putDescriptor(var1, var2.channel.getFDVal());
}
接下来看 interestOps 的是实现。如下:
SelectionKeyImpl#interestOps(int)
-> SelectionKeyImpl#nioInterestOps(int)
-> SocketChannelImpl#translateAndSetInterestOps
(这一步里面有多个实现,这里列举其中一个实现,其余的都差不多)。
可以看到就是判断事件的类型,然后将兴趣事件保存到 pollWrapper 数组中。
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);
}
// 保存兴趣事件
public void putEventOps(SelectionKeyImpl var1, int var2) {
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.open()
和 Selector.register
的流程,下面来分析一下 SelectionKey.select()
方法的流程。
无论是select()
还是 select(long timeout)
亦或是 selectNow()
。最终都会调用 WindowsSelectorImpl#doSelect
方法。所以这里列举select() 方法为例。
调用链路如下: SelectorImpl#select()
-> SelectorImpl#select(long)
->SelectorImpl#lockAndDoSelect
-> SelectorImpl#doSelect
-> WindowsSelectorImpl#doSelect
可以看到整个流程如下:
processDeregisterQueue()
方法对已经取消的 keys 进行注销操作。通过调用cancel()方法将选择键加入已取消的键集合中,这个键并不会立即注销,而是在下一次select操作时进行注销。adjustThreadsCount ()
方法调整守护线程数量,可能增加也可能减少(根据上面growIfNeededregister
注册的channel数量来判断 -> growIfNeeded
方法中会根据注册的数量来调整线程数量 threadsCount
)。this.subSelector.poll();
调用本地方法 poll0 阻塞线程,当有兴趣事件就绪时,才会返回。 protected int doSelect(long var1) throws IOException {
if (this.channelArray == null) {
throw new ClosedSelectorException();
} else {
// 设置超时时间
this.timeout = var1;
// 对已取消的键集合进行处理,通过调用cancel()方法将选择键加入已取消的键集合中,这个键并不会立即注销,而是在下一次select操作时进行注销,注销操作在implDereg完成
this.processDeregisterQueue();
if (this.interruptTriggered) {
this.resetWakeupSocket();
return 0;
} else {
// 必要情况下调整线程数量。在上面 growIfNeeded 方法中进行调整过 threadsCount 数量
this.adjustThreadsCount();
this.finishLock.reset();
this.startLock.startThreads();
try {
this.begin();
try {
// 调用本地的native方法,将之前保存的的pollWrapper数据地址传递过去。这一步是关键
this.subSelector.poll();
} catch (IOException var7) {
this.finishLock.setException(var7);
}
if (this.threads.size() > 0) {
this.finishLock.waitForHelperThreads();
}
} finally {
this.end();
}
this.finishLock.checkForException();
this.processDeregisterQueue();
int var3 = this.updateSelectedKeys();
this.resetWakeupSocket();
return var3;
}
}
}
...
private void adjustThreadsCount() {
int var1;
if (this.threadsCount > this.threads.size()) {
for(var1 = this.threads.size(); var1 < this.threadsCount; ++var1) {
WindowsSelectorImpl.SelectThread var2 = new WindowsSelectorImpl.SelectThread(var1);
this.threads.add(var2);
var2.setDaemon(true);
var2.start();
}
} else if (this.threadsCount < this.threads.size()) {
for(var1 = this.threads.size() - 1; var1 >= this.threadsCount; --var1) {
((WindowsSelectorImpl.SelectThread)this.threads.remove(var1)).makeZombie();
}
}
}
其中
adjustThreadsCount() 方法中创建的线程可以看到是 WindowsSelectorImpl.SelectThread。即 WindowsSelectorImpl 的 内部类。其run方法如下。可以看到,新创建的线程会阻塞在 waitForStart() 方法上。当主线程调用 this.startLock.startThreads(); 时才会被唤醒,并且和主线程一起轮询就绪事件。
// SelectThread.run 方法。
public void run() {
for(; !WindowsSelectorImpl.this.startLock.waitForStart(this); WindowsSelectorImpl.this.finishLock.threadFinished()) {
try {
this.subSelector.poll(this.index);
} catch (IOException var2) {
WindowsSelectorImpl.this.finishLock.setException(var2);
}
}
}
...
// waitForStart 方法,和 run方法并不在一个类中。
private synchronized boolean waitForStart(WindowsSelectorImpl.SelectThread var1) {
while(this.runsCounter == var1.lastRun) {
try {
WindowsSelectorImpl.this.startLock.wait();
} catch (InterruptedException var3) {
Thread.currentThread().interrupt();
}
}
if (var1.isZombie()) {
return true;
} else {
var1.lastRun = this.runsCounter;
return false;
}
}
this.subSelector.poll() 调用本地方法poll0如下,poll0会阻塞。可以看到,poll0将 pollWrapper的地址传递过去。poll0方法会轮询 pollWrapper 中的channel的FD,并将结果记录在临时表中,当有兴趣事件就绪时,则会被唤醒,则会停止轮询并返回。
private final int[] readFds = new int [MAX_SELECTABLE_FDS + 1];//readFds保存发生read的FD
private final int[] writeFds = new int [MAX_SELECTABLE_FDS + 1];//writeFds保存发生写的FD
private final int[] exceptFds = new int [MAX_SELECTABLE_FDS + 1];//exceptFds保存发生except的FD
private int poll() throws IOException {
return this.poll0(WindowsSelectorImpl.this.pollWrapper.pollArrayAddress, Math.min(WindowsSelectorImpl.this.totalChannels, 1024), this.readFds, this.writeFds, this.exceptFds, WindowsSelectorImpl.this.timeout);
}
wakeup用来立即唤醒select()线程。根据代码可以看到,最终调用了本地方法 setWakeupSocket0 。
其内部原理是,当用户调用wakeup方法时,系统会对Sink端写入一个字节,此时 Source端的FD就会处于就绪状态,根据上面介绍的select() 原理,当有就绪的事件时就会返回。
public Selector wakeup() {
synchronized(this.interruptLock) {
if (!this.interruptTriggered) {
this.setWakeupSocket();
this.interruptTriggered = true;
}
return this;
}
}
...
private void setWakeupSocket() {
this.setWakeupSocket0(this.wakeupSinkFd);
}
...
private native void setWakeupSocket0(int var1);
以上:内容部分参考
http://www.myexception.cn/program/1598318.html
https://www.iteye.com/blog/goon-1775421
https://blog.csdn.net/aesop_wubo/article/details/9117655
https://blog.csdn.net/u010412719/article/details/52809669
https://blog.csdn.net/u010412719/article/details/52819191
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正