一、概述
jdk1.4之前提供的io主要是阻塞io(bloking io),服务端需要为每个请求创建一个处理线程,如果没有请求则阻塞等待直到有请求到达;客户端发起请求时,需要判断服务端是否有线程响应,如果有响应,则在响应返回值后继续执行,否则阻塞等待直到响应返回或出现异常。
非阻塞io(no-bloking io)使用单线程或者少量线程等待事件通知,处于等待的线程可以释放出来处理其他请求,当有事件(通过事件驱动模型)通知后,主线程分配资源(线程)处理相关事件。selector就是此模型中的观察者,用户可以把感兴趣的时间注册到selector,当有事件到达时,selector通知主线程处理此事件,当没有感兴趣事件到达时,selector处于阻塞状态。
二、概念模型
无阻塞io使用单线程少量线程等待事件通知,其中selector就是处于观察者模式的核心组件,在没有请求到达时,selector处于阻塞模式,知道请求事件到达或者出现异常,请求事件到达时,selector会从阻塞模式唤醒,selector会调用主线程处理当前请求事件。
客户端请求模型如下图所示:
客户端和服务端所支持的事件并不完全一致,客户端关注读、写和请求链接事件,服务端关注读、写和接受链接事件,ServerSocketChannel和SocketChannel有着共同的父类,除了小部分差异外,两个通道可以互相转换。
三、继承体系及功能
1、Selector的继承体系
四、Selector提供的API
1、Selector API
名称 |
返回值类型 |
功能 |
open |
Selector |
用系统默认的SelectorProvider打开一个selector |
isOpen |
boolean |
判断选择器是否已打开 |
provider |
SelectorProvider |
返回创建选择器的选择器服务提供者 |
keys |
Set |
返回选择器已注册的key集合 |
selectedKeys |
Set |
返回选择器已选择的key集合、及操作事件已就绪的key集合 |
selectNow |
int |
返回关联通道已经就绪的key数量,非阻塞运行,没有通道可选择,则返回0 |
Select(long timeout) |
int |
返回关联通道已经就绪的key数量,非阻塞模式运行,至少有一个通道可选择、wakeup方法被调用、当前线程中断、超时四种情况会导致方法返回 |
wakeup |
Selector |
唤醒一个等待选取key的可用线程,如果当前没有选择操作在进行,则下一个调用选择器的方法将返回。 |
close |
void |
关闭选择器 |
Selector主要用户通道的选择,包括返回当前已注册的key集合,返回当前准备就绪的key集合,唤醒一个阻塞的可用线程等,返回的SelectionKey可以获取当前绑定的通道和选择器。
2、SelectorProvider api
名称 |
返回值 |
功能 |
loadProviderFromProperty |
boolean |
根据系统属性配置加载SelectorProvider实例 |
loadProviderAsService |
boolean |
获取系统加载路径下所有的SelectorProvider实现类,以最后一个为当前SelectorProvider |
provider |
SelectorProvider |
返回jvm默认的SelectorProvider,如果系统属性配置存在,则加载;否则判断系统加载路径下是否配置实例,存在则加载;加载系统默认的实例(区分平台windows、linux)。 |
openDatagramChannel |
DatagramChannel |
打开一个支持UDP通信协议的通道并返回 |
openPipe |
Pipe |
打开一个管道pipe并返回 |
openSelector |
AbstractSelector |
打开一个选择器并返回 |
openServerSocketChannel |
ServerSocketChannel |
打开一个ServerSocketChannel并返回 |
openSocketChannel |
SocketChannel |
打开一个SocketChannel并返回 |
inheritedChannel |
Channel |
返回继承虚拟机创建实例的通道 |
SelectorProvider是为了创建Selector、SocketChannel、ServerSocketChannel、DatagramChannel而存在的,在相应的通道和选择器的open方法中调用系统默认的SelectorProvider的open*方法,创建响应的通道和选择器。
3、SelectionKey api
名称 |
返回值 |
功能 |
channel |
SelectableChannel |
返回SelectionKey关联的通道,即使SelectionKey取消后,这个方法依然会返回通道 |
selector |
Selector |
返回Selector关联的通道,即使Selector取消后,这个方法依然会返回选择器 |
isValid |
boolean |
判断一个SelectionKey是否有效 |
cancel |
void |
取消SelectionKey与通道的注册关系,key将无效,并且将key添加到取消注册集合,在选择器的下一次选择中将会被移除。 |
interestOps |
int |
获取selectionkey感兴趣的操作集合 |
interestOps |
SelectionKey |
设置selectionkey感兴趣的事件 |
readyOps |
int |
获取已经准备就绪的操作事件集 |
isReadable |
boolean |
判断selectionkey的通道是否已经准备好读操作 |
isWritable |
boolean |
判断selectionkey的通道是否已经准备好写操作 |
isConnectable |
boolean |
判断selectionkey的通道是否已经准备好接受链接操作 |
isAcceptable |
boolean |
判断selectionkey的通道是否已经准备好请求链接 |
attach |
Object |
将指定对象设置为附加对象 |
attachment |
Object |
返回当前附加对象 |
SelectionKey表示一个可选择通道与选择器关联的注册器,可以简单理解为一个token。SelectionKey包含两个操作集,分别是兴趣操作事件集interestOps和通道就绪操作事件集readyOps,每个操作集用一个Integer来表示。interestOps用于选择器判断在下一个选择操作的过程中,操作事件是否是通道关注的。兴趣操作事件集在SelectionKey创建时,初始化为注册选择器时的opt值,这个值可能通过interestOps(int)会改变。SelectionKey的readyOps表示一个通道已经准备就绪的操作事件,但不能保证在没有引起线程阻塞的情况下,就绪的操作事件会被线程执行。在一个选择操作完成后,大部分情况下就绪操作事件集会立即更新。如果外部的事件或在通道有IO操作,就绪操作事件集可能不准确。
如果需要经常关联一些应用的特殊数据到SelectionKey,比如一个object表示一个高层协议的状态,object用于通知实现协议处理器。所以,SelectionKey支持通过attach方法将一个对象附加的SelectionKey的attachment上。attachment可以通过#attachment方法进行修改。SelectionKey定义了所有的操作事件,但是具体通道支持的操作事件依赖于具体的通道。所有可选择的通道都可以通过validOps方法,判断一个操作事件是否被通道所支持。测试一个不被通道所支持的通道,将会抛出相关的运行时异常。
SelectionKey多线程并发访问时,是线程安全的。读写兴趣操作事件集的操作都将同步到,选择器的具体操作。同步器执行过程是依赖实现的:在一个本地实现版本中,如果一个选择操作正在进行,读写兴趣操作事件集也许会不确定地阻塞;在一个高性能的实现版本中,可能会简单阻塞。无论任何时候,一个选择操作在操作开始时,选择器总是占用着兴趣操作事件集的值。SelectionKey可以简单理解为通道和选择器的映射关系,并定义了相关的操作事件,分别为OP_READ,OP_WRITE,OP_CONNECT,OP_ACCEPT值分别是,int的值的第四为分别为1,级1,4,8,16。用一个AtomicReferenceFieldUpdater原子更新attachment。
五、selector源码分析
selector设计到的功能主要有打开open()、注册register()、选择select()、和主动唤醒wakeup(),整个阻塞和唤醒的过程设计到的内容非常多,先上一张整体的流程图。
1、Selector.open()
Selector代码如下:
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
主要是返回SelectorProvider,然后调用Provider的openSelector()方法,看下SelectorProvider.provider方法做了什么操作
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction() {
public SelectorProvider run() {
if (loadProviderFromProperty())
return provider;
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
由SelectorProvider api得知,该方法会根据配置返回Provider,首先查看系统配置,如果系统属性已配置,则加载;如果系统属性未配置,则查看系统类加载路径下是否存在provider实例,如果存在,则返回第一个provider作为实例;如果以上都未正确返回provider,则返回系统默认实例 (区分平台,Windows,Linux)。查看create()方法
public static SelectorProvider create() {
String osname = AccessController
.doPrivileged(new GetPropertyAction("os.name"));
if (osname.equals("SunOS"))
return createProvider("sun.nio.ch.DevPollSelectorProvider");
if (osname.equals("Linux"))
return createProvider("sun.nio.ch.EPollSelectorProvider");
return new sun.nio.ch.PollSelectorProvider();
}
可以看到,如果是高版本的Linux,则返回EPollSelectorProvider,如果是低版本的Linux,则返回PollSelectorProvider,终于找到了SelectorProvider的创建方式,我们继续查看SelectorProvider的openSlector方法,以WindowsSelectorProvider为例
public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}
WindowsSelectorImpl(SelectorProvider sp) throws IOException {
super(sp);
pollWrapper = new PollArrayWrapper(INIT_CAP);
wakeupPipe = Pipe.open();
wakeupSourceFd = ((SelChImpl)wakeupPipe.source()).getFDVal();
// Disable the Nagle algorithm so that the wakeup is more immediate
SinkChannelImpl sink = (SinkChannelImpl)wakeupPipe.sink();
(sink.sc).socket().setTcpNoDelay(true);
wakeupSinkFd = ((SelChImpl)sink).getFDVal();
pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
}
WindowsSelectorImpl的初始化做了如下几件事,Pipe.open()打开了一个管道;拿到wakeupSoureFd和wakeupSinkFd两个文件描述符;把唤醒端的文件描述符(wakeupSoureFd)放到了pollWrapper中。为什么需要管道,管道是如何实现的,管道的实现过程中都做了什么工作?
public static Pipe open() throws IOException {
return SelectorProvider.provider().openPipe();
}
熟悉的SelectorProvider返回过程,我们跳过直接看openPipe()方法
public Pipe openPipe() throws IOException {
return new PipeImpl(this);
}
PipeImpl(SelectorProvider sp) {
long pipeFds = IOUtil.makePipe(true);
int readFd = (int) (pipeFds >>> 32);
int writeFd = (int) pipeFds;
FileDescriptor sourcefd = new FileDescriptor();
IOUtil.setfdVal(sourcefd, readFd);
source = new SourceChannelImpl(sp, sourcefd);
FileDescriptor sinkfd = new FileDescriptor();
IOUtil.setfdVal(sinkfd, writeFd);
sink = new SinkChannelImpl(sp, sinkfd);
}
这里是创建pipe的过程,Windows平台使用SourceChannelImpl和SinkChannleImpl实现了管道的source端和sink端,source端由千面的WindowsSlectorImpl放到了PollWrapper中 pollWrapper.addWakeupSocket(wakeupSourceFd, 0);
void addWakeupSocket(int fdVal, int index) {
putDescriptor(index, fdVal);
putEventOps(index, POLLIN);
}
这里讲source的POLLIN事件标识为感兴趣,当sink端有数据写入时,source对应的文件描述符wakeupSourceFd就会处于就绪状态。到此终于完成了Selector.open()方法,主要创建了pipe,并把pipe的wakeupSourceFd放入pollArray中,这个pollArray是selector的枢纽,完成事件流转工作。
2、ServerSocketChannel.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) {
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;
}
}
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);
synchronized (publicKeys) {
implRegister(k);
}
k.interestOps(ops);
return k;
}
继续查看implRegister()方法,以Windows平台的WindowsSelectorImpl为例
protected void implRegister(SelectionKeyImpl ski) {
synchronized (closeLock) {
if (pollWrapper == null)
throw new ClosedSelectorException();
growIfNeeded();
channelArray[totalChannels] = ski;
ski.setIndex(totalChannels);
fdMap.put(ski);
keys.add(ski);
pollWrapper.addEntry(totalChannels, ski);
totalChannels++;
}
}
注册register功能主要就是把SocketChannel的文件描述符放到pollArray中。
3、Selector.select()
public int select(long timeout) throws IOException {
if (timeout < 0)
throw new IllegalArgumentException("Negative timeout");
return lockAndDoSelect((timeout == 0) ? -1 : timeout);
}
private int lockAndDoSelect(long timeout) throws IOException {
synchronized (this) {
if (!isOpen())
throw new ClosedSelectorException();
synchronized (publicKeys) {
synchronized (publicSelectedKeys) {
return doSelect(timeout);
}
}
}
}
protected int doSelect(long timeout) throws IOException {
if (channelArray == null)
throw new ClosedSelectorException();
this.timeout = timeout; // set selector timeout
processDeregisterQueue();
if (interruptTriggered) {
resetWakeupSocket();
return 0;
}
// Calculate number of helper threads needed for poll. If necessary
// threads are created here and start waiting on startLock
adjustThreadsCount();
finishLock.reset(); // reset finishLock
// Wakeup helper threads, waiting on startLock, so they start polling.
// Redundant threads will exit here after wakeup.
startLock.startThreads();
// do polling in the main thread. Main thread is responsible for
// first MAX_SELECTABLE_FDS entries in pollArray.
try {
begin();
try {
subSelector.poll();
} catch (IOException e) {
finishLock.setException(e); // Save this exception
}
// Main thread is out of poll(). Wakeup others and wait for them
if (threads.size() > 0)
finishLock.waitForHelperThreads();
} finally {
end();
}
// Done with poll(). Set wakeupSocket to nonsignaled for the next run.
finishLock.checkForException();
processDeregisterQueue();
int updated = updateSelectedKeys();
// Done with poll(). Set wakeupSocket to nonsignaled for the next run.
resetWakeupSocket();
return updated;
}
这里主要是调用了subSelector.poll(),继续查看
private int poll() throws IOException{ // poll for the main thread
return poll0(pollWrapper.pollArrayAddress,
Math.min(totalChannels, MAX_SELECTABLE_FDS),
readFds, writeFds, exceptFds, timeout);
}
private native int poll0(long pollAddress, int numfds,
int[] readFds, int[] writeFds, int[] exceptFds, long timeout);
到这里,就清楚一些了,退出阻塞(select()方法返回)的方式有:注册在Selector上的SocketChannel处于就绪状态(放在pollArray中的socketchannel的文件描述符就绪);放在pollArray中的wakeupSourceFd就绪。前者就绪就是正常事件到达,也就是正常的阻塞--事件驱动--唤醒的过程,后者是NIO的主动唤醒过程。
4、Selector.wakeup()
public Selector wakeup() {
synchronized (interruptLock) {
if (!interruptTriggered) {
setWakeupSocket();
interruptTriggered = true;
}
}
return this;
}
private void setWakeupSocket() {
setWakeupSocket0(wakeupSinkFd);
}
private native void setWakeupSocket0(int wakeupSinkFd);
这里是个native方法,查看native方法源码:
Java_sun_nio_ch_WindowsSelectorImpl_setWakeupSocket0(JNIEnv *env, jclass this,
jint scoutFd)
{
/* Write one byte into the pipe */
send(scoutFd, (char*)&POLLIN, 1, 0);
}
这里完成了想最开始创建的Pipe的sink端写入一个字节,source的文件描述符处于就绪状态,poll方法就会返回,从而导致select方法返回。所以wakeup方法就是自己给自己写了一个字节,传入了写事件,从而中断select的阻塞,并返回。