NIO

JavaIO演進之路

IO基礎入門

Linux 網(wǎng)絡IO模型簡介

linux內核把所有的外部設備都看做一個文件箭券,對一個文件的讀寫會調用內核提供的系統(tǒng)命令,返回一個file descripter(fd九妈,文件描述符)泰演,對一個Socket的讀寫也會有相應的描述符飒房,稱為sockerfd(socket描述符)

根據(jù)UNIX對IO模型的分類搁凸,UNIX提供了5種I/O模型,分別如下:

  1. 阻塞IO模型狠毯,最常用的IO模型就是阻塞IO模型坪仇,缺省情況下所有的文件操作都是阻塞的,系統(tǒng)調用直到數(shù)據(jù)包到達且被復制到應用進程的緩沖區(qū)中或者發(fā)生錯誤時才會返回垃你,在此期間會一直等待

  2. 非阻塞IO模型椅文,如果緩沖區(qū)沒有數(shù)據(jù)就直接返回一個錯誤喂很,一般都對非阻塞IO輪訓這個狀態(tài),看是不是有數(shù)據(jù)到來

  3. I/O復用模型:Linux提供select/poll皆刺,進程通過一個或多個fd傳遞給select/poll調用少辣,阻塞在select狀態(tài)上,這樣select/poll就可以幫我們偵測多個fd是否處于就緒狀態(tài)羡蛾。select/poll是輪詢掃描fd是否就緒漓帅。linux還提供一個epoll系統(tǒng)調用,epoll使用事件驅動的方式代替掃描痴怨,因此效率更高忙干。當有fd就緒就立刻回調callback

  4. 信號驅動I/O,首先開啟套接口信號驅動I/O功能浪藻,并通過系統(tǒng)調用sigaction執(zhí)行一個信號處理函數(shù) (此系統(tǒng)調用立即返回捐迫,進程立即工作,它是非阻塞的)爱葵。當數(shù)據(jù)準備就緒時施戴,就為該進程準備一個SIGIO信號内舟,回調應用程序讀取數(shù)據(jù)并且通知主循環(huán)處理數(shù)據(jù)

  5. 異步I/O:告知內核啟動某個操作和悦,并讓內核在整個操作完成后通知我們

I/O多路復用技術

在I/O編程過程中需要處理多個客戶端接入請求,可以利用多線程或者I/O多路復用技術來處理郎楼,I/O多路復用通過把多個I/O的阻塞復用到同一個select的阻塞上辆雾,進而使系統(tǒng)在單線程的情況下可以處理多個客戶端請求肪笋,與傳統(tǒng)的線程或者多線程相比,I/O多路復用最大的優(yōu)勢使系統(tǒng)開銷小度迂,系統(tǒng)不需要創(chuàng)建新的額外進程或者線程藤乙,也不需要維護這些進程或者線程的運行,降低了系統(tǒng)的維護工作量英岭,節(jié)省了系統(tǒng)資源,I/O多路復用的主要應用場景:

  1. 服務器需要同時處理多個處于監(jiān)聽狀態(tài)或者多個連接狀態(tài)的套接字
  2. 服務器需要同時處理多種網(wǎng)絡協(xié)議的套接字

目前支持I/O多路復用的系統(tǒng)調用有select湿右、pselect诅妹、poll、epoll毅人,在Linux網(wǎng)絡編程過程中吭狡,很長一段時間都使用select做輪詢和網(wǎng)絡事件通知,然而select的一些固有缺陷導致了他的應用受到了很大的限制丈莺,最終Linux不得不在新的內核版本中尋找select的替代方案划煮,最終選擇了epoll。epoll和select的原理比較類似缔俄,為了克服select的缺點弛秋,epoll做出了很多重大改進器躏,現(xiàn)總結如下:

  1. 支持一個進程打開的socket描述符fd不受限制,僅受限于操作系統(tǒng)的最大文件句柄數(shù)

select最大的缺陷就是單個進程打開的fd是有一定限制的蟹略,它由fd——setsize設置登失,默認是1024,對于那些需喲啊支持上網(wǎng)額TCP連接的大型服務器顯然太少了挖炬,可以選擇修改這個宏然后重新編譯內核揽浙,不過這會帶來網(wǎng)絡效率的下降。我們也可以選擇多進程的方案(傳統(tǒng)的apache)來解決這個問題意敛,不過雖然在linux上創(chuàng)建進程的代價比較小馅巷,但是也是不可忽視的。另外草姻,進程之間的數(shù)據(jù)交換非常麻煩钓猬,對于Java來說,由于沒有共享內存碴倾,需要通過Socket通信或者其他方式進行數(shù)據(jù)同步逗噩,這帶來了額外的性能損耗,增加了程序復雜度跌榔,所以也不是一種完美的解決方案异雁,值得慶幸的是,epoll并沒有這個限制僧须,它所支持的FD上限是操作系統(tǒng)的最大文件句柄數(shù)纲刀,這個數(shù)字遠遠大于1024.例如,在1G 內存的機器上大約是10萬個句柄左右担平,具體的值可以通過cat/proc/sys/fs/file-max查看示绊,通常情況下跟內存的關系較大

  1. I/O效率不會隨著fd數(shù)目的增加而線性下降
    傳統(tǒng)select/poll的另一個致命缺點就是當你有一個很大的socket集合時,由于網(wǎng)絡延遲或者鏈路空閑暂论,任意時刻只有少部分的socker是活躍的面褐,但是select/poll每次調用都會線性掃描全部的集合,導致效率呈現(xiàn)線性下降取胎,epoll不存在這個問題展哭,它只會對活躍的socket進行操作,這是因為在內核實現(xiàn)中epoll是根據(jù)每個fd上面的callback函數(shù)實現(xiàn)的闻蛀。那么匪傍,只有活躍的socket才會去主動調用callback函數(shù),其他idle狀態(tài)的socket則不會觉痛。在這一點上役衡,epoll實現(xiàn)了一個偽AIO。針對epoll和select的benchmark測試表明薪棒,如果所有的socket都處于活躍態(tài)--李儒一個告訴LAN環(huán)境手蝎,epoll并不比select效率高很多榕莺,相反如果過多使用epoll_ctl,效率還有稍微降低柑船,但是一點使用idle connection模擬WAN環(huán)境帽撑,epoll的效率救援在select/poll之上了

  2. 使用mapp加速內核和用戶空間之間的消息傳遞

  3. epoll的API更簡單
    包括創(chuàng)建一個epoll描述符,添加監(jiān)聽事件鞍时,阻塞等待的監(jiān)聽事件的發(fā)生亏拉、關閉epoll描述符等等

epoll是Linux系統(tǒng)下用來克服select/poll缺點的方法

JavaNIO歷史

Java1.4 NIO1.0 Java1.7NIO2.0

1.0的主要問題是:

  1. 沒有統(tǒng)一的文件屬性
  2. API能力比較弱不能實現(xiàn)目錄的級聯(lián)創(chuàng)建和遞歸遍歷
  3. 底層存儲的一些高級API不能使用
  4. 所有文件的操作都是同步阻塞調用,不支持異步文件讀寫

NIO入門

傳統(tǒng)的BIO編程

通常有一個獨立的Accepter線程負責監(jiān)聽客戶端的連接逆巍,它接受到客戶端連接之后偉每一個客戶端創(chuàng)建一個新的線程來進行鏈路處理及塘。處理完成后通過輸出六返回應答給客戶端

問題就是缺乏彈性伸縮的能力,當客戶端并發(fā)訪問量增加之后锐极,服務端的線程個數(shù)喝客戶端并發(fā)數(shù)成一比一的正比關系笙僚,線程數(shù)膨脹后,性能將急劇下降

public class TimeServerHandler implements Runnable{
    private Socket socket;
    public TimeServerHandler(Socket socket){
        this.socket = socket;
    }
    @Override
    public void run(){
        BufferdReader in = null;
        PrintWriter out = null;
        try{
            in = new BufferedReader(new InputStreamReader(this.socket.getInputStream());
            out = new PrintWriter(this.socket.getOutputStream, true);
            String currentTime = null;
            String body = null;
            while(true){
                body = in.readLine();
                if(body == null) break;
                System.out.println("The time server receive order : " + body);
                currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date()
            }
        }catch (Exception e){
            if(in != null){
                try{
                    in.close();
                }catch(IOException e1){
                    e1.printStackTrace();
                }
            }
            if(out != null){
                out.close();
                out = null;
            }
            if(this.socket != null){
                try{
                    this.socket.close();
                }catch(IOException e1){
                    e1.printStackTrace();
                }
                this.socket = null;
            }
        }
    }
}

NIO類庫介紹

NIO彌補了原來同步阻塞I/O的不足灵再,他在標準Java代碼中提供了高速的肋层,面向塊的I/O。通過定義包含數(shù)據(jù)的類翎迁,以及通過以塊的形式處理這些數(shù)據(jù)栋猖,NIO不喲就那個使用本機代碼就可以利用第幾優(yōu)化,這是原來的I/O包無法做到的汪榔。

緩沖區(qū)Buffer

Buffer是一個對象蒲拉,它包含一些要寫入或者要讀出的數(shù)據(jù)。在NIO類庫中痴腌,所有數(shù)據(jù)都使用緩沖區(qū)來處理的雌团。在讀取數(shù)據(jù)時,它是直接讀到緩沖區(qū)中的士聪,在寫入數(shù)據(jù)時锦援,寫入到緩沖區(qū)中。任何時候訪問NIO中的數(shù)據(jù)剥悟,都是用過緩沖區(qū)進行操作灵寺。

緩沖區(qū)實質上是一個數(shù)組。通常他是一個字節(jié)數(shù)組ByteBuffer懦胞,也可以使用其他種類的數(shù)組替久。但是一個緩沖區(qū)不僅僅是一個數(shù)組凉泄,緩沖區(qū)提供了對數(shù)據(jù)的結構化訪問以及維護讀寫位置等信息躏尉。

最常用的緩沖區(qū)是ByteBuffer,除了ByteBuffer還有其他的一些緩沖區(qū)后众。實際上胀糜,每一種Java基本類型(除了Boolean)都對應一種緩沖區(qū):
ByteBuffer颅拦、CharBuffer、IntBuffer...

每一個Buffer類都是Buffer接口的一個子實例教藻。除了ByteBuffer距帅,每一個Buffer類都有完全一樣的操作,大部分標準I/O操作都使用ByteBuffer括堤,所以他在具有一般緩沖區(qū)的操作之外還提供了一些特有的操作碌秸,以方便網(wǎng)絡讀寫。

通道Channel

Channel是一個通道悄窃,它就像自來水管一樣讥电,網(wǎng)絡數(shù)據(jù)通過Channel讀取和寫入。通道與流的不同之處在于通道是雙向的轧抗,流只是在一個方向上流動(一個流必須是InputSteram或者OutputStream的子類)而通道可以用于讀寫恩敌、或者二者同時進行

因為Channel是全雙工的,所以它可以比流更好的映射底層操作系統(tǒng)的API横媚。特別是在UNIX網(wǎng)絡編程模型中纠炮,底層系統(tǒng)的通道都是全雙工的,同時支持讀寫操作灯蝴。

Channel可以分為兩大類:用于網(wǎng)絡讀寫的SelectableChannel和用于文件操作的FileChannel
本書涉及的ServerSocketChannel 和 SocketChannel的子類

多路復用 Selector

多路復用器Select是NIO編程的基礎恢口。多路復用器提供選擇已經(jīng)就緒任務的能力。簡單來講绽乔,selector會不斷輪詢注冊在其上的Channel弧蝇,如果某個Channel上面發(fā)生讀或者寫時間,這個Channel就處于就緒狀態(tài)折砸,會被Selector輪詢出來看疗,然后通過SelectionKey可以獲取就緒Channel的集合,進行后續(xù)的I/O操作睦授。

一個多路復用器可以同時輪詢多個Channel两芳,由于JDK使用了epoll()代替?zhèn)鹘y(tǒng)的select實現(xiàn),所以他沒有最大連接數(shù)的限制去枷,這也就意味著只需要一個線程負責Selector的輪詢怖辆,就可以接入成千上萬的客戶端,這確實是個非常大的進步删顶。

NIO服務端步驟

  1. 打開ServerSocketChannel竖螃,用于監(jiān)聽客戶端連接,是所有客戶端連接的父管道
ServerSocketChannel acceptorSvr = ServerSocketChannel.open();
  1. 綁定過監(jiān)聽端口逗余,設置連接為非阻塞模式
acceptorSvr.socket().bind(new InetSocketAddress(InetAddress.getByName("IP"), port));

acceptorSvr.configureBlocking(false);
  1. 創(chuàng)建Reactor線程特咆,創(chuàng)建多路復用器并且啟動線程
Selector selector = Selector.open();
New Thread(new ReactorTask()).start());
  1. 將ServerSocketChannel注冊到Reactor線程的多路復用器Selector上,監(jiān)聽ACCEPT事件:
SelectionKey key = accptorSvr.register( selector, SelectionKey.OP_ACCEPT, iohandler);
  1. 多路復用器在線程run方法的無限循環(huán)體內輪詢準備就緒的key:
int num = selector.select();
Set sekectedKeys = selector.selectedKeys();
Iterator it = selectedKeys.iterator();
while(it.hasNext()){
    SelectionKey key = (SekectionKey)it.next();
    //...deal with I/O event...
}
  1. 多路復用器監(jiān)聽到有新的客戶端接入录粱,處理新的接入請求腻格,完成TCP三次握手画拾,建立物理鏈路
SocketChannel channel = svrChannel.accept();
  1. 設置客戶端鏈路為非阻塞模式
channel.configureBlocking(false);
channel.socket().setReuseAddress(true);
...
  1. 將新接入的客戶端連接注冊到reactor線程的多路復用器上,監(jiān)聽讀操作菜职,讀取客戶端發(fā)送的網(wǎng)絡消息
SelectionKey key = socketChannel.register( selector, SelectionKey.OP_READ, ioHandler);
  1. 異步讀取客戶端請求消息到緩沖區(qū)
int readNumber = channel.read(reaceivedBuffer);
  1. 對ByteBuffer進行編解碼青抛,如果有半包消息指針reset,繼續(xù)讀取后續(xù)的報文酬核,將解碼成功的消息封裝成Task蜜另,投遞到業(yè)務線程池中,進行業(yè)務邏輯編排
Object message = null;
while(buffer.hashRemain()){
    byteBuffer.mark();
    Object message = decode(byteBuffer);
    if(message == null){
        byteBuffer.reset();
        break;
    }
    messageList.add(message);
}
if(!byteBuffer.hasRemain()){
    byteBuffer.clear();
}else{
    byteBuffer.compact();
}
if(messageList != null & !messageList.isEmpty()){
    for(Object messageE : messageList){
        handlerTask(messageE);
    }
}
  1. 將POJP對象encode成ByteBuffer嫡意,調用SocketChannel的異步write接口蚕钦,將消息異步發(fā)送給客戶端
socketChannel.write(buffer);

簡單描述NIO過程:

  1. 把ServerSocketChannel注冊到選擇器中,綁定accept事件鹅很。當觸發(fā)accept事件時嘶居,就可以獲取到SocketChannel
  2. 接收請求參數(shù)的多路復用是吧SocketChannel注冊到選擇器中,綁定read事件促煮,當選擇器檢測到相應的通道準備好進行讀取邮屁,就觸發(fā)read事件,通過相應的SocketChannel讀取數(shù)據(jù)然后處理數(shù)據(jù)
  3. 處理完成后菠齿,再把SocketChannel綁定到一個新的選擇器中佑吝,綁定wirte事件,等可以寫時绳匀,就把數(shù)據(jù)發(fā)送出去芋忿。

https://blog.csdn.net/an_tao/article/details/45914841

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市疾棵,隨后出現(xiàn)的幾起案子戈钢,更是在濱河造成了極大的恐慌,老刑警劉巖是尔,帶你破解...
    沈念sama閱讀 211,265評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件殉了,死亡現(xiàn)場離奇詭異,居然都是意外死亡拟枚,警方通過查閱死者的電腦和手機薪铜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評論 2 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恩溅,“玉大人隔箍,你說我怎么就攤上這事〗畔纾” “怎么了蜒滩?”我有些...
    開封第一講書人閱讀 156,852評論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我帮掉,道長,這世上最難降的妖魔是什么窒典? 我笑而不...
    開封第一講書人閱讀 56,408評論 1 283
  • 正文 為了忘掉前任蟆炊,我火速辦了婚禮,結果婚禮上瀑志,老公的妹妹穿的比我還像新娘涩搓。我一直安慰自己,他們只是感情好劈猪,可當我...
    茶點故事閱讀 65,445評論 5 384
  • 文/花漫 我一把揭開白布昧甘。 她就那樣靜靜地躺著,像睡著了一般战得。 火紅的嫁衣襯著肌膚如雪充边。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評論 1 290
  • 那天常侦,我揣著相機與錄音浇冰,去河邊找鬼。 笑死聋亡,一個胖子當著我的面吹牛肘习,可吹牛的內容都是我干的。 我是一名探鬼主播坡倔,決...
    沈念sama閱讀 38,921評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼漂佩,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了罪塔?” 一聲冷哼從身側響起投蝉,我...
    開封第一講書人閱讀 37,688評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎征堪,沒想到半個月后墓拜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡请契,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,467評論 2 325
  • 正文 我和宋清朗相戀三年咳榜,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片爽锥。...
    茶點故事閱讀 38,617評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡涌韩,死狀恐怖,靈堂內的尸體忽然破棺而出氯夷,到底是詐尸還是另有隱情臣樱,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站雇毫,受9級特大地震影響玄捕,放射性物質發(fā)生泄漏。R本人自食惡果不足惜棚放,卻給世界環(huán)境...
    茶點故事閱讀 39,882評論 3 312
  • 文/蒙蒙 一枚粘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧飘蚯,春花似錦馍迄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至峦甩,卻和暖如春赘来,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背凯傲。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評論 1 265
  • 我被黑心中介騙來泰國打工撕捍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人泣洞。 一個月前我還...
    沈念sama閱讀 46,315評論 2 360
  • 正文 我出身青樓忧风,卻偏偏與公主長得像,于是被迫代替她去往敵國和親球凰。 傳聞我的和親對象是個殘疾皇子狮腿,可洞房花燭夜當晚...
    茶點故事閱讀 43,486評論 2 348