C++如何构建一个支持自定义协议的RPC远程过程调用框架?(后端架构)

5次阅读

选 protobuf 为序列化协议最稳妥,因其跨语言支持好、idl 易维护、c++ 零拷贝友好;需开启 optional/oneof 保证兼容性,禁用裸指针与 stl 容器,改用 repeated 字段。

C++如何构建一个支持自定义协议的RPC远程过程调用框架?(后端架构)

怎么选序列化协议才不卡在跨语言和性能上

自定义 rpc 框架里,序列化不是“能跑就行”,而是决定你后续能不能加 Go/Python 服务、压测时 latency 突然翻倍的根源。别一上来就手写二进制格式——protobuf 是当前最稳妥的起点,它生成的 C++ 代码零拷贝友好、ParseFromStringSerializeToString 接口稳定、IDL 易维护;flatbuffers 虽快但 C++ 运行时依赖 schema,调试时看不到原始字段名,上线后改个字段容易 silently 失败。

常见错误现象:Deserialize failed: invalid wire type,往往是 client 用新版 proto 发请求,server 还在用旧版头文件编译,没做 forward/backward 兼容校验。

  • 必须开启 proto2optionalproto3oneof 来控制字段可选性,避免默认值污染语义
  • 禁止在 message 里嵌套裸指针或 STL 容器(如 std::vector<int></int>),一律用 repeated 字段,否则序列化结果不可控
  • 如果真要极致性能且只跑 C++,可以后期用 capnproto 替换,但它不支持浮点数 NaN 校验,线上遇到 NaN 会直接 abort

如何让网络层不成为吞吐瓶颈

很多人用 boost::asio 写完 accept + async_read 就以为搞定了,结果 QPS 上不去还查不出原因。根本问题是没把连接生命周期和业务线程解耦:每个 connection 对应一个 io_context 实例?错,应该共享一个或少数几个 io_context,再配固定数量的 thread_pool 去 dispatch 请求。

典型坑:async_read 回调里直接调用耗时函数(比如数据库查询),导致整个 io_context 被 block,后续所有连接卡住。

立即学习C++免费学习笔记(深入)”;

  • 收包后立即用 post 把解析+处理逻辑扔进业务线程池,io_context 只管 I/O
  • 包头必须带长度字段,用 async_read 先读 4 字节再读 body,别用 delimiter(比如 n)——http/2 都不用换行分隔了
  • 禁用 Nagle 算法socket.set_option(tcp::no_delay(true)),否则小包延迟飙升

怎样设计请求 ID 和超时机制才不会丢响应

RPC 最怕“发出去没回音”,表面是网络问题,实际常因 request_id 重复或 timeout 清理策略不对。C++ 里不能靠全局递增 int——多线程++g_req_id 不安全,也不能用 std::chrono::steady_clock::now().time_since_epoch().count() 当 ID,纳秒级时间戳在高并发下极易碰撞。

正确做法是组合:线程局部 ID + 时间戳低 32 位 + 随机 salt。例如 uint64_t req_id = (tid 。

  • 每个 rpc_client 实例维护自己的 std::unordered_map<uint64_t std::promise>></uint64_t>,key 就是这个 req_id
  • 发送前插入 map,同时启动 std::thread 或 timer(推荐 asio::steady_timer)等待 timeout,超时则 promise.set_exception
  • 收到响应后先查 map,存在就 set_value 并 erase;不存在说明已超时,直接丢弃——别试图重试,重试逻辑应在上层业务控制

为什么服务发现和负载均衡不能硬编码在 client 里

本地测试时写死 "127.0.0.1:8080" 很方便,但上线后节点扩缩、故障转移、灰度发布全崩。C++ 没有像 Java 那样成熟的注册中心 SDK,得自己对接 etcdconsul 的 HTTP API,但千万别每发一次请求都去查一次服务列表。

真实场景下,client 启动时拉取一次全量节点,存进 std::vector<endpoint></endpoint>,再用后台线程定期 GET /v1/health 做健康检查,剔除失联节点。负载策略选 round_robin 就够用,别一上来搞一致性哈希——C++ 里 std::hash<:string></:string> 在不同编译器版本可能不一致,导致同一 key 路由到不同 server。

  • etcd watch 接口要用 long polling,别轮询,否则集群压力大
  • 节点 IP+端口字符串作为 key 存进 map,不要用 struct sockaddr_in 直接 hash,字节序和 padding 会导致跨平台不一致
  • 首次连接失败时,别立刻 panic,应 fallback 到本地缓存的节点列表(哪怕过期 30 秒),保证降级可用

真正难的不是写通一个 call,而是当 client 同时发起 10k 并发、server 有 50 个实例、其中 3 个正在滚动更新、etcd 网络抖动 200ms 时,你的 timeout 设置、重试次数、连接复用策略是否还能让 P99 延迟稳定在 50ms 内——这些细节,往往在压测最后一天才暴露。

text=ZqhQzanResources