📢博客主页: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__); \} \
}
关键点解析
- 条件编译
if (level >= LDEFAULT) 根据日志级别动态决定是否输出,运行时判断而非编译期过滤。 - 时间戳生成
time(NULL)
获取当前时间戳(秒级精度)。
localtime(&t)
转换为本地时间,但非线程安全(返回静态缓冲区)。
strftime
格式化时间为 月-日 时:分:秒。 - 上下文信息嵌入
__FILE__
:自动替换为当前源文件名。
__LINE__
:自动替换为日志代码所在行号。 - 可变参数处理
使用##__VA_ARGS__
兼容无额外参数的日志调用(如 DLOG(“Hello”))。
注意:## 是 GNU 扩展语法,非标准 C,但主流编译器支持。 - 格式化字符串拼接
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 messageILOG("Info: x=%d", 100); // 输出: [06-25 14:30:00][test.c:21] Info: x=100ELOG("Error occurred!"); // 输出: [06-25 14:30:00][test.c:22] Error occurred!return 0;
}
1.6 整体代码
#include <stdio.h>
#include <time.h>#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;
}
关键点
- 工厂模式
StreamWriterBuilder
是生产StreamWriter
的工厂类,解耦对象的创建逻辑,便于未来扩展不同写入器类型(如格式化或压缩输出)。 - 智能指针管理资源
std::unique_ptr
自动管理StreamWriter
的生命周期,避免手动delete
导致的内存泄漏。 - 流式写入
sw->write(val, &ss)
将JSON
数据序列化到std::stringstream
中,支持大文件分块写入,降低内存压力。 - 错误处理
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;
}
关键点
- 工厂模式与资源管理
与序列化类似,使用CharReaderBuilder
和unique_ptr
确保解析器安全释放。 - 错误信息收集
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 <iostream>
#include <string>
#include <memory>
#include <sstream>
#include <jsoncpp/json/json.h>// 实现数据的序列化
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)
,也叫通用唯一识别码,通常由 32
位 16
进制数字字符组成。UUID的标准型式包含 32
个 16
进制数字字符,以连字号分为五段,形式为8-4-4-4-12
的32个字符如:550e8400-e29b-41d4-a716-446655440000
。
在这里,uuid生成,我们采用生成8个随机数字
,加上8字节序号
,共16字节数组生成32位16进制字符的组合形式来确保全局唯一的同时能够根据序号来分辨数据(随机数肉眼分辨起来真是太难了.)。
3.1 UUID生成方法思路
该代码旨在生成一个 自定义格式的UUID,结合了 随机数 和 递增序号 两部分,核心思路如下:
- 随机性部分:生成前8字节的随机数据,模拟UUID的随机性。
- 有序性部分:生成后8字节的递增序号,保证局部唯一性。
- 格式化输出:按特定规则插入连字符(-),增强可读性。
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<size_t>
确保多线程安全。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 <iostream>
#include <random>
#include <chrono>
#include <sstream>
#include <iomanip> // setw setfill
#include <atomic>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赋值后自增1size_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 <cstdio>
#include <ctime>// 序列化/反序列化类
#include <iostream>
#include <string>
#include <memory>
#include <sstream>
#include <jsoncpp/json/json.h>// UUID类
#include <chrono>
#include <sstream>
#include <iomanip> // setw setfill
#include <atomic>
#include <random>#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框架】- 项目实现 - 零碎功能接口篇 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~