有的地方,比如HTTP或者邮件协议中需要传说纯文本,如果你想传输二进制,呵呵,里面如果有个0,C语言就会当成字符串结束符。
人们想了各种办法来避免这件事,其中一种编码方法就是base64(rfc2045)
吐一个槽,其实现在用base64的地方不多,在眼睛看的到的地方,几乎没人用,但它又是无处不在的,因为你随便发一个HTTP的POST请求,可能就在使用base64。
base64的原理非常简单,它将二进制转化成肉眼可见的字符,只有64个,是2的6次方。但我们知道,一个字符占用8个比特,所以它其实是用8比特的位置表示6比特的信息,因为会比源信息的长度膨胀4/3。
怎么转呢?其实非常简单,应该也是人类的第一直觉。将3x8转成4x6怎么转,按照2进制位写下来,然后重新分组。就像二十四本排列在书架的某一层上,本来是8本书一组,一共3组,你重新安排一下,变成6本书一组,一共4组。书的顺序是不变的。
下面有个动画可以说明这一点
如果原来的数据长度不是 3Byte的倍数,就在源数据最后用0填满。同时在编码后的添加=号,刚才填了几个0,现在就写几个=号。
编码后的字串长度一定是 4Byte的倍数。
而我们的64个符号如下:
Value Encoding Value Encoding Value Encoding Value Encoding
0 A 17 R 34 i 51 z
1 B 18 S 35 j 52 0
2 C 19 T 36 k 53 1
3 D 20 U 37 l 54 2
4 E 21 V 38 m 55 3
5 F 22 W 39 n 56 4
6 G 23 X 40 o 57 5
7 H 24 Y 41 p 58 6
8 I 25 Z 42 q 59 7
9 J 26 a 43 r 60 8
10 K 27 b 44 s 61 9
11 L 28 c 45 t 62 +
12 M 29 d 46 u 63 /
13 N 30 e 47 v
14 O 31 f 48 w (pad) =
15 P 32 g 49 x
16 Q 33 h 50 y
我们试着用C语言写出来一个程序,其核心部分如下
// 编码过程
// a开头的变量 是源数据
// i开头的变量 是编码后的结果
// b开头的变量 是编码后显示的字符
unsigned a0 = sp[i*3];
unsigned a1 = i*3+1 >= len ? 0 : sp[i*3+1]; // 0 for padding
unsigned a2 = i*3+2 >= len ? 0 : sp[i*3+2];
// printf("('%c', 0x%x) ('%c', 0x%x) ('%c', 0x%x)\n", a0, a0, a1, a1, a2, a2);
int i0, i1, i2, i3;
char b0, b1, b2, b3;
i0 = a0 >> 2;
i1 = ((a0 & 0x3) << 4) | (a1 >> 4);
i2 = ((a1 & 0xF) << 2) | (a2 >> 6);
i3 = a2 & 0x3F;
ret[i*4+0] = b0 = char_table[i0];
ret[i*4+1] = b1 = char_table[i1];
ret[i*4+2] = b2 = char_table[i2];
ret[i*4+3] = b3 = char_table[i3];
而解码过程如下:
// a开头的变量 是编码后字符所对应的索引数
// b开头的变量 是解码之后的的源数据
for (int i = 0; i < len/4; ++i)
{
unsigned a0 = char_table_flip[ src[i*4+0] ];
unsigned a1 = char_table_flip[ src[i*4+1] ];
unsigned a2 = char_table_flip[ src[i*4+2] ];
unsigned a3 = char_table_flip[ src[i*4+3] ];
char b0, b1, b2;
ret[i*3+0] = b0 = (char) (a0 << 2) | a1 >> 4;
ret[i*3+1] = b1 = (char) ((a1 & 0xF) << 4) | (a2 >> 2);
ret[i*3+2] = b2 = (char) ((a2 & 0x3) << 6) | a3;
}
完整的源码在: https://github.com/picasso250/know-more/blob/master/base64/base64.h