Netty源碼(三):I/O模型和Java NIO底層原理

?最近在學(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í)槽唾,所以這里我們只給出幾張圖片來解釋,具體解釋大家可以參看這篇博文

阻塞I/O
非阻塞I/O
多路復(fù)用I/O
信號(hào)驅(qū)動(dòng)
異步I/O

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類似鲤拿,ServerSocketChannelregister函數(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)用了EPollArrayWrapperadd方法,記錄下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)用了其子類EpollSelectorImpldoSelect方法

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)用EPollArrayWapperpoll方法,然后在更新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è)倩仡^看一下selectupdateSelectedKeys方法.

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)原理.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末殴瘦,一起剝皮案震驚了整個(gè)濱河市狠角,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蚪腋,老刑警劉巖丰歌,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異屉凯,居然都是意外死亡立帖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門悠砚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來晓勇,“玉大人,你說我怎么就攤上這事灌旧“笤郏” “怎么了?”我有些...
    開封第一講書人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵枢泰,是天一觀的道長描融。 經(jīng)常有香客問我,道長衡蚂,這世上最難降的妖魔是什么窿克? 我笑而不...
    開封第一講書人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任骏庸,我火速辦了婚禮,結(jié)果婚禮上让歼,老公的妹妹穿的比我還像新娘敞恋。我一直安慰自己,他們只是感情好谋右,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開白布硬猫。 她就那樣靜靜地躺著,像睡著了一般改执。 火紅的嫁衣襯著肌膚如雪啸蜜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評(píng)論 1 308
  • 那天辈挂,我揣著相機(jī)與錄音衬横,去河邊找鬼。 笑死终蒂,一個(gè)胖子當(dāng)著我的面吹牛蜂林,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拇泣,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼噪叙,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了霉翔?” 一聲冷哼從身側(cè)響起睁蕾,我...
    開封第一講書人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎债朵,沒想到半個(gè)月后子眶,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡序芦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年臭杰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谚中。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡硅卢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出藏杖,到底是詐尸還是另有隱情将塑,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布蝌麸,位于F島的核電站点寥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏来吩。R本人自食惡果不足惜敢辩,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一蔽莱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧戚长,春花似錦盗冷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至迫肖,卻和暖如春锅劝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蟆湖。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來泰國打工故爵, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人隅津。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓诬垂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親伦仍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子结窘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

推薦閱讀更多精彩內(nèi)容