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

寫在開頭

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

要想理解系統服務間的交流,拿我們人與人的交流來做類比是個不錯的選擇,我們都知道,人與人之間的實作交流的基本元素主要有以下幾個方面:
- 能夠相互聽懂和理解的交流語言(即雙方要基于相同的"協議"之下)
- 必要的傳播介質(實在的物理介質,空氣紙張等都行)
- 約定好的處理資訊的方式(常見的一問一答 或是先記錄后處理等表現形式)
從而得知,系統服務間的交流的主要表現在以下幾個方面:
- 相同的通信原語:就像人類互相需要使用相同的語言進行交流,計算機服務也必須使用互相能識別的訊息格式進行互動,
- 傳播資訊的介質:人類交流時往往需要某種介質傳播資訊,如空氣、紙張甚至是眼神等,同樣的,網路資訊的傳遞也需要物理介質的幫助,以及作業在其上的一系列相關協議,
- 處理資訊的方式:人類交流時可以是面對面直接問答形式的,也可能是郵件、短信等延時應答形式的,對應的是不同的業務場景,在計算機里進行通信處理方式,
- 實作通信方式:根據不同的協議都能實作通信功能的方式,一般基于一種或者至少一種協議實作,
組成要素
實作系統間通信主要的三個要素:通信格式,通信協議,通信模型,

根據人與人的交流的構成要素,抽象成計算機系統服務中對應的概念(行之有效的概念往往是簡單且趨同的),系統間通信主要考慮以下三個方面:通信格式,通信協議,通信模型,具體詳情如下:
- 通信格式(Communication Format): 主要是指實作通信的訊息格式(Message Format),是表達訊息內容等基本表現形式,常用的訊息格式有xml,json,TLV等,
- 通信協議(Communication Protocol): 主要是指實作通信的網路協議(Network Protocol),常見的TCP/IP協議,UDP協議等,
- 通信模型(Communication Model): 主要是指實作通信的網路模型(Network Model),常見的模型主要有阻塞式通信模型,非阻塞式通信模型,同步通信模型,異步通信模型,
接下來,我們來詳細決議這些組成要素:
- 對于訊息格式來說,是幫助我們識別訊息和表達訊息內容的基本方式:
- XML:和語言無關,常用于對系統環境進行描述,如常見的maven倉庫配置,或者spring配置等,
- JSON:輕量級訊息格式,和語言無關,攜帶同樣的資訊,占用容量比XML小,
- Protocol Buffer:Google定義的訊息格式,只提供了java,c++和python語言的實作,
- TLV:比JSON更輕量級的資料格式,連JSON中的"{}"都沒有了,它是通過位元組的位運算來實作序列化和反序列化,
- 對于網路協議來說,是幫助我們實作訊息傳輸和傳遞的表達方式:
- 資料在網路七層模型中傳遞的時候,在網路層是"資料包",在資料鏈路層被封裝成"幀"(數字信號),在物理層則是"位元"(電信號),
- 不同的協議都能實作通信功能,最適合本系統的通信協議才是最好的,
- 對于網路模型來說,主要是幫助我們理解和選擇適合當前場景的應用框架:
- 在計算機網路層面來說,常見網路模型主要有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 等功能, 再通過運營商提供的互聯網線路把局域網接入到互聯網中, 手機就可以上網玩微信、刷抖音了,常見的網路主要有:
- 局域網 : 小范圍內的私有網路, 一個家庭內的網路、一個公司內的網路、一個校園內 的網路都屬于局域網,
- 廣域網: 把不同地域的局域網互相連接起來的網路,運營商搭建廣域網實作跨區域的網路互連,
- 互聯網: 互聯全世界的網路,互聯網是一個開放、互聯的網路, 不屬于任何個人和任何機構, 接入互聯網后可以和互聯網的任何一臺主機進行通信,
簡單來說,就是手機、無線路由器等設備通過多種網路協議實作通信,網路協議就是為了通信各方能夠互相交流而定義的標準或規則, 設備只要遵循相同的網路協議就能夠實作通信,那網路協議又是誰規定的呢? ISO 制定了一個國際標準OSI , 其中的 OSI 參考模型常被用于網路協議的制定,常見的網路協議:
- 面向連接協議(TCP協議):在發送資料之前, 在收發主機之間連接一條邏輯通信鏈路,好比平常打電話,輸入完對方電話號碼撥出之后, 只有對方接通電話才能真正通話,通話結束后將電話機扣上就如同切斷電源,TCP協議是一種面向有連接的傳輸層協議,能夠對自己提供的連接實施控制,適用于要求可靠傳輸的應用, 例如檔案傳輸,
- 面向無連接協議(UDP協議):不要求建立和斷開連接,發送端可于任何時候自由發送資料,如同去寄信, 不需要確認收件人資訊是否真實存在,也不需要確認收件人是否能收到信件,只要有個寄件地址就可以寄信了,U D P 是一種面向無連接的傳輸層協議,不會對自己提供的連接實施控制,適用于實時應用, 例如: I P 電話、視頻會議、直播等
網路模型

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

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

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

由于 OSI 參考模型把服務劃得過于瑣碎,先定義參考模型再定義協議,有點理想化, TCP / IP 模型則正好相反, 通過已有的協議歸納總結出來的模型,成為業界的實際網路協議標準,TCP / IP 是有由 I E T F 建議、推進其標準化的一種協議, 是 IP 、 TCP 、HTTP 等協議的集合,TCP / IP是為使用互聯網而開發制定的協議族, 所以互聯網的協議就是 TCP / IP,TCP / IP 每層的主要協
議詳情如下:
- 網路接入層:TCP / IP 是以 O S I 參考模型的物理層和資料鏈路層的功能是透明的為前提制定的,并未對這兩層進行定義,所以可以把物理層和資料鏈路層合并稱為網路接入層,網路接入層是對網路介質的管理,定義如何使用網路來傳送資料,但是在通信程序中這兩層起到的作用不一樣, 所以也有把物理層和資料鏈路層分別稱為硬體、網路介面層, TCP / IP分為四層或者五層都可以,只要能理解其中的原理即可,設備之間通過物理的傳輸介質互連, 而互連的設備之間使用 M A C 地址實作資料傳輸,采用 M A C 地址,目的是為了識別連接到同一個傳輸介質上的設備,
- 網路層:相當于 OSI 模型中的第 3 層網路層, 使用 I P 協議, I P 協議基于 I P 地址轉發分包資料,作用是將資料包從源地址發送到目的地址,TCP / IP 分層中的網路層與傳輸層的功能通常由作業系統提供, 路由器就是通過網路層實作轉發資料包的功能,
- 傳輸層:相當于 OSI 模型中的第 4 層傳輸層, 主要功能就是讓應用程式之間互相通信,通過埠號識別應用程式, 使用的協議有面向連接的 TCP 協議和面向無連接的 UDP 協議,
- 應用層:相當于 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模型

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模型

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 協議的穩定性和高效性,網路通信說起來簡單,但實際上是一個非常復雜的程序,這個程序主要包括:對端節點的查找、網路連接的建立、傳輸資料的編碼解碼以及網路連接的管理等等,每一項都很復雜,
對于系統間通信來說,我們需要區分集群和分布式兩個標準:
- 分布式應用:一個業務拆分成多個子業務不熟在不同的服務器
- 集群:同一個業務部署在不同的多臺服務器上
實作方式

在分布式服務誕生以前,主要采用以下幾種方式實作系統間的通信:
- Socket通信,基于TCP/UDP二進制通訊;效率最高,編程最復雜,需要自定義通訊格式;
- JavaEE體系中的RMI或EJB,在Socket基礎之上封裝的實作,直接面象Java物件編程,編程相對簡單,不需要考慮低層實作,效率也不錯,但只能是Java系統間通信
- 基于HTTP的通信,即服務端提供可訪問URL,客戶端模擬http請求完成通信;可跨平臺跨語言,通訊效率相對較低,編程較簡單,http+json,很多專案中應用,但是如果服務越來越多,服務與服務之間的呼叫關系復雜,呼叫URL管理復雜,什么時候添加機器難以確定,
- 基于Hessian,Remoting on HTTP,類似于RMI與Socket的關系;
- 基于JMS,異步通信等,
- 基于WebService,可跨平臺跨語言,工具豐富,復雜通信相對編程簡單,通信效率低,它是基于SOAP協議(http+xml:需要在一個工程中將資料變為xml格式,再傳輸到另外一個專案,并且xml傳輸資料過于臃腫),專案中不推薦使用,
在分布式應用時代,業界通常一般兩種方式可以來實作系統間的通信,主要如下:
- 基于遠程程序呼叫的方式(Remote Procedure Call):RPC服務呼叫,客戶端不需要知道呼叫具體的實作細節,只用直接呼叫實際存在于遠程計算機上的某個物件即可,呼叫方式就像呼叫本地應用程式的物件一樣,使用dubbo,使用rpc協議進行遠程呼叫,直接使用scoket通信(底層實作,使用二進制的流,所以效率高),傳輸效率高,并且可以統計出系統之間的呼叫關系、呼叫次數,管理服務,
- 基于訊息佇列的方式(Message Queue):MQ服務是某個系統負責發送訊息,對于關心這條訊息的系統負責接收訊息,并且在接收到訊息之后轉給其他系統業務處理,
同時,從各系統間通信的整合方式,可以分為:
- ESB方式:有服務順序編排/定義,服務實作隔離、多協議支撐、協議翻譯、轉發代理、事務控制等功能
- 服務注冊中心(很多產品用zookeeper實作):和ESB最大的不同點是:“服務注冊中心”主要提供各原子系統的服務注冊、服務治理、服務隔離、權限控制,當客戶端進行請求時,“服務治理”將告訴客戶端到哪里去訪問真實的服務,自己并不提供服務的轉發,Dubbo就是一個典型的服務治理框架,
RPC服務呼叫(RPC服務)

RPC是一種通過網路從遠程計算機程式上請求服務,不需要我們了解底層網路技術的協議,主要體現在以下幾個方面:
- RPC是一種協議,也是一種規范所有的應用需要遵循這套規范實作,典型的RPC實作主要有Dubbo,Thrift,GRPC等,
- RPC通信對于網路來說是透明的,呼叫方不用關注網路之間的通信協議,網路I/O模型,以及通信的資訊格式,
- RPC呼叫來說,是可以跨語言的,而且呼叫方不用關心服務端使用的是何種語言,

在 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):負責從訊息處理中心獲取訊息,并進行相應的處理,
當然,在技術選型的時候,我們需要選擇最適合我們的,
著作權宣告:本文為博主原創文章,遵循相關著作權協議,如若轉載或者分享請附上原文出處鏈接和鏈接來源,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/500594.html
標籤:架構設計
上一篇:高并發組件了解
