Netty原始碼(三):I/O模型和JavaNIO底層原理
上一篇文章我們主要講解了Netty的 Channel
和 Pipeline
,瞭解到不同的 Channel
可以提供基於不同網路協議的通訊處理.既然涉及到網路通訊,就不得不說一下多執行緒,同步非同步相關的知識了.Netty的網路模型是多執行緒的 Reactor
模式,所有I/O請求都是非同步呼叫,我們今天就來探討一下一些基礎概念和Java NIO的底層機制.
為了節約你的時間,本文主要內容如下:
-
非同步,阻塞的概念
-
作業系統I/O的型別
-
Java NIO的Linux底層實現
非同步,同步,阻塞,非阻塞
同步和非同步關注的是訊息通訊機制,所謂同步就是呼叫者進行呼叫後,在沒有得到結果之前,該呼叫一直不會返回,但是一旦呼叫返回,就得到了返回值,同步就是指呼叫者主動等待呼叫結果;而非同步則相反,執行呼叫之後直接返回,所以可能沒有返回值,等到有返回值時,由被呼叫者通過狀態,通知來通知呼叫者.非同步就是指被呼叫者來通知呼叫者呼叫結果就緒.所以,二者在訊息通訊機制上有所不同,一個是呼叫者檢查呼叫結果是否就緒,一個是被呼叫者通知呼叫者結果就緒*
阻塞和非阻塞關注的是程式在等待呼叫結果(訊息,返回值)時的狀態.阻塞呼叫是指在呼叫結果返回之前,當前執行緒會被掛起,呼叫執行緒只有在得到結果之後才會繼續執行.非阻塞呼叫是指在不能立刻得到結構之前,呼叫執行緒不會被掛起,還是可以執行其他事情.
兩組概念相互組合就有四種情況,分別是同步阻塞,同步非阻塞,非同步阻塞,非同步非阻塞.我們來舉個例子來分別類比上訴四種情況.
比如你要從網上下載一個1G的檔案,按下下載按鈕之後,如果你一直在電腦旁邊,等待下載結束,這種情況就是同步阻塞;如果你不需要一直呆在電腦旁邊,你可以去看一會書,但是你還是隔一段時間來檢視一下下載進度,這種情況就是同步非阻塞;如果你一直在電腦旁邊,但是下載器在下載結束之後會響起音樂來提醒你,這就是非同步阻塞;但是如果你不呆在電腦旁邊,去看書,下載器下載結束後響起音樂來提醒你,那麼這種情況就是非同步非阻塞.
Unix的I/O型別
知道上述兩組概念之後,我們來看一下Unix下可用的5種I/O模型:
-
阻塞I/O
-
非阻塞I/O
-
多路複用I/O
-
訊號驅動I/O
-
非同步I/O
前4種都是同步,只有最後一種是非同步I/O.需要注意的是Java NIO依賴於Unix系統的多路複用I/O,對於I/O操作來說,它是同步I/O,但是對於程式設計模型來說,它是非同步網路呼叫.下面我們就以系統
read
的呼叫來介紹不同的I/O型別.當一個
read
發生時,它會經歷兩個階段: -
1 等待資料準備
-
2 將資料從核心記憶體空間拷貝到程式記憶體空間中
不同的I/O型別,在這兩個階段中有不同的行為.但是由於這塊內容比較多,而且多為表述性的知識,所以這裡我們只給出幾張圖片來解釋,具體解釋大家可以參看這篇博文
Java NIO的Linux底層實現
我們都知道Netty通過JNI的方式提供了Native Socket Transport,為什麼 Netty
要提供自己的Native版本的NIO呢?明明Java NIO底層也是基於 epoll
呼叫(最新的版本)的.這裡,我們先不明說,大家想一想可能的情況.下列的原始碼都來自於OpenJDK-8u40-b25版本.
open方法
如果我們順著 Selector.open()
方法一個類一個類的找下去,很容易就發現 Selector
的初始化是由 DefaultSelectorProvider
根據不同作業系統平臺生成的不同的 SelectorProvider
,對於Linux系統,它會生成 EPollSelectorProvider
例項,而這個例項會生成 EPollSelectorImpl
作為最終的 Selector
實現.
class EPollSelectorImpl extends SelectorImpl
{
.....
// The poll object
EPollArrayWrapper pollWrapper;
.....
EPollSelectorImpl(SelectorProvider sp) throws IOException {
.....
pollWrapper = new EPollArrayWrapper();
pollWrapper.initInterrupt(fd0, fd1);
.....
}
.....
}
EpollArrayWapper
將Linux的epoll相關係統呼叫封裝成了native方法供 EpollSelectorImpl
使用.
private native int epollCreate();
private native void epollCtl(int epfd, int opcode, int fd, int events);
private native int epollWait(long pollAddress, int numfds, long timeout,
int epfd) throws IOException;
上述三個native方法就對應Linux下epoll相關的三個系統呼叫
//建立一個epoll控制程式碼,size是這個監聽的數目的最大值.
int epoll_create(int size);
//事件註冊函式,告訴核心epoll監聽什麼型別的事件,引數是感興趣的事件型別,回撥和監聽的fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待事件的產生,類似於select呼叫,events引數用來從核心得到事件的集合
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
所以,我們會發現在 EpollArrayWapper
的建構函式中呼叫了 epollCreate
方法,建立了一個epoll的控制程式碼.這樣, Selector
物件就算創造完畢了.
register方法
與 open
類似, ServerSocketChannel
的 register
函式底層是呼叫了 SelectorImpl
類的 register
方法,這個 SelectorImpl
就是 EPollSelectorImpl
的父類.
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
//生成SelectorKey來儲存到hashmap中,一共之後獲取
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
//attach使用者想要儲存的物件
k.attach(attachment);
//呼叫子類的implRegister方法
synchronized (publicKeys) {
implRegister(k);
}
//設定關注的option
k.interestOps(ops);
return k;
}
EpollSelectorImpl
的相應的方法實現如下,它呼叫了 EPollArrayWrapper
的 add
方法,記錄下Channel所對應的fd值,然後將ski新增到 keys
變數中.在 EPollArrayWrapper
中有一個byte陣列 eventLow
記錄所有的channel的fd值.
protected void implRegister(SelectionKeyImpl ski) {
if (closed)
throw new ClosedSelectorException();
SelChImpl ch = ski.channel;
//獲取Channel所對應的fd,因為在linux下socket會被當作一個檔案,也會有fd
int fd = Integer.valueOf(ch.getFDVal());
fdToKey.put(fd, ski);
//呼叫pollWrapper的add方法,將channel的fd新增到監控列表中
pollWrapper.add(fd);
//儲存到HashSet中,keys是SelectorImpl的成員變數
keys.add(ski);
}
我們會發現,呼叫 register
方法並沒有涉及到 EpollArrayWrapper
中的native方法 epollCtl
的呼叫,這是因為他們將這個方法的呼叫推遲到 Select
方法中去了.
Select方法
和 register
方法類似, SelectorImpl
中的 select
方法最終呼叫了其子類 EpollSelectorImpl
的 doSelect
方法
protected int doSelect(long timeout) throws IOException {
.....
try {
....
//呼叫了poll方法,底層呼叫了native的epollCtl和epollWait方法
pollWrapper.poll(timeout);
} finally {
....
}
....
//更新selectedKeys,為之後的selectedKeys函式做準備
int numKeysUpdated = updateSelectedKeys();
....
return numKeysUpdated;
}
由上述的程式碼,可以看到, EPollSelectorImpl
先呼叫 EPollArrayWapper
的 poll
方法,然後在更新 SelectedKeys
.其中 poll
方法會先呼叫 epollCtl
來註冊先前在 register
方法中儲存的Channel的fd和感興趣的事件型別,然後 epollWait
方法等待感興趣事件的生成,導致執行緒阻塞.
int poll(long timeout) throws IOException {
updateRegistrations(); ////先呼叫epollCtl,更新關注的事件型別
////導致阻塞,等待事件產生
updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
.....
return updated;
}
等待關注的事件產生之後(或在等待時間超過預先設定的最大時間), epollWait
函式就會返回. select
函式從阻塞狀態恢復.
selectedKeys方法
我們先來看 SelectorImpl
中的 selectedKeys
方法.
//是通過Util.ungrowableSet生成的,不能新增,只能減少
private Set<SelectionKey> publicSelectedKeys;
public Set<SelectionKey> selectedKeys() {
....
return publicSelectedKeys;
}
很奇怪啊,怎麼直接就返回 publicSelectedKeys
了,難道在 select
函式的執行過程中有修改過這個變數嗎?
publicSelectedKeys
這個物件其實是 selectedKeys
變數的一份副本,你可以在 SelectorImpl
的建構函式中找到它們倆的關係,我們再回頭看一下 select
中 updateSelectedKeys
方法.
private int updateSelectedKeys() {
//更新了的keys的個數,或在說是產生的事件的個數
int entries = pollWrapper.updated;
int numKeysUpdated = 0;
for (int i=0; i<entries; i++) {
//對應的channel的fd
int nextFD = pollWrapper.getDescriptor(i);
//通過fd找到對應的SelectionKey
SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
if (ski != null) {
int rOps = pollWrapper.getEventOps(i);
//更新selectedKey變數,並通知響應的channel來做響應的處理
if (selectedKeys.contains(ski)) {
if (ski.channel.translateAndSetReadyOps(rOps, ski)) {
numKeysUpdated++;
}
} else {
ski.channel.translateAndSetReadyOps(rOps, ski);
if ((ski.nioReadyOps() & ski.nioInterestOps()) != 0) {
selectedKeys.add(ski);
numKeysUpdated++;
}
}
}
}
return numKeysUpdated;
}
後記
看到這裡,詳細大家都已經瞭解到了NIO的底層實現了吧.這裡我想在說兩個問題.
一是為什麼Netty自己又從新實現了一邊native相關的NIO底層方法? 聽聽Netty的創始人是怎麼說的吧連結
二是看這麼多原始碼,花費這麼多時間有什麼作用呢?我感覺如果從非功利的角度來看,那麼就是純粹的希望瞭解的更多,有時候看完原始碼或在理解了底層原理之後,都會用一種恍然大悟的感覺,比如說 AQS
的原理.如果從目的性的角度來看,那麼就是你知道底層原理之後,你的把握性就更強了,如果出了問題,你可以更快的找出來,並且解決.除此之外,你還可以按照具體的現實情況,以原始碼為模板在自己造輪子,實現一個更加符合你當前需求的版本.
後續如果有時間,我希望好好了解一下epoll的作業系統級別的實現原理.
相關文章
- Java I/O模型及其底層原理Java模型
- 從 I/O 模型到 Netty(三)模型Netty
- Netty的底層原理Netty
- 五種I/O模型和Java NIO原始碼分析模型Java原始碼
- 從 I/O 模型到 Netty(二)模型Netty
- 從 I/O 模型到 Netty(一)模型Netty
- Java I/O底層是如何工作的?Java
- ArrayList 從原始碼角度剖析底層原理原始碼
- 【雜談】Java I/O的底層實現Java
- 從網路I/O模型到Netty,先深入瞭解下I/O多路複用模型Netty
- 持久層Mybatis3底層原始碼分析,原理解析MyBatisS3原始碼
- Java中 i=i++ 問題底層原理解析Java
- Golang WaitGroup 底層原理及原始碼詳解GolangAI原始碼
- I/O模型、Libuv和Eventloop模型OOP
- ArrayList底層結構和原始碼分析原始碼
- iOS底層原理總結 -- 利用Runtime原始碼 分析Category的底層實現iOS原始碼Go
- 計算機I/O與I/O模型計算機模型
- Java集合類,從原始碼解析底層實現原理Java原始碼
- HasMap 底層原始碼分析ASM原始碼
- Netty權威指南:Linux網路 I/O 模型簡介NettyLinux模型
- 探究synchronized底層原理(基於JAVA8原始碼分析)synchronizedJava原始碼
- 網路I/O模型模型
- ThreadLocal底層原始碼解析thread原始碼
- PHP 底層原始碼學習PHP原始碼
- Netty原始碼分析--Reactor模型(二)Netty原始碼React模型
- 七、真正的技術——CAS操作原理、實現、底層原始碼原始碼
- OC底層探索(十六) KVO底層原理
- PHP 底層原理之類和物件PHP物件
- runtime的底層原理和使用
- synchronized底層原理synchronized
- 沒搞清楚網路I/O模型?那怎麼入門Netty模型Netty
- java 淺析I/O模型Java模型
- Owin Katana 的底層原始碼分析原始碼
- PHP 底層原始碼下載地址PHP原始碼
- JAVA ArrayList集合底層原始碼分析Java原始碼
- Laravel 執行原理分析與原始碼分析,底層看這篇足矣Laravel原始碼
- spring原始碼學習之:springAOP實現底層原理Spring原始碼
- Python教程:精簡概述I/O模型與I/O操作Python模型