我想起了我剛工作的時候,第一次接觸 RPC 協(xié)議,當(dāng)時就很懵,我 HTTP 協(xié)議用的好好的,為什么還要用 RPC 協(xié)議?
于是就到網(wǎng)上去搜。
不少解釋顯得非常官方,我相信大家在各種平臺上也都看到過,解釋了又好像沒解釋,都在用一個我們不認(rèn)識的概念去解釋另外一個我們不認(rèn)識的概念,懂的人不需要看,不懂的人看了還是不懂。
這種看了,又好像沒看的感覺,云里霧里的很難受,我懂。
為了避免大家有強烈的審丑疲勞,今天我們來嘗試重新?lián)Q個方式講一講。
從 TCP 聊起
作為一個程序員,假設(shè)我們需要在 A 電腦的進程發(fā)一段數(shù)據(jù)到 B 電腦的進程,我們一般會在代碼里使用 socket 進行編程。
這時候,我們可選項一般也就 TCP 和 UDP 二選一。TCP 可靠,UDP 不可靠。除非是馬總這種神級程序員(早期 QQ 大量使用 UDP),否則,只要稍微對可靠性有些要求,普通人一般無腦選 TCP 就對了。
類似下面這樣。
fd = socket(AF_INET,SOCK_STREAM,0);
其中 SOCK_STREAM,是指使用字節(jié)流傳輸數(shù)據(jù),說白了就是 TCP 協(xié)議。
在定義了 socket 之后,我們就可以愉快的對這個 socket 進行操作,比如用 bind () 綁定 IP 端口,用 connect () 發(fā)起建連。
▲ 握手建立連接流程
在連接建立之后,我們就可以使用 send () 發(fā)送數(shù)據(jù),recv () 接收數(shù)據(jù)。
光這樣一個純裸的 TCP 連接,就可以做到收發(fā)數(shù)據(jù)了,那是不是就夠了?
不行,這么用會有問題。
使用純裸 TCP 會有什么問題
八股文常背,TCP 是有三個特點,面向連接、可靠、基于字節(jié)流。
▲ TCP 是什么
這三個特點真的概括的非常精辟,這個八股文我們沒白背。
每個特點展開都能聊一篇文章,而今天我們需要關(guān)注的是基于字節(jié)流這一點。
字節(jié)流可以理解為一個雙向的通道里流淌的數(shù)據(jù),這個數(shù)據(jù)其實就是我們常說的二進制數(shù)據(jù),簡單來說就是一大堆 01 串。純裸 TCP 收發(fā)的這些 01 串之間是沒有任何邊界的,你根本不知道到哪個地方才算一條完整消息。
▲ 01 二進制字節(jié)流
正因為這個沒有任何邊界的特點,所以當(dāng)我們選擇使用 TCP 發(fā)送 "夏洛" 和 "特?zé)⿶? 的時候,接收端收到的就是 "夏洛特?zé)⿶?,這時候接收端沒發(fā)區(qū)分你是想要表達 "夏洛"+"特?zé)⿶? 還是 "夏洛特"+"煩惱"。
▲ 消息對比
這就是所謂的粘包問題,之前也寫過一篇專門的文章聊過這個問題。
說這個的目的是為了告訴大家,純裸 TCP 是不能直接拿來用的,你需要在這個基礎(chǔ)上加入一些自定義的規(guī)則,用于區(qū)分消息邊界。
于是我們會把每條要發(fā)送的數(shù)據(jù)都包裝一下,比如加入消息頭,消息頭里寫清楚一個完整的包長度是多少,根據(jù)這個長度可以繼續(xù)接收數(shù)據(jù),截取出來后它們就是我們真正要傳輸?shù)?span style="font-weight:700;">消息體。
▲ 消息邊界長度標(biāo)志
而這里頭提到的消息頭,還可以放各種東西,比如消息體是否被壓縮過和消息體格式之類的,只要上下游都約定好了,互相都認(rèn)就可以了,這就是所謂的協(xié)議。
每個使用 TCP 的項目都可能會定義一套類似這樣的協(xié)議解析標(biāo)準(zhǔn),他們可能有區(qū)別,但原理都類似。
于是基于 TCP,就衍生了非常多的協(xié)議,比如 HTTP 和 RPC。
HTTP 和 RPC
我們回過頭來看網(wǎng)絡(luò)的分層圖。
▲ 四層網(wǎng)絡(luò)協(xié)議
TCP 是傳輸層的協(xié)議,而基于 TCP 造出來的 HTTP 和各類 RPC 協(xié)議,它們都只是定義了不同消息格式的應(yīng)用層協(xié)議而已。
HTTP 協(xié)議(Hyper Text Transfer Protocol),又叫做超文本傳輸協(xié)議。我們用的比較多,平時上網(wǎng)在瀏覽器上敲個網(wǎng)址就能訪問網(wǎng)頁,這里用到的就是 HTTP 協(xié)議。
▲ HTTP 調(diào)用
而 RPC(Remote Procedure Call),又叫做遠程過程調(diào)用。它本身并不是一個具體的協(xié)議,而是一種調(diào)用方式。
舉個例子,我們平時調(diào)用一個本地方法就像下面這樣。
res = localFunc(req)
如果現(xiàn)在這不是個本地方法,而是個遠端服務(wù)器暴露出來的一個方法 remoteFunc,如果我們還能像調(diào)用本地方法那樣去調(diào)用它,這樣就可以屏蔽掉一些網(wǎng)絡(luò)細節(jié),用起來更方便,豈不美哉?
res = remoteFunc(req)
▲ RPC 可以像調(diào)用本地方法那樣調(diào)用遠端方法
基于這個思路,大佬們造出了非常多款式的 RPC 協(xié)議,比如比較有名的 gRPC,thrift。
值得注意的是,雖然大部分 RPC 協(xié)議底層使用 TCP,但實際上它們不一定非得使用 TCP,改用 UDP 或者 HTTP,其實也可以做到類似的功能。
▲ 基于 TCP 協(xié)議的 HTTP 和 RPC 協(xié)議
到這里,我們回到文章標(biāo)題的問題。
既然有 HTTP 協(xié)議,為什么還要有 RPC?
其實,TCP 是 70 年代出來的協(xié)議,而 HTTP 是 90 年代才開始流行的。而直接使用裸 TCP 會有問題,可想而知,這中間這么多年有多少自定義的協(xié)議,而這里面就有 80 年代出來的 RPC。
所以我們該問的不是既然有 HTTP 協(xié)議為什么要有 RPC,而是為什么有 RPC 還要有 HTTP 協(xié)議。
那既然有 RPC 了,為什么還要有 HTTP 呢?
現(xiàn)在電腦上裝的各種聯(lián)網(wǎng)軟件,比如 xx 管家,xx 衛(wèi)士,它們都作為客戶端(client)需要跟服務(wù)端(server)建立連接收發(fā)消息,此時都會用到應(yīng)用層協(xié)議,在這種 client/server (c/s) 架構(gòu)下,它們可以使用自家造的 RPC 協(xié)議,因為它只管連自己公司的服務(wù)器就 ok 了。
但有個軟件不同,瀏覽器(browser),不管是 chrome 還是 IE,它們不僅要能訪問自家公司的服務(wù)器(server),還需要訪問其他公司的網(wǎng)站服務(wù)器,因此它們需要有個統(tǒng)一的標(biāo)準(zhǔn),不然大家沒法交流。于是,HTTP 就是那個時代用于統(tǒng)一 browser/server (b/s) 的協(xié)議。
也就是說在多年以前,HTTP 主要用于 b / s 架構(gòu),而 RPC 更多用于 c / s 架構(gòu)。但現(xiàn)在其實已經(jīng)沒分那么清了,b / s 和 c / s 在慢慢融合。很多軟件同時支持多端,比如某度云盤,既要支持網(wǎng)頁版,還要支持手機端和 pc 端,如果通信協(xié)議都用 HTTP 的話,那服務(wù)器只用同一套就夠了。而 RPC 就開始退居幕后,一般用于公司內(nèi)部集群里,各個微服務(wù)之間的通訊。
那這么說的話,都用 HTTP 得了,還用什么 RPC?
仿佛又回到了文章開頭的樣子,那這就要從它們之間的區(qū)別開始說起。
HTTP 和 RPC 有什么區(qū)別
我們來看看 RPC 和 HTTP 區(qū)別比較明顯的幾個點。
服務(wù)發(fā)現(xiàn)
首先要向某個服務(wù)器發(fā)起請求,你得先建立連接,而建立連接的前提是,你得知道 IP 地址和端口。這個找到服務(wù)對應(yīng)的 IP 端口的過程,其實就是服務(wù)發(fā)現(xiàn)。
在 HTTP 中,你知道服務(wù)的域名,就可以通過 DNS 服務(wù)去解析得到它背后的 IP 地址,默認(rèn) 80 端口。
而 RPC 的話,就有些區(qū)別,一般會有專門的中間服務(wù)去保存服務(wù)名和 IP 信息,比如 consul 或者 etcd,甚至是 redis。想要訪問某個服務(wù),就去這些中間服務(wù)去獲得 IP 和端口信息。由于 dns 也是服務(wù)發(fā)現(xiàn)的一種,所以也有基于 dns 去做服務(wù)發(fā)現(xiàn)的組件,比如 CoreDNS。
可以看出服務(wù)發(fā)現(xiàn)這一塊,兩者是有些區(qū)別,但不太能分高低。
底層連接形式
以主流的 HTTP1.1 協(xié)議為例,其默認(rèn)在建立底層 TCP 連接之后會一直保持這個連接(keep alive),之后的請求和響應(yīng)都會復(fù)用這條連接。
而 RPC 協(xié)議,也跟 HTTP 類似,也是通過建立 TCP 長鏈接進行數(shù)據(jù)交互,但不同的地方在于,RPC 協(xié)議一般還會再建個連接池,在請求量大的時候,建立多條連接放在池內(nèi),要發(fā)數(shù)據(jù)的時候就從池里取一條連接出來,用完放回去,下次再復(fù)用,可以說非常環(huán)保。
▲ connection_pool
由于連接池有利于提升網(wǎng)絡(luò)請求性能,所以不少編程語言的網(wǎng)絡(luò)庫里都會給 HTTP 加個連接池,比如 go 就是這么干的。
可以看出這一塊兩者也沒太大區(qū)別,所以也不是關(guān)鍵。
傳輸?shù)膬?nèi)容
基于 TCP 傳輸?shù)南,說到底,無非都是消息頭 header 和消息體 body。
header 是用于標(biāo)記一些特殊信息,其中最重要的是消息體長度。
body 則是放我們真正需要傳輸?shù)膬?nèi)容,而這些內(nèi)容只能是二進制 01 串,畢竟計算機只認(rèn)識這玩意。所以 TCP 傳字符串和數(shù)字都問題不大,因為字符串可以轉(zhuǎn)成編碼再變成 01 串,而數(shù)字本身也能直接轉(zhuǎn)為二進制。但結(jié)構(gòu)體呢,我們得想個辦法將它也轉(zhuǎn)為二進制 01 串,這樣的方案現(xiàn)在也有很多現(xiàn)成的,比如 json,protobuf。
這個將結(jié)構(gòu)體轉(zhuǎn)為二進制數(shù)組的過程就叫序列化,反過來將二進制數(shù)組復(fù)原成結(jié)構(gòu)體的過程叫反序列化。
▲ 序列化和反序列化
對于主流的 HTTP1.1,雖然它現(xiàn)在叫超文本協(xié)議,支持音頻視頻,但 HTTP 設(shè)計初是用于做網(wǎng)頁文本展示的,所以它傳的內(nèi)容以字符串為主。header 和 body 都是如此。在 body 這塊,它使用 json 來序列化結(jié)構(gòu)體數(shù)據(jù)。
我們可以隨便截個圖直觀看下。
▲ HTTP 報文
可以看到這里面的內(nèi)容非常多的冗余,顯得非常啰嗦。最明顯的,像 header 里的那些信息,其實如果我們約定好頭部的第幾位是 content-type,就不需要每次都真的把 "content-type" 這個字段都傳過來,類似的情況其實在 body 的 json 結(jié)構(gòu)里也特別明顯。
而 RPC,因為它定制化程度更高,可以采用體積更小的 protobuf 或其他序列化協(xié)議去保存結(jié)構(gòu)體數(shù)據(jù),同時也不需要像 HTTP 那樣考慮各種瀏覽器行為,比如 302 重定向跳轉(zhuǎn)啥的。因此性能也會更好一些,這也是在公司內(nèi)部微服務(wù)中拋棄 HTTP,選擇使用 RPC 的最主要原因。
▲ HTTP 原理
▲ RPC 原理
當(dāng)然上面說的 HTTP,其實特指的是現(xiàn)在主流使用的 HTTP1.1,HTTP2 在前者的基礎(chǔ)上做了很多改進,所以性能可能比很多 RPC 協(xié)議還要好,甚至連 gRPC 底層都直接用的 HTTP2。
那么問題又來了。
為什么既然有了 HTTP2,還要有 RPC 協(xié)議?
這個是由于 HTTP2 是 2015 年出來的。那時候很多公司內(nèi)部的 RPC 協(xié)議都已經(jīng)跑了好些年了,基于歷史原因,一般也沒必要去換了。
總結(jié)
純裸 TCP 是能收發(fā)數(shù)據(jù),但它是個無邊界的數(shù)據(jù)流,上層需要定義消息格式用于定義消息邊界。于是就有了各種協(xié)議,HTTP 和各類 RPC 協(xié)議就是在 TCP 之上定義的應(yīng)用層協(xié)議。
RPC 本質(zhì)上不算是協(xié)議,而是一種調(diào)用方式,而像 gRPC 和 thrift 這樣的具體實現(xiàn),才是協(xié)議,它們是實現(xiàn)了 RPC 調(diào)用的協(xié)議。目的是希望程序員能像調(diào)用本地方法那樣去調(diào)用遠端的服務(wù)方法。同時 RPC 有很多種實現(xiàn)方式,不一定非得基于 TCP 協(xié)議。
從發(fā)展歷史來說,HTTP 主要用于 b / s 架構(gòu),而 RPC 更多用于 c / s 架構(gòu)。但現(xiàn)在其實已經(jīng)沒分那么清了,b / s 和 c / s 在慢慢融合。很多軟件同時支持多端,所以對外一般用 HTTP 協(xié)議,而內(nèi)部集群的微服務(wù)之間則采用 RPC 協(xié)議進行通訊。
RPC 其實比 HTTP 出現(xiàn)的要早,且比目前主流的 HTTP1.1 性能要更好,所以大部分公司內(nèi)部都還在使用 RPC。
HTTP2.0 在 HTTP1.1 的基礎(chǔ)上做了優(yōu)化,性能可能比很多 RPC 協(xié)議都要好,但由于是這幾年才出來的,所以也不太可能取代掉 RPC。
最后留個問題吧,大家有沒有發(fā)現(xiàn),不管是 HTTP 還是 RPC,它們都有個特點,那就是消息都是客戶端請求,服務(wù)端響應(yīng)。客戶端沒問,服務(wù)端肯定就不答,這就有點僵了,但現(xiàn)實中肯定有需要下游主動發(fā)送消息給上游的場景,比如打個網(wǎng)頁游戲,站在那啥也不操作,怪也會主動攻擊我,這種情況該怎么辦呢?
|