作者:唐风
Base 64是一种比较古老的编码方式,在通信中非常常见。它实现很简单。
What?
“Base64是一种基于64个可打印字符来表示二进制数据的表示方法(来自维基)”。这句话我一开始没有看懂,现在我用我懂的方式再解释一下:我们可以把通信的数据流分为两种,“二进制流”和“文本流”。(注意,后面的定义并不严谨)。文本流是指数据串是以“人类可读的字符”组成的,数据流中出现的 0x00,0x0a,0x0d 等数据一般都是特殊的控制数据(文本结束,或是换行、或者是其它的),而不是数据本身。二进制流是任意的一串数据(每个字节可以是从 0x00 到 0xff 的值,而不限于字符)。Base64 编码就是用可打印字符(A-Z,a-z,0-9,+/这64个“字符”)组成的“文本流”来表示任意的二进制流数据的一种编码方法。也就是:任意的二进制流,通过base64编码后会变成一串只由可见字符(A-Z,a-z,0-9,+/这64个“字符”)组成的文本数据流。
完整的base64定义可见参考RFC 1421和RFC 2045。
Why?
Base64有什么用?!base 64编码后的数据流会比原数据流更长(是原数据流长度的4/3,原因见How的部分),那为什么还要用这种方式进行编码呢?(一开始我很不理解的地方)
原因是:通信设备的种类繁多,设计各异,比如有些设备只能处理/传输7bit的数据,有些设备的通信模块只能处理文本流,等等,为了保证在这些设备之间仍然能无障碍地进行任何二进制数据(8bit为单位)的通信,就把这些数据重新编码成只由可打印字符组成的文本流。base64中选取的可打印字符几乎在任何设备上都支持(字符集采用US-ASCII标准)。另外还常会见到的一个场景是,要求数据从控制台(或者其它输入/输出设备)输入或是输出,由于这些输入输出设备只支持文本操作或显示,所以也需要把二进制数据流在可打印字符集之间进行转换(编解码)。
How?
Base64编码其实很简单。将二进制数据流每三个字符一组进行分组(24bit),分组后,将24bit分成4个6bit数据,为每6bit数据前加上2个bit的0,就会得到4个8bit数据(变成了4byte),每个byte的值都在0-63的范围之间。然后按预先设定的转换表,把这些值转换成对应的可打印字符。
前文本中引用的维基百科内容说明得也比较清楚。
过程参考下图:
这里有一个小细节需要注意,那就是如果要编码的数据串长度不是 3 的整数倍的情况下,需要在数据串后面补充若干(假设是N)个0x00使得数据长度恰好为 3 的整数倍。然后再进行处理。得到相应的base64字符流数据后,再把串最后面的N个A换成=号。
Base64的解码过程只需要反过来就可以了。
下面是C++的代码实现
static char const convert_table[] = "ABCDEFGHIGKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" ;
vector < char > output;
output.reserve(( _bin .size() * 4) / 3);
auto con_3bytes_to_4bytes = [&]( uint8_t const * _3bytes_bin_data ) {
output.push_back(convert_table[ _3bytes_bin_data [0] >> 2]);
output.push_back(convert_table[(( _3bytes_bin_data [0] & 0x03) << 4) | (( _3bytes_bin_data [1] >> 4) & 0x0f)]);
output.push_back(convert_table[(( _3bytes_bin_data [1] << 2) & 0x3c) | ( _3bytes_bin_data [2] >> 6)]);
output.push_back(convert_table[ _3bytes_bin_data [2] & 0x3f]);
};
auto i = 0u;
for (; (i + 3) <= _bin .size(); i += 3) {
con_3bytes_to_4bytes(& _bin [i]);
}
if (i != _bin .size()) {
uint8_t left_data[3] = { 0 };
std::copy(begin( _bin ) + i, end( _bin ), left_data);
con_3bytes_to_4bytes(left_data);
std::fill(rbegin(output), rbegin(output) + (3 - ( _bin .size() % 3)), '=' );
}
return output;
}
vector < uint8_t > Base64ToBin( vector < char > const & _base64stream ) {
vector < uint8_t > output;
output.reserve( _base64stream .size() * 3 / 4);
static hash_map < char , uint8_t > convert_table = {
{ 'A' , 0 }, { 'B' , 1 }, { 'C' , 2 }, { 'D' , 3 }, { 'E' , 4 }, { 'F' , 5 }, { 'G' , 6 }, { 'H' , 7 },
{ 'I' , 8 }, { 'J' , 9 }, { 'K' , 10 }, { 'L' , 11 }, { 'M' , 12 }, { 'N' , 13 }, { 'O' , 14 }, { 'P' , 15 },
{ 'Q' , 16 }, { 'R' , 17 }, { 'S' , 18 }, { 'T' , 19 }, { 'U' , 20 }, { 'V' , 21 }, { 'W' , 22 }, { 'X' , 23 },
{ 'Y' , 24 }, { 'Z' , 25 }, { 'a' , 26 }, { 'b' , 27 }, { 'c' , 28 }, { 'd' , 29 }, { 'e' , 30 }, { 'f' , 31 },
{ 'g' , 32 }, { 'h' , 33 }, { 'i' , 34 }, { 'j' , 35 }, { 'k' , 36 }, { 'l' , 37 }, { 'm' , 38 }, { 'n' , 39 },
{ 'o' , 40 }, { 'p' , 41 }, { 'q' , 42 }, { 'r' , 43 }, { 's' , 44 }, { 't' , 45 }, { 'u' , 46 }, { 'v' , 47 },
{ 'w' , 48 }, { 'x' , 49 }, { 'y' , 50 }, { 'z' , 51 }, { '0' , 52 }, { '1' , 53 }, { '2' , 54 }, { '3' , 55 },
{ '4' , 56 }, { '5' , 57 }, { '6' , 58 }, { '7' , 59 }, { '8' , 60 }, { '9' , 61 }, { '+' , 62 }, { '/' , 63 },
{ '=' , 0 },
};
auto i = 0u;
for ( ; i < _base64stream .size(); i += 4) {
auto const byte1 = convert_table[ _base64stream [i]];
auto const byte2 = convert_table[ _base64stream [i + 1]];
auto const byte3 = convert_table[ _base64stream [i + 2]];
auto const byte4 = convert_table[ _base64stream [i + 3]];
output.push_back((byte1 << 2) | (byte2 >> 4));
output.push_back(((byte2 & 0x0f) << 4) | (byte3 >> 2));
output.push_back(((byte3 & 0x03) << 6) | byte4);
}
output.erase(end(output) - count(rbegin( _base64stream ), rbegin( _base64stream ) + 4, '=' ),
end(output));
return output;
}