?最近在學(xué)習(xí)Java網(wǎng)絡(luò)編程和Netty相關(guān)的知識(shí)敦冬,了解到Netty是NIO模式的網(wǎng)絡(luò)框架,但是提供了不同的Channel
來支持不同模式的網(wǎng)絡(luò)通信處理桑寨,包括同步伏尼、異步、阻塞和非阻塞尉尾。學(xué)習(xí)要從基礎(chǔ)開始烦粒,所以我們就要先了解一下相關(guān)的基礎(chǔ)概念和Java原生的NIO。這里代赁,就將最近我學(xué)習(xí)的知識(shí)總結(jié)一下,以供大家了解兽掰。
?為了節(jié)約你的時(shí)間芭碍,本文主要內(nèi)容如下:
- 異步,阻塞的概念
- 操作系統(tǒng)I/O的類型
- Java NIO的底層實(shí)現(xiàn)
異步孽尽,同步窖壕,阻塞,非阻塞
同步和異步關(guān)注的是消息通信機(jī)制杉女,所謂同步就是調(diào)用者進(jìn)行調(diào)用后瞻讽,在沒有得到結(jié)果之前,該調(diào)用一直不會(huì)返回熏挎,但是一旦調(diào)用返回速勇,就得到了返回值,同步就是指調(diào)用者主動(dòng)等待調(diào)用結(jié)果坎拐;而異步則相反烦磁,執(zhí)行調(diào)用之后直接返回,所以可能沒有返回值哼勇,等到有返回值時(shí)都伪,由被調(diào)用者通過狀態(tài),通知來通知調(diào)用者.異步就是指被調(diào)用者來通知調(diào)用者調(diào)用結(jié)果就緒.所以积担,二者在消息通信機(jī)制上有所不同陨晶,一個(gè)是調(diào)用者檢查調(diào)用結(jié)果是否就緒,一個(gè)是被調(diào)用者通知調(diào)用者結(jié)果就緒
?阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息帝璧,返回值)時(shí)的狀態(tài).阻塞調(diào)用是指在調(diào)用結(jié)果返回之前先誉,當(dāng)前線程會(huì)被掛起,調(diào)用線程只有在得到結(jié)果之后才會(huì)繼續(xù)執(zhí)行.非阻塞調(diào)用是指在不能立刻得到結(jié)構(gòu)之前聋溜,調(diào)用線程不會(huì)被掛起谆膳,還是可以執(zhí)行其他事情.
?兩組概念相互組合就有四種情況,分別是同步阻塞撮躁,同步非阻塞漱病,異步阻塞买雾,異步非阻塞.我們來舉個(gè)例子來分別類比上訴四種情況.
?比如你要從網(wǎng)上下載一個(gè)1G的文件,按下下載按鈕之后杨帽,如果你一直在電腦旁邊漓穿,等待下載結(jié)束,這種情況就是同步阻塞注盈;如果你不需要一直呆在電腦旁邊晃危,你可以去看一會(huì)書,但是你還是隔一段時(shí)間來查看一下下載進(jìn)度老客,這種情況就是同步非阻塞僚饭;如果你一直在電腦旁邊,但是下載器在下載結(jié)束之后會(huì)響起音樂來提醒你胧砰,這就是異步阻塞鳍鸵;但是如果你不呆在電腦旁邊,去看書尉间,下載器下載結(jié)束后響起音樂來提醒你偿乖,那么這種情況就是異步非阻塞.
Unix的I/O類型
知道上述兩組概念之后,我們來看一下Unix下可用的5種I/O模型:
- 阻塞I/O(bloking IO)
- 非阻塞I/O(nonblocking IO)
- 多路復(fù)用I/O(IO multiplexing)
- 信號(hào)驅(qū)動(dòng)I/O(signal driven IO)
- 異步I/O(asynchronous IO)
前4種都是同步哲嘲,只有最后一種是異步I/O.需要注意的是Java NIO依賴于Unix系統(tǒng)的多路復(fù)用I/O,對(duì)于I/O操作來說贪薪,它是同步I/O,但是對(duì)于編程模型來說眠副,它是異步網(wǎng)絡(luò)調(diào)用.下面我們就以系統(tǒng)read
的調(diào)用來介紹不同的I/O類型.
?當(dāng)一個(gè)read
發(fā)生時(shí)画切,它會(huì)經(jīng)歷兩個(gè)階段:
- 1 等待數(shù)據(jù)準(zhǔn)備
- 2 將數(shù)據(jù)從內(nèi)核內(nèi)存空間拷貝到進(jìn)程內(nèi)存空間中
不同的I/O類型,在這兩個(gè)階段中有不同的行為.但是由于這塊內(nèi)容比較多囱怕,而且多為表述性的知識(shí)槽唾,所以這里我們只給出幾張圖片來解釋,具體解釋大家可以參看這篇博文
Java NIO的底層實(shí)現(xiàn)
我們都知道Netty通過JNI的方式提供了Native Socket Transport光涂,為什么Netty
要提供自己的Native版本的NIO呢庞萍?明明Java NIO底層也是基于epoll
調(diào)用(最新的版本)的.這里,我們先不明說忘闻,大家想一想可能的情況.下列的源碼都來自于OpenJDK-8u40-b25版本.
open方法
?如果我們順著Selector.open()
方法一個(gè)類一個(gè)類的找下去钝计,很容易就發(fā)現(xiàn)Selector
的初始化是由DefaultSelectorProvider
根據(jù)不同操作系統(tǒng)平臺(tái)生成的不同的SelectorProvider
,對(duì)于Linux系統(tǒng)齐佳,它會(huì)生成EPollSelectorProvider
實(shí)例私恬,而這個(gè)實(shí)例會(huì)生成EPollSelectorImpl
作為最終的Selector
實(shí)現(xiàn).
class EPollSelectorImpl extends SelectorImpl
{
.....
// The poll object
EPollArrayWrapper pollWrapper;
.....
EPollSelectorImpl(SelectorProvider sp) throws IOException {
.....
pollWrapper = new EPollArrayWrapper();
pollWrapper.initInterrupt(fd0, fd1);
.....
}
.....
}
?EpollArrayWapper
將Linux的epoll相關(guān)系統(tǒng)調(diào)用封裝成了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;
上述三個(gè)native方法就對(duì)應(yīng)Linux下epoll相關(guān)的三個(gè)系統(tǒng)調(diào)用
//創(chuàng)建一個(gè)epoll句柄,size是這個(gè)監(jiān)聽的數(shù)目的最大值.
int epoll_create(int size);
//事件注冊(cè)函數(shù)炼吴,告訴內(nèi)核epoll監(jiān)聽什么類型的事件本鸣,參數(shù)是感興趣的事件類型,回調(diào)和監(jiān)聽的fd
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待事件的產(chǎn)生硅蹦,類似于select調(diào)用荣德,events參數(shù)用來從內(nèi)核得到事件的集合
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
?所以闷煤,我們會(huì)發(fā)現(xiàn)在EpollArrayWapper
的構(gòu)造函數(shù)中調(diào)用了epollCreate
方法,創(chuàng)建了一個(gè)epoll的句柄.這樣涮瞻,Selector
對(duì)象就算創(chuàng)造完畢了.
register方法
?與open
類似鲤拿,ServerSocketChannel
的register
函數(shù)底層是調(diào)用了SelectorImpl
類的register
方法,這個(gè)SelectorImpl
就是EPollSelectorImpl
的父類.
protected final SelectionKey register(AbstractSelectableChannel ch,
int ops,
Object attachment)
{
if (!(ch instanceof SelChImpl))
throw new IllegalSelectorException();
//生成SelectorKey來存儲(chǔ)到hashmap中署咽,一共之后獲取
SelectionKeyImpl k = new SelectionKeyImpl((SelChImpl)ch, this);
//attach用戶想要存儲(chǔ)的對(duì)象
k.attach(attachment);
//調(diào)用子類的implRegister方法
synchronized (publicKeys) {
implRegister(k);
}
//設(shè)置關(guān)注的option
k.interestOps(ops);
return k;
}
?EpollSelectorImpl
的相應(yīng)的方法實(shí)現(xiàn)如下近顷,它調(diào)用了EPollArrayWrapper
的add
方法,記錄下Channel所對(duì)應(yīng)的fd值,然后將ski添加到keys
變量中.在EPollArrayWrapper
中有一個(gè)byte數(shù)組eventLow
記錄所有的channel的fd值.
protected void implRegister(SelectionKeyImpl ski) {
if (closed)
throw new ClosedSelectorException();
SelChImpl ch = ski.channel;
//獲取Channel所對(duì)應(yīng)的fd,因?yàn)樵趌inux下socket會(huì)被當(dāng)作一個(gè)文件宁否,也會(huì)有fd
int fd = Integer.valueOf(ch.getFDVal());
fdToKey.put(fd, ski);
//調(diào)用pollWrapper的add方法,將channel的fd添加到監(jiān)控列表中
pollWrapper.add(fd);
//保存到HashSet中窒升,keys是SelectorImpl的成員變量
keys.add(ski);
}
?我們會(huì)發(fā)現(xiàn),調(diào)用register
方法并沒有涉及到EpollArrayWrapper
中的native方法epollCtl
的調(diào)用,這是因?yàn)樗麄儗⑦@個(gè)方法的調(diào)用推遲到Select
方法中去了.
Select方法
?和register
方法類似,SelectorImpl
中的select
方法最終調(diào)用了其子類EpollSelectorImpl
的doSelect
方法
protected int doSelect(long timeout) throws IOException {
.....
try {
....
//調(diào)用了poll方法,底層調(diào)用了native的epollCtl和epollWait方法
pollWrapper.poll(timeout);
} finally {
....
}
....
//更新selectedKeys,為之后的selectedKeys函數(shù)做準(zhǔn)備
int numKeysUpdated = updateSelectedKeys();
....
return numKeysUpdated;
}
由上述的代碼,可以看到慕匠,EPollSelectorImpl
先調(diào)用EPollArrayWapper
的poll
方法,然后在更新SelectedKeys
.其中poll
方法會(huì)先調(diào)用epollCtl
來注冊(cè)先前在register
方法中保存的Channel的fd和感興趣的事件類型异剥,然后epollWait
方法等待感興趣事件的生成,導(dǎo)致線程阻塞.
int poll(long timeout) throws IOException {
updateRegistrations(); ////先調(diào)用epollCtl,更新關(guān)注的事件類型
////導(dǎo)致阻塞,等待事件產(chǎn)生
updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);
.....
return updated;
}
?等待關(guān)注的事件產(chǎn)生之后(或在等待時(shí)間超過預(yù)先設(shè)置的最大時(shí)間),epollWait
函數(shù)就會(huì)返回.select
函數(shù)從阻塞狀態(tài)恢復(fù).
selectedKeys方法
?我們先來看SelectorImpl
中的selectedKeys
方法.
//是通過Util.ungrowableSet生成的,不能添加,只能減少
private Set<SelectionKey> publicSelectedKeys;
public Set<SelectionKey> selectedKeys() {
....
return publicSelectedKeys;
}
?很奇怪啊,怎麼直接就返回publicSelectedKeys
了,難道在select
函數(shù)的執(zhí)行過程中有修改過這個(gè)變量嗎?
?publicSelectedKeys
這個(gè)對(duì)象其實(shí)是selectedKeys
變量的一份副本,你可以在SelectorImpl
的構(gòu)造函數(shù)中找到它們倆的關(guān)系,我們?cè)倩仡^看一下select
中updateSelectedKeys
方法.
private int updateSelectedKeys() {
//更新了的keys的個(gè)數(shù),或在說是產(chǎn)生的事件的個(gè)數(shù)
int entries = pollWrapper.updated;
int numKeysUpdated = 0;
for (int i=0; i<entries; i++) {
//對(duì)應(yīng)的channel的fd
int nextFD = pollWrapper.getDescriptor(i);
//通過fd找到對(duì)應(yīng)的SelectionKey
SelectionKeyImpl ski = fdToKey.get(Integer.valueOf(nextFD));
if (ski != null) {
int rOps = pollWrapper.getEventOps(i);
//更新selectedKey變量,并通知響應(yīng)的channel來做響應(yīng)的處理
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;
}
后記
?看到這里,詳細(xì)大家都已經(jīng)了解到了NIO的底層實(shí)現(xiàn)了吧.這里我想在說兩個(gè)問題.
?一是為什么Netty自己又從新實(shí)現(xiàn)了一邊native相關(guān)的NIO底層方法? 聽聽Netty的創(chuàng)始人是怎麼說的吧鏈接絮重。因?yàn)镴ava的版本使用的epoll的level-triggered模式,而Netty則希望使用edge-triggered模式歹苦,而且Java版本沒有將epoll的部分配置項(xiàng)暴露出來青伤,比如說TCP_CORK和SO_REUSEPORT。
?二是看這么多源碼,花費(fèi)這么多時(shí)間有什么作用呢?我感覺如果從非功利的角度來看,那么就是純粹的希望了解的更多,有時(shí)候看完源碼或在理解了底層原理之后,都會(huì)用一種恍然大悟的感覺,比如說AQS
的原理.如果從目的性的角度來看,那么就是你知道底層原理之后,你的把握性就更強(qiáng)了,如果出了問題,你可以更快的找出來,并且解決.除此之外,你還可以按照具體的現(xiàn)實(shí)情況,以源碼為模板在自己造輪子,實(shí)現(xiàn)一個(gè)更加符合你當(dāng)前需求的版本.
?后續(xù)如果有時(shí)間,我希望好好了解一下epoll的操作系統(tǒng)級(jí)別的實(shí)現(xiàn)原理.