工業(yè)b2b網(wǎng)站建設(shè)網(wǎng)站推廣的目的
在單體應用時,一次服務(wù)調(diào)用發(fā)生在同一臺機器上的同一個進程內(nèi)部,也就是說調(diào)用發(fā)生在本機內(nèi)部,因此也被叫作本地方法調(diào)用。在進行服務(wù)化拆分之后,服務(wù)提供者和服務(wù)消費者運行在兩臺不同物理機上的不同進程內(nèi),它們之間的調(diào)用相比于本地方法調(diào)用,可稱之為遠程方法調(diào)用,簡稱RPC(Remote Procedure Call),那么RPC調(diào)用是如何實現(xiàn)的呢?
在介紹RPC調(diào)用的原理之前,先來想象一下一次電話通話的過程。首先,呼叫者A通過查詢號碼簿找到被呼叫者B的電話號碼,然后撥打B的電話。B接到來電提示時,如果方便接聽的話就會接聽;如果不方便接聽的話,A就得一直等待。當?shù)却^一段時間后,電話會因超時被掛斷,這個時候A需要再次撥打電話,一直等到B空閑的時候,才能接聽。
RPC調(diào)用的原理與此類似,我習慣把服務(wù)消費者叫作 客戶端,服務(wù)提供者叫作 服務(wù)端,兩者通常位于網(wǎng)絡(luò)上兩個不同的地址,要完成一次RPC調(diào)用,就必須先建立網(wǎng)絡(luò)連接。建立連接后,雙方還必須按照某種約定的協(xié)議進行網(wǎng)絡(luò)通信,這個協(xié)議就是通信協(xié)議。雙方能夠正常通信后,服務(wù)端接收到請求時,需要以某種方式進行處理,處理成功后,把請求結(jié)果返回給客戶端。為了減少傳輸?shù)臄?shù)據(jù)大小,還要對數(shù)據(jù)進行壓縮,也就是對數(shù)據(jù)進行序列化。
上面就是RPC調(diào)用的過程,由此可見,想要完成調(diào)用,你需要解決四個問題:
-
客戶端和服務(wù)端如何建立網(wǎng)絡(luò)連接?
-
服務(wù)端如何處理請求?
-
數(shù)據(jù)傳輸采用什么協(xié)議?
-
數(shù)據(jù)該如何序列化和反序列化?
客戶端和服務(wù)端如何建立網(wǎng)絡(luò)連接?
根據(jù)我的實踐經(jīng)驗,客戶端和服務(wù)端之間基于TCP協(xié)議建立網(wǎng)絡(luò)連接最常用的途徑有兩種。
1. HTTP通信
HTTP通信是基于應用層HTTP協(xié)議的,而HTTP協(xié)議又是基于傳輸層TCP協(xié)議的。一次HTTP通信過程就是發(fā)起一次HTTP調(diào)用,而一次HTTP調(diào)用就會建立一個TCP連接,經(jīng)歷一次下圖所示的“ 三次握手”的過程來建立連接。

三次握手過程詳解
-
第一次握手:
-
1、【客戶端】向【服務(wù)端】發(fā)送連接請求報文,標記ACK=1, SYN=1,客戶端序列號seq=x,客戶端進入等待狀態(tài)。
-
-
第二次握手:
-
1、【服務(wù)端】收到請求報文,將收到的報文緩存起來,緩存客戶端seq=x -
3、【服務(wù)端】向【客戶端】發(fā)送確認報文,生成一個【服務(wù)端】seq=y,標記ACK=1,SYN=1,【服務(wù)端】自己的序列號seq=y,確認序列號ACK_Number=x+1,發(fā)送給【客戶端】
-
-
第三次握手:
-
1、【客戶端】收到服務(wù)端發(fā)送的確認報文,將收到的報文存起來,緩存【服務(wù)端】seq=y -
2、【客戶端】發(fā)送確認報文給【服務(wù)端】,標記ACK=1,SYN=0,【客戶端】自己的序列號seq=x+1,確認序列號ACK_Number=y+1 -
3、【客戶端】、【服務(wù)端】都會進入ESTABLISHED (連接已建立狀態(tài))
-
完成請求后,再經(jīng)歷一次“四次揮手”的過程來斷開連接。

四次揮手過程詳解
-
第一次揮手:
-
1、【客戶端】向【服務(wù)端】發(fā)送釋放連接報文,并停止發(fā)送數(shù)據(jù),主動關(guān)閉 TCP 連接 -
2、標記FIN=1,【客戶端序列號】seq=x,該序號等于前面已經(jīng)傳送過去的數(shù)據(jù)的最后一個字節(jié)的序號加1 -
3、這時,客戶端 FIN—WAIT-1 (終止等待1)狀態(tài),等待服務(wù)端確認
-
-
第二次揮手:
-
1、【服務(wù)端】收到釋放連接報文,將收到的報文緩存起來,緩存【客戶端】seq=x -
2、【服務(wù)端】向【客戶端】發(fā)出確認釋放報文,標記ACK=1,【服務(wù)端序列號】seq=y,確認序列號ACK_Number=a+1 (a為【服務(wù)端】前面已經(jīng)傳送過的數(shù)據(jù)的最后一個字節(jié)的序號) -
3、此時【服務(wù)端】進入CLOSE—WAIT(關(guān)閉等待)狀態(tài) -
4、此時TCP服務(wù)器進程應該通知上層的應用進程,因為【客戶端】到【服務(wù)端】這個方向的連接就釋放了,這時TCP處于半關(guān)閉狀態(tài),即【客戶端】已經(jīng)沒有數(shù)據(jù)要發(fā)了,但【服務(wù)端】若發(fā)送數(shù)據(jù),【客戶端】仍要接受,也就是說從【服務(wù)端】到【客戶端】這個方向的連接并沒有關(guān)閉,這個狀態(tài)可能會持續(xù)一些時間。
-
-
第三次揮手:
-
1、【客戶端】收到【服務(wù)端】確認報文,并緩存起來 -
2、此時【客戶端】進入FIN—WAIT(終止等待2)狀態(tài),等待【服務(wù)端】發(fā)起釋放連接報文 -
3、如果【服務(wù)端】沒有數(shù)據(jù)要發(fā)送給【客戶端】了,【服務(wù)端】的應用進程就會通知TCP釋放連接 -
4、此時【服務(wù)端】向【客戶端】發(fā)送釋放連接報文,標記FIN=1,確認序列號ACK_Number=a+1(與第二次揮手的確認號一致),【服務(wù)端序號】seq=z+1(z為半關(guān)閉狀態(tài)發(fā)送的數(shù)據(jù)的最后一個字節(jié)的序號) -
5、此時【服務(wù)端】進入最后確認狀態(tài),等待【客戶端】確認
-
-
第四次揮手:
-
1、【客戶端】收到【服務(wù)端】釋放連接請求,必須發(fā)出確認 -
2、【客戶端】向【服務(wù)端】發(fā)送確認報文,標記ACK=1,確認號序列號ACK_Numbe=z+1+1,【客戶端序號】seq=x+1(x為第一次揮手的seq) -
3、此時【客戶端】進入等待狀態(tài),必須經(jīng)過時間等待計時器設(shè)置的時間2倍MSL(報文最大生存時間)后,【客戶端】才進入CLOSED狀態(tài),MSL叫做最長報文壽命,RFC建議設(shè)為2分鐘,實際應用中是30秒。在這2倍MSL期間【客戶端】進入TIME—WAIT狀態(tài)后,要經(jīng)過4分鐘才能進入到CLOSED狀態(tài)。 -
4、【服務(wù)端】只要收到了【客戶端】的確認后,就進入了CLOSED狀態(tài) -
5、當【客戶端】和【服務(wù)端】都進入CLOSED狀態(tài)后,連接就完全釋放了
-
2. Socket通信
Socket通信是基于TCP/IP協(xié)議的封裝,建立一次Socket連接至少需要一對套接字,其中一個運行于客戶端,稱為ClientSocket ;另一個運行于服務(wù)器端,稱為ServerSocket 。就像下圖所描述的,Socket通信的過程分為四個步驟:服務(wù)器監(jiān)聽、客戶端請求、連接確認、數(shù)據(jù)傳輸。
-
服務(wù)器監(jiān)聽:ServerSocket通過調(diào)用bind()函數(shù)綁定某個具體端口,然后調(diào)用listen()函數(shù)實時監(jiān)控網(wǎng)絡(luò)狀態(tài),等待客戶端的連接請求。
-
客戶端請求:ClientSocket調(diào)用connect()函數(shù)向ServerSocket綁定的地址和端口發(fā)起連接請求。
-
服務(wù)端連接確認:當ServerSocket監(jiān)聽到或者接收到ClientSocket的連接請求時,調(diào)用accept()函數(shù)響應ClientSocket的請求,同客戶端建立連接。
-
數(shù)據(jù)傳輸:當ClientSocket和ServerSocket建立連接后,ClientSocket調(diào)用send()函數(shù),ServerSocket調(diào)用receive()函數(shù),ServerSocket處理完請求后,調(diào)用send()函數(shù),ClientSocket調(diào)用receive()函數(shù),就可以得到得到返回結(jié)果。
直接理解可能有點抽象,你可以把這個過程套入前面我舉的“打電話”的例子,可以方便你理解Socket通信過程。

當客戶端和服務(wù)端建立網(wǎng)絡(luò)連接后,就可以發(fā)起請求了。但網(wǎng)絡(luò)不一定總是可靠的,經(jīng)常會遇到網(wǎng)絡(luò)閃斷、連接超時、服務(wù)端宕機等各種異常,通常的處理手段有兩種。
-
鏈路存活檢測:客戶端需要定時地發(fā)送心跳檢測消息(一般是通過ping請求)給服務(wù)端,如果服務(wù)端連續(xù)n次心跳檢測或者超過規(guī)定的時間都沒有回復消息,則認為此時鏈路已經(jīng)失效,這個時候客戶端就需要重新與服務(wù)端建立連接。
-
斷連重試:通常有多種情況會導致連接斷開,比如客戶端主動關(guān)閉、服務(wù)端宕機或者網(wǎng)絡(luò)故障等。這個時候客戶端就需要與服務(wù)端重新建立連接,但一般不能立刻完成重連,而是要等待固定的間隔后再發(fā)起重連,避免服務(wù)端的連接回收不及時,而客戶端瞬間重連的請求太多而把服務(wù)端的連接數(shù)占滿。
服務(wù)端如何處理請求?
假設(shè)這時候客戶端和服務(wù)端已經(jīng)建立了網(wǎng)絡(luò)連接,服務(wù)端又該如何處理客戶端的請求呢?通常來講,有三種處理方式。
-
同步阻塞方式(BIO),客戶端每發(fā)一次請求,服務(wù)端就生成一個線程去處理。當客戶端同時發(fā)起的請求很多時,服務(wù)端需要創(chuàng)建很多的線程去處理每一個請求,如果達到了系統(tǒng)最大的線程數(shù)瓶頸,新來的請求就沒法處理了。
-
同步非阻塞方式 (NIO),客戶端每發(fā)一次請求,服務(wù)端并不是每次都創(chuàng)建一個新線程來處理,而是通過I/O多路復用技術(shù)進行處理。就是把多個I/O的阻塞復用到同一個select的阻塞上,從而使系統(tǒng)在單線程的情況下可以同時處理多個客戶端請求。這種方式的優(yōu)勢是開銷小,不用為每個請求創(chuàng)建一個線程,可以節(jié)省系統(tǒng)開銷。
-
異步非阻塞方式(AIO),客戶端只需要發(fā)起一個I/O操作然后立即返回,等I/O操作真正完成以后,客戶端會得到I/O操作完成的通知,此時客戶端只需要對數(shù)據(jù)進行處理就好了,不需要進行實際的I/O讀寫操作,因為真正的I/O讀取或者寫入操作已經(jīng)由內(nèi)核完成了。這種方式的優(yōu)勢是客戶端無需等待,不存在阻塞等待問題。
從前面的描述,可以看出來不同的處理方式適用于不同的業(yè)務(wù)場景:
-
BIO適用于連接數(shù)比較小的業(yè)務(wù)場景,這樣的話不至于系統(tǒng)中沒有可用線程去處理請求。這種方式寫的程序也比較簡單直觀,易于理解。
-
NIO適用于連接數(shù)比較多并且請求消耗比較輕的業(yè)務(wù)場景,比如聊天服務(wù)器。這種方式相比BIO,相對來說編程比較復雜。
-
AIO適用于連接數(shù)比較多而且請求消耗比較重的業(yè)務(wù)場景,比如涉及I/O操作的圖片服務(wù)器。這種方式相比另外兩種,編程難度最大,程序也不易于理解。
上面兩個問題就是“通信框架”要解決的問題,你可以基于現(xiàn)有的Socket通信,在服務(wù)消費者和服務(wù)提供者之間建立網(wǎng)絡(luò)連接,然后在服務(wù)提供者一側(cè)基于BIO、NIO和AIO三種方式中的任意一種實現(xiàn)服務(wù)端請求處理,最后再花費一些精力去解決服務(wù)消費者和服務(wù)提供者之間的網(wǎng)絡(luò)可靠性問題。這種方式對于Socket網(wǎng)絡(luò)編程、多線程編程知識都要求比較高,感興趣的話可以嘗試自己實現(xiàn)一個通信框架。 但我建議最為穩(wěn)妥的方式是使用成熟的開源方案,比如Netty、MINA等,它們都是經(jīng)過業(yè)界大規(guī)模應用后,被充分論證是很可靠的方案。
假設(shè)客戶端和服務(wù)端的連接已經(jīng)建立了,服務(wù)端也能正確地處理請求了,接下來完成一次正常地RPC調(diào)用還需要解決兩個問題,即數(shù)據(jù)傳輸采用什么協(xié)議以及數(shù)據(jù)該如何序列化和反序列化。
數(shù)據(jù)傳輸采用什么協(xié)議?
首先來看第一個問題,數(shù)據(jù)傳輸采用什么協(xié)議?
最常用的有HTTP協(xié)議,它是一種開放的協(xié)議,各大網(wǎng)站的服務(wù)器和瀏覽器之間的數(shù)據(jù)傳輸大都采用了這種協(xié)議。還有一些定制的私有協(xié)議,比如阿里巴巴開源的Dubbo協(xié)議,也可以用于服務(wù)端和客戶端之間的數(shù)據(jù)傳輸。無論是開放的還是私有的協(xié)議,都必須定義一個“契約”,以便服務(wù)消費者和服務(wù)提供者之間能夠達成共識。服務(wù)消費者按照契約,對傳輸?shù)臄?shù)據(jù)進行編碼,然后通過網(wǎng)絡(luò)傳輸過去;服務(wù)提供者從網(wǎng)絡(luò)上接收到數(shù)據(jù)后,按照契約,對傳輸?shù)臄?shù)據(jù)進行解碼,然后處理請求,再把處理后的結(jié)果進行編碼,通過網(wǎng)絡(luò)傳輸返回給服務(wù)消費者;服務(wù)消費者再對返回的結(jié)果進行解碼,最終得到服務(wù)提供者處理后的返回值。
通常協(xié)議契約包括兩個部分:消息頭和消息體。其中消息頭存放的是協(xié)議的公共字段以及用戶擴展字段,消息體存放的是傳輸數(shù)據(jù)的具體內(nèi)容。
以HTTP協(xié)議為例,主要分為消息頭和消息體兩部分,其中消息頭中存放的是協(xié)議的公共字段,比如Server代表是服務(wù)端服務(wù)器類型、Content-Length代表返回數(shù)據(jù)的長度、Content-Type代表返回數(shù)據(jù)的類型;消息體中存放的是具體的返回結(jié)果,這里就是一段HTML網(wǎng)頁代碼。
數(shù)據(jù)該如何序列化和反序列化?
再看第二個問題,數(shù)據(jù)該如何序列化和反序列化。
一般數(shù)據(jù)在網(wǎng)絡(luò)中進行傳輸前,都要先在發(fā)送方一端對數(shù)據(jù)進行編碼,經(jīng)過網(wǎng)絡(luò)傳輸?shù)竭_另一端后,再對數(shù)據(jù)進行解碼,這個過程就是序列化和反序列化。
為什么要對數(shù)據(jù)進行序列化和反序列化呢?要知道網(wǎng)絡(luò)傳輸?shù)暮臅r一方面取決于網(wǎng)絡(luò)帶寬的大小,另一方面取決于數(shù)據(jù)傳輸量。要想加快網(wǎng)絡(luò)傳輸,要么提高帶寬,要么減小數(shù)據(jù)傳輸量,而對數(shù)據(jù)進行編碼的主要目的就是減小數(shù)據(jù)傳輸量。比如一部高清電影原始大小為30GB,如果經(jīng)過特殊編碼格式處理,可以減小到3GB,同樣是100MB/s的網(wǎng)速,下載時間可以從300s減小到30s。
常用的序列化方式分為兩類:文本類如XML/JSON等,二進制類如PB/Thrift等,而具體采用哪種序列化方式,主要取決于三個方面的因素。
-
支持數(shù)據(jù)結(jié)構(gòu)類型的豐富度。數(shù)據(jù)結(jié)構(gòu)種類支持的越多越好,這樣的話對于使用者來說在編程時更加友好,有些序列化框架如Hessian 2.0還支持復雜的數(shù)據(jù)結(jié)構(gòu)比如Map、List等。
-
跨語言支持。序列化方式是否支持跨語言也是一個很重要的因素,否則使用的場景就比較局限,比如Java序列化只支持Java語言,就不能用于跨語言的服務(wù)調(diào)用了。
-
性能。主要看兩點,一個是序列化后的壓縮比,一個是序列化的速度。以常用的PB序列化和JSON序列化協(xié)議為例來對比分析,PB序列化的壓縮比和速度都要比JSON序列化高很多,所以對性能和存儲空間要求比較高的系統(tǒng)選用PB序列化更合適;而JSON序列化雖然性能要差一些,但可讀性更好,更適合對外部提供服務(wù)。
總結(jié)
今天我們一起了解服務(wù)調(diào)用需要解決的幾個問題:
-
通信框架。它主要解決客戶端和服務(wù)端如何建立連接、管理連接以及服務(wù)端如何處理請求的問題。
-
通信協(xié)議。它主要解決客戶端和服務(wù)端采用哪種數(shù)據(jù)傳輸協(xié)議的問題。
-
序列化和反序列化。它主要解決客戶端和服務(wù)端采用哪種數(shù)據(jù)編解碼的問題。
這三個部分就組成了一個完整的RPC調(diào)用框架,通信框架提供了基礎(chǔ)的通信能力,通信協(xié)議描述了通信契約,而序列化和反序列化則用于數(shù)據(jù)的編/解碼。一個通信框架可以適配多種通信協(xié)議,也可以采用多種序列化和反序列化的格式,比如服務(wù)化框架Dubbo不僅支持Dubbo協(xié)議,還支持RMI協(xié)議、HTTP協(xié)議等,而且還支持多種序列化和反序列化格式,比如JSON、Hession 2.0以及Java序列化等。
本文由 mdnice 多平臺發(fā)布