C中级 数据序列化简单使用和讨论 (二)
引言 - 一种更好的方式
其实不管什么语言, 开发框架都会遇到序列化问题. 序列化可以理解为A 和 B 交互的一种协议.
很久以前利用 printf 和 scanf 的协议实现过一套序列化问题.
本文在上面基础上运用一种新的尝试. 具体的思路是 利用编译器对结构的统一内存编码方式.
具体实现通过进出栈宏 #pragma pack(push, 1) ... #pragma pack(pop) 设置结构体编译器的编码方式.
#pragma pack(push, 1) struct person { int id; char sex; int age; char name[65]; double high; double weight; }; #pragma pack(pop)
当编译器解析 struct person 的时候, 采用1字节对齐. 保证结构体编解析后二进制数据是一样的(VS 和 GCC测试过).
这是不同系统间通信的不变量之一. 通过这种序列化的思路. 不妨设计一个验证的Demo如下
window 生产者代码
// 设置数据, 开始写到测试文件中,再去读取 struct person per = { 1, 1, 19, "simplec王志", 179.0, 70.1 }; // 这里开始写数据到文件中. const char * path = "person.txt"; FILE * txt = fopen(path, "wb"); if (NULL == txt) exit(EXIT_FAILURE); fwrite(&per, sizeof(struct person), 1, txt); fclose(txt);
linux 使用者代码
const char * path = "person.txt"; FILE * txt = fopen(path, "rb"); if (NULL == txt) exit(EXIT_FAILURE); struct person np; fread(&np, sizeof(struct person), 1, txt); printf("[%d, %d, %d, %s, %lf, %lf]\n", np.id, np.sex, np.age, np.name, np.high, np.weight); fclose(txt);
实际运行的结果展示.
不知道你是否感到好奇, 为啥最终结果不对呢. 这就扯出了, 编程开发中所有老鸟都必须要面对的坑. "编码统一的坑".
为了将问题表述的更明白, 再来个Demo. sizeofname.c
#include <stdio.h> #include <wchar.h>
int main(int argc, char * argv[]) { // 系统默认编码, 一共 2 + 3 + 7 + 1 = 13 个 字符 char as[] = "王志 - simplec"; // 采用宽字节, 2字节表示一个字符 wchar_t bs[] = L"王志 - simplec"; // 采用UTF-8编码, 重要 ☆ char cs[] = u8"王志 - simplec"; printf("sizeof as = %zu.\n", sizeof as); printf("sizeof bs = %zu.\n", sizeof bs); printf("sizeof cs = %zu.\n", sizeof cs); return 0; }
在window 上运行结果如下, 我的系统默认是gbk编码(ascii码扩充版). 如果装英文版window默认是utf-8.
中间扯个淡. 我的VS设置中默认是 unix utf-8 有 BOM 文件编码格式. 详细配置可以参照这篇博客 - Visual Studio 默认保存为UTF8编码
在linux上测试结果如下, linux默认UTF-8编码
通过上面你也可以看出来, 主要问题是编码不一致导致了乱码. 最终使fread解析出错. 那么随后就开始解决这个问题.
前言总结
1. #pragma pack(push, 1) ... #pragma pack(pop) 是一种 C/C++ 系统间直接序列化的一种高效手段
2. 系统编码推荐统一采用 UTF-8. linux 默认就是, window 中文版是gbk.
下面将提出一种编码统一的方案. 到这里基本可以了, 后面可以选看. O(∩_∩)O哈哈~
前言 - 需要一些帮助,刚好我已经做了
以前有个GNU 的 libiconv 跨平台的库可以解决不同平台的编码问题. 其最近版本对window不再提供直接支持了.
这里我将其继续拉取到window上了搞了一通,最终生成libiconv.lib. 具体可以看下面项目
libiconv-for-window https://github.com/wangzhione/libiconv-for-window
工程详细的配置步骤如下
======================================================================== 静态库:libiconv for window 项目概述 ======================================================================== ///////////////////////////////////////////////////////////////////////////// 当前移植项目基于 GNU 项目 libiconv-1.15 | http://www.gnu.org/software/libiconv/ 移植到平台 window 10 14393.953 | Visual Studio 2017 项目发起人 : simplec - wz | wangzhione@163.com ///////////////////////////////////////////////////////////////////////////// 具体操作思路: 1. 从官网下载资源 libiconv压缩包, 并解压 [xxx = 解压后的详细path] 2. 在 $(ProjectDir) 项目目录下, 新建 include 目录 2.1 将压缩包中 xxx/include/iconv.h.build.in 复制到 include 目录下, 并重名为 iconv.h 2.2 将 xxx/onfig.h.in 复制到 include 下, 名为 config.h 2.3 将 xxx/lib 下 所有 *.h and *.def 文件复制到 include 目录下 2.4 将 xxx/libcharset/include/localcharset.h.build.in 复制到 include 目录下, 并改名 3. 将 xxx/libcharset/lib/localcharset.c 拷贝到 $(ProjectDir) 目录下 4. 将 xxx/lib/iconv.c 拷贝到 $(ProjectDir) 目录下 5. 将 localcharset.c iconv.c iconv.h localcharset.h config.h 添加到项目中 6. VC++ 目录 -> 包含目录 add [$(ProjectDir)include] 进去 7. C/C++ -> 预处理器 -> add _CRT_SECURE_NO_WARNINGS 去掉不安全的调用 8. 常规 -> 生成目标名 -> 改成 libiconv , Debug下为 libiconvd 详细编译修改步骤: 1. iconv.h 修改 1.1 25 - 29 行 删掉, 30行删除无效宏 1.2 55 - 61 行 删掉 1.3 后面所有的 LIBICONV_DLL_EXPORTED 删掉 , 可以用全部替换 1.4 把后面所有的 @ICONV_CONST@ 删掉 1.4.1 全局删除 ICONV_CONST 1.5 后面遇到 @xxx@ 大段大段的删除 1.6 详细参照我最终的文件底版 1.7 将这个文件编码改成 UTF-8 有 BOM 模式, 我是用NotePad++转换的 2. localcharset.c 修改 2.1 79 - 83 行 删掉 3. localcharset.h 修改 3.1 20 - 26 行删除 3.2 31 行 无效宏删除 4. config.h 修改 4.1 28 - 30 行 删除, 回归 EILSEQ 5. 解决百个警告 5.1 我这个代码可以做1.15 window lib 库源码项目集 /////////////////////////////////////////////////////////////////////////////
通过上面操作, 基本上window上libiconv 项目就搞定差不多了. 后面简单扯个淡. iconv 一共有下面常用的三个接口
/* Allocates descriptor for code conversion from encoding ‘fromcode’ to encoding ‘tocode’. */ extern iconv_t iconv_open (const char* tocode, const char* fromcode); /* Converts, using conversion descriptor ‘cd’, at most ‘*inbytesleft’ bytes starting at ‘*inbuf’, writing at most ‘*outbytesleft’ bytes starting at ‘*outbuf’. Decrements ‘*inbytesleft’ and increments ‘*inbuf’ by the same amount. Decrements ‘*outbytesleft’ and increments ‘*outbuf’ by the same amount. */ extern size_t iconv (iconv_t cd, char* * inbuf, size_t *inbytesleft, char* * outbuf, size_t *outbytesleft); /* Frees resources allocated for conversion descriptor ‘cd’. */ extern int iconv_close (iconv_t cd);
上面函数英文解释的很详细, 更加详细的可以参看编码实现细节.
再扯一点的是, iconv 的 outbytesleft 输出的是 inbuf 接口已经转换的编码字节数.
说真的iconv linux上提供的接口设计很丑陋, 很恶心. linux的信号软中断, 真的是强奸我们的代码.
后面也提供了一个不是很漂亮的sciconv.h 帮助接口
#ifndef _H_SIMPLEC_SCICONV #define _H_SIMPLEC_SCICONV #include <iconv.h> #include <stdbool.h> // // iconv for window helper // by simplec wz // // // si_isutf8 - 判断当前字符串是否是utf-8编码 // in : 待检测的字符串 // return : true表示确实utf8编码, false不是 // extern bool si_isutf8(const char * in); // // si_iconv - 将字符串 in 转码, from 码 -> to 码 // in : 待转码的字符串 // len : 字符数组长度 // from : 初始编码字符串 // to : 转成的编码字符串 // rlen : 返回转换后字符串长度, 传入NULL表示不需要 // return : 返回转码后的串, 需要自己销毁 // extern char * si_iconv(const char * in, const char * from, const char * to, size_t * rlen); // // si_iconv - 将字符串数组in 转码, 最后还是放在in数组中. // in : 字符数组 // from : 初始编码字符串 // to : 转成的编码字符串 // return : void // extern void si_aconv(char in[], const char * from, const char * to); // // si_gbktoutf8 - 将字符串数组in, 转成utf8编码 // in : 字符数组 // len : 字符数组长度 // return : void // extern void si_gbktoutf8(char in[]); // // si_utf8togbk - 将字符串数组in, 转成gbk编码 // in : 字符数组 // len : 字符数组长度 // return : void // extern void si_utf8togbk(char in[]); #endif
方便协助开发. 到这里万事具备了.下面就开始搞了.
正文 - 解决国际化(编码)问题
上面已经解决了库的问题. 那我们开始搞一个demo 试试吧. 我们还是用上面的window生产者代码搞搞.
#include <stdio.h>
#include <stdlib.h>
#include <sciconv.h>
#pragma pack(push, 1)
struct person {
int id;
char sex;
int age;
char name[65];
double high;
double weight;
};
#pragma pack(pop)
//
// 测试数据序列化新思路,采用位对齐
//
int main(int argc, char * argv[]) {
// 设置数据, 开始写到测试文件中,再去读取
struct person per = {
1, 1, 19, "simplec王志", 179.0, 70.1
};
// 打印一下数据
printf("[%d, %d, %d, %s, %lf, %lf]\n",
per.id, per.sex, per.age, per.name, per.high, per.weight);
// 这里开始写数据到文件中.
const char * path = "person.txt";
FILE * txt = fopen(path, "wb+");
if (NULL == txt)
exit(EXIT_FAILURE);
si_gbktoutf8(per.name);
fwrite(&per, sizeof(struct person), 1, txt);
fclose(txt);
return EXIT_SUCCESS;
}
上传person.txt 到 linux上测试一下. linux 测试详细代码如下 personread.c
#include <stdio.h> #include <stdlib.h> #include <iconv.h> #pragma pack(push, 1) struct person { int id; char sex; int age; char name[65]; double high; double weight; }; #pragma pack(pop) // // 测试数据序列化新思路,采用位对齐 // int main(int argc, char * argv[]) { struct person np; // 这里开始写数据到文件中. const char * path = "person.txt"; FILE * txt = fopen(path, "rb"); if (NULL == txt) exit(EXIT_FAILURE); fread(&np, sizeof(struct person), 1, txt); fclose(txt); // 打印一下数据 printf("[%d, %d, %d, %s, %lf, %lf]\n", np.id, np.sex, np.age, np.name, np.high, np.weight); return EXIT_SUCCESS; }
最终实验结果一切正常, 欧耶OY
到这里解决方案的内容已经搞定了. 感兴趣的同学可以多看看关于上面 libiconv for window github 源码.
后记 - 下一步工作
错误是难免的, 这里多是讨论互相交流. 毕竟全部用国外的协议和标准. 感觉可惜 哈哈 ψ(*`ー´)ψ. 欢迎指正.
歌声与微笑 - http://music.163.com/#/m/song?id=395677&userid=16529894