gRPC远程过程调用框架C++/Python使用教程

官网 https://grpc.io/
中文文档 http://doc.oschina.net/grpc/

环境搭建

安装依赖

1
apt install -y build-essential autoconf libtool pkg-config cmake

Clone gRPC repo

1
git clone --recurse-submodules -b v1.56.0 --depth 1 --shallow-submodules https://github.com/grpc/grpc

C++ 构建并安装gRPC和Protocol Buffers

1
2
3
4
5
6
7
8
9
10
11
12
13
export MY_INSTALL_DIR=$HOME/.local
mkdir -p $MY_INSTALL_DIR
export PATH="$MY_INSTALL_DIR/bin:$PATH"

mkdir -p cmake/build
pushd cmake/build
cmake -DgRPC_INSTALL=ON \
-DgRPC_BUILD_TESTS=OFF \
-DCMAKE_INSTALL_PREFIX=$MY_INSTALL_DIR \
../..
make -j 4
make install
popd

这里我们安装了gRPC的头文件和库文件,以及一个工具protoc

Python 安装gRPC和gRPC工具(包含 protoc

1
2
pip install grpcio
pip install grpcio-tools

定义接口和数据

.proto文件

.proto文件用于定义数据(请求参数message和返回值message)和接口(service)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
syntax = "proto3"; // 表示这个文件遵循的proto语法的版本

package calc; // 对C++来说表示生成代码的namespace,如果不指定则为全局

// 定义服务接口
service Calc {
rpc Add (AddRequest) returns (AddReply) {}
}

// 定义Add方法的请求参数
message AddRequest {
int32 a = 1;
int32 b = 2;
}

// 定义Add方法的返回数据
message AddReply {
int32 sum = 1;
}

从.proto文件生成代码

protoc工具用于将使用ProtoBuf定义的.proto文件编译成特定编程语言的源代码。
.proto文件描述了数据和接口的定义。
例如通过.proto生成C++代码的示例:

1
protoc --cpp_out=./output_directory ./protos/proto_file.proto

会在output_directory中生成一个protos文件夹,里面有.proto同名的*.pb.h*.pb.cc文件。
这里有一个细节,生成protos文件夹是因为指定的.proto文件带有路径,生成的路径和指定.proto的路径相同,如果想把.h和.cc文件直接生成在指定的文件夹中,可以通过-I参数指定.proto文件的路径而不在指定.proto文件时携带路径。即

1
protoc --cpp_out=./output_directory -I./protos proto_file.proto

如果使用cmake构建项目,通常将这个生成过程写在CMakeList.txt中,用add_custom_command命令完成。
这里生成的.h和.cc中只包含了对数据(.proto中的message)的封装,而不包含接口(.proto中的service)的黏合剂,要生成service的代码需要利用grpc_cpp_plugin,可以通过在protoc命令中增加参数一并生成接口和数据的代码。

1
protoc --cpp_out=./output_directory --grpc_out=./output_directory --plugin=protoc-gen-grpc=/bin/grpc_cpp_plugin -I./protos proto_file.proto

会在output_directory中生成.proto同名的*.pb.h*.pb.cc文件,以及*.grpc.pb.h*.grpc.pb.cc*.grpc.pb.*里面就包含了根据service生成的代码。

Python版的protoc是已模块的形式安装的,使用方法

1
python -m grpc_tools.protoc --python_out=./output_directory --pyi_out=./output_directory --grpc_out=./output_directory --plugin=protoc-gen-grpc=/bin/grpc_python_plugin -I./protos proto_file.proto

另外其实上面通过C源码编译安装的protoc工具也是支持生成python代码的,但python的protoc模块不能生成C版本的代码。

项目文件存放结构

不是必须的,我感觉这样存放会比较方便,protosprotos_gen是客户端和服务器共用的,而且必须一致,所以只存统一的一份,这里的protos_gen就是由protos生成的意思也就是上面的output_directory目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
├── calc_client
│ ├── CMakeLists.txt
│ └── src
│ └── main.cpp
├── calc_server
│ ├── CMakeLists.txt
│ └── src
│ └── main.cpp
├── protos
│ └── calc.proto
└── protos_gen
├── calc.grpc.pb.cc
├── calc.grpc.pb.h
├── calc.pb.cc
└── calc.pb.h

CMake构建

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
cmake_minimum_required(VERSION 3.10)
project(calc_server CXX C)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++17")

set(CMAKE_C_FLAGS_DEBUG " -std=c99 -g -ggdb -O0 -Wall -Wno-unused-function -fpic -fPIC -D_DEBUG")
set(CMAKE_CXX_FLAGS_DEBUG " -std=c++17 -g -ggdb -O0 -Wall -Wno-unused-function -fpic -fPIC -D_DEBUG")

set(CMAKE_C_FLAGS_RELEASE " -std=c99 -O3 -Wall -Wno-unused-function -fpic -fPIC")
set(CMAKE_CXX_FLAGS_RELEASE " -std=c++17 -O3 -Wall -Wno-unused-function -fpic -fPIC")

# Find Protobuf installation
# Looks for protobuf-config.cmake file installed by Protobuf's cmake installation.
option(protobuf_MODULE_COMPATIBLE TRUE)
find_package(Protobuf CONFIG REQUIRED)
message(STATUS "Using protobuf ${Protobuf_VERSION}")

# Find gRPC installation
# Looks for gRPCConfig.cmake file installed by gRPC's cmake installation.
find_package(gRPC CONFIG REQUIRED)
message(STATUS "Using gRPC ${gRPC_VERSION}")

include_directories(
/include # 这里替换成gRPC的编译安装目录下的include
include
../protos_gen # 这里是通过protoc生成的文件存放的目录有.h
)
link_directories(
/lib64 # 这里替换成gRPC的编译安装目录下的lib64
lib
)

aux_source_directory(src SRC_LIST)
aux_source_directory(../protos_gen PROTOBUF_SRC_LIST) # 这里是通过protoc生成的文件存放的目录有.cc
add_executable(${PROJECT_NAME}
${SRC_LIST}
${PROTOBUF_SRC_LIST}
)

target_link_libraries(${PROJECT_NAME}
pthread
absl::flags
absl::flags_parse
protobuf::libprotobuf
gRPC::grpc++_reflection
gRPC::grpc++
)

服务端和客户端的CMakeLists.txt都可以套用上面的模板

cpp服务端

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
#include <iostream>

#include "absl/flags/flag.h"
#include "absl/flags/parse.h"
#include "absl/strings/str_format.h"

#include <grpcpp/ext/proto_server_reflection_plugin.h>
#include <grpcpp/grpcpp.h>
#include <grpcpp/health_check_service_interface.h>

#include "calc.grpc.pb.h"

using namespace std;

// 定义命令行参数,服务器端口号,默认为 50051
ABSL_FLAG(uint16_t, port, 50051, "Server port for the service");

class CalcServiceImpl final : public calc::Calc::Service { // 继承并实现在helloworld.proto中定义的接口
grpc::Status Add(grpc::ServerContext* context, const calc::AddRequest* request, calc::AddReply* reply) override {
// 从request中取参数
auto a = request->a();
auto b = request->b();

// 调用真正的处理逻辑,这里是简单的加法,ServiceImpl可以作为Proxy存在,持有一个业务对象的指针,然后调用相关的处理函数
auto sum = a + b;

// 设置reply中的字段作为返回值
reply->set_sum(sum);
return grpc::Status::OK;
}
};

int main(int argc, char** argv) {
// 解析命令行参数
absl::ParseCommandLine(argc, argv);
uint16_t port = absl::GetFlag(FLAGS_port);
// 构建服务器地址字符串
std::string server_address = absl::StrFormat("0.0.0.0:%d", port);

// 创建服务实现对象
CalcServiceImpl service;

// 启用默认的健康检查服务和反射插件
grpc::EnableDefaultHealthCheckService(true);
grpc::reflection::InitProtoReflectionServerBuilderPlugin();
// 创建服务器构建器
grpc::ServerBuilder builder;

// Listen on the given address without any authentication mechanism.
builder.AddListeningPort(server_address, grpc::InsecureServerCredentials());

// 注册服务实现对象到服务器构建器
builder.RegisterService(&service);

// 构建并启动服务器
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
std::cout << "Server listening on " << server_address << std::endl;

// 等待服务器关闭
server->Wait();

return 0;
}

在启动时可以指定端口号替换默认的端口号

1
./calc-server --port=12345

cpp客户端

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
#include <iostream>
#include <memory>

#include "absl/flags/flag.h"
#include "absl/flags/parse.h"

#include <grpcpp/grpcpp.h>

#include "calc.grpc.pb.h" // 包含通过 Protocol Buffers 编译生成的 gRPC 服务定义

ABSL_FLAG(std::string, target, "localhost:50051", "Server address");

using namespace std;

class CalcClient {
public:
CalcClient(shared_ptr<grpc::Channel> channel)
: _stub(calc::Calc::NewStub(channel)) {} // 通过通道创建 Stub

// 执行加法操作
int Add(int a, int b) {
// 创建请求
calc::AddRequest request;
request.set_a(a);
request.set_b(b);

// 创建响应容器
calc::AddReply reply;

// 创建客户端上下文
grpc::ClientContext context;

// 实际执行 RPC 调用
grpc::Status status = _stub->Add(&context, request, &reply);

// 根据调用状态进行处理
if (status.ok()) {
return reply.sum(); // 返回相加的结果
} else {
cout << status.error_code() << ": " << status.error_message() << endl; // 打印错误信息
return 0;
}
}

private:
unique_ptr<calc::Calc::Stub> _stub; // gRPC Stub
};

int main(int argc, char** argv) {
absl::ParseCommandLine(argc, argv); // 解析命令行参数
string target_str = absl::GetFlag(FLAGS_target); // 获取服务器地址参数
cout << "target_str=" << target_str << endl;

// 创建客户端
CalcClient client(grpc::CreateChannel(target_str, grpc::InsecureChannelCredentials()));

int a, b;
while(cin >> a >> b) { // 从标准输入读取输入并执行加法操作
auto sum = client.Add(a, b);
cout << a << " + " << b << " = " << sum << endl; // 打印结果
}

return 0;
}

同样地,在启动时可以指定服务器IP和端口号替换默认参数

1
./calc-client --target=127.0.0.1:12345

python服务端

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
from concurrent import futures

import grpc
import sys
sys.path.append("../protos_gen")
from calc_pb2 import AddReply # protoc生成的代码
from calc_pb2_grpc import CalcServicer, add_CalcServicer_to_server # protoc生成的代码


class Calc(CalcServicer):
def Add(self, request, context):
return AddReply(sum=request.a + request.b)


def main():
port = '50051'
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
add_CalcServicer_to_server(Calc(), server)
server.add_insecure_port('0.0.0.0:' + port)
server.start()
print("Server started, listening on " + port)
server.wait_for_termination()


if __name__ == '__main__':
main()

python客户端

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
import grpc
import sys
sys.path.append("../protos_gen")
from calc_pb2 import AddRequest # protoc生成的代码
from calc_pb2_grpc import CalcStub # protoc生成的代码


class Calc:
def __init__(self, server):
self.channel = grpc.insecure_channel(server)
self.stub = CalcStub(self.channel)

def __del__(self):
del self.stub
self.channel.close()

def Add(self, **kw):
response = self.stub.Add(AddRequest(**kw))
return response.sum


def main():
client = Calc(server='localhost:50051')
s = client.Add(a=1, b=2)
print(f"1+2={s}")


if __name__ == '__main__':
main()