C++新手实战:从零构建轻量级JsonRPC框架,掌握RPC核心原理
1. 项目概述为什么新手要从JsonRPC框架入手如果你刚学完C的基础语法正愁找不到一个能串联起网络、序列化、多线程这些“大块头”知识点的实战项目那自己动手实现一个轻量级的JsonRPC框架绝对是个黄金选择。这项目听起来有点唬人好像得是资深工程师才碰的东西但其实它的内核非常清晰就是让一个程序能调用另一个程序里的函数哪怕它们在不同的机器上中间用JSON这种人类和机器都容易看懂的格式来传话。对新手而言它的价值在于你不需要一开始就啃透像gRPC那样庞大的工业级框架而是可以自己从零搭建亲手摸清远程过程调用RPC的每一个齿轮是如何咬合的。为什么是JsonRPC而不是别的首先JSON格式简单直观你不需要像处理Protocol Buffers那样先学一套定义语言.proto文件直接用你熟悉的nlohmann/json这类库就能轻松搞定序列化和反序列化。其次它基于HTTP协议这意味着你可以用浏览器、Postman这些最普通的工具来调试和测试你的服务端学习门槛瞬间降低。最后这个项目像是一个“知识熔炉”它能逼着你去实践Socket网络编程、理解TCP/HTTP协议、设计应用层协议、处理并发请求、管理内存和资源生命周期甚至还要考虑如何设计一个易用的API。当你把这个框架跑通你收获的不仅仅是一个能用的工具更是一张清晰的后端服务通信原理图。这个项目适合已经掌握C核心语法类、模板、STL、对网络编程有基本概念知道socket、端口是什么的开发者。它不要求你事先精通多线程或设计模式因为在实现过程中你会遇到这些问题并带着问题去学习印象会更深刻。接下来我会带你从设计思路到代码实现一步步拆解这个框架的构建过程其中会穿插大量我趟过的坑和总结出的技巧。2. 核心设计思路与架构拆解在动手写代码之前我们必须把蓝图画清楚。一个最简单的JsonRPC框架核心就是三件事客户端怎么发请求、服务端怎么处理请求、数据怎么在网络上传。我们的目标是设计一个清晰、可扩展且对新手友好的结构。2.1 协议层设计理解JsonRPC 2.0规范我们不是凭空造轮子而是遵循 JsonRPC 2.0 规范。这能让我们的框架具备与其他语言实现的JsonRPC服务交互的潜力。一个标准的请求和响应大概长这样请求 (Request):{ jsonrpc: 2.0, method: subtract, params: {minuend: 42, subtrahend: 23}, id: 3 }jsonrpc: 固定为“2.0”标识协议版本。method: 字符串表示要调用的远程方法名。params: 对象或数组包含传给方法的参数。这是设计的关键我们的框架需要能灵活处理这两种形式。id: 请求标识符用于将响应和请求对应起来。可以是数字、字符串null表示通知不需要响应。成功响应 (Success Response):{ jsonrpc: 2.0, result: 19, id: 3 }错误响应 (Error Response):{ jsonrpc: 2.0, error: { code: -32601, message: Method not found }, id: 3 }设计心得一开始我试图支持所有可选字段后来发现对于新手项目严格遵循这个最小集就足够了。重点是处理好id的映射和错误对象的规范构造。2.2 整体架构与模块划分基于协议我们可以把框架划分为几个松耦合的模块这样代码好维护也方便你分阶段实现序列化/反序列化模块核心职责是将C的函数调用信息方法名、参数打包成JSON字符串以及将收到的JSON字符串解析成C能理解的结构。这里我们会重度依赖像nlohmann/json这样的第三方JSON库。网络通信模块负责底层的字节流传输。对于新手我强烈建议先使用HTTP协议。因为HTTP有现成的库如cpp-httplib,Boost.Beast帮你处理了连接管理、消息边界通过Content-Length等复杂问题让你能更专注于RPC逻辑本身。后期可以抽象出一个通信接口再替换为更高效的TCP裸套接字实现。服务端核心模块注册中心维护一个方法名 - 函数对象的映射表。这里的关键是如何存储不同类型的C函数全局函数、类成员函数、lambda表达式请求路由器根据请求中的method字段从注册中心找到对应的函数并调用。执行器负责调用函数并捕获可能的异常将其转化为规范的JsonRPC错误响应。客户端核心模块桩Stub生成器理想情况下我们希望通过像调用本地函数一样调用远程函数。这需要动态生成代理类或利用模板技巧。请求构造与发送将本地调用信息封装成JsonRPC请求并通过网络模块发送。响应处理解析响应提取result或抛出携带error信息的异常。架构演进建议不要想着一口吃成胖子。我的实现路径是先实现一个单线程、同步阻塞的版本。在这个版本里服务端一次只处理一个连接客户端发送请求后也阻塞等待响应。这个版本能让你跑通核心流程。之后再引入线程池让服务端能并发处理多个请求这时你会遇到线程安全和资源管理的问题。最后再考虑异步非阻塞如基于事件循环的高性能版本。3. 关键模块的C实现详解现在我们进入具体的代码实现环节。我会用nlohmann/json作为JSON库用cpp-httplib作为HTTP服务器库来举例因为它们足够轻量且易于集成。3.1 基础数据结构的定义首先我们需要定义代表JsonRPC请求和响应的C数据结构。#include nlohmann/json.hpp using json nlohmann::json; namespace jsonrpc { // 错误对象 struct Error { int code; std::string message; json data; // 可选字段 json to_json() const { json j; j[code] code; j[message] message; if (!data.is_null()) { j[data] data; } return j; } static Error from_json(const json j) { Error e; j.at(code).get_to(e.code); j.at(message).get_to(e.message); if (j.contains(data)) { e.data j[data]; } return e; } }; // 预定义一些标准错误码 namespace error_codes { const int PARSE_ERROR -32700; const int INVALID_REQUEST -32600; const int METHOD_NOT_FOUND -32601; const int INVALID_PARAMS -32602; const int INTERNAL_ERROR -32603; // 服务端自定义错误码范围-32000 到 -32099 } // 请求对象 struct Request { std::string jsonrpc 2.0; std::string method; json params; // 可以是对象或数组 json id; // 可以是数字、字符串或null static std::optionalRequest parse(const std::string json_str) { try { json j json::parse(json_str); Request req; // 校验必需字段 if (j.at(jsonrpc) ! 2.0 || !j.contains(method)) { return std::nullopt; } j.at(method).get_to(req.method); if (j.contains(params)) { req.params j[params]; } if (j.contains(id)) { req.id j[id]; } else { req.id nullptr; // 表示为通知 } return req; } catch (const json::exception e) { // 解析失败 return std::nullopt; } } }; // 响应对象 struct Response { std::string jsonrpc 2.0; std::optionaljson result; // 成功时有值 std::optionalError error; // 失败时有值 json id; json to_json() const { json j; j[jsonrpc] jsonrpc; if (result.has_value()) { j[result] result.value(); } else if (error.has_value()) { j[error] error.value().to_json(); } j[id] id; return j; } }; } // namespace jsonrpc避坑指南注意Request::id和Response::id的类型是json而不是int或std::string。这是因为规范允许字符串ID甚至null对于通知。使用json类型可以自然地容纳这几种可能序列化时也省心。std::optional是C17的好东西它能清晰地表达“可能有可能无”的语义。3.2 服务端核心方法注册与调用分发这是服务端最有趣的部分。我们需要一个地方来注册函数并且这些函数的参数和返回值类型可能千差万别。这里可以利用C的std::function和模板。第一步定义一个通用的函数包装器由于std::function的类型是确定的我们需要一个非模板的基类来把它们存到一个容器里。class Invokable { public: virtual ~Invokable() default; // 核心调用接口输入是JSON参数输出是JSON结果 virtual json invoke(const json params) 0; }; template typename... Args class FunctionInvokable : public Invokable { public: using FuncType std::functionjson(Args...); explicit FunctionInvokable(FuncType func) : func_(std::move(func)) {} json invoke(const json params) override { // 关键如何将json::array或json::object解包为Args... return std::apply(func_, parseParamsArgs...(params)); } private: FuncType func_; // 一个辅助函数将json参数解析为tuple template typename... Params static std::tupleParams... parseParams(const json j) { if (j.is_array()) { return parseArrayParams...(j); } else if (j.is_object()) { return parseObjectParams...(j); } else { throw std::invalid_argument(Params must be array or object); } } // 具体解析数组和对象的实现需要用到索引序列等技巧此处略去详细展开 };实现难点parseParams是这里最复杂的地方。你需要根据Args...的类型列表从JSON数组按位置提取值或从JSON对象按键名提取值。这需要用到模板元编程技巧如std::index_sequence。对于新手可以先实现一个简化版强制要求参数必须以JSON对象形式传递并且键名与函数参数名对应或按固定顺序如arg1,arg2。这虽然不够灵活但能让你快速跑通流程。第二步实现方法注册表class Server { public: template typename... Args void registerMethod(const std::string name, std::functionjson(Args...) func) { auto invokable std::make_uniqueFunctionInvokableArgs...(std::move(func)); methods_[name] std::move(invokable); } // 一个更方便的注册模板函数可以接受普通函数、lambda等 template typename Func void bind(const std::string name, Func func) { // 这里需要将func转换为std::function涉及函数签名推导是另一个模板技巧点 } json handleRequest(const Request req) { Response resp; resp.id req.id; // 如果是通知id为null可以不发送响应 if (req.id.is_null()) { // ... 处理通知但此处我们简化仍执行但不返回 // 根据规范通知不期待响应所以服务端处理完即可不应发送任何回复。 // 这意味着这个函数可能不需要返回值。我们需要调整设计。 // 让我们修改handleRequest 返回 std::optionalResponse仅当不是通知时才返回Response。 } auto it methods_.find(req.method); if (it methods_.end()) { resp.error Error{error_codes::METHOD_NOT_FOUND, Method not found: req.method}; return resp.to_json(); } try { json result it-second-invoke(req.params); resp.result result; } catch (const std::exception e) { // 捕获调用过程中的异常转化为内部错误 resp.error Error{error_codes::INTERNAL_ERROR, std::string(Internal error: ) e.what()}; } catch (...) { resp.error Error{error_codes::INTERNAL_ERROR, Unknown internal error}; } return resp.to_json(); } private: std::unordered_mapstd::string, std::unique_ptrInvokable methods_; };核心技巧registerMethod是框架易用性的关键。我们利用模板让用户可以用server.registerMethod(add, [](int a, int b) - json { return a b; });这样的自然语法来注册函数。背后的模板推导和类型擦除通过Invokable基类是C的进阶知识如果一开始实现困难可以先用一个固定签名的函数类型比如std::functionjson(const json)来简化让用户自己在函数内部解析JSON参数。3.3 集成HTTP服务器与启动服务有了核心的Server类我们可以用cpp-httplib快速搭建一个HTTP接口。#include httplib.h void runJsonRpcServer(int port) { Server rpc_server; // 注册示例方法 rpc_server.registerMethod(add, [](int a, int b) - json { return a b; }); rpc_server.registerMethod(echo, [](const std::string message) - json { return {{reply, message}}; }); httplib::Server svr; svr.Post(/rpc, [rpc_server](const httplib::Request req, httplib::Response res) { // 1. 解析JsonRPC请求 auto opt_rpc_req Request::parse(req.body); if (!opt_rpc_req) { // 解析失败返回PARSE_ERROR Response err_resp; err_resp.id nullptr; // 无法得知id err_resp.error Error{error_codes::PARSE_ERROR, Parse error}; res.set_content(err_resp.to_json().dump(), application/json); return; } // 2. 处理请求 json rpc_response rpc_server.handleRequest(*opt_rpc_req); // 3. 返回HTTP响应 res.set_content(rpc_response.dump(), application/json); }); svr.listen(0.0.0.0, port); }注意事项这里有一个重要的设计点。我们的handleRequest现在总是返回一个JSON响应。但根据JsonRPC规范对于“通知”即请求中id为null服务端不应返回任何响应。因此更严谨的做法是让handleRequest返回std::optionaljson只在需要响应时返回值。HTTP处理函数中如果得到std::nullopt就设置HTTP状态码为204No Content并返回空body。这个细节体现了对协议的严格遵循。4. 客户端Stub的便捷调用实现一个友好的RPC框架应该让客户端像调用本地函数一样进行远程调用。我们可以通过模板和宏来生成轻量级的“桩”代码。4.1 基础客户端类首先实现一个能发送原始请求的客户端。class HttpClient { public: HttpClient(const std::string host, int port) : host_(host), port_(port) {} std::optionalResponse sendRequest(const Request req) { httplib::Client cli(host_, port_); // 设置超时等参数 cli.set_connection_timeout(2); cli.set_read_timeout(5); json j req; // 需要为Request实现到json的转换 auto res cli.Post(/rpc, j.dump(), application/json); if (res res-status 200) { try { json j_res json::parse(res-body); return Response::from_json(j_res); // 需要实现Response的from_json } catch (...) { return std::nullopt; } } return std::nullopt; } private: std::string host_; int port_; };4.2 利用模板实现类型安全的代理我们希望实现这样的调用方式int sum client.callint(add, 10, 20);template typename ResultType, typename... Args ResultType call(const std::string method, Args... args) { Request req; req.method method; req.jsonrpc 2.0; req.id generateId(); // 生成一个唯一ID // 将参数包 args... 转换为json数组或对象 // 这里简化处理转换为数组 req.params json::array({args...}); auto opt_resp sendRequest(req); if (!opt_resp) { throw std::runtime_error(Network or server error); } auto resp *opt_resp; if (resp.error.has_value()) { throw std::runtime_error(RPC Error: resp.error-message); } if (!resp.result.has_value()) { throw std::runtime_error(Response has no result); } try { return resp.result-getResultType(); } catch (const json::exception e) { throw std::runtime_error(Type mismatch when parsing result); } }进阶思考上面的call模板函数虽然能用但每次调用都要拼装Request、发送HTTP请求、解析Response代码是重复的。我们可以利用C的仿函数和运算符重载创造出更神奇的语法。例如可以定义一个Proxy对象通过重载operator-()使得client-add(10, 20)这样的调用成为可能。这需要更深入的模板元编程称为“动态接口”或“编译期反射”的模拟是框架进阶的绝佳练习。5. 从原型到生产性能、并发与错误处理当基础版本跑通后你会自然地去思考如何让它更健壮、更高效。以下是几个关键的进阶方向。5.1 引入线程池处理并发请求单线程服务器只能顺序处理请求吞吐量极低。引入线程池是第一步。#include thread #include vector #include queue #include mutex #include condition_variable #include functional class ThreadPool { public: ThreadPool(size_t num_threads) { for (size_t i 0; i num_threads; i) { workers_.emplace_back([this] { while (true) { std::functionvoid() task; { std::unique_lockstd::mutex lock(queue_mutex_); cv_.wait(lock, [this] { return stop_ || !tasks_.empty(); }); if (stop_ tasks_.empty()) return; task std::move(tasks_.front()); tasks_.pop(); } task(); } }); } } templateclass F void enqueue(F f) { { std::lock_guardstd::mutex lock(queue_mutex_); tasks_.emplace(std::forwardF(f)); } cv_.notify_one(); } ~ThreadPool() { { std::lock_guardstd::mutex lock(queue_mutex_); stop_ true; } cv_.notify_all(); for (std::thread worker : workers_) { worker.join(); } } private: std::vectorstd::thread workers_; std::queuestd::functionvoid() tasks_; std::mutex queue_mutex_; std::condition_variable cv_; bool stop_ false; }; // 在HTTP处理函数中使用线程池 ThreadPool pool(4); svr.Post(/rpc, [pool, rpc_server](const httplib::Request req, httplib::Response res) { // 注意req和res的生命周期它们可能在任务执行时已经失效。 // 因此必须拷贝所需数据。 std::string body req.body; pool.enqueue([body, rpc_server, res]() mutable { // 这里res的引用是危险的 // 处理请求... // 问题如何将结果写回res这需要更复杂的上下文传递。 }); });重大陷阱这是网络编程新手最容易栽跟头的地方。你不能直接在线程池任务中使用原始请求和响应对象的引用因为当任务被执行时处理这个HTTP连接的原始函数可能已经返回req和res对象早已被销毁使用它们会导致未定义行为崩溃或数据错误。正确的做法是捕获所有需要的数据的副本如req.body并且需要一种机制将处理结果传回给HTTP响应流。一种常见模式是使用std::promise和std::future或者传递一个回调函数。但更简单直接的做法是使用支持异步接口的HTTP库如Boost.Beast或者继续使用cpp-httplib但采用其内置的多线程模式设置svr.set_base_dir(...)后它内部会使用线程池我们只需保证rpc_server.handleRequest是线程安全的。5.2 确保服务端核心的线程安全当多个线程同时调用rpc_server.handleRequest并可能同时读写methods_注册表时就会引发数据竞争。我们需要加锁。class ThreadSafeServer { public: template typename... Args void registerMethod(const std::string name, std::functionjson(Args...) func) { std::lock_guardstd::mutex lock(methods_mutex_); auto invokable std::make_uniqueFunctionInvokableArgs...(std::move(func)); methods_[name] std::move(invokable); } json handleRequest(const Request req) { // 读操作也需要加锁因为可能在其他线程注册新方法的同时进行查找 std::unique_lockstd::mutex lock(methods_mutex_); auto it methods_.find(req.method); if (it methods_.end()) { lock.unlock(); // 提前释放锁构造错误响应不需要锁 // ... 返回METHOD_NOT_FOUND错误 } auto invokable it-second; lock.unlock(); // 关键在调用用户函数前释放锁。 // 调用用户函数可能耗时很长持有锁会严重降低并发度。 json result invokable-invoke(req.params); // ... 构造成功响应 return resp.to_json(); } private: std::unordered_mapstd::string, std::unique_ptrInvokable methods_; mutable std::mutex methods_mutex_; // mutable允许在const成员函数中加锁 };性能要点注意handleRequest中锁的粒度。在找到对应的Invokable对象后应立即释放锁lock.unlock()然后再执行耗时的invoke调用。用户函数可能执行任意长时间如果持有锁其他所有请求都会被阻塞线程池就失去了意义。这就是细粒度锁的设计思想。5.3 超时、重试与连接管理客户端一个健壮的客户端不能假设网络和服务端永远可靠。class RobustHttpClient { public: struct Options { int connection_timeout_sec 2; int read_timeout_sec 5; int max_retries 1; std::chrono::milliseconds retry_delay std::chrono::milliseconds(100); }; std::optionalResponse sendRequest(const Request req, const Options opts {}) { int retries 0; while (retries opts.max_retries) { httplib::Client cli(host_, port_); cli.set_connection_timeout(opts.connection_timeout_sec); cli.set_read_timeout(opts.read_timeout_sec); // 长连接管理实际项目中应复用Client实例而不是每次创建。 // 这里简单起见每次新建。生产环境需要考虑连接池。 auto res cli.Post(/rpc, req.to_json().dump(), application/json); if (res) { if (res-status 200) { try { return Response::from_json(json::parse(res-body)); } catch (...) { // 响应体不是合法JSON可能是服务器内部错误 // 是否重试取决于错误类型这里简单重试 } } else if (res-status 500) { // 服务器5xx错误可以重试 } else { // 4xx客户端错误重试无意义直接返回错误 break; } } else { // 网络错误超时、无法连接等应重试 } if (retries opts.max_retries) { std::this_thread::sleep_for(opts.retry_delay); retries; } else { break; } } return std::nullopt; // 所有重试均失败 } };经验之谈错误处理策略是RPC框架的“良心”。不是所有错误都适合重试。例如METHOD_NOT_FOUND-32601是明确的业务逻辑错误重试一万次也没用。而网络超时、服务端内部错误HTTP 500则可能通过重试成功。一个好的框架应该允许用户为不同的错误类型配置不同的重试策略。此外对于高并发客户端连接池是必须的频繁创建销毁TCP连接开销巨大。你可以封装一个ClientPool类来管理一组到同一主机的持久连接。6. 常见问题排查与调试技巧在实际开发和测试中你肯定会遇到各种奇怪的问题。这里记录一些典型场景和排查思路。6.1 编译与链接问题问题找不到nlohmann/json.hpp或cpp-httplib.h原因没有正确包含头文件路径。解决单文件项目直接将这两个单头文件库下载到你的项目目录中。CMake项目使用find_package或FetchContent。对于nlohmann/json现代CMake支持得很好find_package(nlohmann_json REQUIRED) target_link_libraries(your_target PRIVATE nlohmann_json::nlohmann_json)对于cpp-httplib因为它只有头文件可以将其路径加入include_directories或者直接放在项目里。问题undefined reference tostd::this_thread::sleep_for等链接错误原因没有链接 pthread 库。解决在CMakeLists.txt中添加target_link_libraries(your_target PRIVATE pthread)。或者在GCC命令行添加-pthread标志。6.2 运行时问题问题服务端启动后客户端连接被拒绝 (Connection refused)排查步骤检查端口确认服务端监听的端口和客户端连接的端口一致。检查IP服务端监听0.0.0.0表示所有网络接口。客户端连接localhost或127.0.0.1。检查防火墙本地开发通常没问题但在Linux服务器或Windows防火墙上可能阻止了端口。用工具测试永远不要先怀疑自己的代码。用curl或 Postman 发送一个最简单的JSON请求测试服务端是否真的在运行和响应。curl -X POST http://localhost:8080/rpc -H Content-Type: application/json -d {jsonrpc:2.0,method:echo,params:[hello],id:1}问题客户端收到响应但反序列化失败或者结果类型不对排查步骤打印原始响应在客户端收到HTTP响应后第一时间将res-body打印出来。确认它是否是一个合法的JsonRPC响应。检查JSON格式常见的错误是多了或少了逗号、引号不匹配。使用在线的JSON格式化工具验证。核对数据类型服务端返回的result字段类型是否与客户端callint或callstd::string指定的类型匹配如果服务端返回的是{result: 19}字符串而客户端用getint()去解析就会抛出json::type_error异常。查看错误对象如果响应中包含error字段优先处理错误。你的客户端代码应该先判断resp.error.has_value()。问题服务端在处理某个请求时崩溃导致整个进程退出原因用户注册的函数抛出了未捕获的异常穿透了你的try...catch块。解决在Invokable::invoke的实现中用try...catch(...)捕获所有异常。将任何异常都转化为INTERNAL_ERROR的JsonRPC错误响应确保服务端进程的健壮性。日志在捕获异常的地方记录详细的错误日志包括方法名、参数、异常信息这是后期调试的生命线。6.3 性能与并发问题问题服务端在并发请求下响应变慢甚至出现部分请求超时排查思路检查锁竞争使用性能分析工具如perf,vtune或简单地在代码中加计时日志检查methods_mutex_被持有的时间。如果注册的方法很少且查找很快锁竞争通常不是瓶颈。但如果invoke函数内部也持有其他锁或者用户函数本身执行很慢可能导致线程在锁上等待。检查线程池大小线程池大小是否设置合理通常设置为CPU核心数的1-2倍。过多的线程会导致大量的上下文切换开销。检查用户函数是否是某个用户函数本身执行耗时过长如进行复杂的数据库查询或计算阻塞了工作线程考虑对耗时操作进行异步化改造。检查网络与IO如果使用同步IO网络延迟会直接阻塞线程。考虑向异步非阻塞架构演进如使用Boost.Asio。调试技巧日志是王道在框架的关键路径添加详细的日志输出是排查复杂问题的唯一有效方法。入口/出口日志记录每个请求的ID、方法名、开始时间、结束时间、耗时。错误日志记录所有异常和错误码包括堆栈信息如果可能。网络日志在开发阶段可以打印出收发的原始JSON字符串注意脱敏敏感数据。使用日志库不要用std::cout使用像spdlog这样的异步日志库它性能好且支持日志级别控制。7. 项目扩展与进阶方向当你完成了基础版本并且解决了上述大部分问题后这个项目就有了坚实的骨架。接下来你可以选择以下一个或多个方向进行深度扩展这会让你的框架从“玩具”升级为“工具”。7.1 支持更丰富的参数绑定方式目前的实现可能只支持将JSON数组按顺序绑定到函数参数。你可以扩展它支持更灵活的方式按名绑定如果JSON参数是对象{a: 1, b: 2}函数签名是int add(int a, int b)框架能自动按参数名匹配。默认参数支持C函数的默认参数。复杂类型转换自动将JSON对象反序列化为用户自定义的C结构体。这需要结合nlohmann/json的from_json/to_json自定义序列化功能。你可以要求用户为他们自己的类型特化这两个函数然后在你的parseParams中自动调用。7.2 实现中间件Middleware机制中间件是Web框架中常见的概念它允许你在请求处理前后插入逻辑比如身份验证、日志记录、指标收集、限流等。class Middleware { public: virtual ~Middleware() default; // 在调用实际方法前执行 virtual bool beforeInvoke(const Request req, json modified_params) 0; // 在调用实际方法后执行 virtual void afterInvoke(const Request req, json result) 0; // 如果发生异常 virtual void onError(const Request req, Error error) 0; }; class AuthMiddleware : public Middleware { bool beforeInvoke(const Request req, json modified_params) override { if (!req.params.contains(token)) { return false; // 中断执行框架应返回错误 } // 验证token... return true; } // ... 其他方法实现 }; class Server { void use(std::shared_ptrMiddleware mw) { middlewares_.push_back(mw); } json handleRequest(const Request req) { json params req.params; for (auto mw : middlewares_) { if (!mw-beforeInvoke(req, params)) { // 返回预定义的认证错误 } } // ... 调用方法 json result ...; for (auto mw : middlewares_) { mw-afterInvoke(req, result); } return resp; } private: std::vectorstd::shared_ptrMiddleware middlewares_; };7.3 替换底层传输协议当前基于HTTP/1.1虽然方便但性能有开销HTTP头。你可以抽象出一个Transport接口。class Transport { public: virtual std::string send(const std::string endpoint, const std::string data) 0; virtual ~Transport() default; }; class HttpTransport : public Transport { /* 实现 */ }; class TcpTransport : public Transport { // 实现基于原始TCP socket的传输需要自己处理消息边界如长度前缀法 }; class WebSocketTransport : public Transport { /* 实现 */ };然后服务器和客户端都通过Transport接口与网络层交互这样就可以轻松切换底层协议。7.4 代码生成与IDL支持这是工业级RPC框架的标配。你可以定义一个简单的接口定义语言IDL描述服务和方法。// calculator.rpc service Calculator { rpc Add(int a, int b) returns (int); rpc Echo(string msg) returns (string); }然后编写一个代码生成器可以用Python或C自己写读取这个.rpc文件自动生成服务端的抽象基类、客户端的桩代码。这能极大提升开发效率并保证两端类型安全。实现这个完整的JsonRPC框架就像完成一次精致的雕刻。从最初粗糙的原型到逐步打磨出线程安全、易于使用、便于扩展的形态每一步都会加深你对C特性模板、智能指针、多线程、网络编程和软件设计的理解。这个项目本身或许不会直接用于生产但在这个过程中积累的经验和代码将成为你面对更复杂系统时最宝贵的财富。当你下次再看到像gRPC这样的框架你会恍然大悟“哦原来它是这样处理这个问题的和我当初想的异曲同工。” 这种透过现象看本质的能力才是项目实战带给你的最大收获。