[TOC]html
原本是想學習Netty的,可是Netty是一個NIO框架,所以在學習netty以前,仍是先梳理一下NIO的知識。經過剖析源碼理解NIO的設計原理。java
本系列文章針對的是JDK1.8.0.161的源碼。linux
前幾篇文章對Buffer和Channel的源碼的經常使用功能進行了研究,本篇將對Selector源碼進行解析。算法
在網絡傳輸時,客戶端不定時的會與服務端進行鏈接,而在高併發場景中,大多數鏈接其實是空閒的。所以爲了提升網絡傳輸高併發的性能,就出現各類I/O模型從而優化CPU處理效率。不一樣選擇器實現了不一樣的I/O模型算法。同步I/O在linux上有EPoll模型,mac上有KQueue模型,windows上則爲select模型。macos
關於I/O模型相關知識能夠查看《高性能網絡通信原理》。windows
爲了能知道哪些鏈接已就緒,在一開始咱們須要定時輪詢Socket是否有接收到新的鏈接,同時咱們還要監控是否接收到已創建鏈接的數據,因爲大多數狀況下大多數網絡鏈接實際是空閒的,所以每次都遍歷全部的客戶端,那麼隨着併發量的增長,性能開銷也是呈線性增加。緩存
有了Selector
,咱們可讓它幫咱們作"監控"的動做,而當它監控到鏈接接收到數據時,咱們只要去將數據讀取出來便可,這樣就大大提升了性能。要Selector
幫咱們作「監控」動做,那麼咱們須要告知它須要監控哪些Channel
。微信
注意,只有網絡通信的時候才須要經過
Selector
監控通道。從代碼而言,Channel
必須繼承AbstractSelectableChannel
。網絡
首先咱們須要經過靜態方法Selector.open()
從建立一個Selector
。併發
Selector selector = Selector.open();
複製代碼
須要注意的是,Channel必須是非阻塞的,咱們須要手動將Channel設置爲非阻塞。調用Channel
的實例方法SelectableChannel.configureBlocking(boolean block)
。
須要告訴Selector監控哪些Channel
,經過channel.register
將須要監控的通道註冊到Selector
中
註冊是在AbstractSelectableChannel
中實現的,當新的通道向Selector
註冊時會建立一個SelectionKey
,並將其保存到SelectionKey[] keys
緩存中。
public final SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException {
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
//當前Channel是否支持操做
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;
}
}
複製代碼
新的SelectionKey會調用到AbstractSelector.register
,首先會先建立一個SelectionKeyImpl,而後調用方法implRegister
執行實際註冊,該功能是在各個平臺的SelectorImpl
的實現類中作具體實現。
k = ((AbstractSelector)sel).register(this, ops, att);
protected final SelectionKey register(AbstractSelectableChannel ch, int ops, Object attachment) {
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
//建立SelectionKey
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
k.attach(attachment);
synchronized (publicKeys) {
//註冊
implRegister(k);
}
//設置事件
k.interestOps(ops);
return k;
}
複製代碼
建立了SelectionKey
後就將他加入到keys的緩存中,當keys緩存不足時,擴容兩倍大小。
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 {
// 擴容兩倍大小
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++;
}
複製代碼
在討論Selector如何工做以前,咱們先看一下Selector是如何建立的。咱們經過Selector.open()
靜態方法建立了一個Selector
。內部實際是經過SelectorProvider.openSelector()
方法建立Selector
。
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
複製代碼
經過SelectorProvider.provider()
靜態方法,獲取到SelectorProvider
,首次獲取時會經過配置等方式注入,若沒有配置,則使用DefaultSelectorProvider
生成。
public static SelectorProvider provider() {
synchronized (lock) {
if (provider != null)
return provider;
return AccessController.doPrivileged(
new PrivilegedAction<SelectorProvider>() {
public SelectorProvider run() {
//經過配置的java.nio.channels.spi.SelectorProvider值注入自定義的SelectorProvider
if (loadProviderFromProperty())
return provider;
//經過ServiceLoad注入,而後獲取配置的第一個服務
if (loadProviderAsService())
return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;
}
});
}
}
複製代碼
若咱們沒有作特殊配置,則會使用默認的DefaultSelectorProvider
建立SelectorProvider
。 不一樣平臺的DefaultSelectorProvider
實現不同。能夠在jdk\src\[macosx|windows|solaris]\classes\sun\nio\ch
找到實現DefaultSelectorProvider.java
。下面是SelectorProvider
的實現。
//windows
public class DefaultSelectorProvider {
private DefaultSelectorProvider() { }
public static SelectorProvider create() {
return new sun.nio.ch.WindowsSelectorProvider();
}
}
//linux
public class DefaultSelectorProvider {
private DefaultSelectorProvider() { }
@SuppressWarnings("unchecked")
private static SelectorProvider createProvider(String cn) {
Class<SelectorProvider> c;
try {
c = (Class<SelectorProvider>)Class.forName(cn);
} catch (ClassNotFoundException x) {
throw new AssertionError(x);
}
try {
return c.newInstance();
} catch (IllegalAccessException | InstantiationException x) {
throw new AssertionError(x);
}
}
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();
}
}
複製代碼
獲取到SelectorProvider
後,建立Selector
了。經過SelectorProvider.openSelector()
實例方法建立一個Selector
//windows
public class WindowsSelectorProvider extends SelectorProviderImpl {
public AbstractSelector openSelector() throws IOException {
return new WindowsSelectorImpl(this);
}
}
//linux
public class EPollSelectorProvider extends SelectorProviderImpl {
public AbstractSelector openSelector() throws IOException {
return new EPollSelectorImpl(this);
}
...
}
複製代碼
windows下建立了WindowsSelectorImpl
,linux下建立了EPollSelectorImpl
。
全部的XXXSelectorImpl
都繼承自SelectorImpl
,能夠在jdk\src\[macosx|windows|solaris|share]\classes\sun\nio\ch
找到實現XXXSelectorImpl.java
。繼承關係以下圖所示。
接下里咱們討論一下Selector提供的主要功能,後面在分析Windows和Linux下Selector
的具體實現。
在建立SelectorImpl
首先會初始化2個HashSet,publicKeys
存放用於一個存放全部註冊的SelectionKey,selectedKeys
用於存放已就緒的SelectionKey。
protected SelectorImpl(SelectorProvider sp) {
super(sp);
keys = new HashSet<SelectionKey>();
selectedKeys = new HashSet<SelectionKey>();
if (Util.atBugLevel("1.4")) {
publicKeys = keys;
publicSelectedKeys = selectedKeys;
} else {
//建立一個不可修改的集合
publicKeys = Collections.unmodifiableSet(keys);
//建立一個只能刪除不能添加的集合
publicSelectedKeys = Util.ungrowableSet(selectedKeys);
}
}
複製代碼
關於
Util.atBugLevel
找到一篇文章有提到該方法。彷佛是和EPoll的一個空指針異常相關。這個bug在nio bugLevel=1.4版本引入,這個bug在jdk1.5中存在,直到jdk1.7才修復。
前面咱們已經向Selector
註冊了通道,如今咱們須要調用Selector.select()
實例方法從系統內存中加載已就緒的文件描述符。
public int select() throws IOException {
return select(0);
}
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 abstract int doSelect(long timeout) throws IOException;
複製代碼
最終會調用具體SelectorImpl
的doSelect
,具體內部主要執行2件事
updateSelectedKeys
更新已就緒事件的SelectorKey
當獲取到已就緒的SelectionKey
後,咱們就能夠遍歷他們。根據SelectionKey
的事件類型決定須要執行的具體邏輯。
//獲取到已就緒的Key進行遍歷
Set<SelectionKey> selectKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
//處理事件。
if(key.isAcceptable()){
doAccept(key);
}
else if(key.isReadable())
{
doRead(key);
}
...
it.remove();
}
複製代碼
本文對Selector
、SelectorProvider
的建立進行分析,總的流程能夠參考下圖
對於後面步驟的EpollArrayWarpper()
會在SelectorImpl
個平臺具體實現進行講解。後面會分2對WindowsSelectorImpl
和EpollSelectorImpl
進行分析。
![]()
- 微信掃一掃二維碼關注訂閱號傑哥技術分享
- 出處:www.cnblogs.com/Jack-Blog/p…
- 做者:傑哥很忙
- 本文使用「CC BY 4.0」創做共享協議。歡迎轉載,請在明顯位置給出出處及連接。