JDK IO多路复用基础
本文所有源码均基于JDK1.8版本
通过学习JDK提供的IO多路复用相关源码,为后续的Netty源码学习打下基础
IO多路复用底层实现
JDK nio包多路复用基于底层操作系统,linux2.6版本使用epoll实现,低于2.6版本则使用select或poll实现多路复用
JDK NIO
Channel
该接口用于操作(读,写)数据源(文件,网络socket等),并提供多种实现如FileChannel, ServerSocketChannel等,替换基于流的形式操作数据源
-
通道是全双工的,它可以比流更好地映射底层操作系统的API。特别是在UNIX网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作
-
Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据
SelectorProvider
一个抽象类,用于selector和可选择的channel上,包括打开一个selector,打开一个 server-socket channel,或socket channel,同时该类持有自身类的一个属性provider
该类提供了一个静态方法provider(),用于获取当前操作系统范围下默认的支持的selector provider
public abstract class SelectorProvider {
private static final Object lock = new Object();
// 持有自身实例的引用
private static SelectorProvider provider = null;
// 获取可用的selectorProvider
public static SelectorProvider provider() {
synchronized (lock) {
// 如果已存在,直接返回
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
// 1. 从属性配置中利用反射实例化selectorProvider
if (loadProviderFromProperty())
return provider;
// 2. 利用Java SPI加载(META-INF/services/)并反射实例化selectorProvider
if (loadProviderAsService())
return provider;
// 3. 以上都没有的话,就采用默认jdk环境下默认的selectorProvider,windows下为WindowsSelectorProvider
// Linux内核2.6及以上版本,采用EPollSelectorProvider,
//低版本内核使用PollSelectorProvider
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
// 从属性配置中利用反射实例化selectorProvider
private static boolean loadProviderFromProperty() {
String cn = System.getProperty("java.nio.channels.spi.SelectorProvider");
if (cn == null)
return false;
try {
Class<?> c = Class.forName(cn, true,
ClassLoader.getSystemClassLoader());
provider = (SelectorProvider)c.newInstance();
return true;
} catch (ClassNotFoundException x) {
throw new ServiceConfigurationError(null, x);
} catch (IllegalAccessException x) {
throw new ServiceConfigurationError(null, x);
} catch (InstantiationException x) {
throw new ServiceConfigurationError(null, x);
} catch (SecurityException x) {
throw new ServiceConfigurationError(null, x);
}
}
// 利用ServiceLoader加载META-INF/services下全限定接口文件实例化文件内的实现类
private static boolean loadProviderAsService() {
ServiceLoader<SelectorProvider> sl =
ServiceLoader.load(SelectorProvider.class,
ClassLoader.getSystemClassLoader());
Iterator<SelectorProvider> i = sl.iterator();
for (;;) {
try {
if (!i.hasNext())
return false;
provider = i.next();
return true;
} catch (ServiceConfigurationError sce) {
if (sce.getCause() instanceof SecurityException) {
// Ignore the security exception, try the next provider
continue;
}
throw sce;
}
}
}
// 打开一个selector(也是一个文件)
public abstract AbstractSelector openSelector() throws IOException;
// 打开一个 ServerSocketChannel
public abstract ServerSocketChannel openServerSocketChannel() throws IOException;
// 打开一个 SocketChannel
public abstract SocketChannel openSocketChannel() throws IOException;
// ...
}
WindowsSelectorProvider UML 类图
![](https://img.haomeiwen.com/i5362354/ff01bd638bd2344e.png)
抽象类 Selector
jdk.nio中的selector对应操作系统底层的select结构(select/poll/epoll),在linux下也是一个文件
windows下的具体的Selector实现类是WindowsSelectorImpl
WindowsSelectorImpl UML类图
![](https://img.haomeiwen.com/i5362354/3443dafe9dab10b6.png)
Selector抽象类
看下Selector中定义了哪些方法
public abstract class Selector implements Closeable {
// 1. 通过open方法,内部还是调用了SelectorProvider的provider方法得到选择器供应商,再调用其openSelector方法获取selector
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
// 2. 返回是否该selector是打开状态
public abstract boolean isOpen();
// 3. 返回创建了该selector的selectorProvider
public abstract SelectorProvider provider();
// 返回该selector选择器的注册上来的key 集合
public abstract Set<SelectionKey> keys();
// 返回该selector选择器的注册上来并且被选择的key 集合
public abstract Set<SelectionKey> selectedKeys();
// 选择准备好IO操作的相应通道的keys集合,即发起系统调用,该方法会被阻塞,直到至少一个通道被选择
// 或该selector的wakeup方法被调用,或当前线程被打断,或给定的事件timeout超时
// 参数timeout若为正,则阻塞指定毫秒后返回0
// 若为0,则无限阻塞
// 不可为负数
// 返回值代表了准备好IO操作的keys数量,可能返回0
public abstract int select(long timeout)
throws IOException;
public abstract int select() throws IOException;
// 调用该方法,会导致还未阻塞返回的selector立即返回
public abstract Selector wakeup();
}
主要包括如下:
-
获取当前selector上注册的所有keys
-
获取当前selector上所有已选择的keys
-
阻塞方法select()
-
唤醒方法wakeup()
-
其子类AbstractSelector定义的cancelledKeys(),register(),deregister()方法等
记住一点就是selector会维护三种key的集合
-
key set
代表当前通道注册到该selector上的集合 -
selected-key set
代表注册的通道发生了注册感兴趣的事件(ACCEPT/读/写),其selectionKey就会被加入到selected-key set中 -
canceled-key set
当调用了cancel方法,相应的selectionKey会被加入cancelledKeys集合中
AbstractSelector 类
该类继承了Selector类,主要定义了抽象方法register方法以及一个
cancelledKeys的集合和provider属性持有
重点看register方法
protected abstract SelectionKey register(AbstractSelectableChannel ch,
int ops, Object att);
该方法有三个参数
-
可被选择的通道ch
-
感兴趣的事件ops
-
附加属性
返回值SelectionKey对象,代表了一个注册到当前selector上的给定通道,并指定了感兴趣的事件
注册方法的具体实现由SelectorImpl类实现
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);
synchronized(this.publicKeys) {
this.implRegister(var4);
}
var4.interestOps(var2);
return var4;
}
}
可以看到这里就是new了一个SelectionKeyImpl,并将其注册到selector上(即加入到keys set集合中)同时设置感兴趣的事件到该SelectionKey实例的interestOps属性中
注册操作:
-
添加SelectionKey至Java Selector对象的keys集合
-
记录Java Selector对象的fdMap属性,key为该SelectionKey注册的channel的fd值,value为该SelectionKey
-
调用操作系统底层epoll_ctl系统调用中段注册epollfd需要监听的channel指定的事件
SelectionKeyImpl 选择键
一个SelectionKey即代表了一个channel和其注册的selector之间的关系,即通过这个SelectionKey就可以知道是哪个channel注册到了哪个selector上,并且其希望Selector监听该Channel的哪些事件
UML类图
![](https://img.haomeiwen.com/i5362354/b08475347e082d8e.png)
该类有几个主要属性
SelChImpl channel
注册的通道
SelectorImpl selector
通道注册到哪个选择器
int interestOps
注册时感兴趣的操作事件
readyOps
已准备好的操作事件
该类同时定义了一些列的操作常量如:OP_READ, OP_WRITE, OP_CONNECT, OP_ACCEPT
以及一些判断是否该key的channel准备好了相应的事件如:isReadable(),isWritable(), isConnectable(), isAcceptable(),其判断逻辑就是用key的内部属性readyOps中是否含有指定的事件
select方法详解
Java Selector有两个重载的select方法以及一个selectNow()方法
public abstract int select() throws IOException;
public abstract int select(long timeout) throws IOException;
public abstract int selectNow() throws IOException;
- selectNow()
该方法是非阻塞的,如果没有channels变为可选择的,会立即返回0
- select()
该方法时阻塞的,返回的前提是至少有一个channel是可选择的或该selector的wakeup方法被调用,或者运行select方法的当前线程被打断
- select(long timeout)
该方法基本上同select(),只是另外提供了timeout参数用于当没有channel可选择时,阻塞等待的毫秒数,注意该参数为0即代表一直阻塞,需要大于0才生效
以上三个方法最终的调用都会转到doSelect方法
这里以windows环境为例,WindowsSelectorImpl
定义了最大可选择的fd数量为1024
private static final int MAX_SELECTABLE_FDS = 1024;
doSelect伪代码
protected int doSelect(long var1) throws IOException {
// 1. 处理取消注册channel
this.processDeregisterQueue();
// 2. 调用内部SubSelector的poll方法,SubSelector类为WindowsSelectorImpl的内部类,封装了系统调用poll的相关native方法
this.subSelector.poll();
// 3. 再次处理取消注册的channel
this.processDeregisterQueue();
// 4. 更新可选择channel的keys到selector的selectedKeys属性中
int var3 = this.updateSelectedKeys();
// 5. 返回可选择key的数量
return var3;
}
其中1. 中主要处理已取消注册通道,即若cancelledKeys集合不为空,则遍历依次取消该Key
主要包括
-
调用内核取消该fd的指定事件的监听
-
fdMap属性的清除
-
keys属性清除
-
selectedKeys属性清除
-
失效该SelectionKey,设置valid属性为false
而poll()的调用即是发起内核系统调用,阻塞获取可选择的fd
最后一个的updateSelectedKeys()方法就是根据内核返回的可选择的fd去fdMap中找到对应的SelectionKey,再将SelectionKey加入到该selector的selectedKeys集合中,这样我们应用程序的select方法返回后就可以从selectedKeys集合中获取可选择的key,进而进行我们自己的处理
总结
最后以一张图表示整个IO多路复用流程
![](https://img.haomeiwen.com/i5362354/02b60045e4153c28.png)