1. 理解 base64 编码
经常听到——以致 AI 也会这么回答的:base64 编码用于将二进制数据,转换为文本数据。但是,众所周知,在数字电子计算机中,所有数据都是二进制数据。因此,文本数据也是二进制数据。
所以,什么是“文本数据”?
同样,很容易查到,base64 编码会比原数据至少大三分之一,因为它使用四个字节去编码三个字节的源数据。又为什么需要四个字节才能表达人家的三个字节?因为 base64 正如其名,它在一个字节所能表达的 256 种可能的字符中,只允许用到 64 可能。于是,就有了 base64 编码的“核心等式”:2563 = 643 。
64个字符:1) 大小写字母-52个,2) 数字-10个,3)符号 + 和 / -2个
2. 认识、安装 libb64
2.1 简介
libb64 是个有点年纪的 base64 编解码库。
- 官方主页:https://libb64.sourceforge.net
- 第三方帮助克隆到 github:https://github.com/libb64/libb64
其中 github 上的代码有所修改,目的是为了支持能得到“不换行”的base64编码,但其做法(很)不好,建议有此需求的同学,可参考本课堂附录一给出的改进。
归纳起来,libb64有三个主要特点:
- 性能保障: 使用了 C 语言 BUG 一样的 “魔法协程”,即:利用在 switch 结构中跨 case 分支存在的 while 循环,以实现一个普通函数可以类似一个协程,支持保持状态的函数重入(示例代码见本课堂附录二);
- 完全自由使用,零协议。“you can take it and do whatever you want with it”;
- 核心代码使用 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;
}