小池有话说

Base64 编码

2016-02-01

有的地方,比如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