【从零实现Json-Rpc框架】- 项目实现 - 零碎功能接口篇

博客主页:https://blog.csdn.net/2301_779549673
博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
欢迎点赞 收藏 ⭐留言 如有错误敬请指正!
本文由 JohnKi 原创,首发于 CSDN
未来很长,值得我们全力奔赴更美好的生活✨

在这里插入图片描述

在这里插入图片描述

文章目录

  • 前言
  • ️‍一、简单日志宏实现
    • 1.1 意义: 快速定位程序运行逻辑出错的位置。
    • 1.2 设计目标:
    • 1.3 核心日志宏
    • 1.4 具体日志宏
    • 1.5 使用示例
    • 1.6 整体代码
  • ️‍二、Json序列化/反序列化
    • 2.1 设计思路
    • 2.2 序列化函数 serialize
    • 2.3 反序列化函数 unserialize
    • 2.4 使用示例
    • 2.5 整体代码
  • ️‍三、UUID工具类生成
    • 3.1 UUID生成方法思路
    • 3.2 初始化随机数生成器
    • 3.3 生成随机部分并格式化
    • 3.4 生成递增序号部分
    • 2.5 使用示例
    • 2.6 整体代码
  • ️‍四、零碎接口整体封装
  • 总结


前言

前几篇文章中,笔者介绍了rpc的原理和目的,也介绍了需要使用的部分第三方库和我们所需实现的功能

这篇文章,笔者就将整个项目的开头工作做好,也就是将 日志宏序列化、反序列化 封装成 UUID工具类,为后续的整个项目功能的实现去除不必要的阻碍,话不多说,这就开始


️‍一、简单日志宏实现

1.1 意义: 快速定位程序运行逻辑出错的位置。

项目在运行中可能会出现各种问题,出问题不可怕,关键的是要能找到问题,并解决问题。解决问题的方式:

  • gdb调试: 逐步调试过于繁琐,缓慢。主要用于程序崩溃后的定位。
  • 系统运行日志分析: 在任何程序运行有可能逻辑错误的位置进行输出提示,快速定位逻辑问题的位暖。

1.2 设计目标:

该日志宏旨在实现 ​分级日志输出,支持动态控制日志级别,并自动携带 ​时间戳、文件名、行号 等调试信息。

核心功能包括

  • ​按级别过滤日志​(Debug、Info、Error)
  • 自动填充日志的上下文信息
  • 兼容可变参数格式化

日志级别常量

#define LDBG 0      // 调试日志(最低级别)
#define LINF 1      // 提示日志
#define LERR 2      // 错误日志(最高级别)
#define LDEFAULT LDBG // 默认显示 Debug 及以上级别

级别关系:LDBG < LINF < LERR,级别越高表示越重要。
过滤规则:当日志的级别 >= LDEFAULT 时才会输出。

1.3 核心日志宏

#define LOG(level, format, ...) { \
    if (level >= LDEFAULT) { \
        time_t t = time(NULL); \
        struct tm *lt = localtime(&t); \
        char time_tmp[32]; \
        strftime(time_tmp, sizeof(time_tmp) - 1, "%m-%d %T", lt); \
        printf("[%s][%s:%d]" format "\n", time_tmp, __FILE__, __LINE__, ##__VA_ARGS__); \
    } \
}

关键点解析

  1. 条件编译
    if (level >= LDEFAULT) 根据日志级别动态决定是否输出,​运行时判断而非编译期过滤。
  2. 时间戳生成
    time(NULL) 获取当前时间戳(秒级精度)。
    localtime(&t) 转换为本地时间,但非线程安全​(返回静态缓冲区)。
    strftime 格式化时间为 月-日 时:分:秒。
  3. 上下文信息嵌入
    __FILE__:自动替换为当前源文件名。
    __LINE__:自动替换为日志代码所在行号。
  4. 可变参数处理
    使用 ##__VA_ARGS__ 兼容无额外参数的日志调用(如 DLOG(“Hello”))。
    注意:## 是 GNU 扩展语法,非标准 C,但主流编译器支持。
  5. ​格式化字符串拼接
    printf("[%s][%s:%d]" format "\n", ...) 将用户传入的 format 字符串与固定前缀拼接,实现灵活输出。

1.4 具体日志宏

#define DLOG(format, ...) LOG(LDBG, format, ##__VA_ARGS__)
#define ILOG(format, ...) LOG(LINF, format, ##__VA_ARGS__)
#define ELOG(format, ...) LOG(LERR, format, ##__VA_ARGS__)

简化调用:通过宏封装,用户无需手动传递日志级别。
​示例:DLOG(“Value: %d”, 42) 展开为 LOG(0, “Value: %d”, 42)。

1.5 使用示例

int main() {
    DLOG("Debug message");       // 输出: [06-25 14:30:00][test.c:20] Debug message
    ILOG("Info: x=%d", 100);     // 输出: [06-25 14:30:00][test.c:21] Info: x=100
    ELOG("Error occurred!");    // 输出: [06-25 14:30:00][test.c:22] Error occurred!
    return 0;
}

1.6 整体代码

#include 
#include 


#define LDBG 0      // 调试日志
#define LINF 1      // 提示日志
#define LERR 2      // 错误日志

#define LDEFAULT LDBG    // 默认日志等级


// strftime() 目的是格式化时间,将 tm 结构转换为字符串
    // 第一个参数是字符串指针,第二个参数是字符串的最大长度,第三个参数是格式化字符串,第四个参数是 tm 结构指针
// extern size_t strftime (char *__restrict __s, size_t __maxsize,
			// const char *__restrict __format,
			// const struct tm *__restrict __tp) __THROW;

// __FILE__ 会替换为当前源文件名,__LINE__ 会替换为当前行号
#define LOG(level, format, ...){\
    if(level >= LDEFAULT){\
        time_t t = time(NULL);\
        struct tm *lt = localtime(&t);\
        char time_tmp[32];\
        strftime(time_tmp, sizeof(time_tmp) - 1, "%m-%d %T", lt);\
        printf("[%s][%s:%d]" format "\n", time_tmp, __FILE__, __LINE__, ##__VA_ARGS__);\
    }\
}

#define DLOG(format, ...) LOG(LDBG, format, ##__VA_ARGS__);
#define ILOG(format, ...) LOG(LINF, format, ##__VA_ARGS__);
#define ELOG(format, ...) LOG(LERR, format, ##__VA_ARGS__);

️‍二、Json序列化/反序列化

这个部分的代码其实就是前几篇介绍第三方库 jsoncpp 中的方法,这里再给大家重温一下

2.1 设计思路

这两个函数基于 ​JsonCpp 库实现 JSON 的序列化(内存对象 → 字符串)和反序列化(字符串 → 内存对象),核心目标是 ​简化 JSON 数据的转换流程,同时通过工厂模式和智能指针管理资源,确保代码的 ​安全性与可维护性

2.2 序列化函数 serialize

​功能
Json::Value 对象转换为JSON 格式的字符串。

bool serialize(const Json::Value& val, std::string& body) {
    std::stringstream ss;
    Json::StreamWriterBuilder swb;                 // 1. 创建写入器工厂
    std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter()); // 2. 工厂生产写入器
    int ret = sw->write(val, &ss);                 // 3. 将 JSON 数据写入流
    if (ret != 0) {                                // 4. 错误处理
        std::cout << "write json data error" << std::endl;
        return false;
    }
    body = ss.str();                               // 5. 提取字符串结果
    return true;
}

关键点

  1. 工厂模式
    StreamWriterBuilder 是生产 StreamWriter 的工厂类,解耦对象的创建逻辑,便于未来扩展不同写入器类型(如格式化或压缩输出)。
  2. 智能指针管理资源
    std::unique_ptr 自动管理 StreamWriter 的生命周期,避免手动 delete 导致的内存泄漏。
  3. 流式写入
    sw->write(val, &ss)JSON 数据序列化到 std::stringstream 中,支持大文件分块写入,降低内存压力。
  4. 错误处理
    write 返回非零表示失败,需注意 ​JsonCpp 文档中 write 的返回值定义​。

2.3 反序列化函数 unserialize

​功能
JSON 格式字符串解析为 Json::Value 对象。

bool unserialize(const std::string& body, Json::Value& val) {
    Json::CharReaderBuilder crb;                   // 1. 创建解析器工厂
    std::unique_ptr<Json::CharReader> cr(crb.newCharReader()); // 2. 工厂生产解析器
    std::string errs;
    // 3. 解析字符串到 JSON 对象
    bool ret = cr->parse(body.c_str(), body.c_str() + body.size(), &val, &errs);
    if (!ret) {                                     
        std::cout << "parse error: " << errs << std::endl;
        return false;
    }
    return true;
}

关键点

  1. 工厂模式与资源管理
    与序列化类似,使用 CharReaderBuilderunique_ptr 确保解析器安全释放。
  2. 错误信息收集
    errs 字符串用于捕获解析失败的具体原因(如语法错误),提升调试效率。

2.4 使用示例

int main(){
    const char* name = "小明";
    int age = 18;
    const char* sex = "男";
    float score[3] = {80, 90.5, 95};

    Json::Value student;
    student["姓名"] = name;
    student["年龄"] = age;
    student["性别"] = sex;
    student["成绩"].append(score[0]);
    student["成绩"].append(score[1]);
    student["成绩"].append(score[2]);

    Json::Value fav;
    fav["书籍"] = "西游记";
    fav["运动"] = "打篮球";
    student["喜好"] = fav;

    std::string body;
    serialize(student, body);

    std::cout << body << std::endl;

    std::string str = R"({"姓名":"小红","年龄":19,"性别":"","成绩":[85,92,98],"喜好":{"书籍":"红楼梦","运动":"游泳"}})";
    Json::Value stu;
    bool ret = unserialize(str, stu);
    if(ret == false)
        return -1;
    std::cout << "姓名:" << stu["姓名"].asString() << std::endl;
    std::cout << "年龄:" << stu["年龄"].asInt() << std::endl;
    std::cout << "性别:" << stu["性别"].asString() << std::endl;
    std::cout << "成绩:" << stu["成绩"][0].asFloat() << " " << stu["成绩"][1].asFloat() << " " << stu["成绩"][2].asFloat() << std::endl;
    std::cout << "喜好:" << stu["喜好"]["书籍"].asString() << " " << stu["喜好"]["运动"].asString() << std::endl;

    return 0;
}

2.5 整体代码

#include 
#include 
#include 
#include 
#include 


// 实现数据的序列化
bool serialize(const Json::Value& val, std::string& body){
    std::stringstream ss;

    // 先实例化一个工厂类对象
    Json::StreamWriterBuilder swb;
    // 通过工厂类对象来生产派生类对象
    std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());

    // 利用sw的write方法将val序列化到ss中
    int ret = sw->write(val, &ss);
    if(ret != 0){ 
        std::cout << "write json data error" << std::endl;
        return false;
    }
    body = ss.str();
    return true;
}


// 实现json数据的反序列化
bool unserialize(const std::string& body, Json::Value& val){
    // 实例化工厂类对象
    Json::CharReaderBuilder crb;

    // 生产CharReader对象
    std::unique_ptr<Json::CharReader> cr(crb.newCharReader());

    // 记录错误信息
    std::string errs;

    // 利用cr的parse方法将body反序列化到val中
        // parse 是 JsonCpp 库中用于将 ​JSON 格式字符串反序列化为 Json::Value 对象的核心方法。
            // bool parse(
            //     const char* beginDoc,    // JSON 字符串起始地址
            //     const char* endDoc,      // JSON 字符串结束地址(最后一个字符的下一位)
            //     Json::Value* root,       // 输出:解析后的 JSON 对象
            //     std::string* errs        // 输出:错误信息(若解析失败)
            // );
        // tip: 要正确获取std::string对象内部存储的字符串数据的地址(即C风格字符串的指针),需要使用.c_str()或.data()方法
    bool ret = cr->parse(body.c_str(), body.c_str() + body.size(), &val, &errs);
    if(ret == false){
        std::cout << "parse json data error: " << errs << std::endl;
        return false;
    }
    return true;
}

️‍三、UUID工具类生成

UUID(Universally Unique ldentifier),也叫通用唯一识别码,通常由 3216 进制数字字符组成。UUID的标准型式包含 3216 进制数字字符,以连字号分为五段,形式为8-4-4-4-12的32个字符如:550e8400-e29b-41d4-a716-446655440000

在这里,uuid生成,我们采用生成8个随机数字,加上8字节序号共16字节数组生成32位16进制字符的组合形式来确保全局唯一的同时能够根据序号来分辨数据(随机数肉眼分辨起来真是太难了.)。

3.1 UUID生成方法思路

该代码旨在生成一个 ​自定义格式的UUID,结合了 ​随机数​递增序号 两部分,核心思路如下:

  1. ​随机性部分:生成前8字节的随机数据,模拟UUID的随机性。
  2. ​有序性部分:生成后8字节的递增序号,保证局部唯一性。
  3. 格式化输出:按特定规则插入连字符(-),增强可读性。

3.2 初始化随机数生成器

std::random_device rd;                 // 硬件熵源(真随机数)
std::mt19937 generator(rd());          // 梅森旋转伪随机数生成器
std::uniform_int_distribution<int> distribution(0, 255);  // 生成0-255的整数

作用:利用硬件熵源初始化伪随机数生成器,生成高质量的随机字节。
​关键点

  • random_device 提供种子,增强熵值。
  • mt19937 生成均匀分布的伪随机数。
  • distribution(0, 255) 确保每个字节范围正确。

3.3 生成随机部分并格式化

for (int i = 0; i < 8; i++) {
    if (i == 4 || i == 6) ss << "-";    // 在第5和第7字节后插入连字符
    ss << std::hex << std::setw(2) << std::setfill('0') << distribution(generator);
}

输出示例:3e6a-1f-9b(前8字节随机数部分)。
​关键点

  • std::hex 将整数转为十六进制。
  • std::setw(2)std::setfill('0') 保证两位数格式(如 0a 而非 a)。
  • 连字符插入位置控制结构。

3.4 生成递增序号部分

static std::atomic<size_t> seq(1);      // 原子计数器(线程安全)
size_t cur = seq.fetch_add(1);          // 原子递增,获取当前值
for (int i = 7; i >= 0; --i) {
    if (i == 5) ss << "-";              // 在第6字节后插入连字符
    ss << std::hex << std::setw(2) << ((cur >> (i * 8)) & 0xFF);
}

输出示例:0100-000000000000(后8字节序号部分)。
​关键点

  • std::atomic 确保多线程安全。
  • cur >> (i * 8) & 0xFF 提取8字节中的每个字节。
  • 逆序处理字节(从高位到低位),保证序号部分的大端格式。

2.5 使用示例

int main(){
    for(int i = 0; i < 10; ++i){
        std::cout << UUID() << std::endl;
    }
    return 0;
}
wzy@VM-20-5-ubuntu:~/linux_test/lesson/CSDN_code/project1_RPC/source$ g++ -o uuid UUID.cpp
wzy@VM-20-5-ubuntu:~/linux_test/lesson/CSDN_code/project1_RPC/source$ ./uuid
39dbd4a9-76fa-f9f70000-000000000001
6e0045b2-7073-50140000-000000000002
f48f7bc4-c174-ce9b0000-000000000003
ed4cb211-28d6-a2250000-000000000004
ac137810-ba9a-5aef0000-000000000005
b4ea1bcf-91ae-38e00000-000000000006
f7b68da9-8713-91ef0000-000000000007
66799d4e-ec7d-c2c80000-000000000008
a48e5bda-b3eb-ea7c0000-000000000009
b58d78fa-2096-beb30000-00000000000a

2.6 整体代码

#include 
#include 
#include 
#include 
#include   // setw setfill
#include 


std::string UUID(){
    std::stringstream ss;

    // 1. 构造一个机器随机数对象
    std::random_device rd;

    // 2. 以机器随机数为种子构造伪随机数对象
    std::mt19937 generator (rd());

    // 3. 构造限定数据范围的对象
    std::uniform_int_distribution<int> distribution(0, 255);

    // 4. 生产 8 个随机数,按照特定格式组织成为 16 进制数字的字符串
    for(int i = 0; i < 8; i++){
        if (i == 4 || i == 6) ss << "-";
        ss << std::setw(2) << std::setfill('0') << std::hex << distribution(generator);
    }

    // 5. 定义一个 8 字节序号,逐字节组织成为16进制数字字符的字符串
    static std::atomic<size_t> seq(1);          // 原子计数器(线程安全),seq第一次为1,每次给cur赋值后自增1
    size_t cur = seq.fetch_add(1);              // cur 会被赋值为 fetch_add ​操作前的旧值,而 seq 的值已经递增
    for(int i = 7; i >= 0; --i){
        if(i == 5) ss << "-";
        ss << std::setw(2) << std::setfill('0') << std::hex << ((cur >> (i*8)) & 0xFF);
    }
    return ss.str();
}

️‍四、零碎接口整体封装

为了方便调用,这些接口肯定是要放在一起的,就命名 detail.hpp

// 日志宏定义
#pragma once

// 日志类
#include 
#include 

// 序列化/反序列化类
#include 
#include 
#include 
#include 
#include 

// UUID类
#include 
#include 
#include   // setw setfill
#include 
#include 


#define LDBG 0 // 调试日志
#define LINF 1 // 提示日志
#define LERR 2 // 错误日志

#define LDEFAULT LDBG // 默认日志等级

// strftime() 目的是格式化时间,将 tm 结构转换为字符串
// 第一个参数是字符串指针,第二个参数是字符串的最大长度,第三个参数是格式化字符串,第四个参数是 tm 结构指针
// extern size_t strftime (char *__restrict __s, size_t __maxsize,
// const char *__restrict __format,
// const struct tm *__restrict __tp) __THROW;

// __FILE__ 会替换为当前源文件名,__LINE__ 会替换为当前行号
#define LOG(level, format, ...)                                                             \
    {                                                                                       \
        if (level >= LDEFAULT)                                                              \
        {                                                                                   \
            time_t t = time(NULL);                                                          \
            struct tm *lt = localtime(&t);                                                  \
            char time_tmp[32];                                                              \
            strftime(time_tmp, sizeof(time_tmp) - 1, "%m-%d %T", lt);                       \
            printf("[%s][%s:%d]" format "\n", time_tmp, __FILE__, __LINE__, ##__VA_ARGS__); \
        }                                                                                   \
    }

#define DLOG(format, ...) LOG(LDBG, format, ##__VA_ARGS__);
#define ILOG(format, ...) LOG(LINF, format, ##__VA_ARGS__);
#define ELOG(format, ...) LOG(LERR, format, ##__VA_ARGS__);

class JSONUtil // util 多功能包
{
public:
    // 实现数据的序列化
    static bool serialize(const Json::Value &val, std::string &body)
    {
        std::stringstream ss;

        // 先实例化一个工厂类对象
        Json::StreamWriterBuilder swb;
        // 通过工厂类对象来生产派生类对象
        std::unique_ptr<Json::StreamWriter> sw(swb.newStreamWriter());

        // 利用sw的write方法将val序列化到ss中
        int ret = sw->write(val, &ss);
        if (ret != 0)
        {
            ELOG("json serislize error");
            std::cout << "write json data error" << std::endl;
            return false;
        }
        body = ss.str();
        return true;
    }

    // 实现json数据的反序列化
    static bool unserialize(const std::string &body, Json::Value &val)
    {
        // 实例化工厂类对象
        Json::CharReaderBuilder crb;

        // 生产CharReader对象
        std::unique_ptr<Json::CharReader> cr(crb.newCharReader());

        // 记录错误信息
        std::string errs;

        // 利用cr的parse方法将body反序列化到val中
        bool ret = cr->parse(body.c_str(), body.c_str() + body.size(), &val, &errs);
        if (ret == false)
        {
            // LOG 是以C语言风格输出的
            ELOG("json unserislize error: %s", errs.c_str());
            return false;
        }
        return true;
    }
};



class UUID
{
public:
    const std::string uuid()
    {
        std::stringstream ss;

        // 1. 构造一个机器随机数对象
        std::random_device rd;

        // 2. 以机器随机数为种子构造伪随机数对象
        std::mt19937 generator(rd());

        // 3. 构造限定数据范围的对象
        std::uniform_int_distribution<int> distribution(0, 255);

        // 4. 生产 8 个随机数,按照特定格式组织成为 16 进制数字的字符串
        for (int i = 0; i < 8; i++)
        {
            if (i == 4 || i == 6)
                ss << "-";
            ss << std::setw(2) << std::setfill('0') << std::hex << distribution(generator);
        }

        // 5. 定义一个 8 字节序号,逐字节组织成为16进制数字字符的字符串
        static std::atomic<size_t> seq(1);
        size_t cur = seq.fetch_add(1);
        for (int i = 7; i >= 0; --i)
        {
            if (i == 5)
                ss << "-";
            ss << std::setw(2) << std::setfill('0') << std::hex << ((cur >> (i * 8)) & 0xFF);
        }
        return ss.str();
    }
};

总结

本篇博文对 【从零实现Json-Rpc框架】- 项目实现 - 零碎功能接口篇 做了一个较为详细的介绍,不知道对你有没有帮助呢

觉得博主写得还不错的三连支持下吧!会继续努力的~

请添加图片描述

你可能感兴趣的:(json,rpc,网络协议)