gRPC是google开发的一个远程过程调用框架,搭配protobuf使用来实现从客户端调用服务器端对应程序的方法。

protobuf

protobuf可以理解为一种传输协议或者传输格式,相比json等网络传输形式,具有更好的效率。gRPC使用这种协议来实现高效的网络传输。

proto

只需要编写proto文件,再使用gRPC提供的工具,就可以直接生成多种语言的客户端和服务端,以及对应的protobuf协议,因此还可以实现跨语言的程序调用。简单的proto文件格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# service.proto

syntax = "proto3";

package message;

service HelloWorld {
rpc sayHello (Request) returns (Response) {}
}

message Request {
string name = 1;
}

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

syntax关键字指定了protobuf的版本,默认版本是proto2,一般指定使用proto3。
package关键字将会生成一个命名空间,用于隔离代码,下面生成的服务和请求格式都会放到message这个命名空间下。
service关键字指明了我们要生成的客户端和服务端的名称,然后内部通过rpc关键字表示将会调用到的函数,这里我们创建的客户端和服务端的RPC服务就叫HelloWorld,里面有一个会被远程调用的函数叫sayHello,入参是一个Request类型,出参是Response类型,这两个类型在下面通过message关键字来定义。

生成客户端和服务端

当客户端HelloWorld调用sayHello时,会通过网络将Request传递到同名的服务端HelloWorld中,然后HelloWorld调用同名的sayHello函数,将执行结果返回。

服务端

在生成好proto文件后,我们要在自己的代码中实例化我们定义的gRPC服务。
查看生成好的源文件能够看到,在对应的类内部会有一个HelloWorld::Service的类,我们需要继承这个类,然后来实现对应的逻辑。

1
2
3
4
5
6
7
8
// 服务的具体实现
class HelloWorldServiceImpl final : public HelloWorld::Service {
Status SayHello(ServerContext* context, const Request* request, Response* reply) override {
std::string prefix("Hello ");
reply->set_message(prefix + request->name());
return Status::OK;
}
};

然后编写一个启动服务实现类的函数,主函数中调用这个函数就可以启动服务了。

1
2
3
4
5
6
7
8
9
10
11
void RunServer() {
std::string server_address("0.0.0.0:50051");
HelloWorldServiceImpl service;

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();
}

首先是设置监听地址和端口,然后通过gRPC中提供的ServerBuilder构建类来构建出一个服务,最后使用服务的Wait函数,服务将进入阻塞等待接收消息。

客户端

同样,我们也可以定义一个自己的客户端类,并在这个类中包含gRPC客户端对应的“存根”,gRPC客户端主要就是通过这个存根来和服务器端进行通信的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class HelloWorldClient {
public:
HelloWorldClient(std::shared_ptr<Channel> channel)
: stub_(HelloWorld::NewStub(channel)) {}

std::string sayHello(const std::string& user) {
Request request;
request.set_name(user);
Response reply;
ClientContext context;
Status status = stub_->sayHello(&context, request, &reply);
if (status.ok()) {
return reply.code();
} else {
std::cout << status.error_code() << ": " << status.error_message() << std::endl;
return "RPC failed";
}
}

private:
std::unique_ptr<HelloWorld::Stub> stub_;
};

关键部分在于HelloWorld::Stub,Channel和ClientContext,HelloWorld::Stub就是我们说的存根,这个是在生成的gRPC源文件中自动生成的,这个类的实现需要一个通道Channel,Channel中可以设置要通信的服务端的地址和端口号。

1
std::shared_ptr<Channel> channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());

在远程调用sayHello时,还需要传入一个叫做客户端上下文的东西,也就是ClientContext,还有表示接口调用状态的Status,都可以在grcpp中找到,引入对应的头文件即可。