<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          原來你是這樣的 IO 模型

          共 4948字,需瀏覽 10分鐘

           ·

          2022-05-10 03:24

          在學習 Netty 框架前有一個話題是無法繞過的,就是:網(wǎng)絡編程 IO 模型,聽見 IO 模型有些同學就開始背八股文了,Java 常見 IO 模型有:

          • 同步阻塞 BIO
          • 同步非阻塞 NIO
          • 異步非阻塞 AIO

          今天跟大家一起重溫下這些知識點。

          Socket 網(wǎng)絡編程

          網(wǎng)絡編程中有一個重要的概念就是:Socket,我們簡單了解一下。

          在網(wǎng)絡通信中,客戶端和服務端通過一個雙向的通信連接實現(xiàn)數(shù)據(jù)的交換,連接的任意一端都可稱為一個 Socket

          Talk is cheap, show me the diagram,Socket 網(wǎng)絡通信基本過程如下圖所示:

          總結一下流程,可以簡單描述為這四步:

          • (1)服務端啟動,監(jiān)聽指定端口,等待客戶端連接;

          • (2)客戶端嘗試與服務端連接,建立可信數(shù)據(jù)傳輸通道;

          • (3)客戶端與服務端進行數(shù)據(jù)交換;

          • (4)客戶端或者服務端斷開連接,終止通信;

          了解了基本流程,有些小伙伴可能對 Socket 這玩意很感興趣了,Socket 到底是什么東西呢?Socket 中文翻譯過來就是套接字,是網(wǎng)絡通信對象的抽象表達,聽起來還是很模糊,從編碼者視角來看,本質上就是一套編程接口,是對復雜的 TCP/IP 協(xié)議進行封裝供上層應用使用,這樣總明白了吧。

          那 Socket 對象一般包括什么東西呢?一般包括五種信息:連接使用的協(xié)議本地主機的IP地址本地進程的協(xié)議端口遠端主機的IP地址遠端進程的協(xié)議端口。從這里可以看到 Socket 包含的信息非常豐富,也就是說拿到一個 Socket 對象就相當于知己知彼了。

          傳統(tǒng) BIO 模式

          上面小節(jié)從理論角度講解了什么是Socket,現(xiàn)在我們回到開發(fā)語言實現(xiàn)層面上來,以 Java 為例,Java 語言從 1.0 版本就已經(jīng)封裝了 Socket 相關的接口供開發(fā)者使用,對這部分代碼感興趣的小伙伴可以出門向左拐,在java.net 包下面查看源碼。

          我們嘗試用一個 demo 來演示一下傳統(tǒng)的網(wǎng)絡編程:

          服務端代碼:

          public?static?void?main(String[]?args)?throws?IOException?{
          ????????//?創(chuàng)建一個ServerSocket,監(jiān)聽端口8888
          ????????ServerSocket?ss?=?new?ServerSocket(8888);
          ????????
          ????????//?循環(huán)方式監(jiān)聽客戶端的請求
          ????????while?(true)?{
          ????????????//?這里一直會阻塞,直到客戶端連接上
          ????????????Socket?socket?=?ss.accept();

          ????????????//?輸入流用于接收消息
          ????????????InputStream?inputStream?=?socket.getInputStream();
          ????????????BufferedInputStream?bufferedInputStream?=?new?BufferedInputStream(inputStream);
          ????????????
          ????????????//?輸出流用于回復消息
          ????????????OutputStream?outputStream?=?socket.getOutputStream();
          ????????????final?PrintStream?printStream?=?new?PrintStream(outputStream);

          ????????????//?循環(huán)接收并回復客戶端發(fā)送的消息
          ????????????byte[]?bytes?=?new?byte[1024];
          ????????????int?len;
          ????????????while?((len?=?bufferedInputStream.read(bytes))?!=?-1)?{
          ????????????????printStream.print("服務端收到:"?+?new?String(bytes,?0,?len));
          ????????????}
          ????????}
          ????}

          效果演示:

          服務端運行起來后,使用 telnet 命令來模擬客戶端發(fā)送消息:

          telnet?127.0.0.1?8888

          客戶端每發(fā)送一條消息,服務端都會回復,演示效果如下:

          仔細想一下,上面的代碼可能會有問題,如果前面一個客戶端一直不斷開,服務端就不能處理其他客戶端的消息了,也就是說程序不具備并發(fā)的能力。

          我們稍加改造一下,將前面的處理邏輯代碼全部抽取到一個新的handle()方法, 每當有客戶端連接上就新開一個線程處理:

          public?static?void?main(String[]?args)?throws?IOException?{
          ????//?創(chuàng)建一個ServerSocket,監(jiān)聽端口8888
          ????ServerSocket?ss?=?new?ServerSocket(8888);

          ????//?循環(huán)方式監(jiān)聽客戶端的請求
          ????while?(true)?{
          ????????//?這里一直會阻塞,直到客戶端連接上
          ????????Socket?socket?=?ss.accept();
          ????????//?啟動一個新的線程處理
          ????????new?Thread(()?->?handle(socket)).start();
          ????}
          }

          這里為了演示方便直接新起了一個線程,當然更好的辦法是用線程池,但是也解決不了根本性問題。

          看了兩段代碼,先簡單總結一下 BIO 模式的劣勢:

          • 如果 BIO 使用單線程接收連接,則會阻塞其他連接,效率較低。
          • 如果使用多線程,雖然減弱了單線程帶來的影響,但當有大并發(fā)進來時,會導致服務器線程太多,壓力太大而崩潰。
          • 就算使用線程池,也只能同時允許有限個數(shù)的線程進行連接,如果并發(fā)量遠大于線程池設置的數(shù)量,還是與單線程無異。
          • IO 代碼里 read 操作是阻塞操作,如果連接不做數(shù)據(jù)讀寫操作會導致線程阻塞,就是說只占用連接,不發(fā)送數(shù)據(jù),則會浪費資源。比如線程池中 500個連接,只有 100 個是頻繁讀寫的連接,其他占著茅坑不拉屎,浪費資源!
          • 另外多線程也會有線程切換帶來的消耗。

          綜上所述,BIO 模式不能滿足大并發(fā)業(yè)務場景,僅適用于連接數(shù)目比較小且固定的架構。

          同步阻塞 BIO 模式

          根據(jù)上面的例子我們再畫圖抽象一下 BIO 網(wǎng)絡編程場景:

          傳統(tǒng) BIO 的特點是只要來了一個新客戶端連接,服務端就會開辟一個線程處理客戶端請求,但是客戶端連接后并不是一直都對服務端進行 IO 操作,這樣會導致服務端阻塞,一直占用著線程資源,造成很多非要的開銷。

          為了解決這個問題,Java 引入了 NIO,我們接著往下看。

          NIO

          在 Java 1.4 版本之前 BIO 是開發(fā)者唯一的選擇,1.4 版本開始引入了 NIO 框架。

          NIO 的 N 有兩層含義,一層是:New IO,另一層是 Non Blocking IO。

          「New」是相對于傳統(tǒng) BIO 來說的,在當時確實挺新的;Non Blocking IO 又被稱為:同步非阻塞 IO,同步非阻塞體現(xiàn)在:

          • 同步:調用的結果會在本次調用后返回,不存在異步線程回調之類的。
          • 非阻塞:表現(xiàn)為線程不會一直在等待,把連接加入集合后,線程會一直輪詢集合中的連接,有則處理,無則繼續(xù)接受請求。

          NIO 三大基礎組件

          學習 NIO必須得知道下面這三個基礎組件:

          (1)Buffer(緩沖區(qū))

          IO 是面向流(字節(jié)流或者字符流)的,而 NIO 是面向的,指的是 Buffer 緩沖區(qū)。面向塊的方式一次性可以獲取或者寫入一整塊數(shù)據(jù),而不需要一個字節(jié)一個字節(jié)的從流中讀取,這樣處理數(shù)據(jù)的速度會比流方式更快。

          Buffer 緩沖區(qū)的底層實現(xiàn)是數(shù)組,根據(jù)數(shù)組類型可以細分為:ByteBuffe、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer等。

          (2)Channel(通道)

          Channel 翻譯成中文是通道的意思,作用類似于 IO 中的 Stream 流。但是 Channel 和 Stream 不同之處在于 Channel 是雙向的,Stream 只是在一個方向移動,而且 Channel 可以用于讀、寫或者同時用于讀寫。

          常見 Channel 通道類型:

          • FileChannel 用于文件操作場景;
          • ServerSocketChannel 和 SocketChannel 主要用于 TCP 網(wǎng)絡通信 IO,這是本文的重點;
          • DatagramChannel: 從 UDP 網(wǎng)絡中讀取或者寫入數(shù)據(jù)。

          Channel 與 Buffer 之間的關系:

          每個 Channel 對應一個 Buffer 緩沖區(qū),永遠無法將數(shù)據(jù)直接寫入到Channel或者從Channel中讀取數(shù)據(jù)。需要通過Buffer與Channel交互。

          (3)Selector(多路復用器)

          NIO 服務端的實現(xiàn)模式是把多個連接(請求)放入集合中,只用一個線程可以處理多個請求(連接),也就是多路復用,Linux 環(huán)境下多路復用底層主要用的是內(nèi)核函數(shù)(select,poll)來實現(xiàn)的,為了提升效率,Java 1.5 版本開始使用 epoll。

          關于 select、poll、epoll 之間的對比,感興趣的小伙伴可以自行上網(wǎng)查詢。

          在 NIO 中多路復用器我們稱之為:Selector,Channel 會注冊到 Selector 上,由 Selector 根據(jù) Channel 讀寫事件的發(fā)生將其交由某個空閑的線程處理。

          Buffer、Channel、Selector 這三個組件的之間的關系可以用下面的圖來描述:

          基本的工作流程如下:

          (1)首先將 Channel 注冊到 Selector 中;

          (2)初始化 Selector,調用 select() 方法,select 方法會阻塞直到感興趣的事件來臨;

          (3)當某個 Channel 有連接或者讀寫事件時,該 Channel 就會處于就緒狀態(tài);

          (4)Selector 開始輪詢所有處于就緒狀態(tài)的SelectionKey,通過 SelectionKey 可以獲取對應的Channel 集合;

          NIO 比 BIO 好用在哪?

          NIO 相對于 BIO 最大的改進就是使用了多路復用技術,用少量線程處理大量客戶端 IO 請求,提高了并發(fā)量并減少了資源消耗;

          另外NIO 的操作時非阻塞的,比如說,單線程中從通道讀取數(shù)據(jù)到buffer,同時可以繼續(xù)做別的事情,當數(shù)據(jù)讀取到buffer中后,線程再繼續(xù)處理數(shù)據(jù)。寫數(shù)據(jù)也是一樣的。

          NIO 存在的問題

          NIO這么牛了,是不是就是終極解決方案了?其實也不是,NIO 也存在很多問題。

          我們來看看 NIO 有哪些問題?

          (1)NIO 的 API 使用起來非常麻煩,門檻比較高,開發(fā)者需要熟練掌握:Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等類。

          (2)NIO 編程涉及到 Reactor 模式,開發(fā)者需要對多線程和網(wǎng)絡編程非常熟悉才能寫出高質量的 NIO 程序;

          (3)異常場景處理麻煩,比如:客戶端斷連重連、網(wǎng)絡閃斷、拆包粘包、網(wǎng)絡擁塞等等;

          (4)NIO 有 bug,不穩(wěn)定,比如:臭名昭著的 Epoll bug,會導致 Selector 空輪詢,最終導致 CPU 100%。

          NIO 問題這么多,有些開發(fā)者終于不能忍了,最終 Netty 框架橫空出世。

          Netty 框架到底解決了什么問題,有哪些優(yōu)秀的特性,我們下期接著聊。

          -- End --

          昨天買了一個電腦支架,終于可以把兩個筆記本放在一塊了,目前桌面上的線比較凌亂,下一步買個線盒好好整理下,桌面干凈了,人也開心了??

          推薦學習:

          《從零開始造一個 RPC 輪子》

          《玩轉Redis面試》

          《圖解系列》

          好了,今天的技術文就到這里了。我是雷小帥,一個死磕技術的理工男,如果本文對你有幫助,麻煩點贊、分享、在看支持一下,非常感謝~

          瀏覽 59
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  婷婷五月丁香综合网 | 青青草视频免费在线 | 一本到久久 | 伊人性视频 | 91黄色大片 |