
1. 项目概述为什么我们需要自己造一个Json-Rpc轮子在C的后端服务开发里远程过程调用RPC是个绕不开的话题。你可能用过gRPC或者Thrift它们功能强大生态成熟。但有时候项目没那么复杂或者你对协议有洁癖不想引入一堆复杂的依赖和编译工具链就想找一个轻量、透明、自己能完全掌控的通信方案。这时候基于JSON的RPC协议——Json-Rpc就进入了视野。Json-Rpc协议本身极其简单它基于JSON格式来序列化数据通过HTTP或其他传输层发送请求和响应。协议规定了请求要有method、params、id响应要有result、error、id。就这么点东西。市面上有很多成熟的库比如jsoncpp配合个HTTP客户端服务器就能搭起来。那为什么还要“从零实现”呢这恰恰是问题的关键。用现成的库你是在“使用”一个框架它的内部机制、内存管理、错误处理、线程模型对你而言可能是个黑盒。当出现一些诡异的问题比如网络闪断导致连接状态不一致或者高并发下内存暴涨你很难定位和解决。自己动手实现一遍意味着你要亲手处理TCP连接的建立与维护、JSON报文的解析与构造、请求与响应的映射、超时与重试、以及多线程下的并发安全。这个过程会让你对网络编程、协议设计、资源管理的理解深入骨髓。这不是重复造轮子而是一次彻底的技术摸底。当你再遇到“vs2019在请求完成之前与远程仿的json-rpc连接已丢失”这类模糊的错误时你脑子里会立刻浮现出从socketaccept到recv再到send的整个链条能精准地推测出问题可能出在哪个环节是心跳机制没设计好是IO多路复用处理不当还是JSON解析时遇到了非法字符所以这个项目的目标不是做出一个比现有库更优秀的框架而是通过造这个“轮子”掌握构建一个稳健、高效通信中间件的核心技能。这对于想深入系统编程、网络服务的C开发者来说价值远超框架本身。2. 核心设计构建一个清晰、可扩展的框架架构动手之前得先画个蓝图。一个完整的Json-Rpc框架不能只是一堆散乱的文件读写和字符串拼接。我们需要一个清晰的分层架构让数据流和职责各归其位。我设计的核心架构分为四层网络传输层、协议编解码层、服务调用层和服务注册与管理层。2.1 网络传输层选择你的通信基石这一层负责最底层的字节流传输。常见的选择有TCP Socket最基础、最灵活。你需要自己处理粘包/拆包问题通常用长度前缀法或分隔符管理连接池实现心跳保活。优点是控制力极强性能调优空间大。HTTP/1.1基于TCP但利用了成熟的HTTP协议。可以直接使用libcurl作为客户端mongoose或cpp-httplib作为服务器端。好处是天然支持防火墙穿透调试方便用Postman就能发请求并且能复用HTTP的持久连接、压缩等特性。很多云环境和微服务架构更倾向于HTTP。WebSocket适合需要服务端主动推送Server Push的场景虽然Json-Rpc规范本身是请求-响应模型但结合WebSocket可以实现双向RPC。对于从零开始的学习项目我强烈建议从TCP Socket开始。虽然麻烦但这是理解网络编程精髓的最佳路径。我们可以封装一个简单的TcpConnection类内部使用非阻塞IO配合select或epollLinux/IOCPWindows来实现高并发。这里的一个关键设计是异步回调机制。当TcpConnection收到一个完整的数据包后不应该直接处理业务而是通过一个回调函数比如std::functionvoid(const std::string)通知上层。注意在Windows下使用select处理大量连接性能不佳如果是Windows平台且追求性能可以考虑使用IOCP或第三方库如libuv。但在初期select足以帮助我们理解概念。2.2 协议编解码层JSON的解析与构造这一层专攻JSON。我们需要做两件事将内存中的C数据结构请求/响应对象序列化成JSON字符串将接收到的JSON字符串反序列化成C对象。选型jsoncpp和nlohmann/json是两个主流选择。nlohmann/json是现代C风格头文件库无需编译API非常直观像操作std::map一样操作JSON。jsoncpp更老牌有些项目历史包袱在用。本项目为了现代和简洁选用nlohmann/json。核心类设计JsonRpcRequest包含jsonrpc固定为“2.0”、method字符串、params可以是数组或对象、id整数或字符串可为null用于通知。JsonRpcResponse包含jsonrpc、result成功时、error失败时包含code和message、id。JsonCodec类提供静态方法encodeRequest,encodeResponse,decode。decode方法需要根据JSON中是否存在result或error字段来判断是请求还是响应。这里的一个实操心得是处理params的多样性。Json-Rpc允许params是位置数组如[1 “text”]) 或命名对象如{“a”: 1 “b”: “text”}。我们的框架最好能同时支持。在编码时根据传入的参数类型std::vector或std::map决定格式。在解码调用时则需要将这两种形式都适配到服务方法的参数上这需要一些元编程技巧或简单的运行时判断。2.3 服务调用层连接方法与实现这是框架的“大脑”。它需要维护一个“方法名”到“实际调用实体”的映射。这个“调用实体”可以是一个普通的函数、一个类的成员函数、或者一个可调用对象std::function。设计思路定义一个统一的调用接口比如Invoker基类包含一个virtual Json invoke(const Json params) 0纯虚函数。然后为不同类型的可调用对象提供特化的派生类如FunctionInvoker、MemberFunctionInvoker。ServiceRegistry服务注册中心持有一个std::unordered_mapstd::string std::unique_ptrInvoker。关键挑战——参数绑定如何把JSON格式的params转换成C函数所需要的强类型参数这里有几种策略手动绑定要求服务方法的参数类型就是nlohmann::json函数内部自己解析。这太原始失去了类型安全。自动类型推导反射高级利用C17的std::apply和模板元编程如果函数参数类型是基础类型或可识别类型可以自动从JSON转换。这需要为每种支持的类型int double string vector 等编写特化的转换代码。这是框架的进阶目标能极大提升易用性。妥协方案初期我们可以约定服务方法的参数必须是一个const nlohmann::json。这样实现简单但调用方需要知道参数的确切结构。或者我们支持固定参数列表的自动展开例如对于函数int add(int a int b)我们要求调用方必须以数组形式[1 2]传递参数然后在调用层按顺序提取并转换类型。我建议采用渐进策略V1.0版本先支持const nlohmann::json这种通用参数确保框架跑通。V2.0再引入基于模板的自动参数绑定作为亮点功能。2.4 线程模型同步、异步与并发框架的线程模型决定了其性能和复杂度。同步阻塞模型一个连接一个线程。实现简单但连接数一多线程上下文切换开销巨大不适合高并发。Reactor模型事件驱动这是网络高并发的标准模型。一个或少数几个IO线程Reactor负责监听所有socket事件可读、可写当事件发生时将对应的连接对象和事件分发给工作线程池Worker Thread Pool去处理实际的业务逻辑JSON解码、服务调用、编码。我们的TcpConnection应该设计成非阻塞的并且其上的读写操作都在IO线程中完成仅将完整的请求报文投递到任务队列由工作线程处理。Proactor模型异步IO由操作系统完成IO操作后通知你。在Windows的IOCP上实现更自然在Linux上需要模拟。对于我们的学习框架实现一个简单的Reactor模型是最佳选择。它结构清晰能让你理解事件驱动和多线程协作的精髓。我们可以用std::thread创建固定大小的线程池用std::queue和std::mutex/std::condition_variable实现任务队列。3. 关键实现细节与踩坑实录理论说再多不如一行代码。我们来拆解几个最核心、也最容易出错的实现环节。3.1 网络层TCP粘包处理与连接生命周期管理TCP是流式协议没有消息边界。你发送的“Hello”和“World”接收方可能一次收到“HelloWorld”也可能分两次收到“He”和“lloWorld”。粘包/拆包问题是网络编程的第一道坎。解决方案长度前缀法。在发送每个JSON消息前先发送一个固定长度比如4字节的报文头里面存储JSON字符串的字节长度网络字节序。接收方先读4字节解析出长度N然后循环读取直到收满N个字节这才是一个完整的消息。// 简化版的发送函数 void TcpConnection::sendMessage(const std::string msg) { uint32_t len htonl(static_castuint32_t(msg.size())); // 转网络字节序 std::vectorchar data(sizeof(len) msg.size()); memcpy(data.data() len sizeof(len)); memcpy(data.data() sizeof(len) msg.data() msg.size()); // ... 调用send系统调用发送data } // 接收侧的逻辑在IO线程的事件循环中 void TcpConnection::handleRead() { // 1. 先尝试读取头部 if (!header_read_) { int n read(fd_, header_buf_ header_bytes_read_, sizeof(uint32_t) - header_bytes_read_); // ... 处理错误和EAGAIN header_bytes_read_ n; if (header_bytes_read_ sizeof(uint32_t)) { uint32_t len_net; memcpy(len_net header_buf_ sizeof(len_net)); body_length_ ntohl(len_net); // 转主机字节序 if (body_length_ MAX_BODY_LENGTH) { // 防御长度异常断开连接 closeConnection(); return; } body_buffer_.resize(body_length_); header_read_ true; body_bytes_read_ 0; } } // 2. 再读取消息体 if (header_read_ body_bytes_read_ body_length_) { int n read(fd_, body_buffer_.data() body_bytes_read_, body_length_ - body_bytes_read_); // ... 处理错误 body_bytes_read_ n; if (body_bytes_read_ body_length_) { // 3. 得到一个完整报文投递到工作队列 std::string complete_msg(body_buffer_.begin() body_buffer_.end()); io_thread_-postTask([this msg std::move(complete_msg)]() { onMessageCallback_(msg); // 回调给上层 }); // 4. 重置状态准备读取下一条消息 resetReadState(); } } }连接生命周期管理每个TcpConnection对象代表一个客户端连接。需要小心处理它的创建、销毁和共享指针的使用。通常使用std::shared_ptrTcpConnection来管理并在其内部持有对TcpServer的弱引用std::weak_ptrTcpServer避免循环引用导致内存泄漏。当检测到socket错误或对端关闭时需要将连接从服务器的连接映射表中移除。3.2 编解码层错误处理与性能优化使用nlohmann::json解析时必须做好异常处理。无效的JSON字符串会导致解析抛出异常。std::optionalJsonRpcRequest JsonCodec::decodeRequest(const std::string json_str) { try { auto j nlohmann::json::parse(json_str); // 验证必需字段jsonrpc method if (!j.contains(“jsonrpc”) || j[“jsonrpc”] ! “2.0” || !j.contains(“method”)) { return std::nullopt; // 无效请求 } JsonRpcRequest req; req.jsonrpc j[“jsonrpc”].getstd::string(); req.method j[“method”].getstd::string(); if (j.contains(“params”)) { req.params j[“params”]; // 直接存储json对象 } if (j.contains(“id”)) { // id可以是数字、字符串或null if (j[“id”].is_number_integer()) req.id j[“id”].getint(); else if (j[“id”].is_string()) req.id j[“id”].getstd::string(); // null id 表示通知我们不存储 } return req; } catch (const nlohmann::json::exception e) { // 记录日志解析失败 LOG_ERROR “JSON parse failed: ” e.what() “ raw: ” json_str; return std::nullopt; } }性能优化点JSON的序列化和反序列化是CPU密集型操作。在高并发场景下可以考虑复用json对象避免在每次编解码时创建新的nlohmann::json对象可以在连接对象或线程局部存储中缓存。使用更快的库如果性能成为瓶颈可以评估rapidjson需要手动管理内存但速度极快。压缩对于传输大的JSON数据可以在网络层启用压缩如gzip。3.3 服务调用层实现安全的异步调用与超时在工作线程中执行服务方法必须是异常安全的。任何服务方法抛出的异常都不应该导致工作线程崩溃。void WorkerThread::processTask(const Task task) { try { Json result task.invoker-invoke(task.params); JsonRpcResponse resp; resp.jsonrpc “2.0”; resp.result std::move(result); resp.id task.request_id; // 编码响应并发送回网络层 auto resp_str JsonCodec::encodeResponse(resp); task.conn-send(resp_str); } catch (const std::exception e) { // 捕获业务异常转换为Json-Rpc错误响应 JsonRpcResponse err_resp; err_resp.jsonrpc “2.0”; err_resp.error {{“code” -32000} {“message” “Server error: ” std::string(e.what())}}; err_resp.id task.request_id; task.conn-send(JsonCodec::encodeResponse(err_resp)); } catch (...) { // 捕获未知异常 JsonRpcResponse err_resp; err_resp.jsonrpc “2.0”; err_resp.error {{“code” -32603} {“message” “Internal error”}}; err_resp.id task.request_id; task.conn-send(JsonCodec::encodeResponse(err_resp)); } }客户端超时机制一个健壮的RPC客户端必须支持超时。对于同步调用可以在发送请求后启动一个定时器如果超时前未收到响应则取消等待并返回超时错误。对于异步调用基于回调需要维护一个std::mapRequestId CallbackWithTimer的结构定时器触发时清理对应的回调并通知调用方超时。4. 从零到一的搭建步骤与验证让我们抛开理论看看如何一步步把这个框架搭起来并跑通一个例子。4.1 第一步搭建项目骨架与依赖创建项目使用CMake管理项目是C的最佳实践。创建一个清晰的目录结构json_rpc_framework/ ├── CMakeLists.txt ├── include/ # 公共头文件 │ ├── json_rpc/ │ │ ├── codec.h │ │ ├── connection.h │ │ ├── server.h │ │ └── ... ├── src/ # 源文件 │ ├── codec.cpp │ ├── connection.cpp │ ├── server.cpp │ └── ... ├── third_party/ # 放置 nlohmann/json 单头文件 │ └── nlohmann_json.hpp └── examples/ # 示例代码 ├── server_demo.cpp └── client_demo.cpp配置CMake在顶层的CMakeLists.txt中设置C标准为17或更高添加头文件路径并将src目录编译为静态库或动态库。引入依赖将nlohmann/json的单头文件json.hpp下载到third_party目录并在代码中通过#include “../third_party/nlohmann_json.hpp”或配置CMake的include_directories引入。4.2 第二步实现网络层与编解码基础实现TcpConnection先实现一个同步版本的TcpConnection能连接、发送、接收完整报文。使用长度前缀法解决粘包。实现JsonCodec完成JsonRpcRequest和JsonRpcResponse的结构体定义以及它们的编码解码函数。确保能正确处理nullid通知和两种params格式。编写单元测试不要等到全部写完再测。为JsonCodec写几个简单的测试验证编码解码的对称性。可以用Google Test框架。4.3 第三步实现服务注册与调用设计Invoker接口和ServiceRegistry先实现一个最简单的版本只支持全局函数。ServiceRegistry提供一个registerMethod函数将方法名和函数指针绑定。实现Server类Server类内部包含一个TcpServer管理多个TcpConnection、一个ServiceRegistry、一个线程池。它的工作流程是启动监听端口。接受新连接创建TcpConnection对象。为每个连接设置onMessageCallback回调里解码请求根据方法名从ServiceRegistry找到Invoker包装成任务投递到线程池。线程池执行任务调用Invoker得到结果后编码响应通过TcpConnection发送回去。4.4 第四步编写示例并集成测试编写示例服务端在examples/server_demo.cpp中启动一个JsonRpcServer注册几个简单的函数比如int add(int a int b)std::string echo(const std::string msg)。#include “json_rpc/server.h” #include iostream int add(int a int b) { return a b; } std::string echo(const std::string s) { return “Echo: ” s; } int main() { json_rpc::Server server(8080); server.registerMethod(“add” add); server.registerMethod(“echo” echo); std::cout “Json-Rpc Server running on port 8080...\n”; server.start(); // 进入事件循环 return 0; }编写示例客户端实现一个简单的同步客户端JsonRpcClient提供call方法。内部建立TCP连接发送请求同步等待响应。// 伪代码展示调用过程 json_rpc::Client client(“127.0.0.1” 8080); auto result client.callint(“add” 10 20); // 期望返回int类型 std::cout “Result: ” result std::endl; // 应输出 30测试先运行服务端再运行客户端。使用Wireshark或tcpdump抓包查看网络上流动的JSON数据是否符合Json-Rpc 2.0规范。这是验证协议正确性的黄金标准。4.5 第五步迭代优化与功能增强基础版本跑通后就可以按需添加高级功能了异步客户端实现基于回调或std::future的异步接口。参数自动绑定利用模板和std::index_sequence实现从JSON到任意参数列表的自动转换。支持成员函数扩展Invoker使其能绑定到某个对象的成员函数上。心跳与健康检查在TcpConnection中实现Ping/Pong心跳机制自动清理死连接。日志与监控集成日志库如spdlog在关键路径添加日志。添加简单的统计信息如请求数、平均耗时。HTTP传输层基于cpp-httplib实现一个HttpServerTransport和HttpClientTransport替换掉TCP层让框架同时支持两种协议。5. 常见问题排查与性能调优指南在实际开发和测试中你肯定会遇到各种问题。这里记录一些典型问题和解决思路。5.1 连接丢失与资源泄漏问题客户端频繁报告“连接已丢失”服务器端连接数只增不减。排查检查socket关闭逻辑确保在TcpConnection析构函数或错误处理中正确调用了close()。记住关闭socket是双向的shutdown和释放文件描述符close都要做。检查智能指针循环引用如果TcpConnection和TcpServer互相持有shared_ptr会导致永远无法释放。务必使用weak_ptr打破循环。检查异常安全在send或read等系统调用中发生异常是否保证了资源的清理使用RAII对象如自定义的SocketGuard管理资源。工具在Linux下使用lsof -p pid查看进程打开的文件描述符数量是否持续增长。使用netstat -anp | grep port查看连接状态是否有大量CLOSE_WAIT状态通常是服务端未主动关闭连接。5.2 请求超时或无响应问题客户端调用后一直阻塞直到超时。排查确认报文完整性在编解码层和网络层添加详细日志打印出发送和接收的原始字节长度和内容。确认长度前缀是否正确JSON格式是否有效。检查线程池工作线程是否因为某个任务死锁或长时间阻塞而“饿死”了其他任务可以在任务提交时记录队列长度。检查回调链从网络层收到数据到投递任务到工作线程处理再到发送响应这个链条是否畅通有没有某个环节的回调函数被意外置空或未设置技巧实现一个ping方法客户端定期调用可以快速诊断基本的连通性和服务可用性。5.3 性能瓶颈分析当QPS每秒查询率上不去时需要做性能剖析。CPU瓶颈使用perf(Linux) 或VTune(Windows) 分析热点函数。很可能是JSON编解码或锁竞争。JSON优化如前所述考虑更换更快的库或启用压缩。锁优化检查线程池的任务队列锁的粒度是否过大可以考虑使用无锁队列如moodycamel::ConcurrentQueue。内存瓶颈频繁的字符串构造和拷贝会导致大量内存分配。使用内存池或对象池复用std::string或缓冲区。nlohmann::json的解析也会产生大量小对象注意其内存分配策略。网络IO瓶颈单Reactor线程处理所有连接的事件分发可能成为瓶颈。可以考虑主从Reactor模型一个主Acceptor线程负责接受新连接然后将连接分发给多个SubReactor线程每个SubReactor线程管理一组连接上的IO事件。这能有效利用多核。5.4 关于“vs2019在请求完成之前与远程仿的json-rpc连接已丢失”这个错误信息很典型它可能对应我们框架中的多种情况服务器端处理超时服务器端业务逻辑太慢客户端等待超时后主动断开。解决方案优化服务方法或增加客户端超时时间或在服务器端实现异步处理快速返回一个任务ID通过其他通道查询结果。网络不稳定中间网络链路闪断。解决方案实现应用层的心跳机制和自动重连。在客户端检测到连接断开后不是直接报错而是尝试重连并重发未完成的请求需要请求有幂等性。框架Bug服务器端在处理完请求、发送响应之前连接因为其他原因如异常被关闭了。解决方案加强服务器端的异常处理确保在发送响应后再进行可能的资源清理。自己实现框架的最大好处就在于此你可以深入每一行代码打日志、加断点亲眼看到数据是如何流动的错误是在哪一步发生的。这种掌控感是使用黑盒框架永远无法获得的。从零实现一个Json-Rpc框架就像亲手搭建一座微型城市。你既是规划师设计架构也是建筑工编写代码还是市政管理员处理异常和性能。当这座“城市”最终流畅运转承载起服务间的通信时你对分布式系统中最基础的“通信”二字的理解将不再是停留在概念层面。下次当你使用任何一个RPC框架你都会下意识地去思考它底层可能的样子这就是“造轮子”带来的最大收益。