1. 项目缘起与核心价值最近在整理过往的项目笔记发现一个很有意思的现象无论是微服务架构下的服务间通信还是游戏服务器与逻辑服之间的数据交换甚至是物联网设备与云端的数据上报大家似乎都在不约而同地使用一种叫做RPC远程过程调用的技术。而其中基于JSON格式的RPC协议也就是Json-Rpc因其轻量、易读、跨语言的特性成为了很多中小型项目尤其是C后端项目的首选通信方案。然而当我试图为团队的新项目引入一个Json-Rpc框架时却发现市面上成熟的C库要么过于庞大、依赖复杂要么就是功能简陋、性能堪忧。像一些知名的开源框架往往为了通用性引入了大量我们并不需要的模块学习成本和集成成本都偏高。而自己动手写一个似乎又觉得无从下手网络通信、协议解析、序列化反序列化每一个环节都够琢磨一阵子。正是这个痛点促使我决定启动这个系列。我的目标不是再造一个轮子去和那些成熟框架竞争而是希望通过“从零实现”这个过程把Json-Rpc框架的“黑盒子”彻底打开把网络编程、协议设计、C工程实践这些知识点串联起来形成一个完整的、可运行的项目。最终产出的会是一个麻雀虽小但五脏俱全的、代码清晰易懂的、可以直接用于学习和二次开发的C Json-Rpc框架。无论你是想深入理解RPC原理的初学者还是正在为项目寻找轻量级通信方案的开发者这个系列都能给你带来实实在在的收获。2. 整体架构与技术选型背后的思考在动手写第一行代码之前花时间在架构设计和技术选型上是绝对值得的。这决定了项目的可维护性、扩展性和最终的性能表现。我的核心思路是“职责分离模块清晰”将整个框架拆解为几个独立的层次。2.1 核心模块划分整个框架我计划分为四个核心层网络通信层负责最底层的字节流收发、连接管理。这是框架的“腿”决定了框架能跑多快、多稳。协议编解码层负责将网络层收到的字节流按照Json-Rpc协议规范解析成结构化的请求/响应对象反之亦然。这是框架的“翻译官”。服务注册与分发层负责维护一个“服务-方法”的映射表。当收到一个请求时能根据其方法名快速找到对应的C函数并调用。这是框架的“调度中心”。序列化/反序列化层负责将C的函数参数、返回值与JSON格式之间进行转换。这是框架的“数据格式转换器”。这样的分层设计使得每个模块的职责非常清晰。未来如果想更换网络库比如从muduo换成asio或者更换JSON库只需要替换对应的层而不会影响到其他模块的逻辑极大地提高了可维护性。2.2 关键技术选型解析选型的原则很明确成熟、稳定、轻量、易于集成并且符合C项目的现代工程实践。2.2.1 网络库为何选择 muduo这是最关键的一个选择。C的网络编程门槛不低直接使用原生socket或boost::asio虽然灵活但需要处理大量的底层细节如连接管理、缓冲区、事件循环容易出错。我需要一个更上层的、事件驱动的高性能网络库。muduo的优势纯C11无第三方依赖编译部署极其简单符合我们“轻量”的核心诉求。Reactor模式高性能其基于事件循环和非阻塞IO的设计非常适合处理大量并发连接这是RPC服务器的典型场景。代码质量高文档相对齐全由陈硕大神开发代码本身就是学习网络编程的绝佳范例。线程模型清晰它“one loop per thread”的思想让我们很容易设计出多线程并发的RPC服务器充分利用多核CPU。为什么不选 libevent/libuvlibevent是C库与C项目结合需要一些封装且其C风格的回调函数用起来不如C的std::function和std::bind直观。libuv同样强大但它的异步接口和回调风格对于实现一个需要清晰流程的RPC服务器来说代码结构可能不如muduo基于Channel和回调的方式来得直观和易于控制。为什么不选 asioboost::asio功能极其强大是行业标准之一。但它属于Boost库的一部分体积庞大且其基于Proactor模式和async_*接口的学习曲线相对陡峭。对于我们这个以“教学”和“清晰”为首要目标的项目来说muduo的Reactor模式更易于理解和实现。注意选择muduo意味着我们的框架在Linux环境下会有最佳表现。虽然muduo有非官方的Windows移植版但本系列将主要基于Linux如Ubuntu/CentOS进行开发这也是大多数C后端服务器的部署环境。2.2.2 JSON库为何选择 JsonCppJson-Rpc顾名思义数据载体是JSON。我们需要一个库来解析和生成JSON。JsonCpp的优势老牌、稳定经过多年考验API稳定足够满足我们的所有需求解析、生成、查询、修改。MIT许可证非常宽松可以放心用于商业项目。API直观Json::Value这个万能类型用起来很方便虽然类型安全稍弱但对于协议解析层来说足够清晰。其他候选与权衡nlohmann/json现代C JSON库的标杆API优雅支持现代C特性如移动语义。但它是一个纯头文件库在编译大型项目时可能会增加编译时间。对于我们的框架核心稳定性和编译友好性我优先考虑JsonCpp。RapidJSON性能极高但API是C风格的使用起来不如C库方便错误处理也更繁琐。simdjson性能怪兽但更侧重于极速解析超大JSON文件。对于RPC这种通常是小而频繁的JSON消息JsonCpp的性能完全足够且开发效率更高。2.2.3 构建工具为何选择 CMake这几乎是现代C项目的唯一选择。它跨平台语法相对清晰生态强大。我们将使用现代CMake3.10的写法强调target的概念让项目的依赖管理清晰明了。2.2.4 编译器与标准C11/14我们将采用C11作为最低标准并适当使用C14的特性如泛型lambda。这个标准在绝大多数生产环境中都已得到支持能在性能、开发效率和可读性之间取得很好的平衡。我们会用到std::function,std::bind,std::shared_ptr,std::unordered_map, 移动语义等特性来简化代码。3. 开发环境搭建与项目初始化“工欲善其事必先利其器”。一个干净、可复现的开发环境是高效编码的基础。这里我会详细记录从零开始搭建环境的每一步包括可能遇到的坑和解决方法。3.1 Linux开发环境准备以Ubuntu 22.04为例首先我们需要一个Linux环境。如果你用Windows强烈建议使用WSL2Windows Subsystem for Linux它几乎能提供原生的Linux体验。系统更新与基础工具安装sudo apt update sudo apt upgrade -y sudo apt install -y build-essential gdb cmake git curl wgetbuild-essential包含了gcc/g等编译套件cmake是我们的构建工具git用于版本控制。安装 JsonCpp 我们不从源码编译而是使用系统包管理器安装稳定版本这样最省事。sudo apt install -y libjsoncpp-dev安装后头文件通常在/usr/include/jsoncpp/json/库文件在/usr/lib/x86_64-linux-gnu/下。获取 muduo 网络库 muduo不提供二进制包我们需要从源码编译。这是环境搭建中稍复杂的一步。# 1. 克隆 muduo 仓库 (使用官方推荐的 cmake 分支) git clone https://github.com/chenshuo/muduo.git cd muduo # 2. 检查依赖muduo 依赖 Boost 库主要用于线程、时间等非必须全部 sudo apt install -y libboost-dev libboost-system-dev libboost-thread-dev # 3. 使用 CMake 构建并安装 mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease -DCMAKE_INSTALL_PREFIX/usr/local .. # -DCMAKE_INSTALL_PREFIX 指定安装目录通常 /usr/local 是安全的 make -j$(nproc) # 使用所有CPU核心并行编译加快速度 sudo make install # 安装到系统目录实操心得编译muduo可能需要几分钟耐心等待。-DCMAKE_BUILD_TYPERelease指定生成优化后的发布版本性能更好。调试阶段可以换成Debug。安装到/usr/local后CMake可以通过find_package(muduo)来找到它。如果安装到其他路径可能需要手动设置CMAKE_PREFIX_PATH。验证环境 创建一个简单的测试程序test_env.cpp#include muduo/net/TcpServer.h #include json/json.h #include iostream int main() { std::cout Muduo and JsonCpp environment is ready! std::endl; Json::Value root; root[hello] world; std::cout root.toStyledString() std::endl; return 0; }用CMakeLists.txt编译它如果能成功编译运行说明环境基本就绪。3.2 使用CMake初始化项目骨架现在我们来创建我们的Json-Rpc框架项目目录结构。清晰的目录结构是项目可维护性的关键。json_rpc_framework/ ├── CMakeLists.txt # 项目根目录的CMake配置文件 ├── README.md # 项目说明 ├── src/ # 框架源代码目录 │ ├── CMakeLists.txt # 源码层的CMake配置 │ ├── rpc/ # RPC框架核心代码 │ │ ├── server/ # 服务器端相关类 │ │ ├── client/ # 客户端相关类后续实现 │ │ ├── protocol/ # 协议编解码 │ │ ├── serializer/ # 序列化/反序列化 (适配JsonCpp) │ │ └── service/ # 服务注册与管理 │ └── utils/ # 工具类如日志、异常 ├── examples/ # 使用示例 │ ├── CMakeLists.txt │ ├── echo_server.cpp # 示例回声服务 │ └── echo_client.cpp ├── tests/ # 单元测试 │ └── CMakeLists.txt └── third_party/ # 第三方库如果需要本地拷贝 └── CMakeLists.txt根目录CMakeLists.txt的关键配置cmake_minimum_required(VERSION 3.10) project(JsonRpcFramework VERSION 0.1.0 LANGUAGES CXX) # 设置C标准 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # 禁用编译器扩展保证跨编译器兼容性 # 设置输出目录让编译生成的文件可执行文件、库都放在build目录下保持源码目录干净 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 查找依赖包 find_package(JsonCpp REQUIRED) find_package(muduo REQUIRED) # 依赖于我们之前安装到系统的muduo # 添加子目录 add_subdirectory(src) add_subdirectory(examples) # add_subdirectory(tests) # 测试目录后续开启src/CMakeLists.txt# 定义我们的核心库目标 add_library(json_rpc_core rpc/server/rpc_server.cpp rpc/protocol/json_rpc_protocol.cpp rpc/serializer/json_serializer.cpp rpc/service/service_manager.cpp utils/logger.cpp ) # 为库目标链接依赖项使用现代CMake的target_link_libraries target_link_libraries(json_rpc_core PUBLIC muduo::muduo_net JsonCpp::JsonCpp ) # 包含头文件目录 target_include_directories(json_rpc_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} )examples/CMakeLists.txt# 示例回声服务器 add_executable(echo_server echo_server.cpp) target_link_libraries(echo_server json_rpc_core) # 示例回声客户端后续实现 add_executable(echo_client echo_client.cpp) target_link_libraries(echo_client json_rpc_core)这样一个基于CMake的、模块清晰的项目骨架就搭建好了。在项目根目录执行mkdir build cd build cmake .. make -j$(nproc)编译成功后你会在build/bin/目录下看到echo_server可执行文件。4. 第一个里程碑实现一个迷你版RPC服务器在深入协议细节之前我们先搭建一个最小的、能跑通的“架子”验证我们的环境和技术选型是否工作。这个服务器不处理复杂的Json-Rpc协议只实现一个简单的回声Echo服务用于验证muduo网络库的基本使用。4.1 设计一个最简单的RPC服务器类我们在src/rpc/server/rpc_server.h中声明一个最基础的服务器类// src/rpc/server/rpc_server.h #ifndef JSON_RPC_FRAMEWORK_RPC_SERVER_H #define JSON_RPC_FRAMEWORK_RPC_SERVER_H #include muduo/net/TcpServer.h #include muduo/net/EventLoop.h #include muduo/net/InetAddress.h #include functional #include string namespace jsonrpc { class RpcServer { public: using MessageCallback std::functionvoid(const std::string, std::string); RpcServer(muduo::net::EventLoop* loop, const muduo::net::InetAddress listenAddr); ~RpcServer() default; void start(); // 启动服务器 void setMessageCallback(const MessageCallback cb) { messageCallback_ cb; } private: void onConnection(const muduo::net::TcpConnectionPtr conn); void onMessage(const muduo::net::TcpConnectionPtr conn, muduo::net::Buffer* buf, muduo::Timestamp time); muduo::net::EventLoop* loop_; muduo::net::TcpServer server_; MessageCallback messageCallback_; // 消息处理回调 }; } // namespace jsonrpc #endif关键点解析依赖muduo核心成员是muduo::net::TcpServer和EventLoop。一个TcpServer绑定到一个EventLoop上。回调函数MessageCallback是一个std::function用于定义当服务器收到一个完整消息字符串时上层业务逻辑如何处理。这里我们先简单设计为输入一个请求字符串输出一个响应字符串。私有方法onConnection和onMessage是muduo网络事件的标准回调我们将它们注册给TcpServer。4.2 实现服务器核心逻辑在src/rpc/server/rpc_server.cpp中实现// src/rpc/server/rpc_server.cpp #include rpc_server.h #include muduo/net/TcpConnection.h #include muduo/net/Buffer.h #include iostream namespace jsonrpc { RpcServer::RpcServer(muduo::net::EventLoop* loop, const muduo::net::InetAddress listenAddr) : loop_(loop), server_(loop, listenAddr, RpcServer), messageCallback_(nullptr) { // 设置连接建立/断开回调 server_.setConnectionCallback( std::bind(RpcServer::onConnection, this, std::placeholders::_1)); // 设置消息到达回调 server_.setMessageCallback( std::bind(RpcServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } void RpcServer::start() { std::cout RpcServer starting on server_.ipPort() std::endl; server_.start(); } void RpcServer::onConnection(const muduo::net::TcpConnectionPtr conn) { if (conn-connected()) { std::cout New connection from conn-peerAddress().toIpPort() std::endl; } else { std::cout Connection closed from conn-peerAddress().toIpPort() std::endl; } } void RpcServer::onMessage(const muduo::net::TcpConnectionPtr conn, muduo::net::Buffer* buf, muduo::Timestamp time) { // 从Buffer中取出所有可读数据作为一个完整的“消息” // 注意这里我们简单地将所有数据视为一个字符串实际Json-Rpc需要根据协议解析消息边界。 std::string msg(buf-retrieveAllAsString()); std::cout Received message: msg std::endl; std::string response; if (messageCallback_) { // 调用用户设置的消息处理回调 messageCallback_(msg, response); } else { response Error: No message handler set.; } // 将响应发送回客户端 conn-send(response); } } // namespace jsonrpc关键点解析构造函数初始化TcpServer并绑定连接和消息回调。std::bind用于将成员函数和this指针绑定成可调用对象。onMessage这是核心。muduo::net::Buffer是muduo提供的高性能缓冲区。buf-retrieveAllAsString()取出所有数据。这里我们做了一个重要的简化假设TCP流中的每一个“写”操作就是一个完整的JSON消息。实际上TCP是流式协议存在粘包/拆包问题我们需要定义自己的协议如长度前缀、分隔符来界定消息边界。这将是下一篇文章的重点。回调机制通过messageCallback_将网络层与业务逻辑解耦。网络层只负责收/发数据具体这个数据JSON字符串代表什么请求由上层回调函数解析和处理。4.3 编写示例回声服务现在我们在examples/echo_server.cpp中使用这个基础服务器// examples/echo_server.cpp #include rpc/server/rpc_server.h #include muduo/net/EventLoop.h #include muduo/net/InetAddress.h #include iostream int main() { // 1. 创建事件循环Reactor核心 muduo::net::EventLoop loop; // 2. 指定服务器监听地址和端口 muduo::net::InetAddress listenAddr(8888); // 3. 创建我们的RPC服务器实例 jsonrpc::RpcServer server(loop, listenAddr); // 4. 设置消息处理回调简单的回声 server.setMessageCallback([](const std::string request, std::string response) { std::cout [Business Logic] Processing: request std::endl; response Echo: request; // 业务逻辑原样返回 }); // 5. 启动服务器 server.start(); // 6. 进入事件循环等待客户端连接和消息 loop.loop(); // 这是一个阻塞调用直到loop被要求退出 return 0; }4.4 编译与运行测试在项目根目录的build文件夹下执行make生成echo_server。cd build make -j$(nproc) ./bin/echo_server你应该能看到输出RpcServer starting on 0.0.0.0:8888。使用telnet进行测试 打开另一个终端使用telnet连接服务器并发送消息。telnet localhost 8888 Trying 127.0.0.1... Connected to localhost. Escape character is ^]. Hello Json-Rpc! # 你输入的内容 Echo: Hello Json-Rpc! # 服务器返回的内容在服务器终端你会看到相应的连接和消息日志。恭喜至此我们已经完成了最基础的一步一个基于muduo的、能接收和响应TCP消息的服务器骨架。虽然它还不能理解Json-Rpc协议但网络通信的基石已经打好了。5. 常见问题与排查技巧实录在环境搭建和第一个示例编写过程中你可能会遇到以下问题。这里我记录下我踩过的坑和解决方法。5.1 编译问题问题1找不到 muduo 头文件或库fatal error: muduo/net/TcpServer.h: No such file or directory排查确认安装路径sudo find /usr -name TcpServer.h查找头文件位置。如果安装在/usr/local通常会在/usr/local/include下。检查CMake的find_package确保muduo的安装路径在CMake的搜索路径中。如果安装到自定义目录如/opt/muduo需要在CMakeLists.txt中通过set(CMAKE_PREFIX_PATH /opt/muduo)或find_package(muduo REQUIRED PATHS /opt/muduo)来指定。手动编译验证创建一个最简单的测试程序用g直接编译链接g -stdc11 test.cpp -lmuduo_net -lmuduo_base -lpthread看是否能成功。问题2JsonCpp链接错误undefined reference to Json::Value::toStyledString() const‘排查这通常是链接器找不到JsonCpp库。确保find_package(JsonCpp REQUIRED)成功。检查target_link_libraries中是否包含了JsonCpp::JsonCpp现代CMake目标或jsoncpp_lib旧式变量。可以尝试在终端直接编译g test.cpp -ljsoncpp看是否可行。5.2 运行时问题问题服务器启动后telnet连接立即断开或无响应排查检查端口占用netstat -tlnp | grep 8888查看8888端口是否已被其他程序占用。检查防火墙Linux上可能是ufw或firewalld阻止了端口访问。可以临时关闭防火墙测试sudo ufw disable生产环境慎用。检查服务器绑定地址在代码中muduo::net::InetAddress listenAddr(8888)绑定的是0.0.0.0所有接口。如果只想绑定本地回环可以使用muduo::net::InetAddress listenAddr(127.0.0.1, 8888)。查看服务器日志确认onConnection回调是否被触发。如果没触发可能是连接根本没建立成功。5.3 设计思考与经验关于Buffer和消息边界我们第一个版本简单地将一次recv到的所有数据当作一个消息这在实际网络中是绝对错误的。TCP是字节流多次发送的“消息”可能在接收端被合并粘包一次发送的“消息”也可能被拆分成多次接收拆包。解决方案在协议层定义消息边界。常见方法有长度前缀在消息头部固定字节如4字节int表示后续JSON数据的长度。这是最高效、最常用的方法。分隔符用一个特殊的字符如\n作为消息结束标志。但要求消息内容本身不能包含该分隔符需要转义效率较低。 我们将在下一篇文章中实现长度前缀法。关于线程安全我们的RpcServer在onMessage回调中直接调用messageCallback_。muduo保证了对于同一个TcpConnection其回调是顺序执行的但不同的连接可能在不同的IO线程中回调。如果messageCallback_涉及共享数据需要考虑线程安全。一个简单的做法是在业务回调内部加锁或者确保业务逻辑是无状态的。关于错误处理当前版本几乎没有错误处理。例如如果messageCallback_抛出异常整个服务器线程可能会崩溃。一个健壮的生产框架需要捕获异常并返回一个格式化的JSON-RPC错误响应给客户端。6. 下一步规划与核心挑战预览第一弹我们成功搭建了环境并实现了一个能跑通的网络服务器骨架。但这离一个真正的Json-Rpc框架还有很远的距离。下一篇文章我们将直面核心挑战并实现以下功能定义并实现协议层设计基于长度前缀的消息边界协议实现一个ProtocolCodec类负责从TCP流中正确地分割出完整的JSON字符串。解析Json-Rpc协议使用JsonCpp解析JSON字符串根据Json-Rpc 2.0规范提取method,params,id等字段并构建响应对象。实现服务注册与调用设计一个ServiceManager允许用户将C函数注册为RPC方法。当收到请求时能自动查找并调用对应的函数并将返回值序列化为JSON。引入更易用的API封装一个更友好的服务器API让用户通过几行代码就能发布服务。真正的挑战在于如何优雅地将一个字符串形式的“方法名”映射到一个类型各异的C函数上并安全地进行参数传递和返回值序列化。这需要用到一些C模板元编程的技巧。我已经有了初步的实现方案我们下一弹见分晓。环境搭建是万里长征的第一步虽然琐碎但稳定的基础是后续高效开发的保障。如果你在搭建过程中遇到任何问题欢迎在评论区交流。
从零实现C++轻量级Json-RPC框架:基于muduo与JsonCpp的工程实践
1. 项目缘起与核心价值最近在整理过往的项目笔记发现一个很有意思的现象无论是微服务架构下的服务间通信还是游戏服务器与逻辑服之间的数据交换甚至是物联网设备与云端的数据上报大家似乎都在不约而同地使用一种叫做RPC远程过程调用的技术。而其中基于JSON格式的RPC协议也就是Json-Rpc因其轻量、易读、跨语言的特性成为了很多中小型项目尤其是C后端项目的首选通信方案。然而当我试图为团队的新项目引入一个Json-Rpc框架时却发现市面上成熟的C库要么过于庞大、依赖复杂要么就是功能简陋、性能堪忧。像一些知名的开源框架往往为了通用性引入了大量我们并不需要的模块学习成本和集成成本都偏高。而自己动手写一个似乎又觉得无从下手网络通信、协议解析、序列化反序列化每一个环节都够琢磨一阵子。正是这个痛点促使我决定启动这个系列。我的目标不是再造一个轮子去和那些成熟框架竞争而是希望通过“从零实现”这个过程把Json-Rpc框架的“黑盒子”彻底打开把网络编程、协议设计、C工程实践这些知识点串联起来形成一个完整的、可运行的项目。最终产出的会是一个麻雀虽小但五脏俱全的、代码清晰易懂的、可以直接用于学习和二次开发的C Json-Rpc框架。无论你是想深入理解RPC原理的初学者还是正在为项目寻找轻量级通信方案的开发者这个系列都能给你带来实实在在的收获。2. 整体架构与技术选型背后的思考在动手写第一行代码之前花时间在架构设计和技术选型上是绝对值得的。这决定了项目的可维护性、扩展性和最终的性能表现。我的核心思路是“职责分离模块清晰”将整个框架拆解为几个独立的层次。2.1 核心模块划分整个框架我计划分为四个核心层网络通信层负责最底层的字节流收发、连接管理。这是框架的“腿”决定了框架能跑多快、多稳。协议编解码层负责将网络层收到的字节流按照Json-Rpc协议规范解析成结构化的请求/响应对象反之亦然。这是框架的“翻译官”。服务注册与分发层负责维护一个“服务-方法”的映射表。当收到一个请求时能根据其方法名快速找到对应的C函数并调用。这是框架的“调度中心”。序列化/反序列化层负责将C的函数参数、返回值与JSON格式之间进行转换。这是框架的“数据格式转换器”。这样的分层设计使得每个模块的职责非常清晰。未来如果想更换网络库比如从muduo换成asio或者更换JSON库只需要替换对应的层而不会影响到其他模块的逻辑极大地提高了可维护性。2.2 关键技术选型解析选型的原则很明确成熟、稳定、轻量、易于集成并且符合C项目的现代工程实践。2.2.1 网络库为何选择 muduo这是最关键的一个选择。C的网络编程门槛不低直接使用原生socket或boost::asio虽然灵活但需要处理大量的底层细节如连接管理、缓冲区、事件循环容易出错。我需要一个更上层的、事件驱动的高性能网络库。muduo的优势纯C11无第三方依赖编译部署极其简单符合我们“轻量”的核心诉求。Reactor模式高性能其基于事件循环和非阻塞IO的设计非常适合处理大量并发连接这是RPC服务器的典型场景。代码质量高文档相对齐全由陈硕大神开发代码本身就是学习网络编程的绝佳范例。线程模型清晰它“one loop per thread”的思想让我们很容易设计出多线程并发的RPC服务器充分利用多核CPU。为什么不选 libevent/libuvlibevent是C库与C项目结合需要一些封装且其C风格的回调函数用起来不如C的std::function和std::bind直观。libuv同样强大但它的异步接口和回调风格对于实现一个需要清晰流程的RPC服务器来说代码结构可能不如muduo基于Channel和回调的方式来得直观和易于控制。为什么不选 asioboost::asio功能极其强大是行业标准之一。但它属于Boost库的一部分体积庞大且其基于Proactor模式和async_*接口的学习曲线相对陡峭。对于我们这个以“教学”和“清晰”为首要目标的项目来说muduo的Reactor模式更易于理解和实现。注意选择muduo意味着我们的框架在Linux环境下会有最佳表现。虽然muduo有非官方的Windows移植版但本系列将主要基于Linux如Ubuntu/CentOS进行开发这也是大多数C后端服务器的部署环境。2.2.2 JSON库为何选择 JsonCppJson-Rpc顾名思义数据载体是JSON。我们需要一个库来解析和生成JSON。JsonCpp的优势老牌、稳定经过多年考验API稳定足够满足我们的所有需求解析、生成、查询、修改。MIT许可证非常宽松可以放心用于商业项目。API直观Json::Value这个万能类型用起来很方便虽然类型安全稍弱但对于协议解析层来说足够清晰。其他候选与权衡nlohmann/json现代C JSON库的标杆API优雅支持现代C特性如移动语义。但它是一个纯头文件库在编译大型项目时可能会增加编译时间。对于我们的框架核心稳定性和编译友好性我优先考虑JsonCpp。RapidJSON性能极高但API是C风格的使用起来不如C库方便错误处理也更繁琐。simdjson性能怪兽但更侧重于极速解析超大JSON文件。对于RPC这种通常是小而频繁的JSON消息JsonCpp的性能完全足够且开发效率更高。2.2.3 构建工具为何选择 CMake这几乎是现代C项目的唯一选择。它跨平台语法相对清晰生态强大。我们将使用现代CMake3.10的写法强调target的概念让项目的依赖管理清晰明了。2.2.4 编译器与标准C11/14我们将采用C11作为最低标准并适当使用C14的特性如泛型lambda。这个标准在绝大多数生产环境中都已得到支持能在性能、开发效率和可读性之间取得很好的平衡。我们会用到std::function,std::bind,std::shared_ptr,std::unordered_map, 移动语义等特性来简化代码。3. 开发环境搭建与项目初始化“工欲善其事必先利其器”。一个干净、可复现的开发环境是高效编码的基础。这里我会详细记录从零开始搭建环境的每一步包括可能遇到的坑和解决方法。3.1 Linux开发环境准备以Ubuntu 22.04为例首先我们需要一个Linux环境。如果你用Windows强烈建议使用WSL2Windows Subsystem for Linux它几乎能提供原生的Linux体验。系统更新与基础工具安装sudo apt update sudo apt upgrade -y sudo apt install -y build-essential gdb cmake git curl wgetbuild-essential包含了gcc/g等编译套件cmake是我们的构建工具git用于版本控制。安装 JsonCpp 我们不从源码编译而是使用系统包管理器安装稳定版本这样最省事。sudo apt install -y libjsoncpp-dev安装后头文件通常在/usr/include/jsoncpp/json/库文件在/usr/lib/x86_64-linux-gnu/下。获取 muduo 网络库 muduo不提供二进制包我们需要从源码编译。这是环境搭建中稍复杂的一步。# 1. 克隆 muduo 仓库 (使用官方推荐的 cmake 分支) git clone https://github.com/chenshuo/muduo.git cd muduo # 2. 检查依赖muduo 依赖 Boost 库主要用于线程、时间等非必须全部 sudo apt install -y libboost-dev libboost-system-dev libboost-thread-dev # 3. 使用 CMake 构建并安装 mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease -DCMAKE_INSTALL_PREFIX/usr/local .. # -DCMAKE_INSTALL_PREFIX 指定安装目录通常 /usr/local 是安全的 make -j$(nproc) # 使用所有CPU核心并行编译加快速度 sudo make install # 安装到系统目录实操心得编译muduo可能需要几分钟耐心等待。-DCMAKE_BUILD_TYPERelease指定生成优化后的发布版本性能更好。调试阶段可以换成Debug。安装到/usr/local后CMake可以通过find_package(muduo)来找到它。如果安装到其他路径可能需要手动设置CMAKE_PREFIX_PATH。验证环境 创建一个简单的测试程序test_env.cpp#include muduo/net/TcpServer.h #include json/json.h #include iostream int main() { std::cout Muduo and JsonCpp environment is ready! std::endl; Json::Value root; root[hello] world; std::cout root.toStyledString() std::endl; return 0; }用CMakeLists.txt编译它如果能成功编译运行说明环境基本就绪。3.2 使用CMake初始化项目骨架现在我们来创建我们的Json-Rpc框架项目目录结构。清晰的目录结构是项目可维护性的关键。json_rpc_framework/ ├── CMakeLists.txt # 项目根目录的CMake配置文件 ├── README.md # 项目说明 ├── src/ # 框架源代码目录 │ ├── CMakeLists.txt # 源码层的CMake配置 │ ├── rpc/ # RPC框架核心代码 │ │ ├── server/ # 服务器端相关类 │ │ ├── client/ # 客户端相关类后续实现 │ │ ├── protocol/ # 协议编解码 │ │ ├── serializer/ # 序列化/反序列化 (适配JsonCpp) │ │ └── service/ # 服务注册与管理 │ └── utils/ # 工具类如日志、异常 ├── examples/ # 使用示例 │ ├── CMakeLists.txt │ ├── echo_server.cpp # 示例回声服务 │ └── echo_client.cpp ├── tests/ # 单元测试 │ └── CMakeLists.txt └── third_party/ # 第三方库如果需要本地拷贝 └── CMakeLists.txt根目录CMakeLists.txt的关键配置cmake_minimum_required(VERSION 3.10) project(JsonRpcFramework VERSION 0.1.0 LANGUAGES CXX) # 设置C标准 set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # 禁用编译器扩展保证跨编译器兼容性 # 设置输出目录让编译生成的文件可执行文件、库都放在build目录下保持源码目录干净 set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) # 查找依赖包 find_package(JsonCpp REQUIRED) find_package(muduo REQUIRED) # 依赖于我们之前安装到系统的muduo # 添加子目录 add_subdirectory(src) add_subdirectory(examples) # add_subdirectory(tests) # 测试目录后续开启src/CMakeLists.txt# 定义我们的核心库目标 add_library(json_rpc_core rpc/server/rpc_server.cpp rpc/protocol/json_rpc_protocol.cpp rpc/serializer/json_serializer.cpp rpc/service/service_manager.cpp utils/logger.cpp ) # 为库目标链接依赖项使用现代CMake的target_link_libraries target_link_libraries(json_rpc_core PUBLIC muduo::muduo_net JsonCpp::JsonCpp ) # 包含头文件目录 target_include_directories(json_rpc_core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} )examples/CMakeLists.txt# 示例回声服务器 add_executable(echo_server echo_server.cpp) target_link_libraries(echo_server json_rpc_core) # 示例回声客户端后续实现 add_executable(echo_client echo_client.cpp) target_link_libraries(echo_client json_rpc_core)这样一个基于CMake的、模块清晰的项目骨架就搭建好了。在项目根目录执行mkdir build cd build cmake .. make -j$(nproc)编译成功后你会在build/bin/目录下看到echo_server可执行文件。4. 第一个里程碑实现一个迷你版RPC服务器在深入协议细节之前我们先搭建一个最小的、能跑通的“架子”验证我们的环境和技术选型是否工作。这个服务器不处理复杂的Json-Rpc协议只实现一个简单的回声Echo服务用于验证muduo网络库的基本使用。4.1 设计一个最简单的RPC服务器类我们在src/rpc/server/rpc_server.h中声明一个最基础的服务器类// src/rpc/server/rpc_server.h #ifndef JSON_RPC_FRAMEWORK_RPC_SERVER_H #define JSON_RPC_FRAMEWORK_RPC_SERVER_H #include muduo/net/TcpServer.h #include muduo/net/EventLoop.h #include muduo/net/InetAddress.h #include functional #include string namespace jsonrpc { class RpcServer { public: using MessageCallback std::functionvoid(const std::string, std::string); RpcServer(muduo::net::EventLoop* loop, const muduo::net::InetAddress listenAddr); ~RpcServer() default; void start(); // 启动服务器 void setMessageCallback(const MessageCallback cb) { messageCallback_ cb; } private: void onConnection(const muduo::net::TcpConnectionPtr conn); void onMessage(const muduo::net::TcpConnectionPtr conn, muduo::net::Buffer* buf, muduo::Timestamp time); muduo::net::EventLoop* loop_; muduo::net::TcpServer server_; MessageCallback messageCallback_; // 消息处理回调 }; } // namespace jsonrpc #endif关键点解析依赖muduo核心成员是muduo::net::TcpServer和EventLoop。一个TcpServer绑定到一个EventLoop上。回调函数MessageCallback是一个std::function用于定义当服务器收到一个完整消息字符串时上层业务逻辑如何处理。这里我们先简单设计为输入一个请求字符串输出一个响应字符串。私有方法onConnection和onMessage是muduo网络事件的标准回调我们将它们注册给TcpServer。4.2 实现服务器核心逻辑在src/rpc/server/rpc_server.cpp中实现// src/rpc/server/rpc_server.cpp #include rpc_server.h #include muduo/net/TcpConnection.h #include muduo/net/Buffer.h #include iostream namespace jsonrpc { RpcServer::RpcServer(muduo::net::EventLoop* loop, const muduo::net::InetAddress listenAddr) : loop_(loop), server_(loop, listenAddr, RpcServer), messageCallback_(nullptr) { // 设置连接建立/断开回调 server_.setConnectionCallback( std::bind(RpcServer::onConnection, this, std::placeholders::_1)); // 设置消息到达回调 server_.setMessageCallback( std::bind(RpcServer::onMessage, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); } void RpcServer::start() { std::cout RpcServer starting on server_.ipPort() std::endl; server_.start(); } void RpcServer::onConnection(const muduo::net::TcpConnectionPtr conn) { if (conn-connected()) { std::cout New connection from conn-peerAddress().toIpPort() std::endl; } else { std::cout Connection closed from conn-peerAddress().toIpPort() std::endl; } } void RpcServer::onMessage(const muduo::net::TcpConnectionPtr conn, muduo::net::Buffer* buf, muduo::Timestamp time) { // 从Buffer中取出所有可读数据作为一个完整的“消息” // 注意这里我们简单地将所有数据视为一个字符串实际Json-Rpc需要根据协议解析消息边界。 std::string msg(buf-retrieveAllAsString()); std::cout Received message: msg std::endl; std::string response; if (messageCallback_) { // 调用用户设置的消息处理回调 messageCallback_(msg, response); } else { response Error: No message handler set.; } // 将响应发送回客户端 conn-send(response); } } // namespace jsonrpc关键点解析构造函数初始化TcpServer并绑定连接和消息回调。std::bind用于将成员函数和this指针绑定成可调用对象。onMessage这是核心。muduo::net::Buffer是muduo提供的高性能缓冲区。buf-retrieveAllAsString()取出所有数据。这里我们做了一个重要的简化假设TCP流中的每一个“写”操作就是一个完整的JSON消息。实际上TCP是流式协议存在粘包/拆包问题我们需要定义自己的协议如长度前缀、分隔符来界定消息边界。这将是下一篇文章的重点。回调机制通过messageCallback_将网络层与业务逻辑解耦。网络层只负责收/发数据具体这个数据JSON字符串代表什么请求由上层回调函数解析和处理。4.3 编写示例回声服务现在我们在examples/echo_server.cpp中使用这个基础服务器// examples/echo_server.cpp #include rpc/server/rpc_server.h #include muduo/net/EventLoop.h #include muduo/net/InetAddress.h #include iostream int main() { // 1. 创建事件循环Reactor核心 muduo::net::EventLoop loop; // 2. 指定服务器监听地址和端口 muduo::net::InetAddress listenAddr(8888); // 3. 创建我们的RPC服务器实例 jsonrpc::RpcServer server(loop, listenAddr); // 4. 设置消息处理回调简单的回声 server.setMessageCallback([](const std::string request, std::string response) { std::cout [Business Logic] Processing: request std::endl; response Echo: request; // 业务逻辑原样返回 }); // 5. 启动服务器 server.start(); // 6. 进入事件循环等待客户端连接和消息 loop.loop(); // 这是一个阻塞调用直到loop被要求退出 return 0; }4.4 编译与运行测试在项目根目录的build文件夹下执行make生成echo_server。cd build make -j$(nproc) ./bin/echo_server你应该能看到输出RpcServer starting on 0.0.0.0:8888。使用telnet进行测试 打开另一个终端使用telnet连接服务器并发送消息。telnet localhost 8888 Trying 127.0.0.1... Connected to localhost. Escape character is ^]. Hello Json-Rpc! # 你输入的内容 Echo: Hello Json-Rpc! # 服务器返回的内容在服务器终端你会看到相应的连接和消息日志。恭喜至此我们已经完成了最基础的一步一个基于muduo的、能接收和响应TCP消息的服务器骨架。虽然它还不能理解Json-Rpc协议但网络通信的基石已经打好了。5. 常见问题与排查技巧实录在环境搭建和第一个示例编写过程中你可能会遇到以下问题。这里我记录下我踩过的坑和解决方法。5.1 编译问题问题1找不到 muduo 头文件或库fatal error: muduo/net/TcpServer.h: No such file or directory排查确认安装路径sudo find /usr -name TcpServer.h查找头文件位置。如果安装在/usr/local通常会在/usr/local/include下。检查CMake的find_package确保muduo的安装路径在CMake的搜索路径中。如果安装到自定义目录如/opt/muduo需要在CMakeLists.txt中通过set(CMAKE_PREFIX_PATH /opt/muduo)或find_package(muduo REQUIRED PATHS /opt/muduo)来指定。手动编译验证创建一个最简单的测试程序用g直接编译链接g -stdc11 test.cpp -lmuduo_net -lmuduo_base -lpthread看是否能成功。问题2JsonCpp链接错误undefined reference to Json::Value::toStyledString() const‘排查这通常是链接器找不到JsonCpp库。确保find_package(JsonCpp REQUIRED)成功。检查target_link_libraries中是否包含了JsonCpp::JsonCpp现代CMake目标或jsoncpp_lib旧式变量。可以尝试在终端直接编译g test.cpp -ljsoncpp看是否可行。5.2 运行时问题问题服务器启动后telnet连接立即断开或无响应排查检查端口占用netstat -tlnp | grep 8888查看8888端口是否已被其他程序占用。检查防火墙Linux上可能是ufw或firewalld阻止了端口访问。可以临时关闭防火墙测试sudo ufw disable生产环境慎用。检查服务器绑定地址在代码中muduo::net::InetAddress listenAddr(8888)绑定的是0.0.0.0所有接口。如果只想绑定本地回环可以使用muduo::net::InetAddress listenAddr(127.0.0.1, 8888)。查看服务器日志确认onConnection回调是否被触发。如果没触发可能是连接根本没建立成功。5.3 设计思考与经验关于Buffer和消息边界我们第一个版本简单地将一次recv到的所有数据当作一个消息这在实际网络中是绝对错误的。TCP是字节流多次发送的“消息”可能在接收端被合并粘包一次发送的“消息”也可能被拆分成多次接收拆包。解决方案在协议层定义消息边界。常见方法有长度前缀在消息头部固定字节如4字节int表示后续JSON数据的长度。这是最高效、最常用的方法。分隔符用一个特殊的字符如\n作为消息结束标志。但要求消息内容本身不能包含该分隔符需要转义效率较低。 我们将在下一篇文章中实现长度前缀法。关于线程安全我们的RpcServer在onMessage回调中直接调用messageCallback_。muduo保证了对于同一个TcpConnection其回调是顺序执行的但不同的连接可能在不同的IO线程中回调。如果messageCallback_涉及共享数据需要考虑线程安全。一个简单的做法是在业务回调内部加锁或者确保业务逻辑是无状态的。关于错误处理当前版本几乎没有错误处理。例如如果messageCallback_抛出异常整个服务器线程可能会崩溃。一个健壮的生产框架需要捕获异常并返回一个格式化的JSON-RPC错误响应给客户端。6. 下一步规划与核心挑战预览第一弹我们成功搭建了环境并实现了一个能跑通的网络服务器骨架。但这离一个真正的Json-Rpc框架还有很远的距离。下一篇文章我们将直面核心挑战并实现以下功能定义并实现协议层设计基于长度前缀的消息边界协议实现一个ProtocolCodec类负责从TCP流中正确地分割出完整的JSON字符串。解析Json-Rpc协议使用JsonCpp解析JSON字符串根据Json-Rpc 2.0规范提取method,params,id等字段并构建响应对象。实现服务注册与调用设计一个ServiceManager允许用户将C函数注册为RPC方法。当收到请求时能自动查找并调用对应的函数并将返回值序列化为JSON。引入更易用的API封装一个更友好的服务器API让用户通过几行代码就能发布服务。真正的挑战在于如何优雅地将一个字符串形式的“方法名”映射到一个类型各异的C函数上并安全地进行参数传递和返回值序列化。这需要用到一些C模板元编程的技巧。我已经有了初步的实现方案我们下一弹见分晓。环境搭建是万里长征的第一步虽然琐碎但稳定的基础是后续高效开发的保障。如果你在搭建过程中遇到任何问题欢迎在评论区交流。