前言
C中对序列化讨论少, 因为很多传输的内容都有自己解析的轮子. 对于序列化本质是统一编码, 统一解码的方式.
本文探讨是一种简单的序列化方案. 保证不同使用端都能解析出正确结果.
在文章一开始, 看一个最简单的序列化代码 如下
#include#include #define _INT_NAME (64)#define _STR_TXT "student.struct"struct student { int id; char sex; int age; char name[_INT_NAME + 1]; double high; double weight;};// struct student 结构体序列化到文件的方法static void _student_serialize(struct student* stu, FILE* txt) { fprintf(txt, "%d %c %d %s %lf %lf ", stu->id, stu->sex, stu->age, stu->name, stu->high, stu->weight);}// struct student 结构体反序列化static void _student_deserialize(struct student* stu, FILE* txt) { fscanf(txt, "%d %c %d %s %lf %lf ", &stu->id, &stu->sex, &stu->age, stu->name, &stu->high, &stu->weight);}// 简单打印数据static void _student_print(struct student* stu) { static int _idx; printf("%d: %d %c %d %s %lf %lf \n", _idx++, stu->id, stu->sex, stu->age, stu->name, stu->high, stu->weight);}/* * 一种最简单的通用序列化方法 */int main(int argc, char* argv[]) { FILE* txt = fopen(_STR_TXT, "wb+"); if (NULL == txt) { fprintf(stderr, "fopen " _STR_TXT " error!\n"); return -1; } // 这里写入数据 struct student stu = { 0, 0, 23, "鸣人", 172.23, 64.05 }; _student_print(&stu); // 这里序列化并写入数据到文件 _student_serialize(&stu, txt); // 我们读取这个文件, 先设置文件指针到文件开头 fseek(txt, 0, SEEK_SET); // 开始读取数据 struct student ts; _student_deserialize(&ts, txt); _student_print(&ts); fclose(txt); system("pause"); return 0;}
本质在 自定义编码解码,并利用 scanf和printf 对映关系
// struct student 结构体序列化到文件的方法static void _student_serialize(struct student* stu, FILE* txt) { fprintf(txt, "%d %c %d %s %lf %lf ", stu->id, stu->sex, stu->age, stu->name, stu->high, stu->weight);}// struct student 结构体反序列化static void _student_deserialize(struct student* stu, FILE* txt) { fscanf(txt, "%d %c %d %s %lf %lf ", &stu->id, &stu->sex, &stu->age, stu->name, &stu->high, &stu->weight);}
运行结果 如下:
通过这种实现, 是跨平台的. 因为C实现标准和自己定义协议支持
"%d %c %d %s %lf %lf "
最后我们还会讨论这种情况.
正文
1. 一次失败扩展 fscanf -> fread ; fprintf -> fwrite
测试如下, 在window上测试代码 main.c
#include#include #define _INT_NAME (64)#define _STR_TXT "student.struct"struct student { int id; char sex; int age; char name[_INT_NAME + 1]; double high; double weight;};// struct student 结构体序列化到文件的方法static void _student_serialize(struct student* stu, FILE* txt) { fwrite(stu, sizeof(*stu), 1, txt);}// struct student 结构体反序列化static void _student_deserialize(struct student* stu, FILE* txt) { fread(stu, sizeof(*stu), 1, txt);}// 简单打印数据static void _student_print(struct student* stu) { static int _idx; printf("%d: %d %c %d %s %lf %lf \n", _idx++, stu->id, stu->sex, stu->age, stu->name, stu->high, stu->weight);}/** 一种最简单的通用序列化方法*/int main(int argc, char* argv[]) { FILE* txt = fopen(_STR_TXT, "wb+"); if (NULL == txt) { fprintf(stderr, "fopen " _STR_TXT " error!\n"); return -1; } // 这里写入数据 struct student stu = { 0, 0, 23, "鸣人", 172.23, 64.05 }; _student_print(&stu); // 这里序列化并写入数据到文件 _student_serialize(&stu, txt); // 我们读取这个文件, 先设置文件指针到文件开头 fseek(txt, 0, SEEK_SET); // 开始读取数据 struct student ts; _student_deserialize(&ts, txt); _student_print(&ts); fclose(txt); system("pause"); return 0;}
核心是
// struct student 结构体序列化到文件的方法static void _student_serialize(struct student* stu, FILE* txt) { fwrite(stu, sizeof(*stu), 1, txt);}// struct student 结构体反序列化static void _student_deserialize(struct student* stu, FILE* txt) { fread(stu, sizeof(*stu), 1, txt);}
在 linux 上处理的代码 是 serialize.c
#include#include #define _INT_NAME (64)#define _STR_TXT "student.struct"struct student { int id; char sex; int age; char name[_INT_NAME + 1]; double high; double weight;};// struct student 结构体反序列化static void _student_deserialize(struct student* stu, FILE* txt) { fread(stu, sizeof(*stu), 1, txt);}// 简单打印数据static void _student_print(struct student* stu) { static int _idx; printf("%d: %d %c %d %s %lf %lf \n", _idx++, stu->id, stu->sex, stu->age, stu->name, stu->high, stu->weight);}/* * 处理 window 上生成的内存文件, 看是否跨平台 */int main(int argc, char* argv[]) { // 这里就简单读取 _STR_TXT FILE* txt = fopen(_STR_TXT, "rt"); if (NULL == txt) { fprintf(stderr, "fopen " _STR_TXT " error!\n"); return -1; } // 开始读取数据 struct student ts; _student_deserialize(&ts, txt); // 打印读取数据测试 _student_print(&ts); fclose(txt); return 0;}
编译 命令的是
gcc -g -Wall -o serialize.out serialize.c
将 window生成的 student.struct 文件传输到linux测试机上, 运行结果 如下:
期间进行了各种折腾
a. 考虑转码
b. 考虑 fopen 创建utf8 文件
c. 考虑代码转码
.......
还是以失败告终, 估计fread, fwrite是不同平台的直接内存文件. 差异大, 不适合跨平台, 但是同样平台是比较快的考虑方式.
扩展一下, 怎么得到文件字符长度
linux得到文件长度扩展
2. 采用 protobuf - c google一种协议.
pbc 传输协议, 很多公司都在采用, 我看了一下, 网上实现版本比较多, 现在可能有官方版本了. 推荐一个
https://github.com/cloudwu/pbc
可能是最精简的一种实现, 源码写的很好, 但是觉得有点复杂了. 为了这么功能这么搞, 没意思.
最关键的是 pbc 需要生成中间协议文件, 占用内存也不少.
这也是一种解决方案.
3. 采用json协议
这个意思很明了, 大家都通过json来处理问题 这里推荐自己写的一个json引擎
源码在1000行左右,欢迎使用. 这也是一种解决方案. 最近和朋友在讨论问题, 越发觉得, 计算机软件开发无非
围绕 空间和时间来回搞, 通用还是针对.
真实生产环境中可能会更直白些, 快些, 没bug就行, 怎么爽怎么随便, 怎么快怎么来. 哈哈.
但是对于C, 还是有一套自己的哲学, 用最简单完成一场意外. C/C++ 老了, 但却是美的.
这种解决方案讲到这里了.
4. 开始就是结束. 还是从最简单的开始. 实现一个C 序列换流程
后面继续讲解通过 fscanf 和 fprintf 构建 C的序列化. 先看一种实现体, serialize_student.h
#ifndef _H_SERIALIZE_THREE_SERIALIZE_STUDENT#define _H_SERIALIZE_THREE_SERIALIZE_STUDENT#include#include #include // 1.0 定义序列换结构体#define _INT_NAME (64)struct student { int id; char sex; int age; char name[_INT_NAME + 1]; double high; double weight;};// 2.0 定义保存文件交换文件名 当前文件名去掉 .h#define _STR_SERIALIZE_TXT_student "serialize_student"//3.0 定义转换读取协议, printf协议后面跟' ', printf后面跟的, scanf跟的#define _STR_SERIALIZE_PBC_student "%d %c %d %s %lf %lf "#define _F_SERIALIZE_PRINTF_student(p) \ p->id, p->sex, p->age, p->name, p->high, p->weight#define _F_SERIALIZE_SCANF_student(p) \ &p->id, &p->sex, &p->age, p->name, &p->high, &p->weight// 3.0 定义序列换数据写入方法static int serialize_student_printfs(void* data, int len) { assert(data && len > 0); FILE* txt = fopen(_STR_SERIALIZE_TXT_student, "wb"); if (!txt) return -1; struct student* p = data; for (int i = 0; i < len; ++i) { fprintf(txt, _STR_SERIALIZE_PBC_student, _F_SERIALIZE_PRINTF_student(p)); ++p; } fclose(txt); return 0;}// 4.0 定义序列化数据读取方法static int serialize_student_scanfs(void* data, int len) { assert(data); FILE* txt = fopen(_STR_SERIALIZE_TXT_student, "rb"); if (!txt) return -1; int ns = 0; struct student* p = data; int nz = 0; const char* s = _STR_SERIALIZE_PBC_student; while (*s) { if (*s == '%') ++nz; ++s; } while (ns < len && fscanf(txt, _STR_SERIALIZE_PBC_student, _F_SERIALIZE_SCANF_student(p)) == nz) { ++ns; ++p; } fclose(txt); return ns;}#endif // !_H_SERIALIZE_THREE_SERIALIZE_STUDENT
这里看看注释容易明白, 这里讲解一下 头文件导入宏规则.
_H 开头 + _项目名 + _文件名(去掉后缀) 主要为了解决项目特别多的时候联编造成宏碰撞.
测试代码 main.c
#include#include "serialize_student.h"/* * 实现C的序列流程操作 */int main(int argc, char* argv[]) { struct student stu[] = { { 0, 0, 23, "鸣人", 172.23, 64.05 }, { 0, 0, 34, "杀生丸", 178.23, 74.00 } }; // 先序列化到文件 serialize_student_printfs(stu, sizeof(stu) / sizeof(*stu)); // 开始读取序列化内容 struct student sts[2]; serialize_student_scanfs(sts, 2); for (int i = 0; i < 2; ++i) { printf("%d => %s\n", i, sts[i].name); } puts("你喜欢吗, ... "); system("pause"); return 0;}
运行结果是
最后可能来点 封装, 减少以后的工作量. 可能有点复杂, 直接看代码, 能懂得就呵呵一笑而过.
再表述后面封装之前讲一个小知识, linux 上宏调试有个小技巧 通过 gcc -E 导出 *.i 文件, 查看宏命令.
同样 window 上 vs 需要这么 设置
加上 /EP /P 运行时候会生成 main.i
找到问题后再将其去掉. 编译运行.
我们先看一个 C宏模板 序列化注册头文件 serialize-base.h
#ifndef _H_SERIALIZE_THREE_SERIALIZE_BASE#define _H_SERIALIZE_THREE_SERIALIZE_BASE#include#include #include /* * 宏模板, 为想实现序列化的结构注册函数 * name : 结构名称, 例如 student * pbc : 定义的协议, 例如 "%d %c %d %s %lf %lf " * ptf : printf 打印数据参数集, 例如 _->id, _->sex, _->age, _->name, _->high, _->weight | SERIALIZE_PTF * scf : scanf 得到数据的参数集, 例如 &_->id, &_->sex, &_->age, _->name, &_->high, &_->weight | SERIALIZE_SCF */#define SERIALIZE_BASE_REGISTER(name, pbc, ptf, scf) \static int serialize_printfs_##name(void* data, int len) { \ assert(data && len > 0); \ \ FILE* txt = fopen("serialize_"#name, "wb"); \ if (!txt) return -1; \ \ struct name* _ = (struct name*)data; \ for (int i = 0; i < len; ++i) { \ fprintf(txt, pbc, ptf); \ ++_; \ } \ \ fclose(txt); \ return 0; \} \ \static int serialize_scanfs_##name(void* data, int len) { \ assert(data); \ \ FILE* txt = fopen("serialize_"#name, "rb"); \ if (!txt) return -1; \ \ int ns = 0, nz = 0; \ struct name* _ = (struct name*)data; \ const char* s = pbc; \ while (*s) { \ if (*s == '%') \ ++nz; \ ++s; \ } \ \ while (ns < len && fscanf(txt, pbc, scf) == nz) { \ ++ns; \ ++_; \ } \ \ fclose(txt); \ return ns; \} \#endif // !_H_SERIALIZE_THREE_SERIALIZE_BASE
后面写一个结构 来实现序列化 serialize_person.h
#ifndef _H_SERIALIZE_THREE_SERIALIZE_PERSON#define _H_SERIALIZE_THREE_SERIALIZE_PERSON// 必须导入(继承) 序列化基础实现模板#include "serialize-base.h"// 1.0 定义序列换结构体struct person { int id; char sex; int age; char name[65]; double high; double weight;};// 2.0 注册得到 ptf 结构#undef SERIALIZE_PTF#define SERIALIZE_PBC(id, sex, age, name, high, weight) \ _->id, _->sex, _->age, _->name, _->high, _->weight// 3.0 注册得到 sct 结构#undef SERIALIZE_SCF#define SERIALIZE_SCF(id, sex, age, name, high, weight) \ &_->id, &_->sex, &_->age, _->name, &_->high, &_->weight// 4.0 最后开始注册实现体SERIALIZE_BASE_REGISTER( person, "%d %c %d %s %lf %lf ", SERIALIZE_PBC(id, sex, age, name, high, weight), SERIALIZE_SCF(id, sex, age, name, high, weight))#endif // !_H_SERIALIZE_THREE_SERIALIZE_PERSON
是不是很酷炫, 好测试一下 main.c
#include#include "serialize_student.h"#include "serialize_person.h"/* * 实现C的序列流程操作 */int main(int argc, char* argv[]) { struct student stu[] = { { 0, 0, 23, "鸣人", 172.23, 64.05 }, { 1, 0, 34, "杀生丸", 178.23, 74.00 } }; // 先序列化到文件 serialize_student_printfs(stu, sizeof(stu) / sizeof(*stu)); // 开始读取序列化内容 struct student sts[2]; serialize_student_scanfs(sts, 2); for (int i = 0; i < 2; ++i) { printf("%d => %s\n", i, sts[i].name); } puts("你喜欢吗, ... "); struct person ps[] = { { 2, 1, 23, "日向雏田", 162.23, 51.05 }, { 3, 1, 14, "玲", 158.23, 45.00 } }; // 序列化数据 serialize_printfs_person(ps, sizeof(ps) / sizeof(*ps)); // 得到序列化数据 struct person tps[2]; serialize_scanfs_person(tps, 2); for (int i = 0; i < 2; ++i) { printf("%d => %s\n", i, sts[i].name); } system("pause"); return 0;}
测试结果如下, 一切正常
到这里基本都结束了. 主要核心就是上面注册的函数模板.
后记
这次后记我们在linux上测试一下 将刚生成的 serialize_person 上传到 linux平台
测试文件 main.c
#include#include "serialize_person.h"/* * 实现C的序列流程操作 */int main(int argc, char* argv[]) { puts("Play time game, writing code"); struct person tps[2]; serialize_scanfs_person(tps, 2); for (int i = 0; i < 2; ++i) { printf("%d => %s\n", i, tps[i].name); } return 0;}
最终测试结果
源码成功, 到这里基本上可以离开了.
关于C数据序列化的简单操作就到这里了. 错误是难免的, 拜~~~