博客主页:https://blog.csdn.net/2301_779549673
博客仓库:https://gitee.com/JohnKingW/linux_test/tree/master/lesson
欢迎点赞 收藏 ⭐留言 如有错误敬请指正!
本文由 JohnKi 原创,首发于 CSDN
未来很长,值得我们全力奔赴更美好的生活✨
前几篇文章中,笔者介绍了rpc
的原理和目的,也介绍了需要使用的部分第三方库
和我们所需实现的功能
这篇文章,笔者就将整个项目的开头工作做好,也就是将 日志宏
和 序列化、反序列化
封装成 UUID工具类
,为后续的整个项目功能的实现去除不必要的阻碍,话不多说,这就开始
项目在运行中可能会出现各种问题,出问题不可怕,关键的是要能找到问题,并解决问题。解决问题的方式:
该日志宏旨在实现 分级日志输出
,支持动态控制日志级别,并自动携带 时间戳、文件名、行号
等调试信息。
核心功能包括:
日志级别常量
#define LDBG 0 // 调试日志(最低级别)
#define LINF 1 // 提示日志
#define LERR 2 // 错误日志(最高级别)
#define LDEFAULT LDBG // 默认显示 Debug 及以上级别
级别关系:LDBG < LINF < LERR,级别越高表示越重要。
过滤规则:当日志的级别 >= LDEFAULT 时才会输出。
#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__); \
} \
}
关键点解析
time(NULL)
获取当前时间戳(秒级精度)。localtime(&t)
转换为本地时间,但非线程安全(返回静态缓冲区)。strftime
格式化时间为 月-日 时:分:秒。__FILE__
:自动替换为当前源文件名。__LINE__
:自动替换为日志代码所在行号。 ##__VA_ARGS__
兼容无额外参数的日志调用(如 DLOG(“Hello”))。printf("[%s][%s:%d]" format "\n", ...)
将用户传入的 format 字符串与固定前缀拼接,实现灵活输出。#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)。
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;
}
#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__);
这个部分的代码其实就是前几篇介绍第三方库 jsoncpp
中的方法,这里再给大家重温一下
这两个函数基于 JsonCpp
库实现 JSON 的序列化(内存对象 → 字符串)和反序列化(字符串 → 内存对象),核心目标是 简化 JSON 数据的转换流程,同时通过工厂模式和智能指针管理资源,确保代码的 安全性与可维护性。
功能:
将 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
的返回值定义。功能:
将 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
字符串用于捕获解析失败的具体原因(如语法错误),提升调试效率。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;
}
#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(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进制字符的组合形式来确保全局唯一的同时能够根据序号来分辨数据(随机数肉眼分辨起来真是太难了.)。
该代码旨在生成一个 自定义格式的UUID,结合了 随机数 和 递增序号 两部分,核心思路如下:
std::random_device rd; // 硬件熵源(真随机数)
std::mt19937 generator(rd()); // 梅森旋转伪随机数生成器
std::uniform_int_distribution<int> distribution(0, 255); // 生成0-255的整数
作用:利用硬件熵源初始化伪随机数生成器,生成高质量的随机字节。
关键点:
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)。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字节中的每个字节。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
#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框架】- 项目实现 - 零碎功能接口篇 做了一个较为详细的介绍,不知道对你有没有帮助呢
觉得博主写得还不错的三连支持下吧!会继续努力的~