gRPC C++ Generic API 实战:不依赖生成桩代码,用单一处理器调度任意 RPC 方法

发布时间:2026/9/9 20:50:18
gRPC C++ Generic API 实战:不依赖生成桩代码,用单一处理器调度任意 RPC 方法 gRPC C Generic API 实战不依赖生成桩代码用单一处理器调度任意 RPC 方法【免费下载链接】grpcC based gRPC (C, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpcgRPC 的 C 生态通常以 protoc 生成的 Stub桩代码提供类型安全调用但当你需要实现协议代理proxy、网关、流式转发、服务路由这类“同一套代码处理多种不同消息类型”的场景时生成式代码就显得笨重。本指南以仓库中的 generic_api 示例 为核心带你掌握 gRPC C 的 Generic API客户端如何用方法名字符串发起任意 RPC、服务端如何用单一CallbackGenericService处理器完成方法分发与手工序列化读完即可动手写出可运行的通配型客户端与转发型服务端。为什么需要 Generic API何时该放弃生成的 StubgRPC 的标准用法是让protocgrpc_cpp_plugin从.proto文件生成出强类型的 Stub 与服务基类如Greeter::Stub、Greeter::Service。仓库内 examples/cpp/helloworld 就是这种“生成桩代码优先”的经典写法。而 Generic API 恰恰相反它在编译期不绑定任何具体方法名和具体消息类型把 RPC 收敛为「一个名字方法全路径 一段原始字节ByteBuffer」。示例 README.md 明确指出了它的价值取向虽然生成桩代码在大多数情况下更简单、更优但在proxy 实现等特定场景下Generic API 拥有独特优势——它能用一个函数管理多种消息类型让代码极度灵活。由此可以归纳出两类 API 的取舍维度生成式 StubGeneric API类型安全编译期强类型检查无编译期类型约束需自行反序列化新增一个方法需要重新生成代码服务端只需在分发处加一个分支客户端只需换一个方法名字符串多消息类型每个方法一个 handler同一个 handler/入口可路由多种类型典型场景业务 RPC 服务Proxy、网关、透明转发、协议桥接、反射调试注意一个文档与代码的细节差异README 中提到本示例对应greeter_callback_client与greeter_callback_server而实际构建目标在 BUILD 中命名为greeter_client与greeter_server。之所以叫 “callback”是因为两端都采用了 gRPC C 的Callback API见下文源码解读。示例速览目录结构、构建与运行文件构成示例目录examples/cpp/generic_api/只有三个核心文件greeter_client.cc客户端复用helloworld.proto生成的消息类型但不链接Greeter::Stub改用泛型 Stubgreeter_server.cc服务端基于CallbackGenericService的泛型服务CMakeLists.txt 与 BUILDCMake / Bazel 两套构建脚本。两份源码引用的 proto 是 examples/protos/helloworld.proto即经典的package helloworld; service Greeter { rpc SayHello(HelloRequest) returns (HelloReply); }。之所以仍要这条 proto是为了复用HelloRequest/HelloReply两种消息类型做序列化演示——Generic API 不需要方法定义但消息编码仍然要依赖 protobuf 类型。用 Bazel 或 CMake 构建README 指出只要已经拥有一套可用的 gRPC源码安装方式参考 BUILDING.md就可以用 Bazel 或 CMake 编译本示例。Bazel 方式仓库根目录执行bazel build //examples/cpp/generic_api:greeter_client \ //examples/cpp/generic_api:greeter_server # 产物位于 bazel-bin/examples/cpp/generic_api/ 下CMake 方式该示例自带独立的 CMakeLists.txt其注释声明“前提是 protobuf 与 gRPC 已通过 cmake 安装”随后它通过include(../cmake/common.cmake)引入公共配置并对../../protos/helloworld.proto调用protoc与grpc_cpp_plugin生成.pb.cc/.pb.h/.grpc.pb.*文件最终产出可执行文件greeter_client与greeter_server链接目标为hw_grpc_protoabsl::flags系列 gRPC/protobuf 库。运行并验证先在一个终端启动服务端默认监听 50051 端口$ ./greeter_server再在另一个终端启动客户端$ ./greeter_clientREADME 给出的预期输出如下### Send: SayHello(nameWorld) Ok. ReplyMessageHello World ### Send: SayHello(namegRPC) Ok. ReplyMessageHello gRPC示例使用 Abseil Flags 接收参数客户端默认--targetlocalhost:50051见 greeter_client.cc服务端默认--port50051见 greeter_server.cc如服务端无参数启动会打印Server listening on 0.0.0.0:50051。客户端实现解读TemplatedGenericStub与“按名调用”泛型客户端的关键是头文件 include/grpcpp/generic/generic_stub.h。该文件提供了模板类template class RequestType, class ResponseType class TemplatedGenericStub final : public internal::TemplatedGenericStubCallbackInternalRequestType, ResponseType;并给出了最常用的特化别名typedef TemplatedGenericStubgrpc::ByteBuffer, grpc::ByteBuffer GenericStub;注释说明得很清楚Generic stubs provide a type-unaware interface to call gRPC methods by name泛型 Stub 提供一种“不知晓类型”的接口通过方法名调用 RPC且 Request/Response 类型在实践中一般是grpc::ByteBuffer或 protobuf 消息基类。示例客户端没有使用ByteBuffer而是使用了 proto 消息基类作为模板参数greeter_client.ccusing ProtoGenericStub ::grpc::TemplatedGenericStub::google::protobuf::Message, ::google::protobuf::Message;HelloRequest、HelloReply都是::google::protobuf::Message的派生类因此可以安全传参。真正“泛型”的体现是发起调用的那一步——不再调用Greeter::Stub::SayHello(...)而是把方法全路径当成字符串交给UnaryCall// Send a unary call using a generic stub. Unlike generated stubs, // this requires to specify the name of call. stub_-UnaryCall(context, /helloworld.Greeter/SayHello, grpc::StubOptions(), request, reply, { status std::move(s); std::lock_guardstd::mutex lock(mu); done true; cv.notify_one(); });对照 include/grpcpp/impl/generic_stub_internal.hUnaryCall的完整签名为void UnaryCall(ClientContext* context, const std::string method, StubOptions options, const RequestType* request, ResponseType* response, std::functionvoid(grpc::Status) on_completion);三个值得注意的要点方法名必须为完整路径格式为/包名.服务名/方法名。示例中是/helloworld.Greeter/SayHello。对同一个 Stub把该字符串换成/helloworld.Greeter/SayHelloAgain之类的新方法只要服务端实现了分发就能立刻调用——这正是 README 所说“用同一份代码应对多种消息类型”的客户端侧基础。on_completion是回调式完成通知。示例为了让main顺序等待结果用std::mutexstd::condition_variabledone标志把异步回调转成了同步等待这是“Callback 风格客户端”的常见演示手法生产代码中可以直接在回调里继续业务无需阻塞。同一泛型 Stub 还支持PrepareUnaryCall配合ClientUnaryReactor与PrepareBidiStreamingCall配合ClientBidiReactor等 reactor 形态具体接口继承自 generic_stub_internal.hgeneric_stub.h 中还保留了基于CompletionQueue的经典异步 APIPrepareCall/Call头文件注释已标注后者DEPRECATED for multi-threaded use新代码建议走回调或 reactor 形态。服务端实现解读CallbackGenericService与基于方法名的分发一次注册处处接管服务端把“某个服务”替换成了“整个泛型服务”。greeter_server.cc 中业务类直接继承grpc::CallbackGenericServiceclass GreeterServiceImpl final : public CallbackGenericService { ServerGenericBidiReactor* CreateReactor( GenericCallbackServerContext* context) override { if (context-method() /helloworld.Greeter/SayHello) { // Let the SayHello reactor handle this now on. return new SayHelloReactor(); } else { // Forward this to the implementation of the base class returning // UNIMPLEMENTED. return CallbackGenericService::CreateReactor(context); } } ... };底层类型定义可追溯至 include/grpcpp/generic/callback_generic_service.hServerGenericBidiReactor是ServerBidiReactorByteBuffer, ByteBuffer的别名——即泛型服务收到的请求与响应容器统一为ByteBuffer原始字节GenericCallbackServerContext在CallbackServerContext基础上增加了method()与host()两个查询接口分发逻辑正是读取context-method()字符串基类默认的CreateReactor返回一个直接Finish(Status(StatusCode::UNIMPLEMENTED, ))的 reactor所以未被显式分支匹配的方法会自动得到UNIMPLEMENTED未实现错误这与标准 gRPC 对未知方法的语义一致。“单一函数管理多种消息类型”体现在这里无论请求是HelloRequest、HelloReply还是将来新增的第三种 proto入口都只有CreateReactor这一个只是按方法名把字节流交给不同的 reactor 去解码处理。服务端的组装在 RunServer 中完成greeter.GreeterServiceImpl service; // 实际为 GreeterServiceImpl service; grpc::EnableDefaultHealthCheckService(true); // 开启默认健康检查服务 ServerBuilder builder; builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); builder.RegisterCallbackGenericService(service); // 注册泛型服务 std::unique_ptrServer server(builder.BuildAndStart()); std::cout Server listening on server_address std::endl; server-Wait();注意注册入口是RegisterCallbackGenericService而不是生成式代码用的RegisterService——这是把该服务的所有方法都收编到泛型分发器的关键一步。Reactor 生命周期读 → 反序列化 → 处理 → 序列化 → 写SayHelloReactorgreeter_server.cc演示了泛型服务端完整的数据流class SayHelloReactor : public ServerGenericBidiReactor { public: SayHelloReactor() { StartRead(request_); } // ① 构造即发起一次读取存入 ByteBuffer private: void OnReadDone(bool ok) override { // ② 读到请求后回调 if (!ok) return; // ③ 反序列化ByteBuffer - HelloRequest HelloRequest request; grpc::Status result grpc::GenericDeserializeProtoBufferReader, HelloRequest(request_, request); if (!result.ok()) { Finish(result); return; } // ④ 业务处理真正返回 Status 的逻辑 HelloReply reply; result OnSayHello(request, reply); if (!result.ok()) { Finish(result); return; } // ⑤ 序列化HelloReply - ByteBuffer bool own_buffer; result grpc::GenericSerializeProtoBufferWriter, HelloReply( reply, response_, own_buffer); if (!result.ok()) { Finish(result); return; } // ⑥ 写回响应 StartWrite(response_); } void OnWriteDone(bool ok) override { // ⑦ 写完回调收尾 Finish(ok ? Status::OK : Status(StatusCode::UNKNOWN, Unexpected failure)); } void OnDone() override { delete this; } // ⑧ reactor 自我回收 ByteBuffer request_; ByteBuffer response_; };其中的业务校验展示了泛型服务端如何利用Status语义greeter_server.cc当请求name为空时返回INVALID_ARGUMENT并附带错误信息正常时返回拼接后的Hello %s。这与生成式 handler 返回Status的约定完全一致异常照样能传回客户端。OnSayHello之所以命名为 On 前缀并单独抽出是为了体现协议编解码与业务逻辑分离的设计如果未来要接入新方法新增一个 reactor在其中做同样的“解码 → 调 On 业务函数 → 编码”三段式即可。手工序列化的底层原理GenericSerialize/GenericDeserialize生成式 Stub 会自动完成 proto 编解码而泛型路径必须由开发者显式调用相关模板位于 include/grpcpp/impl/generic_serialize.h。从源码可以提炼出它们的实现约束GenericSerializeProtoBufferWriter, T(msg, ByteBuffer* bb, bool* own_buffer, ...)要求ProtoBufferWriter必须是::protobuf::io::ZeroCopyOutputStream的子类文件顶部有static_assert保证。内部先通过msg.ByteSizeLong()取消息长度超过2^31-1字节会返回INTERNAL“Protobuf is too large to be serialized”小消息长度不超过GRPC_SLICE_INLINED_SIZE直接序列化进内联Slice大消息则经ProtoBufferWriterCodedOutputStream落盘到ByteBuffer最后把*own_buffer置为true交给调用方管理缓冲区所有权。GenericDeserializeProtoBufferReader, T(ByteBuffer* buffer, msg)要求ProtoBufferReader是::protobuf::io::ZeroCopyInputStream的子类。空缓冲返回INTERNAL(No payload)解析通过msg-ParseFromZeroCopyStream(reader)完成失败时携带msg-InitializationErrorString()的错误详情返回成功后会执行buffer-Clear()释放字节缓存。示例中传给这两个模板的分别是grpc::ProtoBufferWriter与grpc::ProtoBufferReader它们正是 gRPC 为ByteBuffer与 protobuf ZeroCopy 流之间搭桥的标准实现。这套手工编解码正是“单函数多类型”灵活性的代价消息在传输层始终是ByteBuffer具体是什么 proto 类型完全由分发逻辑说了算。客户端侧的类型体系一览Generic API 在 include/grpcpp/generic/ 目录下集中提供了一套头文件可按需选用generic_stub.hTemplatedGenericStubReq, Resp及ByteBuffer版本别名GenericStub除示例用到的回调式UnaryCall外还带 CompletionQueue 形态的PrepareCall/Call已标注多线程弃用generic_stub_callback.h面向 reactor 的回调式泛型 Stubcallback_generic_service.hCallbackGenericService、GenericCallbackServerContext与ServerGenericBidiReactor本示例所用async_generic_service.h基于经典 CompletionQueue 异步模型的泛型服务抽象适合沿用旧式 CQ tag 编程风格的项目。由示例走向实战构建一个通用代理Proxy的骨架思路本示例的最终目的是为 proxy 类项目铺路。结合两端代码一个“透明转发”型代理通常遵循这样的结构服务端继承CallbackGenericService在CreateReactor中不再用if/else分发到不同业务而是对所有方法名一视同仁或只做黑白名单过滤把请求ByteBuffer原样转发客户端在 reactor 内持有指向上游的TemplatedGenericStub例如以ByteBuffer为模板参数的特化用context-method()得到的方法全路径通过PrepareBidiStreamingCall/UnaryCall将ByteBuffer透传过去编解码转发型代理往往可以跳过GenericSerialize/GenericDeserialize不关心消息内容、只做搬运只有在需要改写消息、校验字段或做内容路由时才按目标方法名把ByteBuffer反序列化成对应 proto 类型再重组。关于可行性要诚实说明示例仓库本身只演示了“一个泛型服务 一个具体方法分支”的最小闭环并未内置完整 proxy 实现上述第 2、3 点的组合方式是从 generic_stub.h 与 callback_generic_service.h 的公开接口推断出的可行方案。落地时还需自行处理ByteBuffer的生命周期管理参考own_buffer语义、流式场景的背压与取消传播等问题。小结与适用边界Generic API 的价值不在于取代生成式 Stub而在于补足“一个入口、多方法多类型”的灵活通道客户端用TemplatedGenericStub加方法名字符串即可调用任意 RPC免去为每个服务重新生成代码服务端用CallbackGenericService::CreateReactor按context-method()分发配以GenericDeserialize/GenericSerialize手工编解码就能在一个进程内同时代理形态各异的消息代价失去编译期类型检查与自动编解码错误只能推迟到运行时以Status如INTERNAL、UNIMPLEMENTED、INVALID_ARGUMENT暴露——这与 gRPC 一贯的错误语义仍然兼容便于统一处理。当项目方法固定、追求开发效率时请继续沿用 examples/cpp/helloworld 的生成式 Stub当需求明确指向 proxy、网关、协议桥接等“多面手”角色时本文示例所在的 examples/cpp/generic_api 就是你最直接的起点。【免费下载链接】grpcC based gRPC (C, Python, Ruby, Objective-C, PHP, C#)项目地址: https://gitcode.com/GitHub_Trending/gr/grpc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询