文章目錄
- RPC
- 行程間通信幾種解決方案:
- 管道(Pipe)或者具名管道(Named Pipe)
- 信號(Signal)
- 信號量(Semaphore)
- 訊息佇列(Message Queue)
- 共享記憶體(Shared Memory)
- 本地套接字介面(IPC Socket)
- RPC要解決的三個問題
- 如何表示資料
- 如何傳遞資料
- 如何表示方法
- REST
- 超文本(或超媒體)
- 資源(Resource)
- 表征(Representation)
- 狀態(State)
- 轉移(Transfer)
- RPC和RESTful的區別
- 總結
RPC
RPC和RESTful都是遠程呼叫介面,那么它們之間到底有什么區別的呢?我以前一直傻傻分不清楚,直到我看了周志明老師寫的《鳳凰架構:構建可靠的大型分布式系統》我才理清了它們之間的區別,怕自己忘了,特意做下筆記,
RPC(Remote Procedure Call,RPC),即遠程程序呼叫,最近幾年頻繁被各種論壇,文章,課程提及,乍一看還以為是什么牛逼的新技術,其實在計算機科學中已經存在超過四十年時間,是個不折不扣的“老古董”,
RPC出現的最初目的,就是為了讓計算機能夠與呼叫本地方法一樣去呼叫遠程方法,
我們先來看一下計算機是如何呼叫本地方的,我抄錄了書中的例子:
// Caller : 呼叫者,代碼里的main()
// Callee : 被呼叫者,代碼里的println()
// Call Site : 呼叫點,即發生方法呼叫的指令流位置
// Parameter : 引數,由Caller傳遞給Callee的資料,即“hello world”
// Retval : 回傳值,由Callee傳遞給Caller的資料,如果方法能夠正常結束,它是void,如果方法例外完成,它是對應的例外
public static void main(String[] args) {
System.out.println("hello world");
}
在完全不考慮編譯器優化的前提下,程式運行至呼叫println()方法輸出hello world這行時,計算機(物理機或者虛擬機)要完成以下幾項作業,
- 傳遞方法引數:
將字串hello world的參考地址壓堆疊, - 確定方法版本:
根據println()方法的簽名,確定其執行版本,這其實并不是一個簡單的程序,無論是編譯時靜態決議,還是運行時動態分派,都必須根據某些語言規范中明確定義的原則,找到明確的Callee,“明確”是指唯一的一個Callee,或者有嚴格優先級的多個Callee,譬如不同的多載版本, - 執行被調方法:
從堆疊中彈出Parameter的值或參考,并以此為輸入,執行Callee內部的邏輯,這里我們只關心方法是如何呼叫的,而不關心方法內部具體是如何執行的, - 回傳執行結果:
將Callee的執行結果壓堆疊,并將程式的指令流恢復到Call Site的下一條指令,繼續向下執行,
我們再來考慮如果println()方法不在當前行程的記憶體地址空間中會發生什么問題,不難想到,這樣會至少面臨兩個直接的障礙,
首先,第一步和第四步所做的傳遞引數、傳回結果都依賴于堆疊記憶體,如果Caller與Callee分屬不同的行程,就不會擁有相同的堆疊記憶體,此時將引數在Caller行程的記憶體中壓堆疊,對于Callee行程的執行毫無意義,
其次,第二步的方法版本選擇依賴于語言規則,如果Caller與Callee不是同一種語言實作的程式,方法版本選擇就將是一項模糊的不可知行為,
我們暫時忽略第二個問題,假設Caller和Callee是使用同一種語言實作的,那么兩個行程之間該如何交換資料呢?這就是“行程間通信”(Inter-Process Communication,IPC)要解決的問題,
行程間通信幾種解決方案:
管道(Pipe)或者具名管道(Named Pipe)
管道類似于兩個行程間的橋梁,可通過管道在行程間傳遞少量的字符流或位元組流,普通管道只用于有親緣關系的行程(由一個行程啟動的另外一個行程)間的通信,具名管道擺脫了普通管道沒有名字的限制,除具有管道的所有功能外,它還允許無親緣關系的行程間的通信,
管道典型的應用就是命令列中的“|”運算子,
譬如:ps -ef | grep java
ps與grep都有獨立的行程,以上命令就是通過管道運算子“|”將ps命令的標準輸出連接到grep命令的標準輸入上,
信號(Signal)
信號用于通知目標行程有某種事件發生,除了行程間通信外,行程還可以給行程自身發送信號,信號的典型應用是kill命令,
譬如:kill -9 pid
以上命令即表示由Shell行程向指定PID的行程發送SIGKILL信號,
信號量(Semaphore)
信號量用于在兩個行程之間同步協作手段,它相當于作業系統提供的一個特殊變數,程式可以在上面進行wait()和notify()操作,
訊息佇列(Message Queue)
以上三種方式只適合傳遞少量訊息,POSIX標準中定義了可用于行程間資料量較多的通信的訊息佇列,行程可以向佇列添加訊息,被賦予讀權限的行程還可以從佇列消費訊息,訊息佇列克服了信號承載資訊量少、管道只能用于無格式位元組流以及緩沖區大小受限等缺點,但實時性相對受限,
共享記憶體(Shared Memory)
允許多個行程訪問同一塊公共記憶體空間,這是效率最高的行程間通信形式,原本每個行程的記憶體地址空間都是相互隔離的,但作業系統提供了讓行程主動創建、映射、分離、控制某一塊記憶體的程式介面,當一塊記憶體被多行程共享時,各個行程往往會與其他通信機制,譬如與信號量結合使用,來達到行程間同步及互斥的協調操作,
本地套接字介面(IPC Socket)
訊息佇列與共享記憶體只適合單機多行程間的通信,套接字介面則是更普適的行程間通信機制,可用于不同機器之間的行程通信,套接字(Socket)起初是由UNIX系統的BSD分支開發出來的,現在已經移植到所有主流的作業系統上,出于效率考慮,當僅限于本機行程間通信時,套接字介面是被優化過的,不會經過網路協議堆疊,不需要打包拆包、計算校驗和、維護序號和應答等操作,只是簡單地將應用層資料從一個行程復制到另一個行程,這種行程間通信方式即本地套接字介面(UNIX Domain Socket),又叫作IPC Socket,
RPC要解決的三個問題
如何表示資料
這里的資料包括傳遞給方法的引數和方法執行之后的回傳值,也就是說一個行程把引數傳給另一個行程,或者從另一個行程獲取回傳值,資料格式怎么表示的問題,你可能會覺得很奇怪,比如用Java語言寫的程式,傳遞String,int等型別不就行了嗎?對于行程內的方法呼叫,使用同一種語言的資料型別,比如雙方的程式都用Java語言寫的,這樣呼叫自然沒有問題,但是,如果呼叫方是用Java語言寫的,被呼叫方是C語言寫的,他們的資料型別定義的都不一樣,該如何兼容呢?
就算雙方都用同一種語言寫的,比如C語言,但是在不同的硬體指令集、不同的作業系統下,同樣的資料型別也完全可能有不一樣的表現細節,譬如資料寬度、位元組序列的差異等等,
那該怎么辦呢?
說來也簡單,就是先把雙方要交流的資料先轉化成大家都認識的中間格式,然后再將中間格式資料轉化成自己所用語言的資料型別,聽起來是不是很熟悉?沒錯,這就是序列化與反序列化,我以前學Java的時候,學到序列化與反序列化就很納悶?引數為什么要序列化呢?直接傳不就行了嗎?原來是要考慮不同語言、硬體、作業系統的情況,
每種RPC協議都有對應的序列化協議,例如:
- ONC RPC的外部資料表示(External Data Representation,XDR)
- CORBA的通用資料表示(Common Data Representation,CDR)
- Java RMI的Java物件序列化流協議(Java Object Serialization Stream Protocol)
- gRPC的Protocol Buffers
- Web Service的XML序列化
- 眾多輕量級RPC支持的JSON序列化
看到Web Service的XML序列化特別有感觸,以前在用Web Service的時候,我很奇怪為什么要用xml費這么大勁去描述一個個欄位,一個個引數,原來是為了讓不同的語言都能識別,
如何傳遞資料
兩個程式之間如何傳遞資料,也就是互相操作,互動資料,除了序列化與反序列化之外還需要考慮:例外、超時、安全、認證、授權、事務等等,都可能產生雙方需要交換資訊的需求,在計算機科學中,專門有一個名詞“Wire Protocol”來表示這種兩個Endpoint之間交換這類資料的行為,常見的Wire Protocol如下:
- Java RMI的Java遠程訊息交換協議(Java Remote Message Protocol,JRMP,也支持RMI-IIOP)
- CORBA的互聯網ORB間協議(Internet Inter ORB Protocol,IIOP,是GIOP協議在IP協議上的實作版本)
- DDS的實時發布訂閱協議(Real Time Publish Subscribe Protocol,RTPS)
- Web Service的簡單物件訪問協議(Simple Object Access Protocol,SOAP)
- 如果要求足夠簡單,雙方都是HTTP Endpoint,直接使用HTTP協議也是可以的(如JSON-RPC)
除了傳遞資料,RPC 還有更吸引人的地方,它真正強大的地方是它的治理功能,比如連接管理、健康檢測、負載均衡、優雅啟停機、例外重試、業務分組以及熔斷限流等等,
如何表示方法
確定表示方法在本地方法呼叫中并不是太大的問題,編譯器或者解釋器會根據語言規范,將呼叫的方法簽名轉換為行程空間中子程序入口位置的指標,不過一旦要考慮不同語言,事情又立刻麻煩起來,每種語言的方法簽名都可能有差別,所以“如何表示同一個方法”“如何找到對應的方法”還是需要一個統一的跨語言的標準才行,
這個標準可以非常簡單,譬如直接給程式的每個方法都規定一個唯一的、在任何機器上都絕不重復的編號,呼叫時壓根不管它是什么方法、簽名是如何定義的,直接傳這個編號就能找到對應的方法,這種聽起既粗魯又寒磣的辦法,還真的就是DCE/RPC當初準備的解決方案,雖然最終DCE還是弄出了一套與語言無關的介面描述語言(Interface Description Language,IDL),成為此后許多RPC參考或依賴的基礎(如CORBA的OMG IDL),但那個唯一的絕不重復的編碼方案UUID(Universally Unique Identifier)也被保留且廣為流傳開來,并被廣泛應用于程式開發的方方面面,
類似地,用于表示方法的協議還有:
- Android的Android介面定義語言(Android Interface Definition Language,AIDL)
- CORBA的OMG介面定義語言(OMG Interface Definition Language,OMG IDL)
- Web Service的Web服務描述語言(Web Service Description Language,WSDL)
- JSON-RPC的JSON Web服務協議(JSON Web Service Protocol,JSON-WSP)
以上RPC中的三個基本問題,全部都可以在本地方法呼叫程序中找到對應的解決方案,RPC的設計始于本地方法呼叫,盡管早已不再追求實作與本地方法呼叫完全一致的目的,但其設計思路仍然帶有本地方法呼叫的深刻烙印,抓住兩者間的聯系來類比,對我們更深刻地理解RPC的本質會很有幫助,
為了解決上面的三個問題,每個RPC的產品解決問題的角度不同,有的著重于簡單性,有的希望能支持更多的語言達到普適性,有的看中高性能,現在,已經相繼出現過RMI(Sun/Oracle)、Thrift(Facebook/Apache)、Dubbo(阿里巴巴/Apache)、gRPC(Google)、Motan1/2(新浪)、Finagle(Twitter)、brpc(百度/Apache)、.NET Remoting(微軟)、Arvo(Hadoop)、JSON-RPC 2.0(公開規范,JSON-RPC作業組)等難以窮舉的協議和框架,這些RPC功能、特點不盡相同,有的是某種語言私有,有的支持跨越多種語言,有的運行在應用層HTTP協議之上,有的直接運行于傳輸層TCP/UDP協議之上,但并不存在哪一款是“最完美的RPC”,
今時今日,任何一款具有生命力的RPC框架,都不再去追求大而全的“完美”,而是以某個具有針對性的特點作為主要的發展方向,同時這也導致了我們的選擇困難癥,更是催生出了一大堆網路文章和課程,來講解各RPC的優缺點,什么時候能有一個RPC的王者出來統一天下呢?
REST
REST源于Roy Thomas Fielding在2000年發表的博士論文“Architectural Styles and the Design of Network-based Software Architectures”,此文的確是REST的源頭,REST(Representational State Transfer,表征狀態轉移),這個名字聽起來很晦澀,什么叫“表征”、什么東西的“狀態”、從哪“轉移”到哪?
我們要理解REST就要先理解什么是HTTP,再配合一些實際例子來對兩者進行類比,以更清楚地了解REST,你會發現REST實際上是“HTT”(Hypertext Transfer)的進一步抽象,兩者的關系就如同介面與實作類的關系一般,
超文本(或超媒體)
“超文本(或超媒體)”是一種“能夠對操作進行判斷和回應的文本(或聲音、影像等)”,這個概念在20世紀60年代提出時應該還屬于科幻的范疇,但是今天大眾已經完全接受了它,互聯網中一段文字可以點擊、可以觸發腳本執行、可以呼叫服務端已毫不稀奇,下面我們繼續嘗試從“超文本”或者“超媒體”的含義來理解什么是“表征”以及REST中的其他關鍵概念,這里使用一個具體事例將其描述如下:
資源(Resource)
譬如你現在正在閱讀一篇名為《REST設計風格》的文章,這篇文章的內容本身(你可以將其理解為蘊含的資訊、資料)稱之為“資源”,無論你是通過閱讀購買的圖書、瀏覽器上的網頁還是列印出來的文稿,無論是在電腦螢屏上閱讀還是在手機上閱讀,盡管呈現的樣子各不相同,但其中的資訊是不變的,你所閱讀的仍是同一份“資源”,
表征(Representation)
當你通過瀏覽器閱讀此文章時,瀏覽器會向服務端發出“我需要這個資源的HTML格式”的請求,服務端向瀏覽器回傳的這個HTML就被稱為“表征”,你也可以通過其他方式拿到本文的PDF、Markdown、RSS等其他形式的版本,它們同樣是一個資源的多種表征,可見“表征”是指資訊與用戶互動時的表示形式,這與我們軟體分層架構中常說的“表示層”(Presentation Layer)的語意其實是一致的,
狀態(State)
當你讀完了這篇文章,想看后面是什么內容時,你向服務端發出“給我下一篇文章”的請求,但是“下一篇”是個相對概念,必須依賴“當前你正在閱讀的文章是哪一篇”才能正確回應,這類在特定語境中才能產生的背景關系資訊被稱為“狀態”,我們所說的有狀態(Stateful)抑或是無狀態(Stateless),都是只相對于服務端來說的,服務端要完成“取下一篇”的請求,要么自己記住用戶的狀態,如這個用戶現在閱讀的是哪一篇文章,這稱為有狀態;要么由客戶端來記住狀態,在請求的時候明確告訴服務端,如我正在閱讀某某文章,現在要讀它的下一篇,這稱為無狀態,也就是說,由服務端來記錄用戶的狀態就叫有狀態;由客戶端記錄狀態,并把它傳到服務端的叫做無狀態,
轉移(Transfer)
無論狀態是由服務端還是由客戶端來提供,“取下一篇文章”這個行為邏輯只能由服務端來提供,因為只有服務端擁有該資源及其表征形式,服務端通過某種方式,把“用戶當前閱讀的文章”轉變成“下一篇文章”,這就被稱為“表征狀態轉移”,
REST的一條核心原則是“統一介面(Uniform Interface)”,REST希望開發者面向資源編程,希望軟體系統設計的重點放在抽象系統該有哪些資源,而不是抽象系統該有哪些行為(服務)上,這條原則你可以類比計算機中對檔案管理的操作來理解,管理檔案可能會涉及創建、修改、洗掉、移動等操作,這些運算元量是可數的,而且對所有檔案都是固定、統一的,如果面向資源來設計系統,同樣會具有類似的操作特征,由于REST并沒有設計新的協議,所以這些操作都借用了HTTP協議中固有的操作命令來完成,
統一介面也是REST最容易陷入爭論的地方,基于網路的軟體系統,到底是面向資源合適,還是面向服務更合適,這個問題恐怕在很長時間里都不會有定論,也許永遠都沒有,但是,已經有一個基本清晰的結論是:面向資源編程的抽象程度通常更高,抽象程度高帶來的壞處是距離人類的思維方式往往會更遠,而好處是通用程度往往會更好,
用這樣的語言去詮釋REST,還是有些抽象,下面以一個例子來說明:譬如,對于幾乎每個系統都有的登錄和注銷功能,如果你理解成登錄對應于login()服務,注銷對應于logout()服務這樣兩個獨立服務,這是“符合人類思維”的;如果你理解成登錄是PUT Session,注銷是DELETE Session,這樣你只需要設計一種“Session資源”即可滿足需求,甚至以后對Session的其他需求,如查詢登錄用戶的資訊,就是GET Session而已,其他操作如修改用戶資訊等也都可以被這同一套設計囊括在內,這便是“抽象程度更高”帶來的好處,
如果想要在架構設計中合理恰當地利用統一介面,建議系統應能做到每次請求中都包含資源的ID,所有操作均通過資源ID來進行;建議每個資源都應該是自描述的訊息;建議通過超文本來驅動應用狀態的轉移,
REST系結于HTTP協議,面向資源編程不是必須構筑在HTTP之上,但REST是,這是缺點,也是優點,因為HTTP本來就是面向資源設計的網路協議,純粹只用HTTP(而不是SOAP over HTTP那樣再構筑協議)帶來的好處是無須考慮RPC中的Wire Protocol問題,REST將復用HTTP協議中已經定義的概念和相關基礎支持來解決問題,HTTP協議已經有效運作了三十年,其相關的技識訓礎設施已是千錘百煉,無比成熟,而壞處自然是,當你想去考慮那些HTTP不提供的特性時,便會徹底束手無策,
HTTP協議中已經提前約定好了一套“統一介面”,它包括GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS七種基本操作,任何一個支持HTTP協議的服務器都會遵守這套規定,對特定的URI采取這些操作,服務器就會觸發相應的表征狀態轉移,
REST介面很容易理解,舉個具體例子,假設一個商城用戶中心的介面設計:用戶資源會擁有多個不同的下級的資源,譬如若干條短訊息資源、一份用戶資料資源、一輛購物吵澩,購物車中又會有自己的下級資源,譬如多本圖書資源,你很容易在程式介面中構造出這些資源的集合關系與層次關系,而且這些關系是符合人們長期在單機或網路環境中管理資料的經驗的,相信你不需要專門閱讀介面說明書,就能輕易推斷出獲取用戶icyfenix的購物車中的第2本書的REST介面應該表示為:
GET /users/icyfenix/cart/2,
再來舉幾個列子:
- GET /books:列出所有書籍
- POST /books:新建一本書
- GET /books/ID:獲取某個指定書籍的資訊
- PUT /books/ID:更新某本書籍的資訊(提供該書籍的全部資訊)
- PATCH /books/ID:更新某本指定書籍的資訊(提供該書籍的部分資訊)
- DELETE /books/ID:洗掉某本書
RPC和RESTful的區別
很多人會拿REST與RPC相比較,其實,REST無論是在思想上、在概念上,還是在使用范圍上,與RPC都不盡相同,充其量只能算是有一些相似,應用會有一部分重合之處,但本質上并不是同一型別的東西,
RPC和REST在思想上差異的核心是抽象的目標不一樣,即面向程序的編程思想與面向資源的編程思想兩者之間的區別,
REST與RPC在概念上的不同是指REST并不是一種遠程服務呼叫協議,甚至可以把定語也去掉,它就不是一種協議,協議都帶有一定的規范性和強制性,最起碼也有一個規約檔案,譬如JSON-RPC,哪怕再簡單,也有《JSON-RPC規范》來規定協議的格式細節、例外、回應碼等資訊,但是REST并沒有定義這些內容,盡管有一些指導原則,但實際上并不受任何強制的約束,常有人批評某個系統介面“設計得不夠RESTful”,其實這句話本身就有些爭議,REST只能說是風格而不是規范、協議,并且能完全符合REST所有指導原則的系統也是不多見的,
REST的基本思想是面向資源來抽象問題,它與此前流行的編程思想——面向程序的編程在抽象主體上有本質的差別,在REST提出以前,人們設計分布式系統服務的唯一方案就只有RPC,RPC是將本地的方法呼叫思路遷移到遠程方法呼叫上,開發者是圍繞“遠程方法”去設計兩個系統間互動的,譬如CORBA、RMI、DCOM,等等,這樣做的壞處不僅使“如何在異構系統間表示一個方法”“如何獲得介面能夠提供的方法清單”成為需要專門協議去解決的問題(RPC的三大基本問題之一),而且對于服務使用者來說,由于服務的每個方法都是完全獨立的,他們必須逐個學習才能正確地使用這些方法,Google在“Google API Design Guide”中曾經寫下這樣一段話,
“以前,人們面向方法去設計RPC API,譬如CORBA和DCOM,隨著時間推移,介面與方法越來越多卻又各不相同,開發人員必須了解每一個方法才能正確使用它們,這樣既耗時又容易出錯,”
也就是說RPC客戶端必須先知道服務器端的方法才能呼叫它們,
我們在呼叫Web Service介面之前要通過服務器提供的WSDL檔案來生成客戶端,客戶端通過WSDL檔案知道了服務器的方法和引數,
同樣gRPC也有服務器和客戶端,gRPC的呼叫示例圖如下:

我們來看一段gRPC的代碼,
服務端:
/**
* 對外暴露服務
**/
private void start() throws IOException {
int port = 50051;
server = ServerBuilder.forPort(port)
.addService(new HelloServiceImpl())
.build()
.start();
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
HelloWorldServer.this.stop();
}
});
}
客戶端:
/**
* 發送rpc請求
**/
public void say(String name) {
// 構建入參物件
HelloRequest request = HelloRequest.newBuilder().setName(name).build();
HelloReply response;
try {
// 發送請求
response = blockingStub.say(request);
} catch (StatusRuntimeException e) {
return;
}
System.out.println(response);
}
客戶端必須先知道服務端的say()方法,才能去呼叫它,
REST是根據資源操作的,如果REST的設計滿足第 3 級成熟度:Hypermedia Controls(超文本驅動),服務器會回傳下一個操作的超鏈接,除了第一個請求是由你在瀏覽器地址欄輸入的資訊所驅動的之外,其他的請求都應該能夠自己描述清楚后續可能發生的狀態轉移,由超文本自身來驅動,
我們來看一下書中的例子:
HTTP/1.1 200 OK
{
schedules:[
{
id: 1234, start:"14:00", end: "14:50", doctor: "mjones",
links: [
{rel: "comfirm schedule", href: "/schedules/1234"}
]
},
{
id: 5678, start:"16:00", end: "16:50", doctor: "mjones",
links: [
{rel: "comfirm schedule", href: "/schedules/5678"}
]
}
],
links: [
{rel: "doctor info", href: "/doctors/mjones/info"}
]
}
就算服務器端沒有回傳下一操作的鏈接,客戶端知道資源后甚至可以猜出下一個操作的URL,
總結
RPC是以一種呼叫本地方法的思路來呼叫遠程方法,通過各種RPC框架隱藏呼叫遠程方法的細節,讓用戶以為呼叫的就是本地方法,RPC隱藏了底層網路通信的復雜度,讓我們更專注于業務邏輯的開發,
REST通過HTTP實作,把用戶的需求抽象成對資源的操作,用戶必須通過HTTP協議的GET、HEAD、POST、PUT、DELETE、TRACE、OPTIONS七種基本操作去和服務器互動,
RPC通常是服務器和服務器之間的通信,比如和中間件的通信,MQ、分布式快取、分布式資料庫等等,
而REST通常是面向客戶端的(一般是瀏覽器),他們的使用場景也是不一樣的,
最后,我再推薦一下周志明老師的《鳳凰架構:構建可靠的大型分布式系統》,本文的內容大都抄錄自此書,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/340697.html
標籤:其他
上一篇:華為網路配置(路由配置)
