JSON-RPC 服务与端点设计
SpinningMomo 的原生核心(C++20 协程)通过一套统一的 JSON-RPC 2.0 分发层对外暴露能力:传输无关的 core::rpc 核心(请求解析、访问控制、方法注册表)+ WebView 桥接(core::webview::rpc_bridge)+ 前端调用封装(web/src/composables/useRpc.ts)。
Purpose and Scope
本页覆盖 JSON-RPC 服务端点的完整设计与实现,即 src/core/rpc/ 命名空间 core::rpc 中的:
- 请求/响应/错误协议数据结构(
JsonRpcRequest、JsonRpcSuccessResponse、JsonRpcErrorResponse、RpcError) - 访问等级模型(
AccessLevel)与权限闸门 - 方法注册机制(
register_method的类型擦除包装) - 统一分发入口
process_request的完整控制流 - 系统内建方法(
system.listMethods、system.getAccessLevel、system.methodSignature) - WebView 桥接层(
rpc_bridge.hpp)暴露的入口签名
有意留给兄弟页面的内容:
- WebView2 消息桥接的进程内实现细节(
rpc_bridge.cpp的 WRL/COM 事件适配)与前端useRpccomposable 的响应式封装 —— 本页仅说明其与core::rpc的边界。 - 具体业务 RPC 方法清单(如各功能域注册的端点)属于各自能力页。
- RPC 状态对象
app_state.rpc(core/rpc/state.hpp)的生命周期管理。
Overview
core::rpc 是一个传输无关的 JSON-RPC 2.0 分发核心。它只关心三件事:
- 协议正确性:严格校验
jsonrpc == "2.0"、方法名存在性、参数可解析性,并用标准错误码(-32700~-32003)返回结构化错误。 - 类型安全:业务处理器以强类型
Request/Response结构体编写,register_method在注册期把泛型 JSON 参数转换为强类型请求,把业务结果转换回 JSON-RPC 成功/错误响应,业务代码完全不接触原始 JSON。 - 安全边界:调用者访问等级(
local/lan)由传输层确定后注入到process_request,请求体自身无法声明权限;每个方法注册时声明required_access,分发时做最终权限闸门。
关键设计意图(WHY):
caller_access由外部确定而非请求体声明(见 rpc.hpp):WebView2 与 loopback HTTP 天然是本机来源(local),经令牌认证的局域网 HTTP 是lan。权限是传输层事实,不是客户端可伪造的声明。- 注册期类型擦除(
std::move_only_function):注册表独占业务处理器所有权,包装层只保留可重复const调用能力,避免了共享可变状态。 - 协程贯穿全链路:处理器签名是
asio::awaitable<RpcResult<T>>(RpcAwaitable),分发入口是asio::awaitable<std::string>(RpcJsonAwaitable),因此任何 asio IO 上下文(WebView 桥接、HTTP 服务)都能直接co_await。
Architecture
分层说明:
| 层 | 组件 | 职责 |
|---|---|---|
| 传输层 | core::webview::rpc_bridge、HTTP 服务 | 接收消息,确定 caller_access,调用 process_request,把返回的 JSON 字符串写回对端 |
| 分发核心 | process_request / handle_system_method / registry / execute_registered_method | 协议解析、版本校验、系统方法分派、方法查找、访问控制、异常兜底 |
| 包装层 | wrapped_handler(register_method 内生成) | rfl::from_generic 参数反序列化 → 调业务协程 → 响应序列化 |
| 业务层 | 各功能域的 AsyncHandler<Request, Response> | 强类型业务逻辑,通过注入的 core::AppState& 访问全局状态 |
| 状态层 | app_state.rpc->registry | std::unordered_map<std::string, MethodInfo>,注册表存活于应用状态 |
分发核心不依赖任何具体传输:它接收 std::string 请求体并返回 std::string 响应体(协程),因此 WebView postMessage 与 HTTP 两种通道共用同一套协议、权限与错误语义。
协议数据模型(types.hpp)
所有协议结构集中在 src/core/rpc/types.hpp,命名空间 core::rpc,并统一通过 rfl::SnakeCaseToCamelCase 做 C++ snake_case ↔ JSON camelCase 映射。
错误码(ErrorCode)
标准 JSON-RPC 2.0 错误码加上一个自定义 AccessDenied:
| 错误码 | 枚举值 | 含义 |
|---|---|---|
-32700 | ParseError | JSON 解析失败 |
-32600 | InvalidRequest | 无效请求(如 jsonrpc != "2.0") |
-32601 | MethodNotFound | 方法未注册 |
-32602 | InvalidParams | 参数无法反序列化为目标 Request 结构 |
-32603 | InternalError | 业务协程抛出未捕获异常 |
-32000 | ServerError | 服务器错误 |
-32003 | AccessDenied | 当前访问等级无权调用该端点 |
Source: types.hpp
访问等级(AccessLevel)
1// RPC 调用者访问等级。local 高于 lan;WebView2 和 loopback HTTP 属于 local,
2// 经过令牌认证的局域网 HTTP 属于 lan。
3enum class AccessLevel : std::uint8_t {
4 lan = 0,
5 local = 1,
6};local = 1 > lan = 0,因此权限比较就是简单的 caller_access < required_access 整数比较。默认注册的方法是 local,公开给 LAN 必须显式标记 —— 这是一个"默认安全"(secure by default)的选择。
Source: types.hpp
核心结构体
RpcError:code(int)、message(string)、可选data。RpcResult<T> = std::expected<T, RpcError>:业务协程的统一返回类型,成功/失败都在类型系统中显式表达,不使用异常做常规错误路径。RpcAwaitable<T> = asio::awaitable<RpcResult<T>>、RpcJsonAwaitable = asio::awaitable<std::string>。JsonRpcRequest:jsonrpc、method、可选params(rfl::Generic,支持对象/数组/null)、可选id。JsonRpcSuccessResponse/JsonRpcErrorResponse:jsonrpc默认"2.0",分别携带result或error。MethodInfo:注册表条目,含name、description、params_schema(JSON Schema)、required_access与类型擦除后的handler。EmptyParams:无参方法使用的空参数结构。
Source: types.hpp
方法注册:register_method 的类型擦除
register_method 是本设计的核心机制:它把任意 AsyncHandler<Request, Response> 包装成签名统一的 (rfl::Generic params, rfl::Generic id) -> RpcJsonAwaitable 协程,从而让单个 unordered_map<std::string, MethodInfo> 能承载所有方法。
1template <typename Request, typename Response>
2inline auto register_method(core::AppState& app_state,
3 std::unordered_map<std::string, MethodInfo>& registry,
4 const std::string& method_name, AsyncHandler<Request, Response> handler,
5 const std::string& description = "",
6 // 默认只允许本机,公开给 LAN 的方法必须显式标记。
7 AccessLevel required_access = AccessLevel::local) -> void {
8 // 注册表独占业务处理器,包装层只保留可重复 const 调用能力
9 auto wrapped_handler = [handler = std::move(handler), &app_state](
10 rfl::Generic params_generic, rfl::Generic id) -> RpcJsonAwaitable {
11 // 把通用 JSON 参数转换为当前方法的强类型请求
12 auto request_result =
13 rfl::from_generic<Request, rfl::SnakeCaseToCamelCase, rfl::DefaultIfMissing>(
14 params_generic);
15 if (!request_result) {
16 co_return create_error_response(id, ErrorCode::InvalidParams,
17 "Invalid parameters: " + request_result.error().what());
18 }
19
20 // 调用业务协程并保留统一的 AppState 注入方式
21 auto result = co_await handler(app_state, request_result.value());
22
23 // 将业务结果转换为 JSON-RPC 成功或错误响应
24 if (result) {
25 JsonRpcSuccessResponse success_response;
26 success_response.id = id;
27 success_response.result = rfl::to_generic<rfl::SnakeCaseToCamelCase>(result.value());
28 co_return rfl::json::write<rfl::SnakeCaseToCamelCase>(success_response);
29 } else {
30 const auto& error = result.error();
31 Logger().error("Error response: {}", error.message);
32 co_return create_error_response(id, static_cast<ErrorCode>(error.code), error.message);
33 }
34 };
35
36 std::string params_schema;
37 if constexpr (core::build_config::rpc_json_schema_enabled()) {
38 params_schema = rfl::json::to_schema<Request, rfl::SnakeCaseToCamelCase>();
39 }
40
41 // 注册表取得包装处理器的唯一所有权
42 registry[method_name] = MethodInfo{.name = method_name,
43 .description = description,
44 .params_schema = std::move(params_schema),
45 .required_access = required_access,
46 .handler = std::move(wrapped_handler)};
47}Source: rpc.hpp
逐段解读:
- 参数转换:
rfl::from_generic<Request, rfl::SnakeCaseToCamelCase, rfl::DefaultIfMissing>把泛型 JSON 转成强类型Request;DefaultIfMissing让缺省字段取类型默认值而不是直接失败。转换失败立即返回-32602 InvalidParams,并把 reflect-cpp 的错误信息拼进 message。 - AppState 注入:
app_state通过引用捕获进闭包,业务协程签名固定为RpcAwaitable<Response>(core::AppState&, const Request&) const,因此业务层无任何全局单例查找。 - 结果双向映射:
result(std::expected)成功 →JsonRpcSuccessResponse;失败 → 直接把RpcError::code静态转换为ErrorCode生成错误响应。也就是说,业务协程自己就能决定返回哪个 JSON-RPC 错误码(例如返回{-32000, "..."}),分发层不做二次翻译。 - Schema 可选编译:
core::build_config::rpc_json_schema_enabled()是编译期开关,关闭时不生成参数 JSON Schema,省去体积/启动开销;system.methodSignature此时返回空 schema 字符串。 - 处理器签名:
AsyncHandler使用std::move_only_function<... const>,注册表独占所有权,包装后的MethodInfo::handler仍是可重复const调用的协程工厂。
核心分发流程:process_request
process_request 是所有传输共用的唯一入口,签名刻意把权限判断外部化:
1// 处理JSON-RPC请求
2// caller_access 由 WebView 或 HTTP 层确定,不能由请求体自行声明。
3auto process_request(core::AppState& app_state, const std::string& request_json,
4 AccessLevel caller_access) -> RpcJsonAwaitable;Source: rpc.hpp
实现在 rpc.cpp,控制流如下:
关键细节(均出自 rpc.cpp):
- id 保值:
const rfl::Generic request_id = request.id.value_or(rfl::Generic())。即使请求未带id(形似 notification 的请求),错误响应也带id: null语义的空Generic,保证客户端总能关联。 - 权限闸门位置:注释明确写着"这是所有注册 RPC 的最终权限闸门,不能只依赖前端隐藏按钮"(rpc.cpp L156-L161)。访问被拒时额外
Logger().warn记录方法名,便于排查可疑探测。 - 参数兜底:
request.params.value_or(rfl::Generic::Object())—— 缺省参数按空对象处理,交给rfl::DefaultIfMissing填默认值。 - 双层异常兜底:
execute_registered_method内部捕获业务协程异常转为-32603 InternalError(含e.what()),process_request外层再兜一层Unexpected error,保证任何异常都变成合法 JSON-RPC 错误响应而不是让协程崩溃。 - 日志分级:成功路径
Logger().trace记录完整响应,错误路径Logger().error,拒绝路径Logger().warn—— 与运维观测需求对齐。
execute_registered_method 本体(rpc.cpp L105-L117)只是 co_await method_info.handler(params_generic, request_id) 加 try/catch,真正的类型转换发生在注册期生成的 wrapped_handler 内 —— 分发层与业务层通过类型擦除协程解耦。
系统内建方法(system.*)
handle_system_method(rpc.cpp L41-L102)在业务注册表查找之前拦截三个元方法,返回 std::nullopt 表示"不是系统方法,继续走业务分发":
| 方法 | 参数 | 行为 | 权限语义 |
|---|---|---|---|
system.listMethods | 无 | 返回调用者等级可见的方法名+描述列表 | get_method_list 内部逐条 caller_access < info.required_access 过滤(rpc.cpp L25-L38) |
system.getAccessLevel | 无 | 返回 "local" 或 "lan" 字符串 | 前端只读传输层已确认的等级,"不重复推断来源" |
system.methodSignature | {method: string} | 返回 method/description/params_schema | 元数据也是能力信息:受限方法的签名同样返回 -32003,防止用它探测受限接口(rpc.cpp L84-L88) |
system.methodSignature 的参数校验分两级:params 缺失返回 InvalidParams("Missing required parameter: method"),rfl::from_generic 失败返回 InvalidParams(带 reflect-cpp 详情),方法不存在返回 MethodNotFound。
传输适配:WebView rpc_bridge
桥接层把 WebView2 的原生消息事件接到 core::rpc,接口极小:
1namespace core::webview::rpc_bridge {
2
3// 初始化rpc桥接
4auto initialize_rpc_bridge(core::AppState& state) -> void;
5
6// 处理来自前端的rpc消息
7auto handle_webview_message(core::AppState& state, const std::string& message)
8 -> asio::awaitable<void>;
9
10// 向前端发送通知 (JSON-RPC notification)
11auto send_notification(core::AppState& state, const std::string& method, const std::string& params)
12 -> void;
13
14// 创建交给 WebView2/WRL 的可复制消息回调;COM 事件适配层需要复制 callable
15auto create_message_handler(core::AppState& state) -> std::function<void(const std::string&)>;
16
17} // namespace core::webview::rpc_bridgeSource: rpc_bridge.hpp
设计意图:
create_message_handler返回 可复制的std::function,因为 COM/WRL 事件适配层要求 callable 可拷贝 —— 这正是MethodInfo::handler用move_only而桥接回调用std::function的原因:所有权语义按宿主 API 约束分别选择。send_notification是服务端主动推送(JSON-RPC notification:有method/params、无id),用于事件广播,与请求-响应通道分开。- WebView 来源固定按
AccessLevel::local进入process_request;前端另有web/src/composables/useRpc.ts封装调用。
交互时序(端到端)
Usage Examples
错误响应构造(分发层统一出口)
1// 创建标准错误响应
2auto create_error_response(rfl::Generic request_id, ErrorCode error_code,
3 const std::string& message) -> std::string {
4 JsonRpcErrorResponse error_response;
5 error_response.id = request_id;
6 error_response.error = RpcError{.code = static_cast<int>(error_code), .message = message};
7 return rfl::json::write<rfl::SnakeCaseToCamelCase>(error_response);
8}Source: rpc.cpp
所有错误路径(解析失败、版本非法、方法未找到、权限不足、参数非法、内部异常)最终都汇聚到这一个函数,保证线上错误响应结构 100% 一致。
可见方法列表的访问过滤
1auto get_method_list(const core::AppState& app_state, AccessLevel caller_access)
2 -> std::vector<MethodListItem> {
3 std::vector<MethodListItem> methods;
4 const auto& registry = app_state.rpc->registry;
5
6 for (const auto& [name, info] : registry) {
7 if (caller_access < info.required_access) {
8 continue;
9 }
10 methods.emplace_back(MethodListItem{.name = name, .description = info.description});
11 }
12
13 return methods;
14}Source: rpc.cpp
system.listMethods 对 LAN 调用者隐藏所有 local 方法,使"能力发现"本身不会泄露本机端点清单。
业务处理器签名(AsyncHandler)
1// 异步处理器签名
2template <typename Request, typename Response>
3using AsyncHandler =
4 std::move_only_function<RpcAwaitable<Response>(core::AppState&, const Request&) const>;Source: rpc.hpp
业务端点的标准形态:接收 AppState& 注入 + const Request& 强类型参数,返回 std::expected 风格的 RpcAwaitable<Response>。各功能域如何据此注册具体端点见对应能力页。
Configuration Options
core::rpc 没有运行时配置文件项;可配置面由编译期开关与注册期参数构成:
| 选项 | 类型 / 位置 | 默认值 | 说明 |
|---|---|---|---|
core::build_config::rpc_json_schema_enabled() | 编译期常量 | (见 build_config) | 为 true 时 register_method 用 rfl::json::to_schema 生成参数 JSON Schema,供 system.methodSignature 返回;为 false 时 params_schema 为空串,节省体积 |
required_access | register_method 参数 | AccessLevel::local | 单个端点的最低访问等级;开放给 LAN 必须显式传 AccessLevel::lan |
description | register_method 参数 | "" | 方法描述,进入 MethodInfo 并通过 system.listMethods / system.methodSignature 暴露 |
method_name | register_method 参数 | 必填 | 注册表键,即 JSON-RPC method 字符串 |
registry | register_method 参数(引用) | 必填 | 目标注册表,实际即 app_state.rpc->registry |
API Reference
create_error_response(request_id, error_code, message) -> std::string
参数:
request_id(rfl::Generic):要回显的请求 ID(可为空Generic)error_code(ErrorCode):标准/自定义错误码message(const std::string&):人类可读错误信息
返回: 序列化后的 JSON-RPC 错误响应字符串(jsonrpc/id/error{code,message},camelCase)。
Source: rpc.cpp
get_method_list(app_state, caller_access) -> std::vector<MethodListItem>
参数:
app_state(const core::AppState&):读取app_state.rpc->registrycaller_access(AccessLevel):调用者等级,用于过滤
返回: 该等级可见的 {name, description} 列表(仅含 caller_access >= required_access 的方法)。
Source: rpc.cpp
handle_system_method(app_state, request, request_id, caller_access) -> std::optional<std::string>
参数:
request(const JsonRpcRequest&):已解析的请求request_id(rfl::Generic):回显 IDcaller_access(AccessLevel):调用者等级
返回: 命中 system.listMethods / system.getAccessLevel / system.methodSignature 时返回完整响应字符串;否则 std::nullopt,由调用方继续业务分发。system.methodSignature 在参数缺失/非法、方法不存在、权限不足时分别返回 InvalidParams / MethodNotFound / AccessDenied。
Source: rpc.cpp
execute_registered_method(method_info, params_generic, request_id) -> RpcJsonAwaitable
参数:
method_info(const MethodInfo&):目标方法条目params_generic(rfl::Generic):泛型参数(缺省时为空对象)request_id(rfl::Generic):回显 ID
返回: 协程,产出响应 JSON。异常: 捕获 std::exception 并转换为 -32603 InternalError(消息含 e.what()),同时 Logger().error 记录。
Source: rpc.cpp
process_request(app_state, request_json, caller_access) -> RpcJsonAwaitable
参数:
app_state(core::AppState&):注入业务处理器request_json(const std::string&):原始请求体caller_access(AccessLevel):必须由传输层确定,禁止从请求体读取
返回: 协程,产出 JSON-RPC 响应字符串;任何内部异常都被兜底为合法错误响应。
可能产出的错误: -32700 / -32600 / -32601 / -32602(由 wrapped_handler) / -32603 / -32003,以及业务协程经 RpcError::code 传入的任意码(如 -32000)。
register_method<Request, Response>(...) -> void(模板)
参数:
app_state(core::AppState&):被闭包引用捕获,注入业务协程registry(std::unordered_map<std::string, MethodInfo>&):目标注册表method_name(const std::string&):JSON-RPC 方法名handler(AsyncHandler<Request, Response>):强类型业务协程description(const std::string&,默认""):方法描述required_access(AccessLevel,默认AccessLevel::local):最低访问等级
返回: 无;以 method_name 覆盖写入 registry 一条 MethodInfo。注意:重复注册同名方法会静默覆盖旧条目(registry[method_name] = ...)。
Source: rpc.hpp
rpc_bridge 公开接口
| 函数 | 签名 | 说明 |
|---|---|---|
initialize_rpc_bridge | (core::AppState&) -> void | 初始化桥接 |
handle_webview_message | (core::AppState&, const std::string&) -> asio::awaitable<void> | 处理前端 RPC 消息(协程) |
send_notification | (core::AppState&, const std::string& method, const std::string& params) -> void | 服务端主动推送 JSON-RPC notification |
create_message_handler | (core::AppState&) -> std::function<void(const std::string&)> | 生成可复制回调,适配 COM/WRL 事件层 |
Source: rpc_bridge.hpp
Failure Modes, Edge Cases & Concurrency
错误路径全景
| 场景 | 检测点 | 响应 |
|---|---|---|
请求体不是合法 JSON / 不符 JsonRpcRequest | rfl::json::read 失败 | -32700 ParseError,id 为空 Generic |
jsonrpc 字段 ≠ "2.0" | 版本校验 | -32600 InvalidRequest |
method 未注册 | registry.find 失败 | -32601 MethodNotFound(含方法名) |
| 权限不足 | caller_access < required_access | -32003 AccessDenied + Logger().warn(含方法名) |
参数无法转成 Request | wrapped_handler 内 rfl::from_generic 失败 | -32602 InvalidParams(含 reflect-cpp 错误详情) |
| 业务协程抛异常 | execute_registered_method catch | -32603 InternalError(含 e.what()) |
| 分发层自身异常 | process_request 顶层 catch | -32603 InternalError("Unexpected error: ...") |
业务显式返回 RpcError | wrapped_handler 的 else 分支 | 以 RpcError::code 作为错误码透传 |
边界情形
- 缺省
params:request.params.value_or(rfl::Generic::Object())→ 空对象交给rfl::DefaultIfMissing填默认值;因此"无参方法 +EmptyParams"与"带默认值的可选字段"都能优雅处理。 - 缺省
id:错误响应使用空Generic(JSONnull语义),成功响应同样回显空id;协议上未区分 notification 与 request,响应总是返回。 system.methodSignature的元数据权限:受限方法即使只是查询签名也返回AccessDenied—— 元数据泄露被视为能力泄露(rpc.cpp L84-L88)。- 权限来源不可伪造:
caller_access只能来自传输层入参;代码注释明确"不能由请求体自行声明",这是防权限提升的关键不变式。
并发与所有权
- 所有权模型:
AsyncHandler是std::move_only_function,MethodInfo::handler同为 move-only,注册表独占处理器;同一个MethodInfo只能同时存在于一个 map 槽位(重复注册即覆盖)。 const可重入:包装层保留const调用语义,多个在飞请求可并发co_await同一MethodInfo::handler(协程每次调用独立帧),处理器自身无共享可变状态。AppState引用捕获:app_state以引用捕获进闭包,要求注册时app_state的存活期覆盖注册表存活期(两者实际同生命周期,见core/state/app_state.hpp);并发下业务协程对共享状态的访问安全由AppState内部各成员的同步策略保证,不属于本层职责。- IO 无关:所有协程运行在调用方提供的 asio executor 上,
core::rpc不创建线程、不持有定时器,传输层可自由选择co_spawn上下文。
Performance & Operational Notes
- 零冗余解析:请求只解析一次(
rfl::json::read),参数以rfl::Generic形式传递到 wrapped_handler 后转强类型,没有二次字符串解析。 - 日志策略:成功响应走
trace级(避免高频刷屏),错误走error,权限拒绝走warn—— 排查"为什么接口被拒"优先看 warn 日志中的方法名。 - Schema 成本控制:参数 JSON Schema 在编译期开关控制,
rpc_json_schema_enabled()关闭时完全跳过rfl::json::to_schema,system.methodSignature仍可用但返回空 schema。 - 扩展面:新增端点只需定义
Request/Response结构体 + 一个AsyncHandler并调用register_method,无需触碰分发核心;新增传输通道只需确定该通道的caller_access并复用process_request。
Related Links
- rpc.hpp —— 注册与分发核心声明
- rpc.cpp —— 分发/系统方法实现
- types.hpp —— 协议结构与错误码
- rpc_bridge.hpp —— WebView 传输桥接接口
- useRpc.ts —— 前端调用封装(兄弟页面主题)