Cap'n Proto C++ RPC 开发指南
概述
Cap'n Proto 是一个高性能的数据交换格式和 RPC 系统。本文将重点介绍其 C++ 实现中的 RPC 层,它构建在序列化层之上,实现了 Cap'n Proto 的 RPC 协议。
RPC 实现现状
当前版本(0.4)的 C++ RPC 实现达到了 Level 1 标准,支持基本 RPC 功能,但暂不支持持久化能力、三方介绍和分布式相等性检查等高级特性。
KJ 并发框架
RPC 天然需要并发模型的支持。Cap'n Proto 的 RPC 基于 KJ 库的事件驱动并发框架。
事件循环并发模型
KJ 采用事件循环并发模型:
- 允许多线程,但每个线程必须有自己的事件循环
- 不鼓励线程间细粒度交互,推荐通过 Cap'n Proto RPC 进行线程间通信
- 模型与 JavaScript(特别是 Node.js)的并发模型相似
Promise 机制
异步操作返回 Promise 对象,代表尚未完成的操作结果。Promise 有三种状态:
- 待定(Pending)
- 已解决(Fulfilled)
- 已拒绝(Rejected)
// 示例 Promise 接口
kj::Promise<kj::String> fetchHttp(kj::StringPtr url);
kj::Promise<void> sendEmail(kj::StringPtr address,
kj::StringPtr title, kj::StringPtr body);
回调处理
使用 then()
方法注册回调处理 Promise 结果:
kj::Promise<kj::String> contentPromise = fetchHttp("http://example.com");
kj::Promise<int> lineCountPromise = contentPromise.then([](kj::String&& content) {
return countChars(content, '\n');
});
错误处理
then()
可接受第二个参数处理异常:
kj::Promise<int> lineCountPromise = promise.then(
[](kj::String&& content) { return countChars(content, '\n'); },
[](kj::Exception&& exception) { return 0; }); // 错误处理
等待机制
在事件回调中禁止等待,但启动事件循环的代码可以使用 wait()
:
kj::EventLoop loop;
kj::WaitScope waitScope(loop);
kj::String content = contentPromise.wait(waitScope);
取消操作
丢弃 Promise 会自动取消相关操作,如需取消通知可使用 attach()
方法。
延迟执行
非异步回调默认延迟执行,可通过以下方式强制立即执行:
- 添加到
kj::TaskSet
- 使用
wait()
- 调用
eagerlyEvaluate()
- 使用
detach()
(不推荐)
代码生成
给定以下接口定义:
interface Directory {
create @0 (name :Text) -> (file :File);
open @1 (name :Text) -> (file :File);
remove @2 (name :Text);
}
编译器会生成客户端和服务端代码。
客户端实现
Client
类代表远程服务引用,是值类型(使用引用计数):
struct Directory {
// ... 省略其他定义
class Client : public virtual capnp::Capability::Client {
public:
// 构造方法
Client(std::nullptr_t);
Client(kj::Own<Directory::Server> server);
Client(kj::Promise<Client> promise);
Client(kj::Exception exception);
// 方法请求接口
capnp::Request<CreateParams, CreateResults> createRequest();
capnp::Request<OpenParams, OpenResults> openRequest();
capnp::Request<RemoveParams, RemoveResults> removeRequest();
};
};
使用示例:
Directory::Client dir = ...;
auto request = dir.openRequest();
request.setName("foo");
auto promise = request.send();
auto promise2 = promise.getFile().getSizeRequest().send();
服务端实现
Server
是抽象接口,子类需实现具体方法:
class DirectoryImpl final: public Directory::Server {
public:
kj::Promise<void> open(OpenContext context) override {
auto iter = files.find(context.getParams().getName());
KJ_REQUIRE(iter != files.end(), "File not found.");
context.getResults().setFile(iter->second);
return kj::READY_NOW;
}
private:
std::map<kj::StringPtr, File::Client> files;
};
RPC 初始化
Cap'n Proto 提供 "EZ RPC" 类简化客户端和服务端启动。
启动客户端
#include <capnp/ez-rpc.h>
int main(int argc, const char* argv[]) {
capnp::EzRpcClient client(argv[1], 5923);
auto& waitScope = client.getWaitScope();
MyInterface::Client cap = client.getMain<MyInterface>();
auto request = cap.fooRequest();
request.setParam(123);
auto response = request.send().wait(waitScope);
std::cout << response.getResult() << std::endl;
return 0;
}
支持连接地址格式:
- DNS 主机名
- IPv4/IPv6 地址
- Unix 域套接字(
unix:/path
) - 抽象 Unix 域套接字(
unix-abstract:identifier
)
启动服务端
#include <capnp/ez-rpc.h>
int main(int argc, const char* argv[]) {
capnp::EzRpcServer server(kj::heap<MyInterfaceImpl>(), argv[1], 5923);
auto& waitScope = server.getWaitScope();
kj::NEVER_DONE.wait(waitScope);
}
支持绑定地址格式:
- DNS 主机名
- IPv4/IPv6 地址
*
表示所有本地接口- Unix 域套接字
- 抽象 Unix 域套接字
最佳实践
- 错误处理:始终为 Promise 添加错误处理回调
- 资源管理:合理使用
attach()
管理资源生命周期 - 线程安全:避免跨线程共享 Client 对象
- 性能优化:利用管道化减少网络往返
- 代码组织:将接口定义与实现分离
通过本文介绍,开发者可以快速掌握 Cap'n Proto C++ RPC 的核心概念和使用方法,构建高效的分布式系统。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考