網路編程之IO模型

来源:https://www.cnblogs.com/dafanjoy/archive/2023/12/18/17910875.html
-Advertisement-
Play Games

我們討論網路編程中的IO模型時,需要先明確什麼是IO以及IO操作為什麼在程式開發中是很關鍵的一部分,首先我們看下IO的定義。 IO的定義 IO操作(Input/Output操作)是電腦系統中的一種重要操作,用於數據的輸入和輸出,通常涉及到電腦與外部設備(如硬碟、網卡、鍵盤、滑鼠、印表機等)之間的 ...


我們討論網路編程中的IO模型時,需要先明確什麼是IO以及IO操作為什麼在程式開發中是很關鍵的一部分,首先我們看下IO的定義。

IO的定義

IO操作(Input/Output操作)是電腦系統中的一種重要操作,用於數據的輸入和輸出,通常涉及到電腦與外部設備(如硬碟、網卡、鍵盤、滑鼠、印表機等)之間的數據傳輸和交互的都可以認為是IO操作。

IO操作可以分為兩種主要類型:

1 輸入操作(Input)指從外部設備或數據源中讀取數據到電腦記憶體或程式當中。例如從硬碟讀取文件內容、從鍵盤接收用戶的輸入、從網卡接收數據等.

2 輸出操作(Output)指將電腦記憶體中的數據寫入到外部設備或數據目標中。例如將數據寫入到硬碟上的文件、文字列印輸出、將數據發送到網路上等都屬於輸出操作。

無論是哪種I/O 操作分為兩個部分:

  • 數據準備,將數據載入到內核緩存。(數據載入到操作系統)
  • 將內核緩存中的數據載入到用戶緩存(從操作系統複製到應用中)

因此開發工作當中涉及到的網路讀寫、資料庫操作、文件操作、日誌列印的,都可以歸為IO操作,IO操作的性能可以受到多種因素的影響,包括硬體性能、操作系統的優化、文件系統的性能等等。而網路IO特指電腦程式與網路之間進行數據交互的過程,在編碼層面我們可以簡單把它定義成的Socket套接字操作,如Socket的創建和關閉,數據的發送和接收。

IO的重要性

那麼為什麼我們要關心IO操並對應存在多種IO模型呢, 因為IO操作的本質是要與硬體進行數據交互,這個過程是需要時間的,返回的結果也是需要等待的,從而也就造成了我們經常說的阻塞問題,你的程式是需要停在那裡等待的,所以在大部分程式開發場景中,IO操作通常是最耗時的操作之一,並且最容易成為性能瓶頸的關鍵因素,特別是一旦你的程式開始上規模的,同樣的一段程式,一旦需要處理的數量級上去,就會升級成一個複雜的問題;同理網路IO之所以重要且被人反覆提及,是因為網路開發特別是服務端的開發中不可能處理的是單一鏈接,處理100個鏈接與處理100萬個鏈接面對的性能挑戰是不能同日而語的,這也是C10K問題的由來,所以一些需要處理海量鏈接的服務應用,比如IOT物聯網服務,推送服務, 為了提高IO操作的性能,通常會使用一些阻塞、非阻塞IO、非同步IO模型來優化IO對整個應用的性能影響,也就是我們經常聽到的BIO、NIO、AIO等IO模型,當然這隻是解決上面所說問題的方案其中的一個環節。

IO依據阻塞,非阻塞,同步,非同步等特點可以劃分為阻塞IO(BIO)、非阻塞IO(NIO)、多路復用IO(multiplexing IO)、非同步IO(AIO)。每一種IO都有他們的使用場景和優勢,其中平常我們說的NIO已經包含了IO的多路復用,以下這張圖各個IO和阻塞非阻塞,同步非同步之間的關係。

其中阻塞、非阻塞、多路IO復用這些需要輪詢處理的都是同步IO,真正的非同步IO中程式只需要等待一個完成的信號的通知,也就是我們通常說的非同步回調機制。所以拉一個子線程去輪訓或使用select、poll、epoll都不是非同步。

網路編程IO模型

上面我們闡述了IO的定義以及重要性,現在結合Java代碼的具體實現看下不同IO模型的具體實現與特點。

1、 阻塞IO模型

BIO(Blocking I/O)

  • BIO是最傳統的阻塞I/O模型,意味著當一個線程執行I/O操作時,它會一直等待直到操作完成。
  • 每個I/O操作都需要一個獨立的線程來處理,這會導致線程數量的大幅增加,降低了系統的併發性能。
  • 適用於一般連接數不多的應用場景。
public class BioServer {  
      
    public static void main(String[] args) throws IOException, InterruptedException {  
         
       ExecutorService executor = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),  
             new ThreadPoolExecutor.CallerRunsPolicy());  
       ServerSocket serverSocket = new ServerSocket();  
       serverSocket.bind(new InetSocketAddress("127.0.0.1",9091));//綁定IP地址與埠,定義一個服務端socket,開啟監聽  
       while (true) {  
          Socket socket = serverSocket.accept();//這裡如果沒有客戶端鏈接,會一直阻塞等待  
          //Thread.sleep(1000*30);  
          executor.execute(new BioServerHandler(socket));  
            
       }  
    }  
}

public class BioServerHandler implements Runnable{  
  
    private final Socket socket;  
      
    public BioServerHandler(Socket socket) {  
       this.socket=socket;  
    }  
      
    @Override  
    public void run() {  
       // TODO Auto-generated method stub  
       try {  
            
          while (true) {  
             byte[] rbytes = new byte[1024];  
             InputStream inputStream =  socket.getInputStream(); //通過IO輸入流接受消息  
                int rlength=inputStream.read(rbytes, 0, 1024); //消息長度  
             byte[] bytes = new byte[rlength];  
             System.arraycopy(rbytes, 0, bytes, 0, rlength);  
             String message = new String(bytes);  
             System.out.printf("Client: %s%n", message);  
  
             PrintStream writer = new PrintStream(socket.getOutputStream()); //通過IO輸出流發送消息  
             writer.println("Hello BIO Client");  
          }  
  
            
       } catch (IOException e) {  
          // TODO Auto-generated catch block  
          e.printStackTrace();  
       }  
         
    }  
  
}

2、 NIO模型

NIO 和多路復用是密切相關的概念,它們通常結合使用,特別是在網路編程中NIO 通常通過多路復用機制來實現非阻塞 I/O。多路復用允許一個線程同時監視多個通道的狀態,當某個通道有數據可讀或可寫時,多路復用機制會通知應用程式,這使得應用程式能夠高效地處理多個通道的 I/O 事件。

NIO引入了Channel和Buffer的概念,允許一個線程管理多個Channel,從而提高了系統的併發性能。

  1. Channel和Buffer:NIO引入了Channel和Buffer的概念,允許一個線程管理多個Channel。Channel表示與數據源(如文件或網路套接字)的連接,而Buffer是用於讀取或寫入數據的緩衝區。線程可以將數據從Channel讀取到Buffer,或者將數據從Buffer寫入到Channel,而無需等待數據準備好。

  2. Selector(選擇器):Selector是NIO的核心組件之一,它允許一個線程同時管理多個Channel。Selector可以檢測多個Channel是否準備好讀取或寫入數據,從而使線程能夠非阻塞地等待數據準備好的通知。這種機制稱為事件驅動,允許一個線程同時監視多個通道上的事件,只處理已經準備好的事件,而不是等待每個通道的數據準備好。

  3. 非阻塞調用:NIO中的Channel和Socket通常可以配置為非阻塞模式。在非阻塞模式下,當進行讀取或寫入操作時,如果沒有數據準備好,不會阻塞線程,而是立即返回一個狀態碼,表示沒有數據可用,這樣線程可以繼續處理其他Channel,而不會被一個阻塞的操作阻塞。

綜合上述機制,NIO允許一個線程同時處理多個連接,併在數據準備好時進行處理,而不會阻塞等待數據準備好。這種基於底層事件驅動的方式提高了應用系統的併發性能,特別適用於需要處理大量連接的場景。

public class NioServer {  
    public static void main(String[] args) {  
       try {  
          // TODO Auto-generated method stub  
            // 1.獲取Selector選擇器  
            Selector selector = Selector.open();  
            // 2.獲取通道  
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();  
            // 3.設置為非阻塞  
            serverSocketChannel.configureBlocking(false);  
            // 4.綁定連接  
            serverSocketChannel.bind(new InetSocketAddress("127.0.0.1", 8091));  
            // 5.將通道註冊到選擇器上,並註冊的操作為:“接收”操作  
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);  
              
              
            // 6.採用輪詢的方式,查詢獲取“準備就緒”的註冊過的操作  
            while (true) {  
                selector.select(); //阻塞
                Set<SelectionKey> selectionKeys = selector.selectedKeys();  
                Iterator<SelectionKey> iterator = selectionKeys.iterator();  
                while (iterator.hasNext()) {  
                    SelectionKey selectionKey = iterator.next();  
                    new NioServerHandler(serverSocketChannel, selectionKey).handle();  
                }  
               selectionKeys.clear();  
            }  
              
              
       } catch (Exception e) {  
          // TODO: handle exception  
       }  
         
  
    }  
  
}
public class NioServerHandler {  
      
    private ServerSocketChannel serverSocketChannel;  
      
    private SelectionKey selectionKey;  
      
    public  NioServerHandler(ServerSocketChannel serverSocketChannel, SelectionKey selectionKey) {  
       this.serverSocketChannel=serverSocketChannel;  
       this.selectionKey=selectionKey;  
    }  
  
    public void handle() {  
        ByteBuffer inputBuff = ByteBuffer.allocate(1024); // 分配讀ByteBuffer  
        ByteBuffer outputBuff = ByteBuffer.allocate(1024); // 分配寫ByteBuffer  
        try {  
            if (selectionKey.isAcceptable()) { //鏈接事件  
                SocketChannel socketChannel = serverSocketChannel.accept();  
                if (socketChannel == null) {  
                    return;  
                }  
                socketChannel.configureBlocking(false);  
                socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ);  
            }  
  
            if (selectionKey.isReadable()) {//讀事件  
                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();  
                if (socketChannel == null) {  
                    return;  
                }  
  
                inputBuff.clear();  
                int length = socketChannel.read(inputBuff);  
                if (length == -1) {  
                    socketChannel.close();  
                    selectionKey.cancel();  
                    return;  
                }  
  
                inputBuff.flip();  
               
             byte[] bytes = new byte[length];  
             System.arraycopy(inputBuff.array(), 0, bytes, 0, length);  
                  
                System.err.println( BytesUtils.toHexString(bytes));  
  
  
                socketChannel.register(selectionKey.selector(), SelectionKey.OP_WRITE);  
                selectionKey.selector().wakeup();//喚醒選擇器  
            }  
  
            if (selectionKey.isWritable()) {//寫事件  
                outputBuff.clear();  
                SocketChannel socketChannel = (SocketChannel) selectionKey.channel();  
                if (socketChannel == null) {  
                    return;  
                }  
                String message = "Hello, Client. " + UUID.randomUUID();  
                System.err.println(message);  
                outputBuff.put(message.getBytes(StandardCharsets.UTF_8));  
                outputBuff.flip();  
                socketChannel.write(outputBuff);  
  
                socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ);  
                selectionKey.selector().wakeup();//喚醒選擇器  
            }  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
  
}

3、 AIO模型

AIO(Asynchronous I/O),也稱為非同步 I/O,是一種用於處理輸入和輸出操作的編程模型。它與傳統的BIO和 NIO 模型有一些顯著的不同之處。它不需要像BIO與NIO通過輪詢方式去檢查數據是否準備好,而是由操作系統完成。當數據準備好後,操作系統會通知應用程式,併在回調函數中進行處理。

AIO使用了三個核心組件:AsynchronousChannel、CompletionHandler和
AsynchronousServerSocketChannel。其中,AsynchronousChannel是讀/寫數據的通道,CompletionHandler是I/O操作完成時的回調方法,AsynchronousServerSocketChannel是非同步伺服器端套接字通道,用於監聽客戶端的連接請求。
以下是 AIO 的主要特點:

  1. 非同步操作:AIO 的最重要特點是非同步操作。在 AIO 模型中,應用程式發起 I/O 操作後,不需要等待操作完成,而是可以繼續執行其他任務。當操作完成時,操作系統會通知應用程式,這種模型不會阻塞應用程式的執行。

  2. 回調機制:AIO 使用回調機制來處理 I/O 完成事件。應用程式在發起非同步操作時需要提供一個回調函數或回調對象,用於處理操作完成後的事件通知。這使得 AIO 編程更具事件驅動的特性。

  3. 提高併發性能:AIO 能夠在高併發環境中提供更好的性能,因為它在NIO允許一個線程管理多個 I/O 操作的基礎上實現了非同步回調,因此可以更好地處理大量連接。

  4. 複雜性:AIO 編程相對複雜,因為它涉及到回調和狀態管理。編寫和維護 AIO 代碼可能需要更多的工作,但可以提供更好的性能和響應性。

總之AIO 是一種適合高併發、低延遲要求的應用程式的編程模型。它的非同步特性和事件驅動的方式使得應用程式能夠更好地利用系統資源,提供更好的性能和響應性。然而AIO通常比傳統BIO或 NIO的具體實現更複雜,因此設計和編碼的複雜度會相對較高。

public class AioServer {  
  
    static ExecutorService executor = new ThreadPoolExecutor(2, 4, 1000, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10),  
            new ThreadPoolExecutor.CallerRunsPolicy());  
    public static void main(String[] args) throws IOException, InterruptedException {  
        AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open();  
        serverChannel.bind(new InetSocketAddress("127.0.0.1", 8091));  
  
            serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() {  
                @Override  
                public void completed(AsynchronousSocketChannel clientChannel, Void attachment) {  
                    // 繼續接受下一個連接  
                    serverChannel.accept(null, this);  
  
                    executor.execute(new AioServerHandler(clientChannel));  
                }  
  
                @Override  
                public void failed(Throwable exc, Void attachment) {  
                    exc.printStackTrace();  
                }  
            });  
  
            Thread.currentThread().join();  
        }  
          
}
public class AioServerHandler implements Runnable {  
  
    private AsynchronousSocketChannel clientChannel;  
    public  AioServerHandler(AsynchronousSocketChannel clientChannel){  
        this.clientChannel=clientChannel;  
    }  
    @Override  
    public void run() {  
        ByteBuffer inputBuff = ByteBuffer.allocate(1024); // 分配讀ByteBuffer  
        ByteBuffer outputBuff = ByteBuffer.allocate(1024); // 分配寫ByteBuffer  
        clientChannel.read(inputBuff, null, new CompletionHandler<Integer, Void>() {  
            @Override  
            public void completed(Integer bytesRead, Void attachment) {  
                if (bytesRead == -1) {  
                    // 客戶端關閉連接  
                    try {  
                        clientChannel.close();  
                    } catch (Exception e) {  
                        e.printStackTrace();  
                    }  
                    return;  
                }  
  
                inputBuff.flip();  
                byte[] data = new byte[bytesRead];  
                inputBuff.get(data);  
                String message = new String(data);  
                System.out.println("Received message: " + message);  
  
  
                outputBuff.clear();  
  
                outputBuff.put(message.getBytes(StandardCharsets.UTF_8));  
                outputBuff.flip();  
                clientChannel.write(outputBuff, null, new CompletionHandler<Integer, Void>() {  
                    @Override  
                    public void completed(Integer result, Void attachment) {  
                        System.out.println("Sent response: " + message);  
                    }  
  
                    @Override  
                    public void failed(Throwable exc, Void attachment) {  
                        exc.printStackTrace();  
                    }  
                });  
  
                inputBuff.clear();  
                clientChannel.read(inputBuff, null, this);  
            }  
  
            @Override  
            public void failed(Throwable exce, Void attachment) {  
                exce.printStackTrace();  
                try {  
                    clientChannel.close();  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            }  
        });  
    }  
}

結語

​瞭解不同IO模型的特點與具體實現是我們設計與開發大型應用程式的基礎,有助於我們根據不同的應用場景與性能要求選用最適合的模型併進行架構設計;同時一些常用的網路服務框架如Mina與Netty也都基於高性能的IO模型進行了深度的封裝與擴展實現,我們可以通過結合這些框架的源碼加深對網路IO模型在實際開發中應用的理解。後續我也會基於本章內容繼續豐富完善,闡述一個完整的網路應用程式需要具備的各種功能併進行實現。







github地址:https://github.com/dafanjoy/jcode

關註微信公眾號,查看更多技術文章。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 數據可視化 自 DataGrip 2023.3 發佈以來,已整合 Lets-Plot 庫,實現數據可視化。該可視化功能可用於所有三種類型的網格: 主選項卡:在打開表、視圖或 CSV 文件時,在分割模式下顯示圖表。 結果選項卡:在 服務 工具視窗中觀察查詢結果時,可以顯示圖表而不是網格。 編輯器中 ...
  • 在Java中,我們經常需要獲取某個類的Class對象,以便進行反射操作、動態載入類或者獲取類的信息。本文將介紹Java中獲取Class對象的三種常見方式。 1. 使用.class語法 獲取Class對象最簡單的方式就是使用.class語法。每個Java類都有一個隱含的靜態成員變數class,可以通過 ...
  • C 語言中的類型轉換 有時,您必須將一種數據類型的值轉換為另一種類型。這稱為類型轉換 隱式轉換 當您將一種類型的值分配給另一種類型的變數時,編譯器會自動進行隱式轉換。 例如,如果您將一個 int 值分配給一個 float 類型: // 自動轉換:int to float float myFloat ...
  • 為什麼需要熱載入 在某些情況,你可能不希望關閉Python進程並重新打開,或者你無法重新啟動Python,這時候就需要實現實時修改代碼實時生效,而不用重新啟動Python 在我的需求下,這個功能非常重要,我將Python註入到了其他進程,並作為一個線程運行。如果我想關閉Python,要麼殺死Pyth ...
  • 哈嘍大家好,我是鹹魚 我們知道 Python 是一門具有動態特性的語言,在編寫 Python 代碼的時候不需要顯式地指定變數的類型 這樣做雖然方便,但是降低了代碼的可閱讀性,在後期 review 代碼的時候容易對變數的類型產生混淆,需要查閱大量上下文,導致後期維護困難 為了提高代碼的可讀性、可維護性 ...
  • Qt 是一個跨平臺C++圖形界面開發庫,利用Qt可以快速開發跨平臺窗體應用程式,在Qt中我們可以通過拖拽的方式將不同組件放到指定的位置,實現圖形化開發極大的方便了開發效率,本章將重點介紹`TreeWidget`樹形選擇組件的常用方法及靈活運用。`QTreeWidget` 是 Qt 中的樹形控制項組件,... ...
  • 學習視頻:【編程不良人】2021年SpringBoot最新最全教程 第十章、項目開發 實現一個登錄註冊,增刪改查功能的系統 10.1 項目開發流程 需求分析 分析用戶主要需求 提取項目核心功能,根據核心功能構建頁面原型 庫表設計: 分析系統有哪些表 分析表之間關聯關係 確定欄位 詳細設計(流程圖、偽 ...
  • 將內容從 Word 文檔中提取出來可以方便我們對其進行其他操作,如將內容儲存在資料庫中、將內容導入到其他程式中、用於 AI 訓練以及製作其他文檔等。第三方庫 Spire.Doc for Python 提供了一個簡單的方法直接提取 Word 文檔中的內容,包括文本和圖片,而不需要大量的複製粘貼操作,也 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...