消息中間件解析 | 如何正確理解軟體應用系統中關於系統通信的那些事?

来源:https://www.cnblogs.com/mazhilin/archive/2022/07/29/16534001.html
-Advertisement-
Play Games

蒼穹之邊,浩瀚之摯,眰恦之美;悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》 寫在開頭 隨著業務需求的發展和用戶數量的激增,對於互聯網應用系統或者服務應用程式則提出了新的挑戰,也對從事系統研發的開發者有了更高的要求。作為一名IT從業研發人員,我們都知道的事,良好的用戶體驗是我們和應用系統間 ...


蒼穹之邊,浩瀚之摯,眰恦之美;悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》

寫在開頭

隨著業務需求的發展和用戶數量的激增,對於互聯網應用系統或者服務應用程式則提出了新的挑戰,也對從事系統研發的開發者有了更高的要求。作為一名IT從業研發人員,我們都知道的事,良好的用戶體驗是我們和應用系統間快速反饋,一直以來都是我們考量一個系統是否穩定和是否高效的設計目標,但是保證這個目標的關鍵之一,主要在於如何保證系統間的通信穩定和高效。從而映射出,如何正確理解軟體應用系統中關於系統通信的那些事?是我們必須瞭解和理解的一項關鍵工作,接下來,我們就一起來總結和探討一下。

基本概述

要想理解系統服務間的交流,拿我們人與人的交流來做類比是個不錯的選擇。我們都知道,人與人之間的實現交流的基本元素主要有以下幾個方面:

  • 能夠相互聽懂和理解的交流語言(即雙方要基於相同的"協議"之下)
  • 必要的傳播介質(實在的物理介質,空氣紙張等都行)
  • 約定好的處理信息的方式(常見的一問一答 或是先記錄後處理等表現形式)

從而得知,系統服務間的交流的主要表現在以下幾個方面:

  1. 相同的通信原語:就像人類互相需要使用相同的語言進行交流,電腦服務也必須使用互相能識別的消息格式進行交互。
  2. 傳播信息的介質:人類交流時往往需要某種介質傳播信息,如空氣、紙張甚至是眼神等。同樣的,網路信息的傳遞也需要物理介質的幫助,以及工作在其上的一系列相關協議。
  3. 處理信息的方式:人類交流時可以是面對面直接問答形式的,也可能是郵件、簡訊等延時應答形式的,對應的是不同的業務場景,在電腦里進行通信處理方式。
  4. 實現通信方式:根據不同的協議都能實現通信功能的方式,一般基於一種或者至少一種協議實現。

組成要素

實現系統間通信主要的三個要素:通信格式,通信協議,通信模型。

根據人與人的交流的構成要素,抽象成電腦系統服務中對應的概念(行之有效的概念往往是簡單且趨同的),系統間通信主要考慮以下三個方面:通信格式,通信協議,通信模型。具體詳情如下:

  1. 通信格式(Communication Format): 主要是指實現通信的消息格式(Message Format),是表達消息內容等基本表現形式。常用的消息格式有xml,json,TLV等。
  2. 通信協議(Communication Protocol): 主要是指實現通信的網路協議(Network Protocol)。常見的TCP/IP協議,UDP協議等。
  3. 通信模型(Communication Model): 主要是指實現通信的網路模型(Network Model)。常見的模型主要有阻塞式通信模型,非阻塞式通信模型,同步通信模型,非同步通信模型。

接下來,我們來詳細解析這些組成要素:

  1. 對於消息格式來說,是幫助我們識別消息和表達消息內容的基本方式:
    • XML:和語言無關,常用於對系統環境進行描述,如常見的maven倉庫配置,或者spring配置等。
    • JSON:輕量級消息格式,和語言無關。攜帶同樣的信息,占用容量比XML小。
    • Protocol Buffer:Google定義的消息格式,只提供了java,c++和python語言的實現。
    • TLV:比JSON更輕量級的數據格式,連JSON中的"{}"都沒有了。它是通過位元組的位運算來實現序列化和反序列化。
  2. 對於網路協議來說,是幫助我們實現消息傳輸和傳遞的表達方式:
    • 數據在網路七層模型中傳遞的時候,在網路層是"數據包",在數據鏈路層被封裝成"幀"(數字信號),在物理層則是"比特"(電信號)。
    • 不同的協議都能實現通信功能,最適合本系統的通信協議才是最好的。
  3. 對於網路模型來說,主要是幫助我們理解和選擇適合當前場景的應用框架:
    • 在電腦網路層面來說,常見網路模型主要有OSI 參考模型和TCP/IP 模型兩種。
    • 除此之外,還有Linux 網路I/O 模型和Java JDK中的I/O 模型

網路協議

我們用手機連接上網的時候,會用到許多網路協議。從手機連接 W i F i 開始, 使用的是 8 0 2 . 11 (即 W L A N ) 協議, 通過 W L A N 接入網路; 手機自動獲取網路配置,使用的是 D H C P 協議,獲取配置後手機才能正常通信。這時手機已經連入區域網,可以訪問區域網內的設備和資源, 但還不能使用互聯網應用,例如:微信、抖音等。想要訪問互聯網,還需要在手機的上聯網路設備上實現相關協議, 即在無線路由器上配置 N AT、 P P P O E 等功能, 再通過運營商提供的互聯網線路把區域網接入到互聯網中, 手機就可以上網玩微信、刷抖音了。常見的網路主要有:

  1. 區域網 : 小範圍內的私有網路, 一個家庭內的網路、一個公司內的網路、一個校園內 的網路都屬於區域網。
  2. 廣域網: 把不同地域的區域網互相連接起來的網路。運營商搭建廣域網實現跨區域的網路互連。
  3. 互聯網: 互聯全世界的網路。互聯網是一個開放、互聯的網路, 不屬於任何個人和任何機構, 接入互聯網後可以和互聯網的任何一臺主機進行通信。

簡單來說,就是手機、無線路由器等設備通過多種網路協議實現通信。網路協議就是為了通信各方能夠互相交流而定義的標準或規則, 設備只要遵循相同的網路協議就能夠實現通信。那網路協議又是誰規定的呢? ISO 制定了一個國際標準OSI , 其中的 OSI 參考模型常被用於網路協議的制定。常見的網路協議:

  1. 面向連接協議(TCP協議):在發送數據之前, 在收發主機之間連接一條邏輯通信鏈路。好比平常打電話,輸入完對方電話號碼撥出之後, 只有對方接通電話才能真正通話,通話結束後將電話機扣上就如同切斷電源。TCP協議是一種面向有連接的傳輸層協議,能夠對自己提供的連接實施控制。適用於要求可靠傳輸的應用, 例如文件傳輸。
  2. 面向無連接協議(UDP協議):不要求建立和斷開連接。發送端可於任何時候自由發送數據。如同去寄信, 不需要確認收件人信息是否真實存在,也不需要確認收件人是否能收到信件,只要有個寄件地址就可以寄信了。U D P 是一種面向無連接的傳輸層協議,不會對自己提供的連接實施控制。適用於實時應用, 例如: I P 電話、視頻會議、直播等

網路模型

從電腦網路層面來說,常見網路模型主要有OSI 參考模型和TCP/IP 模型兩種,主要表達如下:

OSI 參考模型:

O S I 參考模型將網路協議提供的服務分成 7 層,並定義每一層的服務內容, 實現每一層服務的是協議, 協議的具體內容是規則。上下層之間通過介面進行交互,同一層之間通過協議進行交互。 O S I 參考模型只對各層的服務做了粗略的界定, 並沒有對協議進行詳細的定義,但是許多協議都對應了 7 個分層的某一層。所以要瞭解網路,首先要瞭解 O S I 參考模型:

  1. 應用層:O S I 參考模型的第 7 層( 最高層)。應用程式和網路之間的介面, 直接向用戶提供服務。應用層協議有電子郵件、遠程登錄等協議。
  2. 表示層:O S I 參考模型的第 6 層。負責數據格式的互相轉換, 如編碼、數據格式轉換和加密解密等。保證一個系統應用層發出的信息可被另一系統的應用層讀出。
  3. 會話層:O S I 參考模型的第 5 層。主要是管理和協調不同主機上各種進程之間的通信(對話),即負責建立、管理和終止應用程式之間的會話。
  4. 傳輸層:O S I 參考模型的第 4 層。為上層協議提供通信主機間的可靠和透明的數據傳輸服務, 包括處理差錯控制和流量控制等問題。只在通信主機上處理, 不需要在路由器上處理。
  5. 網路層:O S I 參考模型的第 3 層。在網路上將數據傳輸到目的地址, 主要負責定址和路由選擇。
  6. 數據鏈路層:O S I 參考模型的第 2 層。負責物理層面上兩個互連主機間的通信傳輸, 將由 0、 1 組成的比特流劃分成數據幀傳輸給對端,即數據幀的生成與接收。通信傳輸實際上是通過物理的傳輸介質實現的。 數據鏈路層的作用就是在這些通過傳輸介質互連的設備之間進行數據處理。網路層與數據鏈路層都是基於目標地址將數據發送給接收端的,但是網路層負責將整個數據發送給最終目標地址, 而數據鏈路層則只負責送一個分段內的數據。
  7. 物理層:O S I 參考模型的第 1 層( 最底層)。負責邏輯信號( 比特流) 與物理信號(電信號、光信號)之間的互相轉換,通過傳輸介質為數據鏈路層提供物理連接。
TCP/IP 模型:

由於 OSI 參考模型把服務劃得過於瑣碎,先定義參考模型再定義協議,有點理想化。 TCP / IP 模型則正好相反, 通過已有的協議歸納總結出來的模型,成為業界的實際網路協議標準。TCP / IP 是有由 I E T F 建議、推進其標準化的一種協議, 是 IP 、 TCP 、HTTP 等協議的集合。TCP / IP是為使用互聯網而開發制定的協議族, 所以互聯網的協議就是 TCP / IP。TCP / IP 每層的主要協
議詳情如下:

  1. 網路接入層:TCP / IP 是以 O S I 參考模型的物理層和數據鏈路層的功能是透明的為前提制定的,並未對這兩層進行定義,所以可以把物理層和數據鏈路層合併稱為網路接入層。網路接入層是對網路介質的管理,定義如何使用網路來傳送數據。但是在通信過程中這兩層起到的作用不一樣, 所以也有把物理層和數據鏈路層分別稱為硬體、網路介面層。 TCP / IP分為四層或者五層都可以,只要能理解其中的原理即可。設備之間通過物理的傳輸介質互連, 而互連的設備之間使用 M A C 地址實現數據傳輸。採用 M A C 地址,目的是為了識別連接到同一個傳輸介質上的設備。
  2. 網路層:相當於 OSI 模型中的第 3 層網路層, 使用 I P 協議。 I P 協議基於 I P 地址轉發分包數據,作用是將數據包從源地址發送到目的地址。TCP / IP 分層中的網路層與傳輸層的功能通常由操作系統提供。 路由器就是通過網路層實現轉發數據包的功能。
  3. 傳輸層:相當於 OSI 模型中的第 4 層傳輸層, 主要功能就是讓應用程式之間互相通信,通過埠號識別應用程式, 使用的協議有面向連接的 TCP 協議和麵向無連接的 UDP 協議。
  4. 應用層:相當於 OSI 模型中的第 5 - 7 層的集合, 不僅要實現 O S I 模型應用層的功能,還要實現會話層和表示層的功能。 HTTP 、 POP3 、 TELNET 、 SSH、 F T P 、 SNMP 都是應用層協議。

除此之外,我們還需要知道Linux 網路I/O 模型和Java JDK中的I/O 模型:

Linux 網路I/O 模型:

Linux的內核將所的外部設備看作一個文件來操作,對於一個文件的讀寫操作會調用內核提供的系統命令,返回一個文件描述符(fd,File Descriptor);同時,在面對一個Socket的讀寫時也會有相應的套接字描述符(socketfd,Socket File Descriptor),描述符是一個數字,它指向內核中的一個結構體,比如文件路徑,數據區等。Linux 網路I/O 模型是按照UNIX網路編程來定義的,主要有:

阻塞I/O模型(Blocking I/O ):

最流行的I/O模型,本書到目前為止的所有例子都使用該模型。預設情形下,所有套接字都是阻塞的。使用UDP而不是TCP為例子的原因在於就UDP而言,數據準備好讀取的概念比較簡單:要麼整個數據報已經收到,要麼還沒有。對於TCP而言,諸如套接字低水位標記等額外變數開始起作用,道指這個概念複雜。我們把recvfrom函數視為系統調用,因為我們正在區分應用進程和內核。不管如何實現,一般都會從在應用進程空間中國運行切換到在內核空間中運行,一端時間之後再切換回來。 在上圖中,進程調用recvfrom,其系統調用直到數據報到達且被覆制到應用進程的緩衝區中或者發送錯誤才返回。最常見的錯誤是系統調用被信號中斷,我們說進程在從調用recvfrom開始到它返回的整段時間內是被阻塞的。recvfrom成功返回後,應用進程開始處理數據報。

非阻塞I/O模型(NoneBlocking I/O):

進程把一個套接字設置成非阻塞是在通知內核:當所有請求的I/O操作非得把本進程投入睡眠才能完成時,不要把本進程投入睡眠,而是返回一個錯誤。前三次調用recvfrom時沒有數據可返回,因此內核轉而立即返回一個EWOULDBLOCK錯誤。第四次調用recvfrom時已有一個數據報準備好,它被覆制到應用進程緩衝區,於是recvfrom成功返回。接著處理數據。當一個應用進程像這樣對一個非阻塞描述符迴圈調用recvfrom時,我們成為輪詢,應用進程持續輪詢內核,以查看某個操作是否就緒。這麼做往往耗費大量CPU時間,不過這種模型偶爾也會遇到。

I/O復用模型(IO Multiplexing):

I/O復用,我們就可以調用select或者poll,阻塞在這兩個系統調用中的某一個,而不是阻塞在真正的I/O系統調用上。我們阻塞與select調用,等待數據報套接字變為可讀。當select返回套接字可讀這一條件時,我們調用recvfrom把所可讀數據報複制到應用進程緩衝區。比較上面兩圖,I/O復用並不顯得有什麼優勢,事實上由於使用select需要兩個而不是單個系統調用,其優勢在於可以等待多個描述符就緒。

信號驅動I/O復用模型(Signal Driven IO):

可以用信號,讓內核在描述符就緒時發送SIGIO信號通知我們。稱為信號驅動式I/O。我們首先開啟套接字的信號驅動式I/O功能,並通過sigaction系統調用安裝一個信號處理函數。該系統調用將立即返回,我們的進程繼續工作,也就是說它沒有被阻塞。當數據報準備好讀取時,內核就為該進程產生一個SIGIO信號。我們隨後既可以在信號處理函數中調用recvfrom讀取數據報,並通知主迴圈數據已準備好待處理。也可以立即通知迴圈,讓它讀取數據報。無論如何處理SIGIO信號,這種模型的優勢在於等待數據報到達期間進程不被阻塞。主迴圈可以繼續執行,只要等待來自信號處理函數的通知:既可以是數據已準備好被處理,也可以是數據報已準備好被讀取。

非同步I/O模型(Asynchronous IO ):

告知內核啟動某個操作,並讓內核在整個操作(包括將數據從內核覆制到我們自己的緩衝區)完成後通知我們。這種模型與前一節介紹的信號驅動模型的主要區別在於:信號驅動I/O是由內核通知我們如何啟動一個I/O操作,而非同步I/O模型是由內核通知我們I/O操作何時完成。我們調用aio_read函數,給內核傳遞描述符、緩衝區指針。緩衝區大小和文件偏移,並告訴內核當整個操作完成時如何通知我們。該系統調用立即返回,而且在等到I/O完成期間,我們的進程不被阻塞。

Java JDK中的I/O 模型:

在Java語言中,應用程式發起 I/O 調用後,會經歷兩個階段:

  • 內核等待 I/O 設備準備好數據;
  • 內核將數據從內核空間拷貝到用戶空間。

其中,阻塞和非阻塞:

  • 阻塞調用會一直等待遠程數據就緒再返回,即上面的階段1會阻塞調用者,直到讀取結束;
  • 而非阻塞無論在什麼情況下都會立即返回,雖然非阻塞大部分時間不會被block,但是它仍要求進程不斷地去主動詢問kernel是否準備好數據,也需要進程主動地再次調用recvfrom來將數據拷貝到用戶記憶體。

而我們常說的同步和非同步主要如下:

  • 同步方法會一直阻塞進程,直到I/O操作結束,註意這裡相當於上面的階段1,階段2都會阻塞調用者。其中BIO,NIO,IO多路復用,信號驅動IO,這四種IO都可以歸類為同步IO;
  • 而非同步方法不會阻塞調用者進程,即使是從內核空間的緩衝區將數據拷貝到進程中這一操作也不會阻塞進程,拷貝完畢後內核會通知進程數據拷貝結束。
BIO模型

同步阻塞 IO 模型中,伺服器應用程式發起 read 系統調用後,會一直阻塞,直到內核把數據拷貝到用戶空間。完整的架構應該是 客戶端-內核-伺服器,客戶端發起IO請求,伺服器發起系統調用,內核把IO數據從內核空間拷貝到用戶空間,伺服器應用程式才能使用到客戶端發送的數據。一般來說,客戶端、服務端其實都屬於用戶空間,藉助內核交流數據。

當用戶進程發起了read系統調用,kernel就開始了IO的第一個階段:準備數據。對於網路IO來說,很多時候數據在一開始還沒有到達內核(比如說客戶端目前只是建立了連接,還沒有發送數據 或者是 網卡等待接收數據),所以kernel就需要要等待足夠的數據到來。而在伺服器進程這邊,整個進程會被阻塞。當kernel一直等到數據準備好了,它就會將數據從kernel中拷貝到用戶記憶體,然後kernel返回結果,用戶進程才解除阻塞狀態,重新運行起來。

Java中的JDBC也使用到了BIO技術。BIO在客戶端連接數量不高的情況下是沒問題的,但是當面對十萬甚至百萬級連接的時候,無法處理這種高併發情況,因此我們需要一種更高效的 I/O 處理模型來應對。

NIO模型

file

Java 中的 NIO 於 JDK 1.4 中引入,對應 java.nio 包,提供了 Channel , Selector,Buffer 等抽象。NIO 中的 N 可以理解為 Non-blocking,不單純是 New。它支持面向緩衝的,基於通道的 I/O 操作方法。 對於高負載、高併發的(網路)情況下,應使用 NIO 。

當伺服器進程發出read操作時,如果kernel中數據還沒準備好,那麼並不會阻塞伺服器進程,而是立即返回error,用戶進程判斷結果是error,就知道數據還沒準備好,此時用戶進程可以去乾其他的事情。一段時間後用戶進程再次發read,一直輪詢直到kernel中數據準備好,此時用戶發起read操作,產生system call,kernel 馬上將數據拷貝到用戶記憶體,然後返回,進程就能使用到用戶空間中的數據了。

BIO一個線程只能處理一個IO流事件,想處理下一個必須等到當前IO流事件處理完畢。而NIO其實也只能串列化的處理IO事件,只不過它可以在內核等待數據準備數據時做其他的工作,不像BIO要一直阻塞住。NIO它會一直輪詢操作系統,不斷詢問內核是否準備完畢。但是,NIO這樣又引入了新的問題,如果當某個時間段里沒有任何客戶端IO事件產生時,伺服器進程還在不斷輪詢,占用著CPU資源。所以要解決該問題,避免不必要的輪詢,而且當無IO事件時,最好阻塞住(線程阻塞住就會釋放CPU資源了)。所以NIO引入了多路復用機制,可以構建多路復用的、同步非阻塞的IO程式。

AIO模型

file

AIO 也就是 NIO 2。Java 7 中引入了 NIO 的改進版 NIO 2,它是非同步 IO 模型。非同步 IO 是基於事件和回調機制實現的,也就是進程操作之後會直接返回,不會阻塞在那裡,當後臺處理完成,操作系統會通知相應的線程進行後續的操作。用戶進程發起read操作之後,立刻就可以開始去做其它的事。

內核收到一個asynchronous read之後,首先它會立刻返回,所以不會對用戶進程產生任何阻塞。kernel會等待數據準備完成,然後將數據拷貝到用戶記憶體,當這一切都完成之後,kernel會給用戶進程發送一個signal,告訴它read操作完成了。

IO多路復用模型

Java 中的 NIO ,提供了 Selector(選擇器)這個封裝了操作系統IO多路復用能力的工具,通過Selector.select(),我們可以阻塞等待多個Channel(通道),知道任意一個Channel變得可讀、可寫,如此就能實現單線程管理多個Channels(客戶端)。當所有Socket都空閑時,會把當前線程(選擇器所處線程)阻塞掉,當有一個或多個Socket有I/O事件發生時,線程就從阻塞態醒來,並返回給服務端工作線程所有就緒的socket(文件描述符)。各個操作系統實現方案:

  • linux:select、poll、epoll
  • MacOS/FreeBSD:kqueue
  • Windows/Solaris:IOCP

IO多路復用題同非阻塞IO本質一樣,只不過利用了新的select系統調用,由內核來負責本來是伺服器進程該做的輪詢操作。看似比非阻塞IO還多了一個系統調用的開銷,不過因為可以支持多路復用IO,即一個進程監聽多個socket,才算提高了效率。進程先是阻塞在select/poll上(進程是因為select/poll/epoll函數調用而阻塞,不是直接被IO阻塞的),再是阻塞在讀寫操作的第二階段上(等待數據從內核空間拷貝到用戶空間)。

IO多路復用的實現原理:利用select、poll、epoll可以同時監聽多個socket的I/O事件的能力,而當有I/O事件產生時會被註冊到Selector中。在所有socket空閑時,會把當前選擇器進程阻塞掉,當有一個或多個流有I/O事件(或者說 一個或多個流有數據到達)時,選擇器進程就從阻塞態中喚醒。通過select或poll輪詢所負責的所有socket(epoll是只輪詢那些真正產生了事件的socket),返回fd文件描述符集合給主線程串列執行事件。

⚠️[特別註意]:

select和poll每次調用時都需要將fd_set(文件描述符集合)從用戶空間拷貝到內核空間中,函數返回時又要拷貝回來(epoll使用mmap,避免了每次wait都要將數組進行拷貝)。

在實際開發過程中,基於消息進行系統間通信,我們一般會有四種方法實現:

基於TCP/IP+BIO實現:

在Java中可基於Socket、ServerSocket來實現TCP/IP+BIO的系統通信。

  • Socket主要用於實現建立連接即網路IO的操作
  • ServerSocket主要用於實現伺服器埠的監聽即Socket對象的獲取

為了滿足服務端可以同時接受多個請求,最簡單的方法是生成多個Socket。但這樣會產生兩個問題:

  • 生成太對Socket會消耗過多資源
  • 頻繁創建Socket會導致系統性能的不足

為瞭解決上面的問題,通常採用連接池的方式來維護Socket。一方面能限制Socket的個數;另一方面避免重覆創建Socket帶來的性能下降問題。這裡有一個問題就是設置合適的相應超時時間。因為連接池中Socket個數是有限的,肯定會造成激烈的競爭和等待。

Server服務端:

//創建對本地埠的監聽
PrintWriter out = new PrintWriter(socket.getOutputStream(),true);
//向伺服器發送字元串信息
out.println("hello");
//阻塞讀取服務端的返回信息
in.readLine();

Client客戶端:

//創建連接
Socket socket = new Socket(目標IP或功能變數名稱, 目標埠);
//BufferedReader用於讀取服務端返回的數據
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//PrintWriter向伺服器寫入流
PrintWriter out = new PrintWriter(socket.getOutputStream(),true);
//像服務端發送流
out.println("hello");
//阻塞讀取服務端的返回信息
in.readLine();
基於TCP/IP+NIO實現:

Java可以基於Clannel和Selector的相關類來實現TCP/IP+NIO方式的系統間通信。Channel有SocketClannel和ServerSocketChannel兩種:

  • SocketClannel: 用於建立連接、監聽事件及操作讀寫。
  • ServerSocketClannel: 用於監聽埠即監聽連接事件。
  • Selecter: 獲取是否有要處理的事件。

Server服務端:

SocketChannel channel = SocketChannel.open();
//設置為非阻塞模式
channel.configureBlocking(false);
//對於非阻塞模式,立即返回false,表示連接正在建立中
channel.connect(SocketAdress);
Selector selector = Selector.open();
//向channel註冊selector以及感興趣的連接事件
channel.regester(selector,SelectionKey.OP_CONNECT);
//阻塞至有感興趣的IO事件發生,或到達超時時間
int nKeys = selector.select(超時時間【毫秒計】);
//如果希望一直等待知道有感興趣的事件發生
//int nKeys = selector.select();
//如果希望不阻塞直接返回當前是否有感興趣的事件發生
//int nKeys = selector.selectNow();

//如果有感興趣的事件
SelectionKey sKey = null;
if(nKeys>0){
    Set keys = selector.selectedKeys();
    for(SelectionKey key:keys){
        //對於發生連接的事件
        if(key.isConnectable()){
            SocketChannel sc = (SocketChannel)key.channel();
            sc.configureBlocking(false);
            //註冊感興趣的IO讀事件
            sKey = sc.register(selector,SelectionKey.OP_READ);
            //完成連接的建立
            sc.finishConnect();
        }
        //有流可讀取
        else if(key.isReadable()){
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            SocketChannel sc = (SocketChannel) key.channel();
            int readBytes = 0;
            try{
                int ret = 0;
                try{
                    //讀取目前可讀取的值,此步為阻塞操作
                    while((ret=sc.read(buffer))>0){
                        readBytes += ret;
                    }
                }
                fanally{
                    buffer.flip();
                }
             }
             finally{
                 if(buffer!=null){
                        buffer.clear();
                 }
             }
        }
        //可寫入流
        else if(key.isWritable()){
            //取消對OP_WRITE事件的註冊
            key.interestOps(key.interestOps() & (!SelectionKey.OP_WRITE));
            SocketChannel sc = (SocketChannel) key.channel();
            //此步為阻塞操作
            int writtenedSize = sc.write(ByteBuffer);
            //如未寫入,則繼續註冊感興趣的OP_WRITE事件
            if(writtenedSize==0){
                key.interestOps(key.interestOps()|SelectionKey.OP_WRITE);
            }
        }
    }
    Selector.selectedKeys().clear();
}
//對於要寫入的流,可直接調用channel.write來完成。只有在未寫入成功時才要註冊OP_WRITE事件
int wSize = channel.write(ByteBuffer);
if(wSize == 0){
    key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
}

Server端實體:

ServerSocketChannel ssc = ServerSocketChannel.open();
ServerSocket serverSocket = ssc.socket();
//綁定要監聽的介面
serverSocket.bind(new InetSocketAdress(port));
ssc.configureBlocking(false);
//註冊感興趣的連接建立事件
ssc.register(selector,SelectionKey.OP_ACCEPT);
基於UDP/IP+BIO實現:

Java對UDP/IP方式的網路數據傳輸同樣採用Socket機制,只是UDP/IP下的Socket沒有建立連接,因此無法雙向通信。如果需要雙向通信,必須兩端都生成UDP Server。
Java中通過DatagramSocket和DatagramPacket來實現UDP/IP+BIO方式和系統間通信:

  • DatagramSocket:負責監聽埠和讀寫數據

  • DatagramPacket:作為數據流對象進行傳輸
    由於UDP雙端不建立連接,所以也就不存在競爭問題,只是最終讀寫流的動作是同步的。
//如果希望雙向通信,必須啟動一個監聽埠承擔伺服器的職責
//如果不能綁定到指定埠,則拋出SocketException
DatagramSocket serverSocket = new DatagramSocket(監聽的埠);
byte[] buffer = new byte[65507];
DatagramPacket receivePacket = new DatagramPacket(buffer,buffer.length);
DatagramSocket socket = new DatagramSocket();
DatagramPacket packet = new DatagramPacket(datas,datas.length,server.length);
//阻塞方式發送packet到指定的伺服器和埠
socket.send(packet);
//阻塞並同步讀取流消息,如果讀取的流消息比packet長,則刪除更長的消息
//當連接不上目標地址和埠時,拋出PortUnreachableException
DatagramSocket.setSoTimeout(超時時間--毫秒級);
serverSocket.receive(receivePacket);
基於UDP/IP+NIO實現:

Java中可以通過DatagramClannel和ByteBuffer來實現UDP/IP方式的系統間通信:

  • DatagramClannel:負責監聽埠及進行讀寫
  • ByteBuffer:用於數據傳輸
//讀取流信息
DatagramChannel receiveChannel = DatagramChannel.open();
receiveChannel.configureBlocking(false);
DatagramSocket socket = receiveChannel.socket();
socket.bind(new InetSocketAddress(rport));
Selector selector = Selector.open();
receiveChannel.register(selector, SelectionKey.OP_REEAD);
//之後即可像TCP/IP+NIO中對selector遍歷一樣的方式進行流信息的讀取
//...
//寫入流信息
DatagramChannel sendChannel = DatagramChannel.open();
sendChannel.configureBlocking(false);
SocketAdress target = new InetSocketAdress("127.0.0.1",sport);
sendChannel.connect(target);
//阻塞寫入流
sendChannel.write(ByteBuffer);

發展歷程

從軟體系統的發展歷程來看,在分散式應用出現之前,市面上幾乎所有的軟體系統都是集中式的,軟體,硬體以及各個組件之間的高度耦合組成了單體架構軟體平臺,即就是所謂的單機系統。

一般來說,大型應用系統通常會被拆分成多個子系統,這些子系統可能會部署在多台機器上,也有可能只在一臺機器上的多個線程中,這就是我們常說的分散式應用。

從部署形態上來說,以多台伺服器和多個進程部署服務,都是為了實現一個業務需求和程式功能。分散式系統中的網路通信一般都會採用四層的 TCP 協議或七層的 HTTP 協議,在我的瞭解中,前者占大多數,這主要得益於 TCP 協議的穩定性和高效性。網路通信說起來簡單,但實際上是一個非常複雜的過程,這個過程主要包括:對端節點的查找、網路連接的建立、傳輸數據的編碼解碼以及網路連接的管理等等,每一項都很複雜。

對於系統間通信來說,我們需要區分集群和分散式兩個標準:

  • 分散式應用:一個業務拆分成多個子業務不熟在不同的伺服器
  • 集群:同一個業務部署在不同的多台伺服器上

實現方式

在分散式服務誕生以前,主要採用以下幾種方式實現系統間的通信:

  1. Socket通信,基於TCP/UDP二進位通訊;效率最高,編程最複雜,需要自定義通訊格式;
  2. JavaEE體系中的RMI或EJB,在Socket基礎之上封裝的實現,直接面象Java對象編程,編程相對簡單,不需要考慮低層實現,效率也不錯,但只能是Java系統間通信
  3. 基於HTTP的通信,即服務端提供可訪問URL,客戶端模擬http請求完成通信;可跨平臺跨語言,通訊效率相對較低,編程較簡單。http+json。很多項目中應用。但是如果服務越來越多,服務與服務之間的調用關係複雜,調用URL管理複雜,什麼時候添加機器難以確定。
  4. 基於Hessian,Remoting on HTTP,類似於RMI與Socket的關係;
  5. 基於JMS,非同步通信等。
  6. 基於WebService,可跨平臺跨語言,工具豐富,複雜通信相對編程簡單,通信效率低。它是基於SOAP協議(http+xml:需要在一個工程中將數據變為xml格式,再傳輸到另外一個項目,並且xml傳輸數據過於臃腫)。項目中不推薦使用。

在分散式應用時代,業界通常一般兩種方式可以來實現系統間的通信,主要如下:

  • 基於遠程過程調用的方式(Remote Procedure Call):RPC服務調用,客戶端不需要知道調用具體的實現細節,只用直接調用實際存在於遠程電腦上的某個對象即可,調用方式就像調用本地應用程式的對象一樣。使用dubbo。使用rpc協議進行遠程調用,直接使用scoket通信(底層實現,使用二進位的流,所以效率高)。傳輸效率高,並且可以統計出系統之間的調用關係、調用次數,管理服務。
  • 基於消息隊列的方式(Message Queue):MQ服務是某個系統負責發送消息,對於關心這條消息的系統負責接收消息,並且在接收到消息之後轉給其他系統業務處理。

同時,從各系統間通信的整合方式,可以分為:

  • ESB方式:有服務順序編排/定義,服務實現隔離、多協議支撐、協議翻譯、轉發代理、事務控制等功能
  • 服務註冊中心(很多產品用zookeeper實現):和ESB最大的不同點是:“服務註冊中心”主要提供各原子系統的服務註冊、服務治理、服務隔離、許可權控制。當客戶端進行請求時,“服務治理”將告訴客戶端到哪裡去訪問真實的服務,自己並不提供服務的轉發。Dubbo就是一個典型的服務治理框架。

RPC服務調用(RPC服務)

RPC是一種通過網路從遠程電腦程式上請求服務,不需要我們瞭解底層網路技術的協議。主要體現在以下幾個方面:

  1. RPC是一種協議,也是一種規範所有的應用需要遵循這套規範實現。典型的RPC實現主要有Dubbo,Thrift,GRPC等。
  2. RPC通信對於網路來說是透明的,調用方不用關註網路之間的通信協議,網路I/O模型,以及通信的信息格式。
  3. RPC調用來說,是可以跨語言的,而且調用方不用關心服務端使用的是何種語言。

file

在 RPC 框架裡面,我們是怎麼支持插件化架構的呢?我們可以將每個功能點抽象成一個介面,將這個介面作為插件的契約,然後把這個功能的介面與功能的實現分離,並提供介面的預設實現。在 Java 裡面,JDK 有自帶的 SPI(Service Provider Interface)服務發現機制,它可以動態地為某個介面尋找服務實現。使用 SPI 機制需要在 Classpath 下的 META-INF/services 目錄里創建一個以服務介面命名的文件,這個文件里的內容就是這個介面的具體實現類。

但在實際項目中,我們其實很少使用到 JDK 自帶的 SPI 機制,首先它不能按需載入,ServiceLoader 載入某個介面實現類的時候,會遍歷全部獲取,也就是介面的實現類得全部載入並實例化一遍,會造成不必要的浪費。另外就是擴展如果依賴其它的擴展,那就做不到自動註入和裝配,這就很難和其他框架集成,比如擴展裡面依賴了一個 Spring Bean,原生的 Java SPI 就不支持。

我們將每個功能點抽象成一個介面,將這個介面作為插件的契約,然後把這個功能的介面與功能的實現分離並提供介面的預設實現。這樣的架構相比之前的架構,有很多優勢。首先它的可擴展性很好,實現了開閉原則,用戶可以非常方便地通過插件擴展實現自己的功能,而且不需要修改核心功能的本身;其次就是保持了核心包的精簡,依賴外部包少,這樣可以有效減少開發人員引入 RPC 導致的包版本衝突問題。

一般一個RPC 框架裡面都有會涉及兩個模塊:

  • 傳輸模塊:RPC 本質上就是一個遠程調用,那肯定就需要通過網路來傳輸數據。雖然傳輸協議可以有多種選擇,但考慮到可靠性的話,我們一般預設採用 TCP 協議。為了屏蔽網路傳輸的複雜性,我們需要封裝一個單獨的數據傳輸模塊用來收發二進位數據。
  • 協議封裝:用戶請求的時候是基於方法調用,方法出入參數都是對象數據,對象是肯定沒法直接在網路中傳輸的,我們需要提前把它轉成可傳輸的二進位,這就是我們說的序列化過程。但只是把方法調用參數的二進位數據傳輸到服務提供方是不夠的,我們需要在方法調用參數的二進位數據後面增加“斷句”符號來分隔出不同的請求,在兩個“斷句”符號中間放的內容就是我們請求的二進位數據。

除此之外,我們還可以在協議模塊中加入壓縮功能,這是因為壓縮過程也是對傳輸的二進位數據進行操作。在實際的網路傳輸過程中,我們的請求數據包在數據鏈路層可能會因為太大而被拆分成多個數據包進行傳輸,為了減少被拆分的次數,從而導致整個傳輸過程時間太長的問題,我們可以在 RPC 調用的時候這樣操作:在方法調用參數或者返回值的二進位數據大於某個閾值的情況下,我們可以通過壓縮框架進行無損壓縮,然後在另外一端也用同樣的壓縮演算法進行解壓,保證數據可還原。

傳輸和協議這兩個模塊是 RPC 裡面最基礎的功能,它們使對象可以正確地傳輸到服務提供方。但距離 RPC 的目標——實現像調用本地一樣地調用遠程,還缺少點東西。因為這兩個模塊所提供的都是一些基礎能力,要讓這兩個模塊同時工作的話,我們需要手寫一些黏合的代碼,但這些代碼對我們使用 RPC 的研發人員來說是沒有意義的,而且屬於一個重覆的工作,會導致使用過程的體驗非常不友好。

消息隊列(MQ服務)

分散式子系統之間需要通信時,就發送消息。一般通信的兩個要點是:消息處理和消息傳輸。

  • 消息處理:例如讀取數據和寫入數據。基於消息方式實現系統通信的消息處理可以分為同步消息和非同步消息。同步消息一般採用的是BIO(Blocking IO)和NIO(Non-Blocking IO);非同步消息一般採用AIO方式。
  • 消息傳輸:消息傳輸需要藉助網路協議來實現,TCP/IP協議和UDP/IP協議可以用來完成消息傳輸。

消息隊列本質上是一種系統間相互協作的通信機制。一般使用消息隊列可以業務解耦,流量削峰,日誌收集,事務最終一致性,非同步處理等業務場景。在我們實際開發工作中,一般消息隊列的使用需要實現:

  • 消息處理中心(Message Broker):負責消息的接收,存儲,轉發等。
  • 消息生產者(Message Producer):負責產生和發送消息的消息處理中心。
  • 消息消費者(Message Consumber):負責從消息處理中心獲取消息,併進行相應的處理。

當然,在技術選型的時候,我們需要選擇最適合我們的。

版權聲明:本文為博主原創文章,遵循相關版權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源。


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

-Advertisement-
Play Games
更多相關文章
  • FrameDataTrans教程 博客園 乳鴿菌 20220729 核心原理是使用postMessage發送數據,window.addEventListener("message",fun)監聽。 插件地址 index.htm // <iframe id="fr" src="child.html"> ...
  • 記錄一個清除計時器的小bug,使用計時器的時候迴圈一直停不下來,把計時器賦值給了變數,仍舊無法停止。後來發現是清除器放置的位置有問題。 最初的位置: 這樣放置就導致if語句值運行了一次!我潛意識預設了整個getprogress方法為迴圈體,但其實只有那個計時器部分是迴圈體,導致我百思不得其解為什麼循 ...
  • 是不是見到google,facebook等大型專業網站的擁有不同的語言站群,可以不同語言間切換很給力 今天要介紹的就是如何識別不同國家語言,只需要簡單幾步,使你的web應用更有國際範。 安裝vue-i18n npm i vue-i18n --save 新建多語言json文件 在src目錄下新建 co ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 最近鼓搗了一下 Vue3 + Vite2,遇到了不少問題,整理了5個可以提高開發效率的小知識,讓你在 Vue3 的項目開發中更加絲滑、順暢。 一、setup name 增強 Vue3的setup語法糖是個好東西,但使用setup語法 ...
  • 最近寫二維碼的時候,突然想起之前項目遇到過的一個問題,網上也沒有這方面解答,想到大家今後可能也會遇到這類問題,在此記錄下來,希望對你們有所幫助,大佬們不喜勿噴,qrcode配合畫布canvas本地生成二維碼的時候第一次能夠正常顯示,最下方會貼出代碼 跨頁面後顯示異常 剛開始是有些頭疼的,因為控制台一 ...
  • 自定義事件 tips 推薦始終使用 kebab-case 的事件名。(v-on會將事件名自動轉換為小寫,避免匹配不到) changeData × change-data √ 自定義組件的v-model 用法: 父組件定義數據源(不需要定義修改數據的方法),在子組件標簽上通過v-model="data ...
  • 因為涉及到有些監控是沒有音頻的,所以使用flv.js插件,通過<web-view>標簽跳轉到h5頁面。 在真機上調試發現是沒有發現flv.js文件,出現跨域現象,但是我只是導入了js文件,不知道什麼情況; 然後,就只是移動端不支持flv.js。經過不懈努力,我找到了另外一個插件 DPlayer.js ...
  • 蒼穹之邊,浩瀚之摯,眰恦之美; 悟心悟性,善始善終,惟善惟道! —— 朝槿《朝槿兮年說》 寫在開頭 眾所周知,在電腦操作系統中,進程(Process)是一個很關鍵的概念,最本質的理解就是操作系統執行的一個應用程式(Application Program)。與每個進程相關的是地址空間(Address ...
一周排行
    -Advertisement-
    Play Games
  • 使用原因: 在我們服務端調用第三方介面時,如:支付寶,微信支付,我們服務端需要模擬http請求並加上一些自己的邏輯響應給前端最終達到我們想要的效果 1.使用WebClient 引用命名空間 using System.Net; using System.Collections.Specialized; ...
  • WPF 實現帶蒙版的 MessageBox 消息提示框 WPF 實現帶蒙版的 MessageBox 消息提示框 作者:WPFDevelopersOrg 原文鏈接: https://github.com/WPFDevelopersOrg/WPFDevelopers.Minimal 框架使用大於等於.N ...
  • 一、JSON(JavaScript Object Notation)的簡介: ① JSON和XML類似,主要用於存儲和傳輸文本信息,但是和XML相比,JSON更小、更快、更易解析、更易編寫與閱讀。 ② C、Python、C++、Java、PHP、Go等編程語言都支持JSON。 二、JSON語法規則: ...
  • 1.避免Scoped模式註冊的服務變成Singleton模式 當提供一個生命周期模式為Singleton的服務實例時,如果發現該服務中還依賴生命周期模式為Scoped的服務實例(Scoped服務實例將被一個Singleton服務實例所引用),那麼這個被依賴的Scoped服務實例最終會成為一個Sing ...
  • 索引時資料庫提高數據查詢處理性能的一個非常關鍵的技術,索引的使用可以對性能產生上百倍甚至上千倍的影響。接下來,會介紹索引的基本原理、概念,並深入學習資料庫中所使用的索引結構和存儲方式,以及如何管理、維護索引等。 1.索引的基本概念 索引時用來快速查詢表記錄的一種存儲結構,一般使用索引有一下兩個方面: ...
  • django2 路由控制器 Route路由,是一種映射關係。路由是把客戶端請求的url路徑和用戶請求的應用程式,這裡意指django裡面的視圖進行綁定映射的一種關係。 請求路徑和視圖函數不是一一對應的關係 在django中所有的路由最終都被保存到一個叫urlpatterns的文件里,並且該文件必須在 ...
  • 1、我們的目標是獲取微博某博主的全部圖片、視頻 2、拿到網址後 我們先觀察 打開F12 隨著下滑我們發現載入出來了一個叫mymblog的東西,展開響應發現需要的東西就在裡面 3、重點來了!!! 通過觀察發現第二頁比第一頁多了參數since_id 而第二頁的since_id參數剛好在上一頁中能獲取到, ...
  • 一、實現原理 在Servlet3協議規範中,包含在JAR文件/META-INFO/resources/路徑下的資源可以直接訪問。 二、舉例說明 如下圖所示,是我新建的一個Spring Boot Starter項目:zimug-minitor-threadpool,用於實現可配置、可觀測的線程池。其中 ...
  • 精華筆記: static final常量:應用率高 必須聲明同時初始化 由類名打點來訪問,不能被改變 建議:常量所有字母都大寫,多個單詞用_分隔 編譯器在編譯時會將常量直接替換為具體的數,效率高 何時用:數據永遠不變,並且經常使用 抽象方法: 由abstract修飾 只有方法的定義,沒有具體的實現( ...
  • Python有一個for...else語法,它的寫法如下 for i in range(0,100): if i == 3: break else: print("Not found") 該語句表示:若for迴圈遍歷完畢,則執行else部分的語句。也就是說上述代碼不會有任何輸出,而下述代碼會輸出“N ...