Java 原生網絡編程(BIO | NIO | Reactor 模式)

1、基本常識

Socket 是應用層與 TCP/IP 協議族通信的中間軟件抽象層,是一組接口,使用了門面模式對應用層隱藏了傳輸層以下的實現細節。TCP 用主機的 IP 地址加上主機端口號作為 TCP 連接的端點,該端點叫做套接字 Socket。

比如三次握手,調用 Socket.connect() 就能完成,應用開發者無須關心如何具體實現三次握手。

長連接與短連接沒有哪一個更好之說,只是結合具體業務使用哪一個更合適。

任何網絡通信編程關注的三件事:

  1. 連接(客戶端連接服務器,服務器接收客戶端的連接)
  2. 讀網絡數據
  3. 寫網絡數據

常見的網絡編程方式有三種:

  1. BIO:阻塞式 IO,當線程無法讀取到數據或無法寫入數據時,線程會進入阻塞狀態
  2. NIO:非阻塞式 IO,也稱 IO 多路復用,即一個線程為多個客戶端執行讀寫操作。當一個客戶端無法讀寫數據即將陷入阻塞狀態之前,線程會切換到其他客戶端的讀寫工作中,避免阻塞帶來的效率低下問題
  3. AIO:異步 IO,Linux 的異步 IO 實際上是通過 NIO 實現的,而 Windows 才提供了真正的異步 IO,因此在 Linux 和 Java 這一側關注的是 BIO 與 NIO

2、BIO

服務端通過 ServerSocket 獲取到客戶端的連接 Socket,為每個連接分配一個單獨的線程,通過 IO 流進行同步阻塞式通信:

public class Server {// 別用 CachedThreadPool,與 new Thread() 沒啥區別private static ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);public static void main(String[] args) {ServerSocket serverSocket = null;try {serverSocket = new ServerSocket();serverSocket.bind(new InetSocketAddress(10001));System.out.println("Start server...");while (true) {executorService.submit(new ServerTask(serverSocket.accept()));}} catch (IOException e) {e.printStackTrace();} finally {try {if (serverSocket != null) {serverSocket.close();}} catch (IOException e) {e.printStackTrace();}}}static class ServerTask implements Runnable {private Socket socket;public ServerTask(Socket socket) {this.socket = socket;}@Overridepublic void run() {try (ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream())) {String userName = objectInputStream.readUTF();objectOutputStream.writeUTF("Hello," + userName);objectOutputStream.flush();} catch (IOException e) {e.printStackTrace();}}}
}

客戶端使用 Socket 連接綁定服務器端口后與服務器通信:

public class Client {public static void main(String[] args) throws IOException {Socket socket = new Socket();socket.connect(new InetSocketAddress("127.0.0.1", 10001));try (ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream())) {objectOutputStream.writeUTF("James");objectOutputStream.flush();System.out.println(objectInputStream.readUTF());} finally {socket.close();}}
}

3、NIO

首先要清楚一點,在配置參數相同的情況下,單次網絡通信,BIO 的效率是比 NIO 高的,但是由于 NIO 中一個服務端線程可以與多個客戶端通信,所以 NIO 這個 IO 多路復用的機制,總體上比 BIO 效率更高。從成本角度考慮,NIO 節省成本,而 BIO 則是以成本換效率。

3.1 三大核心組件

NIO 的三大核心組件:

  • Selector:選擇器,也稱為輪詢代理器或事件訂閱器,可以在一個單獨的線程中操作 Selector 選擇不同的 Channel,從而實現在一個線程中管理多個通道。應用程序向 Selector 注冊需要其關注的 Channel 以及 Channel 感興趣的 IO 事件,Selector 內則保存已經注冊的 Channel 的容器
  • Channels:通道,是應用程序與操作系統讀寫數據的渠道,通道中的數據總是先讀到 Buffer 或從 Buffer 寫入。Selector 注冊的是 SelectableChannel,其子類 ServerSocketChannel 支持應用程序向操作系統注冊 IO 多路復用的端口監聽,同時支持 TCP 和 UDP;而另一個子類 SocketChannel 則是 TCP Socket 的監聽通道
  • Buffer:本質上就是一個數組,其內存被包裝成 Buffer 對象并提供了方便訪問該內存的方法。僅與 Channel 做數據交換。

3.2 重要概念 SelectionKey

除此之外還有一個重要概念 SelectionKey,表示 SelectableChannel 在 Selector 中注冊的標識。Channel 向 Selector 注冊時,會創建 SelectionKey 建立 Channel 與 Selector 的聯系,同時維護 Channel 事件。

SelectionKey 有四種類型:

  1. OP_READ:操作系統讀緩沖區可讀,并非所有時刻都有數據可讀,因此需要注冊該操作
  2. OP_WRITE:操作系統寫緩沖區有空閑空間,一般情況下都有空閑空間,因此沒必要注冊該類型,否則浪費 CPU;但如果是寫密集型的任務,比如下載文件,緩沖區可能會滿,此時就需要注冊該操作類型,并在寫完后取消注冊
  3. OP_CONNECT:只給客戶端使用,在 SocketChannel.connect() 連接成功后就緒
  4. OP_ACCEPT:只給服務器使用,在接收到客戶端連接請求時就緒

這四種類型也再次闡明了網絡編程關注的三件事:連接(客戶端連接服務器,服務器接收客戶端的連接)、讀、寫網絡數據。

不同的 Channel 允許注冊的事件類型不同:

  • 服務器 ServerSocketChannel:僅 OP_ACCEPT
  • 服務器 SocketChannel:OP_READ、OP_WRITE
  • 客戶端 SocketChannel:OP_READ、OP_WRITE 和 OP_CONNECT

3.3 Buffer

緩沖區本質上是一塊可以寫入數據,然后可以從中讀取數據的內存(其實就是數組),這塊內存被包裝成 NIO Buffer 對象,并提供了一組方法,用來方便的訪問該塊內存。

Buffer 位于 Channel 和應用程序之間。應用程序對外寫數據時,是寫到 Buffer,由 Channel 將 Buffer 中的數據讀出并發送出去;讀也是類似的,數據是先從 Channel 讀到 Buffer 后,應用程序再讀 Buffer 中的數據。

Buffer 有三個重要屬性:

  1. capacity:內存容量,只能寫 capacity 個 byte、long、char 類型數據,Buffer 滿了之后需要通過讀數據或清除數據將其清空后,才能繼續寫數據
  2. position:表示操作數據的位置,寫模式下,每寫完一個數據會向下移動一個單位,最大為 capacity - 1;讀模式下,每讀完一個數據會向前移動到下一個可讀的位置。讀寫模式切換時,position 會被重置為 0
  3. limit:寫模式下表示最多能向 Buffer 中寫多少數據,此時 limit 等于 capacity;讀模式下表示最多能讀到多少數據,切換到讀模式時,limit 會被置為寫模式下的 position,即可讀取此前所有寫入的數據

Buffer 既可以讀也可以寫,需要通過 flip() 從寫模式切換到讀模式,而當讀完數據后,可以通過 clear() 或 compact() 清理緩沖區并切換成寫模式,其中前者會清空整個緩沖區,而后者則只清除已經讀取過的數據。

完整的通信結構如下:

請添加圖片描述

大致步驟:

  1. 服務端 ServerSocketChannel 向 Selector 注冊 OP_ACCEPT
  2. 客戶端連接服務器,Selector 會通知 ServerSocketChannel 連接事件,此時 ServerSocketChannel 可以產生一個 SocketChannel 與客戶端進行通信,并注冊 OP_READ
  3. 客戶端發送數據,Selector 會通知服務端的 SocketChannel 讀取數據,這些數據會被寫入 Buffer,服務器的應用程序可以從 Buffer 中讀取這些數據
  4. 當服務器的應用程序發送應答消息給客戶端時,是向 Buffer 中寫入數據,SocketChannel 會從 Buffer 中讀取這些數據并發送出去

BIO 時,假如分三次向對端寫 100 個字節,那么就要進行三次系統調用。而使用 NIO,可以將 100 個字節寫入 Buffer,從 Buffer 讀取數據再進行一次系統調用就可以發送數據了。由于系統調用會消耗大量系統資源,所以 NIO 是提升了性能的。類似的,BIO 在讀取數據時,不論從系統讀取到多少數據都要經過一次系統調用交給應用程序,而 NIO 可以將從操作系統讀取的數據先存入 Buffer 中,然后從 Buffer 通過一次系統調用傳輸給應用程序。

3.4 NIO 編程實踐

基礎使用代碼見 GitHub 上相關章節,注意事項見課程文檔。這里主要說一下在讀寫數據時為什么一般不注冊寫事件 OP_WRITE。

一般情況下,服務器在寫數據時,是不注冊 OP_WRITE 直接通過 SocketChannel.write() 寫的:

	private void handleInput(SelectionKey key) throws IOException {// 由于 SelectionKey 是可以取消的,因此使用前需要先判斷是否可用if (key.isValid()) {if (key.isAcceptable()) {// 只有 ServerSocketChannel 才關注 OP_ACCEPTServerSocketChannel ssc = (ServerSocketChannel) key.channel();// 獲取和客戶端通信的 SocketSocketChannel sc = ssc.accept();System.out.println("有客戶端連接");sc.configureBlocking(false);sc.register(selector, SelectionKey.OP_READ);}// 讀數據if (key.isReadable()) {SocketChannel sc = (SocketChannel) key.channel();// 如果要讀取的數據多于 1024 字節,那么讀事件會被觸發多次直到讀完ByteBuffer buffer = ByteBuffer.allocate(1024);int readBytes = sc.read(buffer);if (readBytes > 0) {// 因為 Channel 寫入了 Buffer,因此讀的時候需要進行模式切換buffer.flip();// 讀取數據做業務處理byte[] bytes = new byte[readBytes];buffer.get(bytes);String message = new String(bytes, "UTF-8");System.out.println("服務器收到消息: " + message);String result = Const.response(message);// 發送應答消息doWrite(sc, result);} else if (readBytes < 0) {// 小于 0 說明鏈路已經關閉,釋放資源key.cancel();sc.close();}}}}private void doWrite(SocketChannel sc, String result) throws IOException {byte[] bytes = result.getBytes();ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);// 將字節數組復制到 writeBufferwriteBuffer.put(bytes);// 切換到讀模式writeBuffer.flip();sc.write(writeBuffer);}

假如想在 OP_WRITE 下向客戶端寫數據,就要修改為如下這樣:

	private void handleInput(SelectionKey key) throws IOException {// 由于 SelectionKey 是可以取消的,因此使用前需要先判斷是否可用if (key.isValid()) {...// 添加寫數據邏輯if (key.isWritable()) {System.out.println("writable...");SocketChannel sc = (SocketChannel) key.channel();ByteBuffer attachment = (ByteBuffer) key.attachment();if (attachment.hasRemaining()) {System.out.println("write :" + sc.write(attachment) + " byte");} else {// 寫完數據后要取消對寫事件的注冊,否則系統會一直通知寫事件key.interestOps(SelectionKey.OP_READ);}}}}private void doWrite(SocketChannel sc, String result) throws IOException {byte[] bytes = result.getBytes();ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);// 將字節數組復制到 writeBufferwriteBuffer.put(bytes);// 切換到讀模式writeBuffer.flip();
//        sc.write(writeBuffer);// register() 注冊哪一個事件就只關注該事件,因此這里在注冊寫事件時不要忘了讀,同時將 writeBuffer// 作為附件也一并注冊sc.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, writeBuffer);}

之所以在寫完之后要取消對寫事件的關注,主要是因為讀寫事件的觸發機制是不一樣的。當客戶端向服務器發送數據時,服務端有數據可讀,就會觸發 OP_READ 事件。

而 OP_WRITE 則不同,當通信雙方 Socket 連接成功后,操作系統會為每個 Socket 創建兩個操作系統級別的緩存(注意并不是應用程序中用到的 Buffer,應用程序是感知不到這個緩存的,這個緩存在操作系統內核中),一個輸出緩存,一個輸入緩存。當輸入緩存中有對端發來的數據時,就會觸發 OP_READ 事件,而輸出緩存中,只要有空閑空間,就會一直不停的觸發 OP_WRITE。

通常都是要寫的數據非常多,數據量大于緩沖區需要多次寫的時候,才注冊 OP_WRITE。

3.5 Reactor 模式

Reactor 翻譯為“反應器”,可以延伸為“倒置”、“控制逆轉”,即事件處理程序不調用反應器,而是向反應器注冊一個事件處理器,當事件到來時調用事件處理程序做出反應。這種控制逆轉又稱為“好萊塢法則”。

NIO 的 Selector 就扮演著 Reactor 的角色。Reactor 模式又可以分為三種流程:

  1. 單線程 Reactor 模式:服務器全程只使用一個線程,即 IO 操作(accept、read、write)與業務操作(decode、compute、encode)都在一個線程上處理。這樣有一個問題增大 IO 響應的時間。示意圖如下:
    請添加圖片描述
  2. 單線程 Reactor,工作者線程池:添加工作者線程池,將非 IO 操作從 Reactor 線程中移出交給工作者線程池執行。這種模式在處理大并發、大數據量的業務時是不合適的。因為面對成百上千的 IO 操作,一個線程的處理能力始終是有限的。再比如讀取 10M 的數據,在讀取時其他 IO 操作是無法進行的。示意圖如下:
    請添加圖片描述
  3. 多 Reactor 線程模式:針對第二種模式的缺點,再引入一個 Reactor 線程池。Reactor 線程池中的每一 Reactor 線程都會有自己的 Selector、線程和分發的事件循環邏輯。mainReactor 可以只有一個,但 subReactor 一般會有多個。mainReactor 線程主要負責接收客戶端的連接請求,然后將接收到的 SocketChannel 傳遞給 subReactor,由 subReactor 來完成和客戶端的通信。示意圖如下:
    請添加圖片描述

Reactor 模式看似與觀察者模式很像,二者的主要區別是觀察者模式與單個事件源關聯,而反應器模式則與多個事件源關聯。當一個主體發生改變時,所有依屬體都得到通知。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/83447.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/83447.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/83447.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

OpenCV透視變換

概念 OpenCV 透視變換是將圖像從一個視平面投影到另一個視平面的過程&#xff0c;也叫投影映射 &#xff0c;屬于空間立體三維變換。它基于透視原理&#xff0c;通過 33 的變換矩陣作用于圖像像素坐標來實現映射轉換 &#xff0c;能模擬人眼或相機鏡頭觀看三維空間物體時的透視…

STM32F103定時器1每毫秒中斷一次

定時器溢出中斷&#xff0c;在程序設計中經常用到。在使用TIM1和TIM8溢出中斷時&#xff0c;需要注意“TIM_TimeBaseStructure.TIM_RepetitionCounter0;”&#xff0c;它表示溢出一次&#xff0c;并可以設置中斷標志位。 TIM1_Interrupt_Initializtion(1000,72); //當arr1…

數據庫——SQL約束窗口函數介紹

4.SQL約束介紹 &#xff08;1&#xff09;主鍵約束 A、基本內容 基本內容 p r i m a r y primary primary k e y key key約束唯一表示數據庫中的每條記錄主鍵必須包含唯一的值&#xff08;UNIQUE&#xff09;主鍵不能包含NULL值&#xff08;NOT NULL&#xff09;每個表都應…

【typenum】 8 常量文件(consts.rs)

一、源碼 這段代碼通過類型級編程&#xff08;type-level programming&#xff09;在編譯期實現數值計算。以下是常量定義部分&#xff1a; // THIS IS GENERATED CODE #![allow(missing_docs)] use crate::int::{NInt, PInt}; /** Type aliases for many constants.This fil…

第8講、Multi-Head Attention 的核心機制與實現細節

&#x1f914; 為什么要有 Multi-Head Attention&#xff1f; 單個 Attention 機制雖然可以捕捉句子中不同詞之間的關系&#xff0c;但它只能關注一種角度或模式。 Multi-Head 的作用是&#xff1a; 多個頭 多個視角同時觀察序列的不同關系。 例如&#xff1a; 一個頭可能專…

百度智能云千帆攜手聯想,共創MCP生態宇宙

5月7日&#xff0c;2025聯想創新科技大會&#xff08;Tech World&#xff09;在上海世博中心舉行&#xff0c;本屆大會以“讓AI成為創新生產力”為主題。會上&#xff0c;聯想集團董事長兼CEO楊元慶展示了包括覆蓋全場景的超級智能體矩陣&#xff0c;包括個人超級智能體、企業超…

【OpenCV】幀差法、級聯分類器、透視變換

一、幀差法&#xff08;移動目標識別&#xff09;&#xff1a; 好處&#xff1a;開銷小&#xff0c;不怎么消耗CPU的算力&#xff0c;對硬件要求不高&#xff0c;但只適合固定攝像頭 1、優點 計算效率高&#xff0c;硬件要求 響應速度快&#xff0c;實時性強 直接利用連續幀…

數據庫遷移的藝術:團隊協作中的沖突預防與解決之道

title: 數據庫遷移的藝術:團隊協作中的沖突預防與解決之道 date: 2025/05/17 00:13:50 updated: 2025/05/17 00:13:50 author: cmdragon excerpt: 在團隊協作中,數據庫遷移腳本沖突是常見問題。通過Alembic工具,可以有效地管理和解決這些沖突。沖突預防的四原則包括功能分…

Linux常用命令43——bunzip2解壓縮bz2文件

在使用Linux或macOS日常開發中&#xff0c;熟悉一些基本的命令有助于提高工作效率&#xff0c;bunzip2可解壓縮.bz2格式的壓縮文件。bunzip2實際上是bzip2的符號連接&#xff0c;執行bunzip2與bzip2 -d的效果相同。本篇學習記錄bunzip2命令的基本使用。 首先查看幫助文檔&#…

盲盒:拆開未知的驚喜,收藏生活的儀式感

一、什么是盲盒&#xff1f;—— 一場關于“未知”的浪漫冒險 盲盒&#xff0c;是一種充滿神秘感的消費體驗&#xff1a; &#x1f381; 盒中藏驚喜——每個盲盒外觀相同&#xff0c;但內含隨機商品&#xff0c;可能是普通款、稀有款&#xff0c;甚至是“隱藏款”&#xff1b;…

Android 中使用通知(Kotlin 版)

1. 前置條件 Android Studio&#xff1a;確保使用最新版本&#xff08;2023.3.1&#xff09;目標 API&#xff1a;最低 API 21&#xff0c;兼容 Android 8.0&#xff08;渠道&#xff09;和 13&#xff08;權限&#xff09;依賴庫&#xff1a;使用 WorkManager 和 Notificatio…

使用大模型預測急性結石性疾病技術方案

目錄 1. 數據預處理與特征工程偽代碼 - 數據清洗與特征處理數據預處理流程圖2. 大模型構建與訓練偽代碼 - 模型訓練模型訓練流程圖3. 術前預測系統偽代碼 - 術前風險評估術前預測流程圖4. 術中實時調整系統偽代碼 - 術中風險預警術中調整流程圖5. 術后護理系統偽代碼 - 并發癥預…

每日Prompt:生成自拍照

提示詞 幫我生成一張圖片&#xff1a;圖片風格為「人像攝影」&#xff0c;請你畫一張及其平凡無奇的iPhone對鏡自拍照&#xff0c;主角是穿著JK風格cos服的可愛女孩&#xff0c;在自己精心布置的可按風格的房間內的落地鏡前用后置攝像頭隨手一拍的快照。照片開啟了閃光燈&…

動態規劃-64.最小路徑和-力扣(LetCode)

一、題目解析 從左上角到右下角使得數字總和最小且只能向下或向右移動 二、算法原理 1.狀態表示 我們需要求到達[i,j]位置時數字總和的最小值&#xff0c;所以dp[i][j]表示&#xff1a;到達[i,j]位置時&#xff0c;路徑數字總和的最小值。 2.狀態轉移方程 到達[i,j]之前要先…

LeetCode LCR 010 和為 K 的子數組 (Java)

兩種解法詳解&#xff1a;暴力枚舉與前綴和哈希表尋找和為k的子數組 在解決數組中和為k的連續子數組個數的問題時&#xff0c;我們可以采用不同的方法。本文將詳細解析兩種常見的解法&#xff1a;暴力枚舉法和前綴和結合哈希表的方法&#xff0c;分析它們的思路、優缺點及適用…

OpenVLA (2) 機器人環境和環境數據

文章目錄 [TOC](文章目錄) 前言1 BridgeData V21.1 概述1.2 硬件環境 2 數據集2.1 場景與結構2.2 數據結構2.2.1 images02.2.2 obs_dict.pkl2.2.3 policy_out.pkl 3 close question3.1 英偉達環境3.2 LIBERO 環境更適合仿真3.3 4090 運行問題 前言 按照筆者之前的行業經驗, 數…

深度學習(第3章——亞像素卷積和可形變卷積)

前言&#xff1a; 本章介紹了計算機識別超分領域和目標檢測領域中常常使用的兩種卷積變體&#xff0c;亞像素卷積&#xff08;Subpixel Convolution&#xff09;和可形變卷積&#xff08;Deformable Convolution&#xff09;&#xff0c;并給出對應pytorch的使用。 亞像素卷積…

大模型在腰椎間盤突出癥預測與治療方案制定中的應用研究

目錄 一、引言 1.1 研究背景 1.2 研究目的與意義 二、腰椎間盤突出癥概述 2.1 定義與病因 2.2 癥狀與診斷方法 2.3 治療方法概述 三、大模型技術原理與應用基礎 3.1 大模型的基本原理 3.2 大模型在醫療領域的應用現狀 3.3 用于腰椎間盤突出癥預測的可行性分析 四、…

Vue3學習(組合式API——ref模版引用與defineExpose編譯宏函數)

目錄 一、ref模版引用。 &#xff08;1&#xff09;基本介紹。 &#xff08;2&#xff09;核心基本步驟。(以獲取DOM、組件為例) &#xff08;3&#xff09;案例&#xff1a;獲取dom對象演示。 <1>需求&#xff1a;點擊按鈕&#xff0c;讓輸入框聚焦。 &#xff08;4&…

公鏈開發及其配套設施:錢包與區塊鏈瀏覽器

公鏈開發及其配套設施&#xff1a;錢包與區塊鏈瀏覽器的技術架構與生態實踐 ——2025年區塊鏈基礎設施建設的核心邏輯與創新突破 一、公鏈開發&#xff1a;構建去中心化世界的基石 1. 技術架構設計的三重挑戰 公鏈作為開放的區塊鏈網絡&#xff0c;需在性能、安全性與去中心…