java nio原理

在2018年十月份的十多次面試中晓猛,幾乎每一場面試都會nio,可見nio的重要性凡辱。每當面試官問到nio的時候戒职,我都會從操作系統(tǒng)層面的IO多路復用說起,只要說的明白透乾,一般這一關(guān)就算過了洪燥。

首先要搞清楚nio中n的含義。如果告訴別人n的意思是new续徽,那就給人留下很不好的印象了蚓曼,這里的n一般是被理解為non-blocking亲澡。傳統(tǒng)的InputStream/OutputStream體系的讀寫操作都是阻塞的钦扭,為什么它們是阻塞的?這是因為當調(diào)用讀寫操作時床绪,上層應用程序并不知道底層的socket緩沖區(qū)是否是就緒的客情,如果正好是就緒的,那么不會阻塞癞己;如果不是就緒的膀斋,就會阻塞直到socket緩沖區(qū)就緒。一般情況下痹雅,socket寫緩沖區(qū)大部分時間是就緒的仰担,而讀緩沖區(qū)由于沒有數(shù)據(jù)到來而處于等待狀態(tài)。

所以绩社,阻塞和非阻塞的區(qū)別在于摔蓝,阻塞IO在進行IO操作時可能需要阻塞等待,而非阻塞IO在進行IO操作時不需要等待愉耙。因為后者在進行IO操作時通過一定的機制得知底層socket已經(jīng)準備就緒了贮尉,至于機制,別急朴沿,各位看官往下接著看猜谚。

這里談到了阻塞非阻塞的概念,在IO中還有一個容易混淆的概念是同步和異步赌渣。上面說了傳統(tǒng)InputStream/OutputStream體系是阻塞的魏铅,而nio是非阻塞的。注意這里并沒有說nio是異步的坚芜,因為它本身就不支持異步览芳。nio還需要再經(jīng)過一層封裝才能實現(xiàn)異步的功能,java中netty货岭、nima都是封裝nio實現(xiàn)異步的框架路操。

那么什么是異步疾渴,或者說同步和異步有什么區(qū)別呢?同步是指A對B發(fā)生調(diào)用時屯仗,直到B完成了操作才返回結(jié)果給A搞坝;而異步是指A對B發(fā)生調(diào)用時,B會馬上返回一個狀態(tài)給A魁袜,當B操作完成時桩撮,通過某種方式來通知A。寫過前端js代碼的同學肯定能對異步有比較好的理解峰弹,在前端js代碼中經(jīng)常會設(shè)置異步回調(diào)函數(shù)店量。這里的異步回調(diào)函數(shù)就是當前面的操作完成時能夠得到通知,進而做后續(xù)的處理鞠呈。所以融师,說到異步,總是離不開另一個詞匯:回調(diào)蚁吝。

言歸正傳旱爆,說回nio。nio中窘茁,面向開發(fā)人員的組件主要有三個:ByteBuffer怀伦、Channel、Selector山林。ok房待,先上一段nio的demo代碼。

server端:

public class NioServerTest {

    public static void main(String[] args) throws Exception {
        Selector selector = Selector.open();
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        serverSocketChannel.socket().bind(new InetSocketAddress("127.0.0.1", 3000));
        while (true) {
            int num = selector.select();
            if (num > 0) {
                Set<SelectionKey> set = selector.selectedKeys();
                Iterator<SelectionKey> it = set.iterator();
                while (it.hasNext()) {
                    SelectionKey sk = it.next();
                    if (sk.isAcceptable()) {
                        System.out.println("sk.isAcceptable()");

                        SocketChannel sc = serverSocketChannel.accept();
                        sc.configureBlocking(false);
                        sc.register(selector, SelectionKey.OP_READ);
                    } else if (sk.isReadable()) {
                        System.out.println("sk.isReadable()");

                        SocketChannel sc = (SocketChannel) sk.channel();
                        ByteBuffer byteBuffer = ByteBuffer.allocate(3);
                        sc.read(byteBuffer);
                        String s = new String(byteBuffer.array()).trim();
                        System.out.println(s);
                        sc.write(ByteBuffer.wrap(s.getBytes()));
                    } else if (sk.isWritable()) {
                        System.out.println("sk.isWritable()");

                    } else if (sk.isConnectable()) {
                        System.out.println("sk.isConnectable()");
                    }
                    it.remove();
                }
            }
        }
    }
}


client端:

public class NioClientTest {

    public static void main(String[] args) throws Exception {
        Selector selector = Selector.open();
        SocketChannel sc = SocketChannel.open(new InetSocketAddress("127.0.0.1", 3000));
        sc.configureBlocking(false);
        sc.register(selector, SelectionKey.OP_READ | SelectionKey.OP_CONNECT);
        while (true) {
            int num = selector.select();
            if (num > 0) {
                Set<SelectionKey> set = selector.selectedKeys();
                Iterator<SelectionKey> it = set.iterator();
                while (it.hasNext()) {
                    SelectionKey sk = it.next();
                    if (sk.isAcceptable()) {
                        System.out.println("sk.isAcceptable()");

                    } else if (sk.isReadable()) {
                        System.out.println("sk.isReadable()");

                        SocketChannel sc2 = (SocketChannel) sk.channel();
                        ByteBuffer bb = ByteBuffer.allocate(256);
                        sc2.read(bb);
                        System.out.println(new String(bb.array()).trim());
                    } else if (sk.isWritable()) {
                        System.out.println("sk.isWritable()");
                    } else if (sk.isConnectable()) {
                        System.out.println("sk.isConnectable()");
                    }
                    it.remove();
                }
            }
            sc.write(ByteBuffer.wrap(("hello world" + (int)(Math.random()*1000)).getBytes()));
            Thread.sleep(1000L);
        }
    }
}

下面驼抹,我們來詳細分析nio的ByteBuffer桑孩、Channel、Selector三個組件砂蔽。

ByteBuffer

ByteBuffer可以理解為操作一段內(nèi)存的工具類洼怔。Channel的數(shù)據(jù)讀寫都是通過ByteBuffer,而不像傳統(tǒng)IO那樣直接操作流左驾。這也是nio和bio很重要的一個不同點镣隶,nio的讀寫是面向緩沖的,bio的讀寫則是面向流的诡右。

其內(nèi)部維護了一個字節(jié)數(shù)組和3個位置相關(guān)的變量:

final byte[] hb;
private int position =0;
private int limit;
private int capacity;

其使用步驟:調(diào)用put()寫入數(shù)據(jù)安岂,當需要讀數(shù)據(jù)時,先flip()切換為讀模式帆吻,然后就可以get()了域那。從position到limit表示數(shù)組中可以操作(讀/寫)的部分,當limit達到capacity時,就不能讀/寫了次员。如:初始狀態(tài)limit==capacity败许,當put()時,position不斷增加淑蔚,直到limit市殷。當flip()時,position變?yōu)?刹衫,limit變?yōu)橹皃osition的位置醋寝,就變成讀模式了,從position的位置開始讀带迟。ByteBuffer還提供了mark/reset來實現(xiàn)做標記音羞,以便重復讀。

ByteBuffer有多個實現(xiàn)類仓犬,用來實現(xiàn)便捷地操作不同基本類型的數(shù)據(jù)嗅绰。另外,ByteBuffer根據(jù)操作的內(nèi)存不同婶肩,可分為HeapByteBuffer和DirectByteBuffer办陷。前者是在jvm的堆中分配一個數(shù)組,后者則是通過Unsafe.allocateMemory()來申請一塊堆外內(nèi)存律歼,并保存內(nèi)存地址,所有的操作都是通過計算內(nèi)存地址然后做讀寫操作啡专。ByteBuffer提供了便捷方法:wrap()险毁、array(),前者是將字符串封裝成ByteBuffer们童,后者是講ByteBuffer導成字符串畔况。后面會單獨寫文章詳細介紹堆外內(nèi)存的相關(guān)知識。

Channel

nio中Channel可以理解為是對Socket的封裝慧库,read跷跪、write、connect齐板、bind都是非阻塞的吵瞻,能夠?qū)崿F(xiàn)非阻塞主要是依賴不同操作系統(tǒng)支持的IO多路復用技術(shù),如window下的IOCP甘磨、Linux下的Epoll橡羞、Mac下的KQueue。針對不同的操作系統(tǒng)济舆,jvm做了不同的封裝卿泽,在Mac下KQueue的操作是封裝為KQueueSelectorImpl以及KQueueArrayWrapper。

Channel的實現(xiàn)類有四個:FileChannel滋觉、SocketChannel签夭、ServerSocketChannel齐邦、DatagramChannel。在nio編程中常用的是SocketChannel第租、ServerSocketChannel侄旬,下面簡單說下這兩個。

SocketChannel負責客戶端連接的讀寫煌妈。其讀寫操作都是通過ByteBuffer儡羔,寫的時候先將數(shù)據(jù)寫到ByteBuffer,再由ByteBuffer傳遞給內(nèi)核璧诵,內(nèi)核傳遞給網(wǎng)卡汰蜘,最終由網(wǎng)卡發(fā)送出去。讀則是先將socket緩沖區(qū)的數(shù)據(jù)經(jīng)過內(nèi)核放到ByteBuffer中之宿,再由SocketChannel從ByteBuffer中讀到數(shù)據(jù)族操。當Selector監(jiān)聽到讀就緒時調(diào)用read,讀已經(jīng)是就緒的比被,可以一次讀完并返回色难,如果沒有讀完會繼續(xù)觸發(fā)讀就緒事件;寫操作道理類似等缀。

ServerSocketChannel負責服務(wù)端監(jiān)聽端口枷莉、接收客戶端連接。它可以理解為封裝了ServerSocket尺迂,它沒有讀寫方法笤妙,只有bind()、accept()方法噪裕。bind()是封裝了socket的bind蹲盘、listen操作,bind做的事情是將socket和地址/端口綁定膳音,listen的作用是監(jiān)聽端口召衔,并提供參數(shù)設(shè)置全連接隊列大小,默認50祭陷,全連接隊列和三次握手的知識有關(guān)苍凛,有興趣的可以自己去查查資料;accept()是去讀全連接隊列颗胡,如果沒有socket連接到來就會阻塞毫深,如果有則返回第一個。nio中通常使用Selector去監(jiān)聽是否有客戶端連接到來毒姨,如果有連接就緒事件哑蔫,應用程序就可以去accept(),這樣就避免了accept時的阻塞。

SocketChannel和ServerSocketChannel都提供了open()來快速創(chuàng)建其實現(xiàn)類的實例闸迷。

Selector

Selector能夠監(jiān)聽多個Channel的多種IO事件嵌纲。只要將channel注冊到Selector上,那么當channel有IO就緒事件到來時腥沽,Selector.select()就會返回就緒Channel的數(shù)量逮走,接下來就可以通過selectedKey()拿到所有就緒的Channel,進而處理Channel今阳。IO就緒事件分為4類:讀就緒(OP_READ)师溅、寫就緒(OP_WRITE)、連接就緒(OP_CONNECT)盾舌、接收就緒(OP_ACCEPT)墓臭。

在mac os上,是用KQueueSelectorImpl作為Selector的實現(xiàn)類妖谴,它利用操作系統(tǒng)提供的KQueue模型窿锉,Selector.select()最終其實是調(diào)用KQueueArrayWrapper的poll(),后者返回就緒數(shù)量膝舅,并且會把就緒事件對應的socket的文件描述符放到一個指定的隊列中嗡载,調(diào)用getDiscriptor(int i)可以得到隊列中的某個元素,然后再根據(jù)返回的FD去找到對應的Channel(對應關(guān)系在Selector中維護)仍稀。

Selector重要的方法:wakeup洼滚、select、register

  • wakeup():是通過操作系統(tǒng)的管道實現(xiàn)的琳轿。當創(chuàng)建一個Selector時判沟,會同時創(chuàng)建一個管道,管道分為兩頭崭篡,一頭是讀的,一頭是寫的吧秕,將讀的這頭的socket信息注冊到Selector中琉闪,當需要wakeup時,就往寫的那頭隨便寫一個字節(jié)就好了砸彬。
  • register():颠毙,新建一個SelectionKeyImpl,然后向KQueueArrayWrapper的updateList添加一個元素砂碉,其中封裝了channel和interestOps蛀蜜,當selector.select()時會一一取出updatelist中的Update,注冊到kqueue事件增蹭,然后調(diào)用kevent0()去監(jiān)聽是否有就緒事件滴某。
  • select():,會去調(diào)用KQueueArrayWrapper的poll(),其中會將updateList中的Update一一注冊kqueue事件霎奢,然后去調(diào)用kevent0()阻塞監(jiān)聽就緒事件户誓。

要理解Selector的工作原理,光看代碼是不夠的幕侠,因為其中相關(guān)的類涉及到很多的本地方法調(diào)用帝美,而大部分的人沒有勇氣也沒有能力或者說精力去看JVM源碼。要理解Selector的工作原理晤硕,其核心是要理解上面提到的IO多路復用模型悼潭。當明白了IO多路復用是怎么回事之后,猜也能猜到Selector的工作原理了舞箍。關(guān)于IO多路復用舰褪,網(wǎng)上有很多的資料,下面我們簡單說下创译。

以Linux下的epoll為例抵知,我們說說IO多路復用。
epoll模型中软族,有三個核心的系統(tǒng)調(diào)用:epoll_create刷喜、epoll_ctrl、epoll_wait

  • epoll_create:系統(tǒng)啟動時分配內(nèi)存立砸,構(gòu)造一棵紅黑樹掖疮。
  • epoll_ctrl:將一個socket信息和感興趣的事件注冊到紅黑樹中,并綁定一個回調(diào)函數(shù)颗祝,當內(nèi)核收到該socket的相關(guān)IO事件就緒時浊闪,將socket信息寫到一個列表中。
  • epoll_wait:讀上面說到的那個列表

以上關(guān)于IO多路復用模型的描述忽略了很多東西螺戳,因為它不是本文要說的重點搁宾,而是結(jié)合nio說說nio如何通過IO多路復用模型來實現(xiàn)非阻塞的io調(diào)用。當調(diào)用register()時倔幼,其實就是將socket信息和感興趣的事件注冊到內(nèi)核盖腿;selector.select()時阻塞去讀上面說的那個列表,當列表有數(shù)據(jù)時损同,將讀到的socket信息保存起來翩腐;read、write膏燃、accept都是selector.select()之后才會去執(zhí)行的操作茂卦,因為此時socket緩存已經(jīng)處于就緒狀態(tài)了,自然不會阻塞了组哩。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末等龙,一起剝皮案震驚了整個濱河市处渣,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌而咆,老刑警劉巖霍比,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異暴备,居然都是意外死亡悠瞬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進店門涯捻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來浅妆,“玉大人,你說我怎么就攤上這事障癌×柰猓” “怎么了?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵涛浙,是天一觀的道長康辑。 經(jīng)常有香客問我,道長轿亮,這世上最難降的妖魔是什么疮薇? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮我注,結(jié)果婚禮上按咒,老公的妹妹穿的比我還像新娘。我一直安慰自己但骨,他們只是感情好励七,可當我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著奔缠,像睡著了一般掠抬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上校哎,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天剿另,我揣著相機與錄音,去河邊找鬼贬蛙。 笑死,一個胖子當著我的面吹牛谚攒,可吹牛的內(nèi)容都是我干的阳准。 我是一名探鬼主播,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼馏臭,長吁一口氣:“原來是場噩夢啊……” “哼野蝇!你這毒婦竟也來了讼稚?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤绕沈,失蹤者是張志新(化名)和其女友劉穎锐想,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體乍狐,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡赠摇,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了浅蚪。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片藕帜。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖惜傲,靈堂內(nèi)的尸體忽然破棺而出洽故,到底是詐尸還是另有隱情,我是刑警寧澤盗誊,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布时甚,位于F島的核電站,受9級特大地震影響哈踱,放射性物質(zhì)發(fā)生泄漏荒适。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一嚣鄙、第九天 我趴在偏房一處隱蔽的房頂上張望吻贿。 院中可真熱鬧,春花似錦哑子、人聲如沸舅列。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽帐要。三九已至,卻和暖如春弥奸,著一層夾襖步出監(jiān)牢的瞬間榨惠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工盛霎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留赠橙,地道東北人。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓愤炸,卻偏偏與公主長得像期揪,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子规个,可洞房花燭夜當晚...
    茶點故事閱讀 45,086評論 2 355

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