59、gRPC:集成gRPC框架,编译.proto服务定义

说实话,gRPC 这玩意儿在微服务架构里已经快成标配了。我最早接触它是在一个跨语言通信的项目里,当时用 HTTP+JSON 传数据,解析慢、类型还容易丢。后来换成 gRPC,性能直接翻倍。嗯,今天我们就聊聊怎么在 CMake 里把它集成进来。

gRPC 到底解决了什么问题?

你想想看,传统的 RESTful 接口,客户端和服务端得约定好 URL、请求体、响应体。一旦接口变了,两边都得改。而且 JSON 序列化/反序列化在 C++ 里效率并不高。

gRPC 的思路是:先定义接口,再生成代码。你写一个 .proto 文件,里面描述好服务和方法。然后 protoc 编译器帮你生成客户端和服务端的 C++ 代码。这样接口就是「契约」,谁也别耍赖。

核心流程:
  1. 编写 .proto 文件(定义服务和消息)
  2. 用 protoc 编译 .proto → 生成 .pb.h / .pb.cc / .grpc.pb.h / .grpc.pb.cc
  3. 在 CMake 里把这些生成的文件加入构建
  4. 链接 gRPC 和 protobuf 的库

一个简单的 .proto 示例

先看个最基础的。假设我们要做一个「问候服务」——客户端传个名字,服务端回一句问候。

syntax = "proto3";

package helloworld;

// 定义请求消息
message HelloRequest {
  string name = 1;
}

// 定义响应消息
message HelloReply {
  string message = 1;
}

// 定义服务
service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

这个文件保存为 helloworld.proto。注意 package 会影响生成的 C++ 命名空间。我个人习惯把 package 和目录结构保持一致,这样找文件方便。

CMake 里怎么集成?

这里有个坑——gRPC 的 CMake 集成方式有好几种。我踩过最深的坑是手动调用 protoc 命令,结果路径写错了,编译出来一堆空文件。后来我改用 CMake 的 find_package 配合自定义命令,稳定多了。

下面是我常用的模板:

cmake_minimum_required(VERSION 3.16)
project(grpc_demo LANGUAGES CXX)

# 找 protobuf 和 gRPC 的包
find_package(Protobuf REQUIRED)
find_package(gRPC REQUIRED)

# 设置 .proto 文件路径
set(PROTO_FILE ${CMAKE_CURRENT_SOURCE_DIR}/helloworld.proto)

# 设置生成文件的输出目录
set(PROTO_GEN_DIR ${CMAKE_CURRENT_BINARY_DIR}/generated)

# 自定义命令:编译 .proto
add_custom_command(
  OUTPUT
    ${PROTO_GEN_DIR}/helloworld.pb.h
    ${PROTO_GEN_DIR}/helloworld.pb.cc
    ${PROTO_GEN_DIR}/helloworld.grpc.pb.h
    ${PROTO_GEN_DIR}/helloworld.grpc.pb.cc
  COMMAND ${Protobuf_PROTOC_EXECUTABLE}
    --proto_path=${CMAKE_CURRENT_SOURCE_DIR}
    --cpp_out=${PROTO_GEN_DIR}
    --grpc_out=${PROTO_GEN_DIR}
    --plugin=protoc-gen-grpc=${gRPC_CPP_PLUGIN_EXECUTABLE}
    ${PROTO_FILE}
  DEPENDS ${PROTO_FILE}
  COMMENT "Compiling helloworld.proto..."
)

# 把生成的文件打包成一个库
add_library(helloworld_proto
  ${PROTO_GEN_DIR}/helloworld.pb.cc
  ${PROTO_GEN_DIR}/helloworld.grpc.pb.cc
)

# 让编译器能找到生成的头文件
target_include_directories(helloworld_proto
  PUBLIC ${PROTO_GEN_DIR}
)

# 链接必要的库
target_link_libraries(helloworld_proto
  PUBLIC
    protobuf::libprotobuf
    gRPC::grpc++
    gRPC::grpc++_reflection
)
小技巧: 如果你用 find_package 找不到 gRPC,八成是安装路径没在 CMake 的搜索范围内。可以手动设置 -DgRPC_DIR=/path/to/grpc/cmake

生成的文件结构

执行 CMake 后,generated 目录下会出现四个文件:

文件 作用
helloworld.pb.h 消息类的头文件(HelloRequest、HelloReply)
helloworld.pb.cc 消息类的实现
helloworld.grpc.pb.h 服务类的头文件(Greeter 的客户端/服务端基类)
helloworld.grpc.pb.cc 服务类的实现

说白了,.pb 文件管数据序列化,.grpc.pb 文件管 RPC 调用。两者缺一不可。

编写服务端和客户端

有了生成的文件,写服务端就很简单了。继承 Greeter::Service,重写 SayHello 方法:

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

class GreeterServiceImpl final : public helloworld::Greeter::Service {
  grpc::Status SayHello(
    grpc::ServerContext* context,
    const helloworld::HelloRequest* request,
    helloworld::HelloReply* reply
  ) override {
    std::string prefix("Hello ");
    reply->set_message(prefix + request->name());
    return grpc::Status::OK;
  }
};

int main() {
  GreeterServiceImpl service;
  grpc::ServerBuilder builder;
  builder.AddListeningPort("0.0.0.0:50051", grpc::InsecureServerCredentials());
  builder.RegisterService(&service);
  std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
  server->Wait();
  return 0;
}

客户端更直接:

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

int main() {
  auto channel = grpc::CreateChannel("localhost:50051", grpc::InsecureChannelCredentials());
  auto stub = helloworld::Greeter::NewStub(channel);

  helloworld::HelloRequest request;
  request.set_name("World");

  helloworld::HelloReply reply;
  grpc::ClientContext context;
  grpc::Status status = stub->SayHello(&context, request, &reply);

  if (status.ok()) {
    std::cout << reply.message() << std::endl;
  }
  return 0;
}
注意: 生产环境千万别用 InsecureServerCredentials。我曾经在测试环境忘了加 SSL,结果被安全扫描工具报了个高危漏洞。gRPC 支持 TLS,建议用 SslServerCredentials

集成到 CMake 的完整示例

把服务端和客户端也加进 CMakeLists.txt:

add_executable(server server.cpp)
target_link_libraries(server PRIVATE helloworld_proto)

add_executable(client client.cpp)
target_link_libraries(client PRIVATE helloworld_proto)

这样整个项目就完整了。先启动 server,再运行 client,就能看到输出 Hello World

gRPC 集成流程总览

下面这张图帮你理清整个流程:

gRPC 集成流程 编写 .proto 文件 定义服务 + 消息 protoc 编译 生成 .pb / .grpc.pb 文件 CMake 构建 编译成库 + 可执行文件 服务端 (server.cpp) 继承 Service 基类,实现 RPC 方法 客户端 (client.cpp) 创建 Stub,发起 RPC 调用 启动 server,监听端口 client 连接 server,发送请求 ✅ 客户端收到响应:Hello World

避坑指南

我总结几个常见问题:

  • protoc 版本不匹配: gRPC 和 protobuf 的版本要对应。我曾经把 protobuf 3.20 和 gRPC 1.45 混用,结果编译报了一堆模板错误。建议用 vcpkg 或 conan 统一管理版本。
  • 找不到 gRPC 插件: protoc-gen-grpc 这个可执行文件必须存在。如果你是从源码编译的 gRPC,它在 build/grpc_cpp_plugin 里。
  • 链接顺序: gRPC 依赖 protobuf,链接时要把 gRPC 放前面。CMake 的 target_link_libraries 会自动处理,但如果你手动写 target_link_libraries 顺序错了,会报 undefined reference。
我的习惯: 把 .proto 编译和库打包封装成一个 CMake 函数,这样多个 .proto 文件可以复用。代码量能减少一半。

好了,gRPC 集成的基本套路就这些。说白了就是三步:写 .proto、编译、链接。只要把 CMake 的自定义命令写对,后面就顺了。


公众号:蓝海资料掘金营,微信deep3321