一文帶你搞懂RPC

uu365發表於2021-05-31

  1. RPC是什麼

  RPC可以分為兩部分:使用者呼叫介面 + 具體網路協議。前者為開發者需要關心的,後者由框架來實現。

  舉個例子,我們定義一個函式,我們希望函式如果輸入為“Hello World”的話,輸出給一個“OK”,那麼這個函式是個本地呼叫。如果一個遠端服務收到“Hello World”可以給我們返回一個“OK”,那麼這是一個遠端呼叫。我們會和服務約定好遠端呼叫的函式名。因此,我們的使用者介面就是:輸入、輸出、遠端函式名,比如用 SRPC 開發的話,client端的程式碼會長這樣:

  int main()

  {

  Example::SRPCClient client(IP, PORT);

  EchoRequest req; // 使用者自定義的請求結構

  EchoResponse resp; // 使用者自定義的回覆結構

  req.set_message("Hello World");

  client.Echo(&req, &resp, NULL); // 呼叫遠端函式名為Echo

  return 0;

  }

  具體網路協議,是框架來實現的,把開發者要發出和接收的內容以某種應用層協議打包進行網路收發。這裡可以和HTTP進行一個明顯的對比:

  HTTP也是一種網路協議,但包的內容是固定的,必須是:請求行 + 請求頭 + 請求體;

  RPC是一種自定義網路協議,由具體框架來定,比如SRPC裡支援的RPC協議有:SRPC/thrift/BRPC/tRPC

  這些RPC協議都和HTTP平行,是應用層協議。我們再進一步思考,HTTP只包含具體網路協議,也可以返回比如我們常見的HTTP/1.1 200 OK,但彷彿沒有使用者呼叫介面,這是為什麼呢?

  這裡需要搞清楚,使用者介面的功能是什麼?最重要的功能有兩個:

  定位要呼叫的服務;

  讓我們的訊息向前/向後相容;

  我們用一個表格來看一下HTTP和RPC分別是怎麼解決的:

  定位要呼叫的服務 訊息前後相容

  HTTP URL 開發者自行在訊息體裡解決

  RPC 指定Service和Method名 交給具體IDL

  因此,HTTP的呼叫減少了使用者呼叫介面的函式,但是犧牲了一部分訊息向前/向後相容的自由度。但是,開發者可以根據自己的習慣進行技術選型,因為RPC和HTTP之間大部分都是協議互通的!是不是很神奇?接下來我們看一下RPC的層次架構,就可以明白為什麼不同RPC框架之間、以及RPC和HTTP協議是如何做到互通的。

  2. RPC有什麼

  我們可以從SRPC的架構層次上來看,RPC框架有哪些層,以及SRPC目前所橫向支援的功能是什麼:

  使用者程式碼(client的傳送函式/server的函式實現)

  IDL序列化(protobuf/thrift serialization)

  資料組織 (protobuf/thrift/json)

  壓縮(none/gzip/zlib/snappy/lz4)

  協議 (Sogou-std/Baidu-std/Thrift-framed/TRPC)

  通訊 (TCP/HTTP)

  我們先關注以下三個層級:

  如圖從左到右,是使用者接觸得最多到最少的層次。IDL層會根據開發者定義的請求/回覆結構進行程式碼生成,目前小夥伴們用得比較多的是protobuf和thrift,而剛才說到的使用者介面和前後相容問題,都是IDL層來解決的。SRPC對於這兩個IDL的使用者介面實現方式是:

  thrift:IDL純手工解析,使用者使用srpc是不需要鏈thrift的庫的 !!!

  protobuf:service的定義部分純手工解析

  中間那列是具體的網路協議,而各RPC能互通,就是因為大家實現了對方的“語言”,因此可以協議互通。

  而RPC作為和HTTP並列的層次,第二列和第三列理論上是可以兩兩結合的,只需要第二列的具體RPC協議在傳送時,把HTTP相關的內容進行特化,不要按照自己的協議去發,而按照HTTP需要的形式去發,就可以實現RPC與HTTP互通。

  3. RPC的生命週期

  到此我們可以透過SRPC看一下,把request透過method傳送出去並處理response再回來的整件事情是怎麼做的:

  根據上圖,可以更清楚地看到剛才提及的各個層級,其中壓縮層、序列化層、協議層其實是互相解耦打通的,在SRPC程式碼上實現得非常統一,橫向增加任何一種壓縮演算法或IDL或協議都不需要也不應該改動現有的程式碼,才是一個精美的架構~

  我們一直在說生成程式碼,到底有什麼用呢?圖中可以得知,生成程式碼是銜接使用者呼叫介面和框架程式碼的橋樑,這裡以一個最簡單的protobuf自定義協議為例:example.proto

  syntax = "proto3";

  message EchoRequest

  {

  string message = 1;

  };

  message EchoResponse

  {

  string message = 1;

  };

  service Example

  {

  rpc Echo(EchoRequest) returns (EchoResponse);

  };

  我們定義好了請求、回覆、遠端服務的函式名,透過以下命令就可以生成出介面程式碼example.srpc.h:

  protoc example.proto --cpp_out=./ --proto_path=./

  srpc_generator protobuf ./example.proto ./

  我們一窺究竟,看看生成程式碼到底可以實現什麼功能:

  // SERVER程式碼

  class Service : public srpc::RPCService

  {

  public:

  // 使用者需要自行派生實現這個函式,與剛才pb生成的是對應的

  virtual void Echo(EchoRequest *request, EchoResponse *response,

  srpc::RPCContext *ctx) = 0;

  };

  // CLIENT程式碼

  using EchoDone = std::function<void (echoresponse *, srpc::rpccontext *)>;

  class SRPCClient : public srpc::SRPCClient

  {

  public:

  // 非同步介面

  void Echo(const EchoRequest *req, EchoDone done);

  // 同步介面

  void Echo(const EchoRequest *req, EchoResponse *resp, srpc::RPCSyncContext *sync_ctx);

  // 半同步介面

  WFFuture<std::pair<echoresponse, srpc::rpcsynccontext>> async_Echo(const EchoRequest *req);

  };

  作為一個高效能RPC框架,SRPC生成的client程式碼中包括了:同步、半同步、非同步介面,文章開頭展示的是一個同步介面的做法。

  而server的介面就更簡單了,作為一個服務端,我們要做的就是收到請求->處理邏輯->返回回覆,而這個時候,框架已經把剛才提到的網路收發、解壓縮、反序列化等都給做好了,然後透過生成程式碼呼叫到使用者實現的派生service類的函式邏輯中。

  由於一種協議定義了一種client/server,因此其實我們同樣可以得到的server型別有第二部分提到過的若干種:

  SRPCServer

  SRPCHttpServer

  BRPCServer

  TRPCServer

  ThriftServer

  ...

  4. 一個完整的server例子

  最後我們用一個完整的 server 例子,來看一下使用者呼叫介面的使用方式,以及如何跨協議使用HTTP作為client進行呼叫。剛才提到,srpc_generator 在生成介面的同時,也會自動生成空的使用者程式碼,我們這裡開啟 server.pb_skeleton.cc 直接改兩行,即可 run 起來:

  #include "example.srpc.h"

  #include "workflow/WFFacilities.h"

  using namespace srpc;

  static WFFacilities::WaitGroup wait_group(1);

  void sig_handler(int signo)

  {

  wait_group.done();

  }

  class ExampleServiceImpl : public Example::Service

  {

  public:

  void Echo(EchoRequest *request, EchoResponse *response, srpc::RPCContext *ctx) override

  {

  response->set_message("OK"); // 具體邏輯在這裡新增,我們簡單地回覆一個OK

  }

  };

  int main()

  {

  unsigned short port = 80; // 因為要啟動Http服務

  SRPCHttpServer server; // 我們需要構造一個SRPCHttpServer

  ExampleServiceImpl example_impl;

  server.add_service(&example_impl);

  server.start(port);

  wait_group.wait();

  server.stop();

  return 0;

  }

  只要安裝了srpc,linux下即可透過以下命令編譯出可執行檔案:

  g++ -o server server.pb_skeleton.cc example.pb.cc -std=c++11 -lsrpc

  接下來是激動人心的時刻了,我們用人手一個的curl來發起一個HTTP請求:

  $ curl -i 127.0.0.1:80/Example/Echo -H 'Content-Type: application/json' -d '{message:"Hello World"}'

  HTTP/1.1 200 OK

  SRPC-Status: 1

  SRPC-Error: 0

  Content-Type: application/json

  Content-Encoding: identity

  Content-Length: 16

  Connection: Keep-Alive

  {"message":"OK"}

  5. 總結

  今天我們基於 C++ 實現的開源專案 SRPC 深入分析了 RPC 的基本原理。SRPC 整體程式碼風格簡潔、架構層次精巧,整體約1萬行程式碼,如果你使用 C++,那可能非常適合你用來學習 RPC 架構。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69997029/viewspace-2774726/,如需轉載,請註明出處,否則將追究法律責任。

相關文章