RPC 系列(一):RPC 与 gRPC 全景

在分布式系统开发中,进程之间经常需要通过网络进行远程通信,RPC 就是让这一过程像调用本地函数一样简单的技术。本文先梳理 RPC 的基本原理,再对 gRPC 框架做整体介绍,最后演示如何编译 gRPC 源码,并用 C++ 跑通一个简单的登录/登出示例。

RPC 远程过程调用

RPC(Remote Procedure Call Protocol)——远程过程调用协议,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC 协议假定某些传输协议的存在,如 TCP 或 UDP,为通信程序之间携带信息数据。在 OSI 网络通信模型中,RPC 跨越了传输层和应用层。RPC 使得开发包括网络分布式多程序在内的应用程序更加容易。

RPC 采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息的到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息。最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。

RPC 远程过程调用原理:客户端调用者经 stub 把调用转化为消息,通过网络发送到服务端,skeleton 把消息解析为本地方法调用,提供者执行方法后,结果再原路逐层返回给调用者

gRPC 简介

gRPC 一开始由 Google 开发,是一款语言中立、平台中立、开源的远程过程调用(RPC)系统。

gRPC 是一个高性能、开源和通用的 RPC 框架,面向移动和 HTTP/2 设计。目前提供 C、Java 和 Go 语言版本,分别是:grpc、grpc-java、grpc-go。其中 C 版本支持 C++、Node.js、Python、Ruby、Objective-C、PHP 和 C# 等语言。

gRPC 官方支持的语言一览:C++、Java、Python、Go、Ruby、C#、Node.js、Android Java、Objective-C、PHP、Dart,每种语言都提供快速入门、基础教程和 API 参考

gRPC 调用模型:C++ 编写的 gRPC Server 可以同时与 Ruby 客户端、Android-Java 客户端通信,双方通过各自的 gRPC Stub 以 Proto Request / Proto Response 交换数据

gRPC 默认使用 protocol buffers,这是 Google 开源的一套成熟的结构数据序列化机制(当然也可以使用其他数据格式如 JSON)。

源码编译

$ git clone https://github.com/grpc/grpc.git
$ cd grpc
$ git submodule update --init
$ make
$ sudo make install

编译测试

下面以 C++ 为测试语言,实现一个简单的登录/登出服务。

先编写 proto 文件 Login.proto:

// Login.proto
syntax="proto3";

package rs;

message Response{
    int32 code = 1;
    string errmsg = 2;
}

message LoginRequest{
    string username = 1;
    string password = 2;
}

message LoginResponse{
    int32 id = 1;
    string username = 2;
    string token = 3;
}

message LogoutRequest{
    int32 id = 1;
    string username = 2;
    string token = 3;
}

service LoginService{
    rpc Login(LoginRequest) returns (LoginResponse) {}
    rpc Logout(LogoutRequest) returns (Response) {}
}

编译 proto 文件,生成 protobuf 与 gRPC 的 C++ 代码:

$ protoc -Iproto --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` rs/Login.proto --grpc_out=cpp

$ protoc -Iproto rs/Login.proto --cpp_out=cpp

服务端代码:

#include <grpcpp/grpcpp.h>
#include "rs/Login.grpc.pb.h"

using namespace rs;
using grpc::Server;
using grpc::ServerBuilder;
using grpc::ServerContext;
using grpc::Status;

using rs::Response;
using rs::LoginRequest;
using rs::LoginResponse;
using rs::LogoutRequest;

class LoginServiceImpl : public LoginService::Service{
public:
    LoginServiceImpl() {}
    ~LoginServiceImpl() {}
    virtual ::grpc::Status Login(ServerContext* context, const LoginRequest* request, LoginResponse* response){
        response->set_id(1);
        response->set_username(request->username());
        response->set_token("ABCDEFGHIJKLMNOPQRSTUVWXYZ123456");
        return Status::OK;
    }

    virtual ::grpc::Status Logout(ServerContext* context, const LogoutRequest* request, Response* response){
        response->set_code(200);
        response->set_errmsg("OK");
        return Status::OK;
    }
};

void RunServer() {
    std::string server_address("0.0.0.0:12345");
    LoginServiceImpl service;

    ServerBuilder builder;
    // Listen on the given address without any authentication mechanism.
    builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());
    // Register "service" as the instance through which we'll communicate with
    // clients. In this case it corresponds to an *synchronous* service.
    builder.RegisterService(&service);
    // Finally assemble the server.
    std::unique_ptr<Server> server(builder.BuildAndStart());
    std::cout << "Server listening on " << server_address << std::endl;

    // Wait for the server to shutdown. Note that some other thread must be
    // responsible for shutting down the server for this call to ever return.
    server->Wait();
}

int main(int argc, char** argv) {
    RunServer();

    return 0;
}

编译服务端:

$ g++ -g -Wall -std=c++11 server.cpp rs/Login.grpc.pb.cc rs/Login.pb.cc -I. -o server -lprotobuf -lgrpc++

客户端代码:

#include <grpcpp/grpcpp.h>
#include "rs/Login.grpc.pb.h"

using grpc::Channel;
using grpc::ClientContext;
using grpc::Status;

using namespace rs;

int main(int argc, char* argv[]){
    std::string srvaddr = "127.0.0.1:12345";
    std::shared_ptr<Channel> channel = grpc::CreateChannel(srvaddr, grpc::InsecureChannelCredentials());

    std::unique_ptr<LoginService::Stub> stub = LoginService::NewStub(channel);

    ClientContext ctx;
    LoginRequest req;
    req.set_username("hw");
    req.set_password("admin123");
    LoginResponse res;
    Status status = stub->Login(&ctx, req, &res);
    if (status.ok()){
        req.PrintDebugString();
        res.PrintDebugString();
    }else{
        printf("{errcode:%d,errmsg:%s,detail:%s}", status.error_code(),
            status.error_message().c_str(), status.error_details().c_str());
    }

    LogoutRequest reqLogout;
    reqLogout.set_id(res.id());
    reqLogout.set_username(res.username());
    reqLogout.set_token(res.token());
    Response resLogout;
    ClientContext ctx2;
    status = stub->Logout(&ctx2, reqLogout, &resLogout);
    if (status.ok()){
        resLogout.PrintDebugString();
    }else{
        printf("{errcode:%d,errmsg:%s,detail:%s}", status.error_code(),
            status.error_message().c_str(), status.error_details().c_str());
    }

    return 0;
}

编译客户端:

$ g++ -g -Wall -std=c++11 client.cpp rs/Login.grpc.pb.cc rs/Login.pb.cc -I. -o client -lprotobuf -lgrpc++

系列导航


   转载规则


《RPC 系列(一):RPC 与 gRPC 全景》 由 吴杭沉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
RPC 系列(二):Go gRPC 环境安装 RPC 系列(二):Go gRPC 环境安装
在开始编写 gRPC 应用之前,需要先安装 protoc 编译器以及 Go 语言相关的依赖包。本篇介绍环境搭建的完整过程。 安装 protoc根据自身电脑的操作系统,选择 protoc 最新的 releases 版本下载。 解压后在 bi
2023-09-12
下一篇 
RPC 系列导读:从 gRPC 实战到 brpc 内核的完整路径 RPC 系列导读:从 gRPC 实战到 brpc 内核的完整路径
这个系列把 RPC 的知识整理成了一条由浅入深的完整路径:从「RPC 与 gRPC 全景」的第一问开始,用一套 Go gRPC 教程跑通四种通信形态与超时、认证,再进入 gRPC 的高级专题,随后以 brpc 为主线深入一个工业级 RPC
2023-09-04
  目录