gRPC C++ 入门教程

之前曾经写过 Python 使用 gRPC 收发消息的教程,可以参考文章 《体验 gRPC 那些事儿》。最近计划在 C++ 项目中使用 gRPC,故写一篇文章来记录一下如何使用 C++ 语言来实现一个简单的 gRPC 服务端和客户端程序。

本教程需要先安装 gRPC,有关 gRPC 的安装教程可以参考文章 《CentOS 7 安装 gRPC》《体验 gRPC 那些事儿》

本文涉及的程序包括四部分,分别是客户端源代码 client.cc,服务端源代码 server.cc,proto 文件 mathtest.proto 以及 Makefile。

Makefile

Makefile 的作用是编译源代码,Makefile 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
LDFLAGS = -L/usr/local/lib `pkg-config --libs protobuf grpc++`\
-Wl,--no-as-needed -lgrpc++_reflection -Wl,--as-needed\
-ldl

CXX = g++
CPPFLAGS += `pkg-config --cflags protobuf grpc`
CXXFLAGS += -std=c++11

GRPC_CPP_PLUGIN = grpc_cpp_plugin
GRPC_CPP_PLUGIN_PATH ?= `which $(GRPC_CPP_PLUGIN)`

all: client server

client: mathtest.pb.o mathtest.grpc.pb.o client.o
$(CXX) $^ $(LDFLAGS) -o $@

server: mathtest.pb.o mathtest.grpc.pb.o server.o
$(CXX) $^ $(LDFLAGS) -o $@

%.grpc.pb.cc: %.proto
protoc --grpc_out=. --plugin=protoc-gen-grpc=$(GRPC_CPP_PLUGIN_PATH) $<

%.pb.cc: %.proto
protoc --cpp_out=. $<

clean:
rm -f *.o *.pb.cc *.pb.h client server

执行这个 Makefile 文件,会生成可执行客户端 client 和服务端 server 可执行文件。
客户端和服务端可执行文件都依赖于由 proto 文件生成的源代码,Makefile 在 clean 前面的两个规则的作用即是生成这些代码。

Makefile 文件中出现了一些特殊符号:

  • $@:规则的目标名称,例如上面 Makefile 中代表 clientserver
  • %:匹配字符,例如上面 Makefile 中匹配 mathtest.proto
  • $<:依赖的第一个目标名称,例如上面 Makefile 中的 mathtest.proto
  • $^:依赖的所有目标名称,上面 Makefile 中的 server 规则中,即代表 mathtest.pb.o mathtest.grpc.pb.o server.o

有关 Makefile 的解析,可以参考阮一峰的 《Make 命令教程》

Proto

.proto 文件用来定义客户端和服务端的消息格式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
syntax = "proto3";

option java_package = "ex.grpc";

package mathtest;

// Defines the service
service MathTest {
// Function invoked to send the request
rpc sendRequest (MathRequest) returns (MathReply) {}
}

// The request message containing requested numbers
message MathRequest {
int32 a = 1;
int32 b = 2;
}

// The response message containing response
message MathReply {
int32 result = 1;
}
  • syntax 定义了使用 proto3 的语法(还有 proto2)
  • java_package 用来定义生成 Java 类所在的包
  • package 表示定义消息的包名,对于 C++ 程序,消息类将会被包装在对应的命名空间中,例如,使用 mathtest::MathRequest 来引用 MathRequest 消息
  • service 用来定义在 RPC 调用中的服务接口,这里我们定义了一个服务接口 sendRequest,它接受一个 MathRequest 消息,并返回 MathReply 消息
  • message 用来定义消息的字段,例如,MathRequest 消息包含两个整型字段 abMathReply 包含一个整型字段 result。字段后面的数字并不表示该字段的值,只是 proto 程序用来生成相关源代码使用。

有关 Protocol Buffers 的介绍,可以参考官方文档,https://developers.google.com/protocol-buffers/docs/overview

服务端

服务端代码 server.cc 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 
// server.cc
// Created by leo on 2020/1/31.
//

#include <string>

#include <grpcpp/grpcpp.h>
#include "mathtest.grpc.pb.h"

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

using mathtest::MathTest;
using mathtest::MathRequest;
using mathtest::MathReply;

class MathServiceImplementation final : public MathTest::Service {
Status sendRequest(
ServerContext* context,
const MathRequest* request,
MathReply* reply
) override {
int a = request->a();
int b = request->b();

reply->set_result(a * b);

return Status::OK;
}
};

void Run() {
std::string address("0.0.0.0:5000");
MathServiceImplementation service;

ServerBuilder builder;

builder.AddListeningPort(address, grpc::InsecureServerCredentials());
builder.RegisterService(&service);

std::unique_ptr<Server> server(builder.BuildAndStart());
std::cout << "Server listening on port: " << address << std::endl;

server->Wait();
}

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

return 0;
}

在代码开头,我们引入了 grpc 头文件以及 proto 生成的头文件,然后我们声明了在服务端代码中使用的相应命名空间下的类型。

MathServiceImplementation 实现服务接口 sendRequest(),该接口获取请求消息中的 ab 字段的值,并返回 a * b 的积。MathServiceImplementation 是个实现类,继承于 MathTest::ServiceMathTest::Servicemathtest.grpc.pb.h 声明。

Run() 函数指定了服务地址 0.0.0.0:5000,对 MathServiceImplementation 服务注册后,我们启动了服务端,并接受客户端的请求。

客户端

客户端 client.cc 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
//
// client.cc
// Created by leo on 2020/1/31.
//

#include <string>

#include <grpcpp/grpcpp.h>
#include "mathtest.grpc.pb.h"

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

using mathtest::MathTest;
using mathtest::MathRequest;
using mathtest::MathReply;

class MathTestClient {
public:
MathTestClient(std::shared_ptr<Channel> channel) : stub_(MathTest::NewStub(channel)) {}

int sendRequest(int a, int b) {
MathRequest request;

request.set_a(a);
request.set_b(b);

MathReply reply;

ClientContext context;

Status status = stub_->sendRequest(&context, request, &reply);

if(status.ok()){
return reply.result();
} else {
std::cout << status.error_code() << ": " << status.error_message() << std::endl;
return -1;
}
}

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

void Run() {
std::string address("0.0.0.0:5000");
MathTestClient client(
grpc::CreateChannel(
address,
grpc::InsecureChannelCredentials()
)
);

int response;

int a = 5;
int b = 10;

response = client.sendRequest(a, b);
std::cout << "Answer received: " << a << " * " << b << " = " << response << std::endl;
}

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

return 0;
}

MathTestClient 类提供向服务端发送请求的接口 sendRequest()sendRequest() 接受两个整型参数,然后通过私有成员变量 stub_ 向服务端发送请求消息并接收返回结果。

Run() 函数指定服务地址并初始化 MathTestClient 对象 client,通过对象 client 调用服务接口并获取返回结果。可以看到,在客户端调用服务端的接口时,就像是调用本地接口一样,这也是 RPC 调用的特色。

编译运行

执行命令编译客户端和服务端源代码:

1
make

执行服务端程序:

1
./server

可以看到服务端输出:

Server listening on port: 0.0.0.0:5000

执行客户端程序:

1
./client

可以看到客户端输出:

Answer received: 5 * 10 = 50

如果执行编译过程时报错,可以参考以下处理方法。

解决 pkg-config 报错

上述 Makefile 中使用了 pkg-config 命令,如果执行 make 编译代码时报错,例如:

Package protobuf was not found in the pkg-config search path.
Perhaps you should add the directory containing `protobuf.pc’
to the PKG_CONFIG_PATH environment variable
No package ‘protobuf’ found

如报错提示,这是由于 pkg-config 无法搜到 *pc 文件所在路径导致,可以通过在 ~/.bashrc 文件添加环境变量 PKG_CONFIG_PATH

1
2
PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig
export PKG_CONFIG_PATH

其中,/usr/local/lib/pkgconfigprotobuf.pc 所在路径(可以通过 find 命令搜索到)。
添加 PKG_CONFIG_PATH 环境变量后,执行 source ~/.bashrc ,再执行编译代码,不再报错。

解决 libgrpc++.so 无法找到报错

如果运行时报 libgrpc++.so 库文件无法找到的错误,例如:

./server: error while loading shared libraries: libgrpc++.so.1: cannot open shared object file: No such file or directory

这是由于 gRPC 安装时库文件所在的路径无法被系统找到导致。可以通过增加 ld 搜索路径解决。
/etc/ld.so.conf.d/ 目录增加 grpc.conf 文件,内容是 libgrpc++.so 文件所在的目录(可以通过 find 命令搜索到)。

1
/usr/local/lib/

然后执行命令 ldconfig,再运行程序即可。

参考资料