Learning-gRPC

About google protobuf

這邊主要探討關於 protobuf 的格式與其對應的意義

我們就以下列這個程式碼來做探討:

syntax = "proto2";

package tutorial;

message Person {
  required string name = 1;
  required int32 id = 2;
  optional string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    required string number = 1;
    optional PhoneType type = 2 [default = HOME];
  }

  repeated PhoneNumber phones = 4;
}

message AddressBook {
  repeated Person people = 1;
}

Package

  • Protobuf 以 package 作為開始,透過 package 指定名稱的方式來防止命名衝突的問題

  • 而在 python 的 grpc 版本當中, package 則由 directory structure 來做決定; 所以在當 python 版本運行時,由 proto 定義的 package 則會失去作用

Message

先來看看使用 message 作為定義關鍵字的使用:

Field Type

  • Protobuf 也定義許多 type 的關鍵字:

    • bool
    • int32
    • float
    • double
    • string
  • 除了使用原生的定義關鍵字外,也可以用其他定義好的 message type 作為 field type 於其他 message type 的物件內使用

    • 舉例來說,像是上方 example code 中 Person message 當中包含的 PhoneNumber messages; 並且在 message AddressBook 當中使用 Person 作為 field type 使用!
  • enum:

    • 讓使用者能夠從中多個選項一個作選擇
    • 詳細可以看 example code 的 PhoneType,於 Person 當中做 enum 定義出 PhoneType 後, 在後面的 PhoneNumber 內做使用!

=1, =2 ...

  • 在上面範例 code 當中可以看到每個 attribute 都被賦予一個等號數值,這裡並不是單純的附值! 而是於 protobuf 中特殊的使用!

  • 這些數字標記用來給予一個獨一無二的 "tag" 給這個 field, 作為後續 binary encoding 使用

    • 而 range 在 1~15 中的 tag,則會比其他大於 15 的數值所使用的 bytes 上會小 1 個 byte
    • 可以視其為一個 optimization -> 當這個欄位會被 重複使用(e.g. repeated), 或是經常被使用 的話, 則可以利用這個 tag 的特性,藉由賦予其較小數值的 tag,來減少其使用的 bytes 數量

Annotation

  • 每個 field 在除了定義其 field type 之外,也會定義他的 annotation:
    • required
      • 代表這個欄位的數值必須一定要被提供,否則該 message 則會被認為是 uninitialized
      • 對一個 uninitialized 的 message 做 serializing 會發生 exception; 而對其做 parsing 的動作則會發生 fail; 除此之外基本上行為與 optional 相同
    • optional
      • 可以選擇性做賦值的動作; 若一個 optional 的欄位沒有被賦值,則系統會使用其預設值
    • repeated
      • 這個欄位的數值可以重複多次,當然也包括 0 次(e.g. 空的)
      • 可以想像他是一個 dynamic sized 的 array

Service

在來看到 service 的使用

當你想要透過 RPC (Remote Procedure Call) 來使用你所定義的 message 時,則可以透過 service 這個定義字來建立一個 RPC 服務介面於 .proto 當中

剩餘的部份則透過 protobuf compiler 來產生 interface 相依的程式碼(你所選擇的程式)及 stub

舉例,我們定義一個 RPC Service,接收 SearchRequest 後,並將結果 - SearchResponse 做回傳(return):

service SearchService {
  rpc Search (SearchRequest) returns (SearchResponse);
}

則透過 protobuf 編譯後,則會幫忙產生 SearchService 的抽象介面以及對應的 stub 的實作 而這個 stub 則會把所有 call forward 到一個 RpcChannel 當中,其為一個抽象介面,必須透過使用者自己的 RPC 系統程式來做定義,可見(以 C++ 為範例):

Client 端的部份:

using google::protobuf;

protobuf::RpcChannel* channel;
protobuf::RpcController* controller;
SearchService* service;
SearchRequest request;
SearchResponse response;

void DoSearch() {
  // You provide classes MyRpcChannel and MyRpcController, which implement
  // the abstract interfaces protobuf::RpcChannel and protobuf::RpcController.
  channel = new MyRpcChannel("somehost.example.com:1234");
  controller = new MyRpcController;

  // The protocol compiler generates the SearchService class based on the
  // definition given above.
  service = new SearchService::Stub(channel);

  // Set up the request.
  request.set_query("protocol buffers");

  // Execute the RPC.
  service->Search(controller, request, response, protobuf::NewCallback(&Done));
}

void Done() {
  delete service;
  delete channel;
  delete controller;
}

而 server 端可以這麼做:

using google::protobuf;

class ExampleSearchService : public SearchService {
 public:
  void Search(protobuf::RpcController* controller,
              const SearchRequest* request,
              SearchResponse* response,
              protobuf::Closure* done) {
    if (request->query() == "google") {
      response->add_result()->set_url("http://www.google.com");
    } else if (request->query() == "protocol buffers") {
      response->add_result()->set_url("http://protobuf.googlecode.com");
    }
    done->Run();
  }
};

int main() {
  // You provide class MyRpcServer.  It does not have to implement any
  // particular interface; this is just an example.
  MyRpcServer server;

  protobuf::Service* service = new ExampleSearchService;
  server.ExportOnPort(1234, service);
  server.Run();

  delete service;
  return 0;
}

小結

大致上可以從上面看出 grpc 以及 pb 之間的關係!

grpc 透過 pb 的特性,實作這些關鍵字作為其生成相依的來源,並為使用者實作 RPC 的介面,以及處理底層網路傳輸方面的程式碼; 讓使用者只需要著重在如何使用即可!

Reference

01-HelloWorld 教學

開始

安裝 gRPC

  • 在使用這篇教學前,需要安裝 gRPC 於你的系統上 依照以下的指示來做操作:安裝說明

說明

  • 本章教學主要使用來自 grpc 官方教學中的 helloworld 教學
  • 說明原始碼位置於專案 grpc-practice/01-HelloWorld 底下
  • 而本章主要著重在:
    • 如何編譯 protocol buffer
    • 如何使用 pb 編譯後產生的檔案(也是就是 grpc 的主要功能)

使用

# 產生 grpc 所有程式碼
$ make

# 清理所有 grpc 編譯產生 object 檔案
$ make clean

# 產生 document(透過轉換 markdown 成 static web)
$ make doc_page

# 清除 document 檔案
$ make clean_doc

gRPC client/server

編譯 protocol buffer code

  • 透過由 protoc 這個 protocol buffer compiler,並且利用他來編譯產生 .pb.cc.pb.h.grpc.pb.cc.grpc.pb.h 的相依性檔案
  • 分別透過兩次編譯,來產生:
    • 針對 service 定義所產生的: *.grpc.pb.*
    • 針對 message 定義所產生的: *.pb.*
# 第一次編譯
$ protoc -I path_to_your_protos --grpc_out=. --plugin=protoc-gen-grpc=path_to_your_cpp_plugin yourfile.proto

# 第二次編譯
$ protoc -I path_to_your_protos --cpp_out=. yourfile.proto 
  • 第一次編譯時,會產生對應的 *.grpc.pb.*,同時也是當初在 yourfile.proto 裡頭定義的 service class

    • 這支檔案內會需要 *.pb.* 的定義,因此需要第二次編譯
    • path_to_your_protos: 這邊填入你 proto files 放置的位置
    • --grpc_out=<path>: 這邊指定輸出的檔案位置
    • --plugin=...: 這邊是指定轉換的格式,在這邊使用到的是 cpp,可以在 terminal 輸入 which grpc_cpp_plugin 來找到 cpp plugin 在你的開發環境的位置
    • 最後就是要轉換的那支 .proto
  • 第二次編譯,會產生 *.pb.*,同時也是當初 yourfile.proto 裡頭定義的 message 的部份

    • 這支檔案則是主要提供這些方便功能的主要程式碼
  • 到此為止,我們便成功的使用原有 protocol buffer 檔案轉換成為 c++ 可以使用的程式碼

實作 gRPC 的 client

Client 的部份可以看到,在 header files 的部份需要 include 來源 .proto 所產生的相依檔案 - helloworld.grpc.pb.h, 以及 C++ 的 grpc core library - grpc++/grpc++.h

再來就可以引入 .proto 內的 message, services 做使用!這邊可以看到程式所定義的 namespace 有 service 的 Greeter, 以及 message 的 HelloRequest 以及 HelloReply

有了這些 class, 使用者便可以利用這些來做一個封裝 - GreeterClient 的 class 產生! 如此一來我們便可以利用這些 grpc 幫我們產生好的東西來做 RPC 傳輸的動作(在這個 scenario 當中,是 client 與 server 間相互傳遞一個問候語)

詳細對應的操作可以看原始碼 01-HelloWorld 中的 grpc_client.cpp.

實作 gRPC 的 server

Server 的部份在 include 的部份也和 client 相同

而在 server 的部份,則是單純做一個 listening 的動作,透過 grpc 提供的 ServerBuilder 來建立 service (一樣繼承 .proto 所實作的 service - Greeter 來做新的實作), 並把這個 service 加入到 server 的 class 當中(由 grpc 提供),完成後即可等待使用者的呼叫

這邊可以看到,在 client 與 server 中 message class 對於 access 自己的 attribute 的 method 則是以 set_ 加上 attribute name 做命名

小結

grpc 的使用,主要還是透過其本身提供的 library 做實作,所以在使用前需要詳細閱讀其規格與定義!

不過可以從這個例子看到,在實作一個 RPC 服務來說, grpc 已經為我們做了大部份底層的功夫,剩餘的部份只需要我們去寫我們要的規格(protocol)以及 client, server 的主架構即可使用!

Top

Created by @ToolBuddy/papoGen(papercss)