什麼樣的RPC才是好用的RPC

晚來風急發表於2017-06-02

現在RPC框架很多,但是真正好用的RPC卻是少之又少。那麼什麼是好用的RPC,什麼是不好用的RPC呢,有一個評判標準嗎?下面是我列舉出來的衡量RPC好用與否的幾條標準:

真的像本地函式一樣呼叫

使用簡單,使用者只需要關注業務即可

靈活,RPC呼叫的序列化方式可以自由定製,比如支援json,支援msgpack等方式

下面來分別解釋這幾條標準。

標準1:真的像本地函式一樣呼叫

RPC的本質是為了遮蔽網路的細節和複雜性,提供易用的api,讓使用者就像呼叫本地函式一樣實現遠端呼叫,所以RPC最重要的就是“像呼叫本地函式一樣”實現遠端呼叫,完全不讓使用者感知到底層的網路。真正好用的RPC介面,他的呼叫形式是和本地函式無差別的,但是本地函式呼叫是靈活多變的。伺服器如果提供和客戶端完全一致的呼叫形式將是非常好用的,這也是RPC框架的一個巨大挑戰

標準2:使用簡單,使用者只需要關注業務即可

RPC的使用簡單直接,非常自然,就是和呼叫本地函式一樣,不需要寫一大堆額外程式碼,使用者只用寫業務邏輯程式碼,而不用關注框架的細節,其他的事情都由RPC框架完成。

標準3:靈活,RPC呼叫的序列化方式可以自由定製

RPC呼叫的資料格式支援多種編解碼方式,比如一些通用的json格式、msgpack格式或者boost.serialization等格式,甚至支援使用者自己定義的格式,這樣使用起來才會更靈活。

RPC框架評估

下面根據這幾個標準來評估一些國內外知名大公司的RPC框架,這些框架的用法在github的wiki中都有使用示例,使用示例程式碼均來自官方提供的例子。

谷歌 gRPC

gRPC最近釋出了1.0版本,他是谷歌公司用c++開發的一個RPC框架,並提供了多種客戶端。

協議定義

先定義一個.proto的檔案,例如

// Obtains the feature at a given position.
    rpc GetFeature(Point) returns (Feature) {}

定義了一個服務介面,接收客戶端傳過來的Point,返回一個Feature,接下來定義protocol buffer的訊息型別,用於序列化/反序列化

message Point {
      int32 latitude = 1;
      int32 longitude = 2;
    }

伺服器程式碼

class RouteGuideImpl final : public RouteGuide::Service {
    Status GetFeature(ServerContext* context, const Point* point, Feature* feature) override {
          feature->set_name(GetFeatureName(*point, feature_list_));
          feature->mutable_location()->CopyFrom(*point);
          return Status::OK;
    }
}

void RunServer(const std::string& db_path) {
  std::string server_address("0.0.0.0:50051");
  RouteGuideImpl service(db_path);

  ServerBuilder builder;
  builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
  builder.RegisterService(&service);
  std::unique_ptr<Server> server(builder.BuildAndStart());
  std::cout << "Server listening on " << server_address << std::endl;
  server->Wait();
}

客戶端程式碼

bool GetOneFeature(const Point& point, Feature* feature) {
    ClientContext context;
    Status status = stub_->GetFeature(&context, point, feature);
    if (!status.ok()) {
      std::cout << "GetFeature rpc failed." << std::endl;
      return false;
    }
    if (!feature->has_location()) {
      std::cout << "Server returns incomplete feature." << std::endl;
      return false;
    }

    return true;
}

評價

gRPC呼叫的序列化用的是protocal buffer,RPC服務介面需要在.proto檔案中定義,使用稍顯繁瑣。根據標準1,gRPC並沒有完全實現像本地呼叫一樣,雖然很接近了,但做不到,原因是RPC介面中必須帶一個Context的引數,並且返回型別必須是Status,這些限制導致gRPC無法做到像本地介面一樣呼叫。
根據標準2,gRPC的使用不算簡單,需要關注諸多細節,比如Context和Status等框架的細節。根據標準3,gRPC只支援pb協議,無法擴充套件支援其他協議。

綜合評價:70分。

百度sofa-pbRPC

sofa-pbRPC是百度用c++開發的一個RPC框架,和gRPC有點類似,也是基於protocal buffer的,需要定義協議。

協議定義

// 定義請求訊息 
message EchoRequest { 
required string message = 1; 
}
// 定義回應訊息
message EchoResponse {
    required string message = 1;
}

/`javascript
/ 定義RPC服務,可包含多個方法(這裡只列出一個)
service EchoServer {

rpc Echo(EchoRequest) returns(EchoResponse);

}

伺服器端程式碼

include // sofa-pbrpc標頭檔案

include “echo_service.pb.h” // service介面定義標頭檔案

class EchoServerImpl : public sofa::pbrpc::test::EchoServer
{
public:

EchoServerImpl() {}
virtual ~EchoServerImpl() {}

private:

virtual void Echo(google::protobuf::RpcController* controller,
                  const sofa::pbrpc::test::EchoRequest* request,
                  sofa::pbrpc::test::EchoResponse* response,
                  google::protobuf::Closure* done)
{
    sofa::pbrpc::RpcController* cntl =
        static_cast<sofa::pbrpc::RpcController*>(controller);
    SLOG(NOTICE, "Echo(): request message from %s: %s",
        cntl->RemoteAddress().c_str(), request->message().c_str());
    response->set_message("echo message: " + request->message());
    done->Run();
}

};

注意:

服務完成後必須呼叫done->Run(),通知RPC系統服務完成,觸發傳送Response;
在調了done->Run()之後,Echo的所有四個引數都不再能訪問;
done-Run()可以分派到其他執行緒中執行,以實現了真正的非同步處理;
客戶端程式碼

int main()
{

SOFA_PBRPC_SET_LOG_LEVEL(NOTICE);

// 定義RpcClient物件,管理RPC的所有資源
// 通常來說,一個client程式只需要一個RpcClient例項
// 可以通過RpcClientOptions指定一些配置引數,譬如執行緒數、流控等
sofa::pbrpc::RpcClientOptions client_options;
client_options.work_thread_num = 8;
sofa::pbrpc::RpcClient rpc_client(client_options);

// 定義RpcChannel物件,代表一個訊息通道,需傳入Server端服務地址
sofa::pbrpc::RpcChannel rpc_channel(&rpc_client, "127.0.0.1:12321");

// 定義EchoServer服務的樁物件EchoServer_Stub,使用上面定義的訊息通道傳輸資料
sofa::pbrpc::test::EchoServer_Stub stub(&rpc_channel);

// 定義和填充呼叫方法的請求訊息
sofa::pbrpc::test::EchoRequest request;
request.set_message("Hello world!");

// 定義方法的回應訊息,會在呼叫返回後被填充
sofa::pbrpc::test::EchoResponse response;

// 定義RpcController物件,用於控制本次呼叫
// 可以設定超時時間、壓縮方式等;預設超時時間為10秒,預設壓縮方式為無壓縮
sofa::pbrpc::RpcController controller;
controller.SetTimeout(3000);

// 發起呼叫,最後一個引數為NULL表示為同步呼叫
stub.Echo(&controller, &request, &response, NULL);

// 呼叫完成後,檢查是否失敗
if (controller.Failed()) {
    // 呼叫失敗後的錯誤處理,譬如可以進行重試
    SLOG(ERROR, "request failed: %s", controller.ErrorText().c_str());
}

return EXIT_SUCCESS;

}

評價

sofa-pbRPC的使用並沒有像sofa這個名字那樣sofa,根據標準1,服務端的RPC介面比gRPC更加複雜,更加遠離本地呼叫了。根據標準2,使用者要做很多額外的事,需要關注框架的很多細節,比較難用。根據標準3,同樣只支援pb協議,無法支援其他協議。

綜合評價:62分。

騰訊Pebble

騰訊開源的Pebble也是基於protocal buffer的,不過他的用法比gRPC和sofaRPC更好用,思路都是類似的,先定義協議。

協議定義

struct HeartBeatInfo {
1: i64 id,
2: i32 version = 1,
3: string address,
4: optional string comment,
}

service BaseService {

i64 heartbeat(1:i64 id, 2:HeartBeatInfo data),

oneway void log(1: string content)

}

伺服器端程式碼

class BaseServiceHandler : public BaseServiceCobSvIf {
public:

void log(const std::string& content) {
    std::cout << "receive request : log(" << content << ")" << std::endl;
}

};

int main(int argc, char* argv[]) {

// 初始化RPC
pebble::rpc::Rpc* rpc = pebble::rpc::Rpc::Instance();
rpc->Init("", 0, "");

// 註冊服務
BaseServiceHandler base_service;
rpc->RegisterService(&base_service);

// 配置服務監聽地址
std::string listen_addr("tcp://127.0.0.1:");
if (argc > 1) {
    listen_addr.append(argv[1]);
} else {
    listen_addr.append("8200");
}

// 新增服務監聽地址
rpc->AddServiceManner(listen_addr, pebble::rpc::PROTOCOL_BINARY);

// 啟動server
rpc->Serve();

return 0;

}
客戶端程式碼

// 初始化RPC
pebble::rpc::Rpc* rpc = pebble::rpc::Rpc::Instance();
rpc->Init(“”, -1, “”);

// 建立rpc client stub
BaseServiceClient client(service_url, pebble::rpc::PROTOCOL_BINARY);

// 同步呼叫
int ret = client.log(“pebble simple test : log”);
std::cout << “sync call, ret = ” << ret << std::endl;

評價

Pebble比gRPC和sofa-pbrpc更好用,根據標準1,呼叫方式和本地呼叫一致了,介面中沒有任何限制。根據標準2,除了定義協議稍顯繁瑣之外已經比較易用了,不過伺服器在使用上還是有一些限制,比如註冊服務的時候只能註冊一個類物件的指標,不能支援lambda表示式,std::function或者普通的function。根據標準3,gRPC只支援pb協議,無法擴充套件支援其他協議。

綜合評價:75分。


apache msgpack-RPC


msgpack-RPC是基於msgpack定義的RPC框架,不同於基於pb的RPC,他無需定義專門的協議。

伺服器端程式碼

include

class myserver : public msgpack::rpc::server::base {
public:

void add(msgpack::rpc::request req, int a1, int a2)
{
    req.result(a1 + a2);
}

public:

void dispatch(msgpack::rpc::request req)
try {
    std::string method;
    req.method().convert(&method);

    if(method == "add") {
        msgpack::type::tuple<int, int> params;
        req.params().convert(&params);
        add(req, params.get<0>(), params.get<1>());

    } else {
        req.error(msgpack::rpc::NO_METHOD_ERROR);
    }

} catch (msgpack::type_error& e) {
    req.error(msgpack::rpc::ARGUMENT_ERROR);
    return;

} catch (std::exception& e) {
    req.error(std::string(e.what()));
    return;
}

};

客戶端程式碼

include

include

int main(void)
{

msgpack::rpc::client c("127.0.0.1", 9090);
int result = c.call("add", 1, 2).get<int>();
std::cout << result << std::endl;

}

評價

msgpack-RPC使用起來也很簡單,不需要定義proto檔案,根據標準1,客戶端的呼叫和本地呼叫一致,不過,伺服器的RPC介面有一個msgpack::rpc::request物件,並且也必須派生於base類,使用上有一定的限制。根據標準2,伺服器端提供RPC服務的時候需要根據method的名字來dispatch,這種方式不符合開閉原則,使用起來有些不方便。根據標準3,msgpack-rpc只支援msgpack的序列化,不能支援其他的序列化方式。

綜合評價:80分。

總結

目前雖然國內外各大公司都推出了自己的RPC框架,但是真正好用易用的RPC框架卻是不多的,這裡對各個廠商的RPC框架僅從好用的角度做一個評價,一家之言,僅供參考,希望可以為大家做RPC的技術選型的時候提供一些評判依據。


**文章轉載自 開源中國社群[http://www.oschina.net]**


相關文章