本文所有源碼均基于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 類圖
抽象類 Selector
jdk.nio中的selector對應操作系統底層的select結構(select/poll/epoll),在linux下也是一個文件
windows下的具體的Selector實現類是WindowsSelectorImpl
WindowsSelectorImpl UML類圖
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類圖
該類有幾個主要屬性
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多路復用流程