一、什么是RPC
RPC 的全稱是 Remote Procedure Call ,是一種進程間通信方式。它允許程序調用另一個地址空間(通常是共享網絡的另一台機器上)的過程或函數,而不用程序員顯式編碼這個遠程調用的細節。即無論是調用本地接口/服務的還是遠程的接口/服務,本質上編寫的調用代碼基本相同。
說起RPC,就不能不提到分布式,這個促使RPC誕生的領域。
假設你有一個計算器接口,Calculator模塊,以及它的實現類CalculatorImpl,那么在系統還是單體應用時,你要調用Calculator的add方法來執行一個加運算,直接實例一個CalculatorImpl對象,然后調用add方法就行了,這其實就是非常普通的本地函數調用,因為在同一個地址空間,或者說在同一塊內存,所以可以直接實現。
現在,基於高性能和高可靠等因素的考慮,你決定將系統改造為分布式應用,將很多可以共享的功能都單獨拎出來,比如上面說到的計算器,你單獨把它放到一個服務里頭,讓別的服務去調用它。
這下問題來了,服務A里頭並沒有CalculatorImpl這個類,那它要怎樣調用服務B的CalculatorImpl的add方法呢?
有同學會說,可以模仿B/S架構的調用方式呀,在B服務暴露一個Restful接口,然后A服務通過調用這個Restful接口來間接調用CalculatorImpl的add方法。
res=requests.get("URL")
1、http協議較為復雜,效率低,相對笨重
二 如何實現RPC
2.1 RPC實現原理
實際情況下,RPC很少用到http協議來進行數據傳輸,畢竟我只是想傳輸一下數據而已,何必動用到一個文本傳輸的應用層協議呢,所以一般會選擇直接傳輸二進制數據
不管你用何種協議進行數據傳輸,一個完整的RPC過程,都可以用下面這張圖來描述:

以左邊的Client端為例,Application就是rpc的調用方,Client Stub就是我們上面說到的代理對象,也就是那個看起來像是Calculator的實現類,其實內部是通過rpc方式來進行遠程調用的代理對象,至於Client Run-time Library,則是實現遠程調用的工具包,比如python的socket模塊,最后通過底層網絡實現實現數據的傳輸。
2.2 python實現RPC
# 客戶端 import rpyc # 參數主要是host, port conn = rpyc.connect('localhost', 9999) # test是服務端的那個以"exposed_"開頭的方法 print('start') for i in range(100): cResult = conn.root.cal(i) print(cResult) print('end') conn.close() # 服務端 from rpyc import Service from rpyc.utils.server import ThreadedServer class TestService(Service): # 對於服務端來說, 只有以"exposed_"打頭的方法才能被客戶端調用,所以要提供給客戶端的方法都得加"exposed_" def exposed_cal(self, num): return num*2 sr = ThreadedServer(TestService, port=9999, auto_register=False) sr.start()
2.3 GRPC框架
目前流行的開源 RPC 框架還是比較多的,有阿里巴巴的 Dubbo、Facebook 的 Thrift、Google 的 gRPC、Twitter 的 Finagle 等。
gRPC:是 Google 公布的開源軟件,基於最新的 HTTP 2.0 協議,並支持常見的眾多編程語言。RPC 框架是基於 HTTP 協議實現的,底層使用到了 Netty 框架的支持。
Thrift:是 Facebook 的開源 RPC 框架,主要是一個跨語言的服務開發框架。用戶只要在其之上進行二次開發就行,應用對於底層的 RPC 通訊等都是透明的。不過這個對於用戶來說需要學習特定領域語言這個特性,還是有一定成本的。
Dubbo:是阿里集團開源的一個極為出名的 RPC 框架,在很多互聯網公司和企業應用中廣泛使用。協議和序列化框架都可以插拔是極其鮮明的特色。
以使用較為廣泛的gRPC為例學習下RPC框架的使用
gRPC 是 Google 開放的一款 RPC (Remote Procedure Call) 框架,建立在 HTTP2 之上,使用 Protocol Buffers。
2.3.1 Protocol Buffers 簡介
protocol buffers 是 Google 公司開發的一種數據描述語言,采用簡單的二進制格式,比 XML、JSON 格式體積更小,編解碼效率更高。用於數據存儲、通信協議等方面。
通過一個 .proto
文件,你可以定義你的數據的結構,並生成基於各種語言的代碼。目前支持的語言很多,有 Python、golang、js、java 等等。
2.3.2 gRPC 簡介
有了 protocol buffers 之后,Google 進一步推出了 gRPC。通過 gRPC,我們可以在 .proto
文件中也一並定義好 service,讓遠端使用的 client 可以如同調用本地的 library 一樣使用。

可以看到 gRPC Server 是由 C++ 寫的,Client 則分別是 Java 以及 Ruby,Server 跟 Client 端則是通過 protocol buffers 來信息傳遞。
1. 定義功能函數
calculate.py
# -*- coding: utf-8 -*- import math # 求平方 def square(x): return math.sqrt(x)
2. 創建 .proto 文件
在這里描述我們要使用的 message 以及 service
syntax = "proto3"; message Number { float value = 1; } service Calculate { rpc Square(Number) returns (Number) {} }
3. 生成 gRPC 類
這部分可能是整個過程中最“黑盒子”的部分。我們將使用特殊工具自動生成類。
$ pip install grpcio grpcio-tools
$ python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. calculate.proto
你會看到生成來兩個文件:
- calculate_pb2.py — 包含 message(calculate_pb2.Number)
- calculate_pb2_grpc.py — 包含 server(calculate_pb2_grpc.CalculatorServicer) and client(calculate_pb2_grpc.CalculatorStub)
4. 創建 gRPC 服務端
server.py
# -*- coding: utf-8 -*- import grpc import calculate_pb2 import calculate_pb2_grpc import calculate from concurrent import futures import time # 創建一個 CalculateServicer 繼承自 calculate_pb2_grpc.CalculateServicer class CalculateServicer(calculate_pb2_grpc.CalculateServicer): def Square(self, request, context): response = calculate_pb2.Number() response.value = calculate.square(request.value) return response # 創建一個 gRPC server server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) # 利用 add_CalculateServicer_to_server 這個方法把上面定義的 CalculateServicer 加到 server 中 calculate_pb2_grpc.add_CalculateServicer_to_server(CalculateServicer(), server) # 讓 server 跑在 port 50051 中 print 'Starting server. Listening on port 50051.' server.add_insecure_port('[::]:50051') server.start() # 因為 server.start() 不會阻塞,添加睡眠循環以持續服務 try: while True: time.sleep(24 * 60 * 60) except KeyboardInterrupt: server.stop(0)
啟動 gRPC server:
$ python server.py
Starting server. Listening on port 50051.
5. 創建 gRPC 客戶端
client.py
# -*- coding: utf-8 -*- import grpc import calculate_pb2 import calculate_pb2_grpc # 打開 gRPC channel,連接到 localhost:50051 channel = grpc.insecure_channel('localhost:50051') # 創建一個 stub (gRPC client) stub = calculate_pb2_grpc.CalculateStub(channel) # 創建一個有效的請求消息 Number number = calculate_pb2.Number(value=16) # 帶着 Number 去調用 Square response = stub.Square(number) print response.value
啟動 gRPC client:
$ python client.py
4.0
最終的文件結構:

三 總結
RPC 主要用於公司內部的服務調用,性能消耗低,傳輸效率高,實現復雜。
HTTP 主要用於對外的異構環境,瀏覽器接口調用,App 接口調用,第三方接口調用等。
RPC 使用場景(大型的網站,內部子系統較多、接口非常多的情況下適合使用 RPC):
- 長鏈接。不必每次通信都要像 HTTP 一樣去 3 次握手,減少了網絡開銷。
- 注冊發布機制。RPC 框架一般都有注冊中心,有豐富的監控管理;發布、下線接口、動態擴展等,對調用方來說是無感知、統一化的操作。
- 安全性,沒有暴露資源操作。
- 微服務支持。就是最近流行的服務化架構、服務化治理,RPC 框架是一個強力的支撐。
四 RPC沒那么簡單
要實現一個RPC不算難,難的是實現一個高性能高可靠的RPC框架。
比如,既然是分布式了,那么一個服務可能有多個實例,你在調用時,要如何獲取這些實例的地址呢?
這時候就需要一個服務注冊中心,比如在Dubbo里頭,就可以使用Zookeeper作為注冊中心,在調用時,從Zookeeper獲取服務的實例列表,再從中選擇一個進行調用。
那么選哪個調用好呢?這時候就需要負載均衡了,於是你又得考慮如何實現復雜均衡,比如Dubbo就提供了好幾種負載均衡策略。
這還沒完,總不能每次調用時都去注冊中心查詢實例列表吧,這樣效率多低呀,於是又有了緩存,有了緩存,就要考慮緩存的更新問題,blablabla......
你以為就這樣結束了,沒呢,還有這些:
- 客戶端總不能每次調用完都干等着服務端返回數據吧,於是就要支持異步調用;
- 服務端的接口修改了,老的接口還有人在用,怎么辦?總不能讓他們都改了吧?這就需要版本控制了;
- 服務端總不能每次接到請求都馬上啟動一個線程去處理吧?於是就需要線程池;
- 服務端關閉時,還沒處理完的請求怎么辦?是直接結束呢,還是等全部請求處理完再關閉呢?
- ......
如此種種,都是一個優秀的RPC框架需要考慮的問題。