应选用 protobuf 序列化:因其跨语言、向后兼容、idl 清晰、c++ 代码稳定;避免裸传 struct、手写 json 或使用 grpc;需实现长度前缀包界定、抽象基类服务分发、严格超时控制。

怎么选序列化方式:别碰 std::string 直接传 struct
二进制结构体裸传在不同平台会因字节序、对齐、padding 不一致直接崩溃,memcpy 过去的 struct 在另一端读出来字段全错是常态。真实场景里必须走明确的序列化协议。
推荐从 protobuf 入手,不是因为它最好,而是它自带跨语言、向后兼容、IDL 定义清晰、C++ 生成代码稳定。别自己写 JSON 序列化——浮点精度、空值处理、嵌套深度都容易翻车,且 JSON 解析本身比 protobuf 慢 3–5 倍(实测 MessageLite::ParseFromString vs jsoncpp)。
-
protoc --cpp_out=. service.proto生成service.pb.h和service.pb.cc,直接 include 就能用 - 避免在 proto 中用
optional字段做业务逻辑分支——旧客户端不发该字段时,新服务端默认值可能引发意料外行为 - 不要把
bytes字段当万能兜底:它不校验内容,网络传输中损坏了也毫无提示
socket 层怎么接 RPC 请求:别用阻塞 recv 等完整包
TCP 是流式协议,recv 返回的可能是半包、粘包或跨调用碎片。直接按“一次 recv 读完一个请求”写法,在高并发或弱网下必然出错——常见现象是 ParseFromString 失败返回 false,但错误码没打日志,最后查半天发现是包没收全。
必须实现包界定(framing)。最简方案是头部 4 字节大端长度前缀:uint32_t len = htonl(msg.ByteSizeLong()),先收够 4 字节再收 body。别用换行符或特殊字符分隔——protobuf 二进制数据里完全可能出现这些字节。
立即学习“C++免费学习笔记(深入)”;
- 每次
recv后检查返回值:等于 0 表示对端关闭;小于 0 且errno == EAGAIN || errno == EWOULDBLOCK才算正常未就绪 - 接收缓冲区不能复用同一块内存:多个连接共用一个
std::vector<uint8_t></uint8_t>导致数据覆盖是高频 bug - 别在主线程循环
accept+recv:单线程扛不住并发,用epoll或io_uring(Linux 5.11+)更实际
如何组织服务端调用逻辑:拒绝全局函数指针注册表
用 std::map<:string std::function>> handlers</:string> 注册方法名到函数,看着简单,但无法做参数类型校验、无法绑定上下文(比如 this)、无法统一处理异常和超时。上线后加个新接口就得改 map 初始化逻辑,极易漏注册或拼错字符串。
真正可维护的做法是定义抽象基类 + 模板派生:
class RpcService {
public:
virtual ~RpcService() = default;
virtual void Call(const std::string& method, const google::protobuf::Message& req,
google::protobuf::Message* resp) = 0;
};
每个具体服务继承它,Call 内部用 if-else 或 switch 匹配 method 名并 downcast 到对应 proto request 类型。虽然多几行代码,但编译期能检查参数类型,IDE 能跳转,单元测试能 mock。
- 别在
Call里直接 new 对象:堆分配慢,且容易忘记 delete;用栈上对象 +req.ParseFromString()更安全 - 响应 message 必须提前 clear:否则多次调用后字段残留,尤其 repeated 字段会不断追加
- method 字符串建议全小写 + 下划线(如
"get_user_by_id"),避免大小写混用导致 Windows/Linux 行为不一致
为什么不用现成框架:grpc 的坑比你想象的多
grpc 默认用 HTTP/2,调试困难:tcpdump 抓出来全是二进制帧,curl 根本没法测;C++ client stub 生成的代码体积大,链接时容易触发 ODR violation;更麻烦的是它强制要求 TLS——哪怕本地开发也得配证书,很多团队卡在这一步就退回手写。
如果你只需要内网服务间调用、QPS 在 1w 以下、不跨语言,手写一个基于 protobuf + epoll 的轻量框架反而更可控。重点不是“造轮子”,而是把序列化、包界定、调用分发这三层边界划清楚,后续替换底层网络库(比如换成 libuv)或序列化协议(比如换成 capnproto)成本极低。
最常被忽略的一点:所有网络收发操作必须有超时控制。没有 setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, ...) 或等效机制,一个坏连接就能让整个服务 hang 死。这不是可选项,是必选项。









