加载中...
libb64-理解并玩转base64编码
第1节:libfswatch-文件变动监控
第2节:libiconv-字符集编码转换
第3节:CLI11-命令行参数解析
第4节:nlohmann/json-自然的JSON库
第5节:libb64-理解并玩转base64编码
第6节:libSnappy-快速压缩工具
课文封面
  1. 正确认识二进制数据和文本数据的关系;
  2. 深刻理解 base64 编码核心等式:256×256×256 = 64×64×64×64
  3. 10 秒完成 libb64 安装
  4. 两个例程玩转 libb64 的 C++ 接口

1. 理解 base64 编码

经常听到——以致 AI 也会这么回答的:base64 编码用于将二进制数据,转换为文本数据。但是,众所周知,在数字电子计算机中,所有数据都是二进制数据。因此,文本数据也是二进制数据。

所以,什么是“文本数据”?

同样,很容易查到,base64 编码会比原数据至少大三分之一,因为它使用四个字节去编码三个字节的源数据。又为什么需要四个字节才能表达人家的三个字节?因为 base64 正如其名,它在一个字节所能表达的 256 种可能的字符中,只允许用到 64 可能。于是,就有了 base64 编码的“核心等式”:2563 = 643

64个字符:1) 大小写字母-52个,2) 数字-10个,3)符号 + 和 / -2个

2. 认识、安装 libb64

2.1 简介

libb64 是个有点年纪的 base64 编解码库。

其中 github 上的代码有所修改,目的是为了支持能得到“不换行”的base64编码,但其做法(很)不好,建议有此需求的同学,可参考本课堂附录一给出的改进。

归纳起来,libb64有三个主要特点:

  1. 性能保障: 使用了 C 语言 BUG 一样的 “魔法协程”,即:利用在 switch 结构中跨 case 分支存在的 while 循环,以实现一个普通函数可以类似一个协程,支持保持状态的函数重入(示例代码见本课堂附录二);
  2. 完全自由使用,零协议。“you can take it and do whatever you want with it”;
  3. 核心代码使用 C 实现,同时提供简捷的 C++ 封装。

2.2 安装

  • msys2/UCRT64 环境: pacman -S mingw-w64-ucrt-x86_64-libb64
  • msys2/mingw64 环境 : pacman -S mingw-w64-x86_64-libb64

3. 实战 libb64

3.1 C++ 接口介绍

// 头文件 #include <b64/encode.h> // 编码 #include <b64/decode.h> // 解码 // 编码(bin → base64): void base64::encoder::encode(std::istream& is, std::ostream& os); // 解码(base64 → bin): void base64::decoder::decode(std::istream& is, std::ostream& os);

C++接口,编码解码均使用流作为参数。参数1为输入流,用于从中得到原数据,参数2为输出流,用于输出编码结果。因此,官网上给出的最简单的编码例子是:

base64::encoder er; er.encode(std::cin, std::cout);

3.2 认识、安装、使用(视频)

4. 附一:解决强制换行问题

插入换行的 base64 编码同样符合标准,见:RFC 4648 3.3 小节 “Interpretation of Non-Alphabet Characters in Encoded Data”

但实际工作中,确实仍有极端情况,某些 base64 数据的接收方,不支持处理换行,解决方法之一是,使用 libb64 C 接口,然后在某个步骤做点小动作即可。直接修改 libb64 的 C++ 封装代码(仅一个头文件,且仅需加一行代码),对 C++ 程序员来说会更一劳永逸,但想想还是尽量不要直接动三方库的源代码,因此我给出一个单独的头文件,定义了一个新的类,该类支持生成不带换行的 base64 编码。

只需处理编码,解码不受影响。

作为对比,github 上的 libb64 (维护人已不是 lib64 原作者)同样为此功能做了改动,我认真阅读了其改动方法,改动地方又多又乱,性能还慢……所以不建议大家用它。

请自行新建一个 C++ 头文件,依你喜好,取个名字(我随便取了个:“b64_encoder_ex.hpp”),然后复制、粘贴以下代码。

  • b64_encoder_ex.hpp
#include <b64/encode.h> #include <limits> namespace base64 { struct encoderEx : private encoder { encoderEx(int buffersize_in = BUFSIZ) : encoder(buffersize_in) {} using encoder::encode; void encode(std::istream& istream_in, std::ostream& ostream_in, bool line_break) { if (line_break) { encoder::encode(istream_in, ostream_in); return; } base64_init_encodestate(&_state); const int N = _buffersize; char* plaintext = new char[N]; char* code = new char[2*N]; int plainlength; int codelength; do { istream_in.read(plaintext, N); plainlength = istream_in.gcount(); codelength = encode_without_line_break(plaintext, plainlength, code); ostream_in.write(code, codelength); } while (istream_in.good() && plainlength > 0); codelength = encode_end(code); ostream_in.write(code, codelength); base64_init_encodestate(&_state); delete [] code; delete [] plaintext; } int encode_without_line_break(const char* code_in, const int length_in, char* plaintext_out) { _state.stepcount = std::numeric_limits<decltype(_state.stepcount)>::min(); return base64_encode_block(code_in, length_in, plaintext_out, &_state); } }; } // namespace base64

我们的解决思路很简单: libb64 借助 stepcount 这个成员数据累计当前行长度,我们让它永远到达不到那个需要换行的值(好像是76?)……搞定!

  • 使用示例
encoderEx ex; bool needLineBreak = false; // 是否需要插入换行符? ex.encode(std::cin, std::cout, needLineBreak);

5. 附二:魔法协程

下面是 libb64 编码的主要函数,注意其中的 switch 结构中,如果跨 case 循环。建议了解,学习,不建议实际应用。

int base64_encode_block(const char* plaintext_in, int length_in, char* code_out, base64_encodestate* state_in) { const char* plainchar = plaintext_in; const char* const plaintextend = plaintext_in + length_in; char* codechar = code_out; char result; char fragment; result = state_in->result; switch (state_in->step) { while (1) { case step_A: if (plainchar == plaintextend) { state_in->result = result; state_in->step = step_A; return codechar - code_out; } fragment = *plainchar++; result = (fragment & 0x0fc) >> 2; *codechar++ = base64_encode_value(result); result = (fragment & 0x003) << 4; case step_B: if (plainchar == plaintextend) { state_in->result = result; state_in->step = step_B; return codechar - code_out; } fragment = *plainchar++; result |= (fragment & 0x0f0) >> 4; *codechar++ = base64_encode_value(result); result = (fragment & 0x00f) << 2; case step_C: if (plainchar == plaintextend) { state_in->result = result; state_in->step = step_C; return codechar - code_out; } fragment = *plainchar++; result |= (fragment & 0x0c0) >> 6; *codechar++ = base64_encode_value(result); result = (fragment & 0x03f) >> 0; *codechar++ = base64_encode_value(result); ++(state_in->stepcount); if (state_in->stepcount == CHARS_PER_LINE/4) { *codechar++ = '\n'; state_in->stepcount = 0; } } } /* control should not reach here */ return codechar - code_out; }