1. 程式人生 > 資訊 >波導股份第一季度淨利潤 552.94 萬元,同比增長 75.79%

波導股份第一季度淨利潤 552.94 萬元,同比增長 75.79%

轉載連結:https://mp.weixin.qq.com/s/m1nK32og0DlWyp1rMhPbiA

在學習 Netty 框架前有一個話題是無法繞過的,就是:網路程式設計 IO 模型,聽見 IO 模型有些同學就開始背八股文了,Java 常見 IO 模型有:

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

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

Socket 網路程式設計

網路程式設計中有一個重要的概念就是:Socket,我們簡單瞭解一下。

在網路通訊中,客戶端和服務端通過一個雙向的通訊連線實現資料的交換,連線的任意一端都可稱為一個 Socket

Talk is cheap, show me the diagram

,Socket 網路通訊基本過程如下圖所示:

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

  • (1)服務端啟動,監聽指定埠,等待客戶端連線;

  • (2)客戶端嘗試與服務端連線,建立可信資料傳輸通道;

  • (3)客戶端與服務端進行資料交換;

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

瞭解了基本流程,有些小夥伴可能對 Socket 這玩意很感興趣了,Socket 到底是什麼東西呢?Socket 中文翻譯過來就是套接字,是網路通訊物件的抽象表達,聽起來還是很模糊,從編碼者視角來看,本質上就是一套程式設計介面,是對複雜的 TCP/IP 協議進行封裝供上層應用使用,這樣總明白了吧。

那 Socket 物件一般包括什麼東西呢?一般包括五種資訊:連線使用的協議

本地主機的IP地址本地程序的協議埠遠端主機的IP地址遠端程序的協議埠。從這裡可以看到 Socket 包含的資訊非常豐富,也就是說拿到一個 Socket 物件就相當於知己知彼了。

傳統 BIO 模式

上面小節從理論角度講解了什麼是Socket,現在我們回到開發語言實現層面上來,以 Java 為例,Java 語言從 1.0 版本就已經封裝了 Socket 相關的介面供開發者使用,對這部分程式碼感興趣的小夥伴可以出門向左拐,在java.net 包下面檢視原始碼。

我們嘗試用一個 demo 來演示一下傳統的網路程式設計:

服務端程式碼:

public static void main(String[] args) throws IOException {
        // 建立一個ServerSocket,監聽埠8888
        ServerSocket ss = new ServerSocket(8888);
        
        // 迴圈方式監聽客戶端的請求
        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);

            // 迴圈接收並回復客戶端傳送的訊息
            byte[] bytes = new byte[1024];
            int len;
            while ((len = bufferedInputStream.read(bytes)) != -1) {
                printStream.print("服務端收到:" + new String(bytes, 0, len));
            }
        }
    }

效果演示:

服務端執行起來後,使用 telnet 命令來模擬客戶端傳送訊息:

telnet 127.0.0.1 8888

客戶端每傳送一條訊息,服務端都會回覆,演示效果如下:

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

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

public static void main(String[] args) throws IOException {
    // 建立一個ServerSocket,監聽埠8888
    ServerSocket ss = new ServerSocket(8888);

    // 迴圈方式監聽客戶端的請求
    while (true) {
        // 這裡一直會阻塞,直到客戶端連線上
        Socket socket = ss.accept();
        // 啟動一個新的執行緒處理
        new Thread(() -> handle(socket)).start();
    }
}

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

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

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

綜上所述,BIO 模式不能滿足大併發業務場景,僅適用於連線數目比較小且固定的架構。

同步阻塞 BIO 模式

根據上面的例子我們再畫圖抽象一下 BIO 網路程式設計場景:

傳統 BIO 的特點是隻要來了一個新客戶端連線,服務端就會開闢一個執行緒處理客戶端請求,但是客戶端連線後並不是一直都對服務端進行 IO 操作,這樣會導致服務端阻塞,一直佔用著執行緒資源,造成很多非要的開銷。

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

NIO

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

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

「New」是相對於傳統 BIO 來說的,在當時確實挺新的;Non Blocking IO 又被稱為:同步非阻塞 IO,同步非阻塞體現在:

  • 同步:呼叫的結果會在本次呼叫後返回,不存在非同步執行緒回撥之類的。
  • 非阻塞:表現為執行緒不會一直在等待,把連線加入集合後,執行緒會一直輪詢集合中的連線,有則處理,無則繼續接受請求。

NIO 三大基礎元件

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

(1)Buffer(緩衝區)

IO 是面向流(位元組流或者字元流)的,而 NIO 是面向的,指的是 Buffer 緩衝區。面向塊的方式一次性可以獲取或者寫入一整塊資料,而不需要一個位元組一個位元組的從流中讀取,這樣處理資料的速度會比流方式更快。

Buffer 緩衝區的底層實現是陣列,根據陣列型別可以細分為:ByteBuffe、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer等。

(2)Channel(通道)

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

常見 Channel 通道型別:

  • FileChannel 用於檔案操作場景;
  • ServerSocketChannel 和 SocketChannel 主要用於 TCP 網路通訊 IO,這是本文的重點;
  • DatagramChannel: 從 UDP 網路中讀取或者寫入資料。

Channel 與 Buffer 之間的關係:

每個 Channel 對應一個 Buffer 緩衝區,永遠無法將資料直接寫入到Channel或者從Channel中讀取資料。需要通過Buffer與Channel互動。

(3)Selector(多路複用器)

NIO 服務端的實現模式是把多個連線(請求)放入集合中,只用一個執行緒可以處理多個請求(連線),也就是多路複用,Linux 環境下多路複用底層主要用的是核心函式(select,poll)來實現的,為了提升效率,Java 1.5 版本開始使用 epoll。

關於 select、poll、epoll 之間的對比,感興趣的小夥伴可以自行上網查詢。

在 NIO 中多路複用器我們稱之為:Selector,Channel 會註冊到 Selector 上,由 Selector 根據 Channel 讀寫事件的發生將其交由某個空閒的執行緒處理。

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

基本的工作流程如下:

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

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

(3)當某個 Channel 有連線或者讀寫事件時,該 Channel 就會處於就緒狀態;

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

NIO 比 BIO 好用在哪?

NIO 相對於 BIO 最大的改進就是使用了多路複用技術,用少量執行緒處理大量客戶端 IO 請求,提高了併發量並減少了資源消耗;

另外NIO 的操作時非阻塞的,比如說,單執行緒中從通道讀取資料到buffer,同時可以繼續做別的事情,當資料讀取到buffer中後,執行緒再繼續處理資料。寫資料也是一樣的。

NIO 存在的問題

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

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

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

(2)NIO 程式設計涉及到 Reactor 模式,開發者需要對多執行緒和網路程式設計非常熟悉才能寫出高質量的 NIO 程式;

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

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

NIO 問題這麼多,有些開發者終於不能忍了,最終 Netty 框架橫空出世。