项目02--JsonRpc
基于 C++、JsonCpp、muduo 网络库实现一个简单、易用的 RPC 通信框架,涉及基础的网络开发知识,它实现了同步调用、异步 callback 调用、异步 futrue 调用、服务注册 / 发现,服务上线 / 下线以及发布订阅等功能设计。
一、JsonRpc框架项目准备
1.1 Rpc介绍
RPC 是 Remote Procedure Call 的缩写,翻译成中文是远程过程调用,是一种计算机通信协议,它可以让程序像调用本地函数 / 方法一样,调用另一台计算机上的函数 / 方法,不需要开发者手动处理底层的网络通信细节。
工作流程:
需实现的功能框架:
RPC与HTTP 接口的区别:
| 对比维度 | RPC | HTTP 接口 |
|---|---|---|
| 调用方式 | 像调用本地方法,更简洁 | 需要手动构造请求、处理响应,通常使用 HTTP 的 GET/POST 等方法 |
| 性能 | 通常使用更高效的序列化协议(如Protobuf)和传输协议(如 TCP),性能更高 | 基于 HTTP 协议,序列化通常用JSON,性能相对较低 |
| 适用场景 | 多用于微服务架构内部的服务之间通信,追求性能和开发效率 | 多用于对外提供的接口,比如给前端、第三方系统调用,兼容性更强 |
1.2选择合适的实现方案
问题1:RPC 的两种实现方案
(一)client 和 server 继承公共接口
- 1.上图中根据IDL(接口描述语言)定义公共接口
- 2.编写代码生成器根据 IDL 语言生成相关的 C++、Java 代码
- 3. 然后我们的客户端和服务器程序共同向上继承公共接口即可
- 4. 比如我们常用的Protobuf、json可以定义 IDL 接口,并生成 RPC 相关的代码
- 缺点:使用Protobuf因为生成一部分代码(
protoc编译器会自动生成 C++ 代码(消息类、序列化 / 反序列化方法、RPC 服务桩代码),方便但不了解其生成的代码细节所以对理解不够友好;如果是json定义 IDL 语言需要自己编写代码生成器难度大,暂不考虑这种方案
(二)实现一个通用的远程调用接口call
使用者仅需传入函数名、参数等信息,即可完成 RPC 接口调用。无需让客户端 / 服务端继承公共接口、也无需依赖 IDL(接口描述语言)和代码生成器,降低了使用门槛,更适配 “简单、易用” 的框架定位。这类方案的典型实现逻辑通常包括:
- 参数封装:将函数名、参数序列化为统一格式(如 JSON),通过
call接口传输; - 服务端路由:服务端接收请求后,根据函数名找到对应的实现方法,执行后返回结果;
- 通用性:接口
call是统一入口,可适配同步、异步等不同调用方式,兼容性较强。
问题2:将参数和返回值映射到对应的 RPC 接口上
网络传输的参数和返回值怎么映射到对应的 RPC 接口上?
(一)使用 protobuf 的反射机制
需深入掌握 Protobuf 的 C++ 反射 API,理解 “消息描述符 - 字段映射 - 服务桩” 的底层逻辑,对不懂 Protobuf 内核的开发者不友好。需手动编写反射逻辑绑定 “接口名 - 服务方法”,反射调用需处理大量类型校验(如字段类型不匹配、必填字段缺失),增加调试成本。
(二)使用 C++ 模板、类型萃取、函数萃取等机制
需精通 C++模板元编程(TMP)、SFINAE、类型萃取、函数签名萃取、需理解 “编译期类型推导” 与 “运行时网络传输” 的衔接逻辑,需手写大量模板工具类,模板萃取代码通常晦涩难懂(如嵌套的typename、decltype、模板特化),调试困难。
(三)使用 JSON 类型,设计好参数和返回值协议即可
仅需理解 JSON 的基础结构(对象、数组、字符串 / 数字),无需掌握任何 C++ 高级特性或 Protobuf 内核;只需按约定的 JSON 协议封装参数、解析返回值,像使用普通接口一样调用 RPC,完全屏蔽底层映射逻辑;接口参数 / 返回值结构变更时,仅需修改 JSON 协议解析逻辑,无需重构框架核心代码;客户端可基于任意语言的 JSON 库(如 Java 的 Gson、Go 的 encoding/json)调用你的 RPC 接口,轻松实现跨语言通信;与 muduo 网络库无缝集成:仅需将 JSON 字符串作为网络传输载体,muduo 负责字节流收发,无需适配复杂的类型映射;可无缝支撑同步 / 异步调用;服务注册 / 发现、发布订阅等功能,均可通过扩展 JSON 协议字段(如服务元信息、订阅 topic)实现,扩展成本低;调试时无需借助 Protobuf 解析工具。
前两种技术难度和学习成本较高,我们使用第三种方式。
问题3:网络传输怎么做
(一)使用原生 socket
需手动处理 TCP连接建立、数据收发、粘包 / 拆包、超时重传、异常断开等网络细节,需从零实现异步 IO(如 select/poll/epoll)、线程池等网络基础组件,原生 socket代码耦合网络逻辑与业务逻辑,后续扩展难,需深入理解网络协议栈、IO 多路复用等底层知识。
(二)使用Boost asio 库的异步通信
需引入 Boost 库(体积大),项目编译 / 部署复杂度增加,Boost asio 的异步模型(回调 / 协程)设计复杂,Boost asio 的接口较为冗余,代码可读性相对较差,与轻量型 RPC 框架的定位不匹配。
(三)使用muduo 库 学习开发成本较低
是专为 C++ 服务端设计的轻量网络库,与 “简单 RPC 框架”定位完全匹配;接口设计简洁,封装了 epoll、线程池等底层细节,上手极快,开箱即用的异步 IO 模型,无需手动实现网络基础组件,内置Buffer类自动处理粘包 / 拆包,网络层与业务层解耦,后续扩展(如 SSL、UDP)成本低。基于 epoll 的高效 IO 复用,性能接近原生 socket,同时避免了原生开发的复杂度。
muduo 是当前场景下的最优选择:既匹配技术栈、降低学习成本,又能高效实现网络传输,完全支撑 “简单、易用” 的 RPC 框架设计目标。
1.3 环境搭建(Ubuntu22.04&CentOS)
主要使用的是Ubuntu示例如下:(ceontos7由于老化等原因不好连接VScode)
Ubuntu
CentOS
注意事项1:
如果需要使用VSCode与虚拟机进行网络连接,可用如下操作获得虚拟机IP地址后再与VSCode相连接,也可跳过此步骤不用VSCode后续直接在虚拟机环境中操作:
(1)安装Wget
(2)更新软件源
(换成国内的否则下载速度会慢)
注意事项2:
上述命令是CentOS 7 系统中替换 yum 软件源为阿里云镜像源的操作;
(3)安装scl与epel软件源
(阿里可能没有的就从这两处下载)
(4)安装lrzsz传输工具
(用于从虚拟机到主机传输文件)
(5)安装高版本的编译器
注意事项3:
(6)make/gdb/git的安装
(常规开发工具的安装同上述的Ubuntu)
make是编译构建工具,用于执行Makefile脚本,自动化编译、链接等项目构建流程,是 C/C++ 等项目开发的基础工具之一。
gdb是程序调试工具,可用于断点调试、查看内存 / 变量、跟踪程序执行流程等,是定位 C/C++ 程序 bug 的核心工具。
git用于代码版本管理、协同开发,可实现代码的提交、分支管理、远程仓库同步(如 GitHub/GitLab)等功能。
(7)JsonCpp库的安装
注意事项4:序列化与反序列化的需要
jsoncpp-devel包含 JsonCpp 的头文件和编译依赖,是 C++ 项目中解析 / 生成 JSON 数据的常用库,安装后可在代码中通过#include <json/json.h>使用。
/usr/include/jsoncpp/json/是 JsonCpp 头文件的默认安装目录,执行ls命令可验证头文件(如assertions.h、autolink.h、config.h等)是否成功安装,确保后续编译项目时能找到 JsonCpp 的依赖文件。
(8)安装cmake
CMake 是跨平台的项目构建工具,用于生成 Makefile、Visual Studio 工程等构建文件,适用于多平台、大型C/C++ 项目的编译管理,是替代手动编写 Makefile的常用工具。
(9)安装Muduo&Boost库
muduo 是一款基于 Reactor 模式的C++ 网络库,专注于Linux 服务端高性能网络通信开发,常被用于构建 RPC 框架、服务器程序等场景(与要搭建的 RPC 框架技术栈高度匹配)。克隆源码后,通常需要编译、安装,才能在项目中链接使用其网络通信能力。
Reactor 模式是一种事件驱动的并发编程模型,核心是 “通过一个事件循环监听事件,再分发到对应的处理逻辑”
boost:安装 Boost 库的运行时组件,提供程序运行时依赖的基础功能;
boost-devel:安装 Boost 库的开发包,包含头文件、静态库等编译依赖,用于基于 Boost 开发 C++ 程序。
Boost 是 C++ 的通用库集合,提供了智能指针、线程、网络、序列化等丰富功能,常被用于增强 C++ 程序的开发效率与功能完整性( 本次的RPC 框架需用到线程同步、数据序列化等能力,会依赖 Boost)
用git获取muduo方案可能会比较慢,用wget可能下不完整,也可用下方gitee中的压缩包下载解压:
获取后记得运行下述命令编译、安装:
./build.sh:仅编译 muduo 库(不安装)、生成静态库 / 共享库文件,但仅保留在源码目录的编译产物目录中,不拷贝到系统全局路径执行./build.sh时,脚本会在muduo目录外创建build目录,存放编译过程的临时文件、最终的库产物(如libmuduo_base.a)。
。./build.sh install:编译并安装 muduo 库(全局可用),将 muduo 的头文件、库文件拷贝到系统全局路径,让所有项目可直接链接使用。
1.4 Json&JsonCpp介绍
可嵌套使用如下:
其主要通过以下3个类实现序列化与反序列化:
Json::Value
JSON 数据的核心载体类
- 作用:表示任意 JSON 数据(对象、数组、字符串、数字等),是 Jsoncpp 中最基础的类,用于存储 JSON 结构的内容。
- 使用场景:
- 序列化时,通过
Json::Value构建 JSON 对象 / 数组,再转换为字符串; - 反序列化时,解析 JSON 字符串后得到的结果会存储在
Json::Value中,再从中提取数据。
- 序列化时,通过
Json::Value类的核心成员函数说明(涵盖了 JSON 数据的赋值、操作、类型转换等功能):
上述接口是在 RPC 框架中封装请求参数、解析响应数据的核心工具 ——通过operator[]构建 JSON 对象,通过append处理数组,再通过asXXX()将 JSON 值转为业务代码所需的 C++ 类型,完成数据的序列化与反序列化。
Json::Writer
JSON 序列化类
- 作用:将
Json::Value对象转换为 JSON 格式的字符串(即序列化)。
这些类是 Jsoncpp 提供的更灵活的序列化接口,可通过StreamWriterBuilder配置序列化格式(如缩进、空格),更适配复杂场景下的 RPC 数据封装。
Json::Reader
JSON 反序列化类
- 作用:将 JSON 格式的字符串解析为
Json::Value对象(即反序列化)。 - 使用场景:接收网络传输的 JSON 字符串(如你的 RPC 框架的请求参数)后,通过
Json::Reader解析为Json::Value,再提取其中的字段值。
序列化与反序列案例:
1.5Muduo网络库介绍
muduo 是一款基于 Reactor 模式的C++ 网络库,专注于Linux 服务端高性能网络通信开发
有一个多路转接模型进行 socket 事件监控,触发 IO 事件后进行 IO 处理的这种通信处理模型就叫做Reactor 模型,核心是“通过一个事件循环监听事件,再分发到对应的处理逻辑”.
核心组件(类与函数)
Muduo 网络库(基于 Reactor 模型)的核心组件,承担 TCP 通信中的不同职责
EventLoop
TcpServer
TcpClient
TcpConnection
Buffer
4.Buffer类(数据缓冲类)
作用:解决 TCP“粘包 / 拆包” 问题,提供安全的字节流读写接口(基于 “可读 / 可写”缓冲区的设计)。
| 函数 | 作用 |
|---|---|
size_t readableBytes() | 获取缓冲区中可读取的数据长度(用于判断是否有数据待处理)。 |
const char* peek() | 获取缓冲区中数据的起始地址(仅读取、不修改缓冲区,用于临时查看数据)。 |
int32_t peekInt32() const | 从缓冲区读取 4 字节数据,转换为网络字节序→主机字节序的整数(数据不删除,仅 “预览”)。 |
void retrieveInt32() | 将缓冲区的读取位置后移 4 字节(本质是 “逻辑删除” 起始 4 字节数据)。 |
int32_t readInt32() | 组合peekInt32()和retrieveInt32():读取 4 字节整数并删除对应数据。 |
string retrieveAllAsString() | 取出缓冲区中所有数据(转为 string),并清空缓冲区。 |
string retrieveAsString(size_t len) | 取出缓冲区中指定长度len的数据(转为 string),并删除对应数据。 |
CountDownLatch
核心是 “你定义逻辑,库决定调用时机”,是异步编程的核心机制
使用案例
服务器端:
客户端
MakeFile文件
补充说明
std::bind是 “适配器”,让你的成员函数能适配 Muduo 库的回调接口,实现 “你定义逻辑,库触发调用” 的异步回调机制。
回调注册函数想调用成员函数,调用前要看参数格式对不对,发现缺少隐式参数this,bind可以绑定this,使得参数格式正确,所以要用bind绑定。
1.6 异步操作future介绍
与async搭配
案例对比:
运行效果:先输出into add!(异步任务启动),1 秒后输出分隔线和结果33。
运行效果:先输出分隔线,再输出into add!(get()触发任务执行),最后输出结果33。
与packaged_task配合
案例:
与promise搭配使用
案例:
实际使用时可能会需要线程池
三种方式的对比
实际应用时的注意事项:
二、 项目设计
2.1项目预期功能
功能结构图如下:
RPC远程调用+服务注册与发现/上下线通知+消息的发布与订阅
2.2服务端模块设计
1. 网络通信NetWork:
该模块复杂庞大,主要依据Muduo库搭建,实现底层的网络通信功能。
2. 应用层协议通信Protocol:
LV格式:
3.Dispatcher:
流程:
补充:本次项目主要有3种消息类型,分别对应后面的RpcRouter模块/Publish-Subscribe模块/Registry-Discovery模块
4.RpcRouter模块
该模块包含一个存储 “方法名 - 服务描述” 的映射的哈希表,其中服务描述包含回调函数、方法名称、参数字段格式、参数校验接口等进行检验。
5.Publish-Subscribe
6. Registry-Discovery
补充:之所以在服务端设计该模块是为了让每个服务器都有能力变为注册中心,以防一个注册中心坏掉而造成服务瘫痪。
网络请求与回应格式如下:
7.serve:上述6大模块的整合
2.3 客户端模块设计
Requestor 对客户端每一条请求进行管理
补充:注意其有同步、异步、回调3种处理方式,RpcCaller模块也对应要实现3种
RpcCaller模块
2.4 框架设计
抽象出的类:
三、 项目实现
(展示核心部分,完整源码会存在gitee)
3.1常用零碎功能类设计
(detail.hpp)
除上述提及的类,还有些在很多其他项目也会用到的通用类的设计
日志宏/JSON/UUID
日志用于记录、输出项目信息,JSON序列化、UUID通用唯一识别码(一种用于在分布式系统中唯一标识信息的数字,表示形式为32 个十六进制数字,以连字符分隔为 5 段,格式为8-4-4-4-12,共 36 个字符),在这里,采用生成 8 字节随机数字,加上 8字节序号,共 16 字节数组生成 128 位 16 进制字符(即32个16进制数)的组合形式来确保全局唯一的同时能够根据序号来分辨数据 。注意:1字节=8Bit,通常4Bit表示一个16进制,即1字节=>2个16进制,16字节=>32个
3.2 项目消息类型字段定义
(fields.hpp)
- 该文件是 RPC 框架的公共定义头文件,提供了框架通用的常量、枚举和工具函数,是框架各模块间通信的“协议约定”。
- 大量使用强类型枚举提升代码安全性,使用宏定义统一常量,使用无序映射实现高效错误查询,体现了良好的 C++ 编码规范。
- 功能上覆盖了RPC 通信的消息类型、状态码、请求类型、主题操作、服务操作等核心维度,为后续的消息序列化、服务治理、主题通信提供了基础支撑。
包含以下6个类型的定义:
3.3 抽象层设计
(abstract.hpp)
用虚函数、抽象类为后面的具像层要具体实现的功能提供接口函数,模块间关系如下:
3.4 消息抽象的实现
(message.hpp)
继承上述的BaseMessage抽象类,实现对消息对象处理的具体功能(解决了 JSON 请求与响应消息/RPC请求与响应消息/Topic请求与响应消息/Service请求与响应消息的编解码、合法性校验等检测),并用工厂模式,将消息对象的创建逻辑集中管理。
3.5 网络相关抽象的实现
(net.hpp)
依据Muduo库实现了缓冲区、消息处理协议,连接,服务器,客户端;所有具体实现类(MuduoBuffer、LVProtocol 等)均通过工厂类创建,降低耦合,便于扩展。
3.6 消息分发的实现
消息回调处理与消息分发核心模块,负责注册不同类型消息的处理函数,并在收到消息时完成精准分发与类型转换
3.7服务端RpcRouter模块
RPC(远程过程调用)服务端的核心路由与服务管理模块,主要实现了 RPC 方法的注册、查询、参数校验、业务调度及响应反馈等功能。
补充:
3.8客户端Requestor模块
是 RPC客户端的核心请求管理组件,通过 rid (消息的id)关联请求与响应,支持三种请求模式,保证线程安全和内存安全。
补充:
3.9客户端RpcCaller模块
客户端的RPC请求模块,发送RPC请求。
补充:
promis是函数内的局部变量,出了函数就失效,用智能指针管理起来,可保证出了函数后想用其时还能有效。