Base64原理和实现

1,252 阅读4分钟

概念

Base64是开发过程中常见的一种编码格式,其作用在于使用64个可打印字符来表示二进制数据。

为什么要使用Base64

当我们在网络上传递数据的时候,原始数据都是使用ASCII编码,ASCII编码里面除了可打印字符之外还有很多不可打印的字符,这些不可打印字符在不同的软件或者系统中可能会被用作控制字符,例如Http请求行里面的数据使用换行来表示新的一行,这时候如果不进行编码那么可能行中的数据就会被当作换行,从而影响整个Http Header的格式。相较于不可打印字符的副作用,可打印字符一般在各个系统和软件环境中都比较稳定,因此我们在一些场合需要使用Base64编码。

为什么是64

基本的ASCII 字符集共有128 个字符,其中有95 个可打印字符,包括常用的字母、数字、标点符号等,另外还有33 个控制字符。当我们使用可打印字符编码的时候,能够使用的字符必然是小于95的,因此字节里面能够使用的字节数目只能小于或等于2^6(64 < 95, 2^7 = 128),所以为了最大化利用这些字符,只能使用6位bit来编码这些数据,那么使用5位,4位能不能进行编码的呢,当然可以,但是这样会浪费很多空间,因此使用6位bit,即64个可打印字符来编码。

算法

通过上面的讨论我们明白了Base64的编码就是把原始的字节按照每6位一个分组,然后对这6位数据映射到可打印的字符标。Base64里面这个字符表如下: 截屏2021-05-28 上午11.14.41.png 但是这就要求二进制的长度要被6整除,我们知道字节都是8位,因此6整除整个条件不好满足,因此Base64使用6和8的最小公倍数24,即把原始字节每3字节(24 bit)分为一组,然后填充到编码后的4字节里面。
具体的例子可以参考维基百科。我们对Man这个数据的编码规则进行一下分析,查询ASCII表可以得到Man对应的二进制数据分别为77,97,110,转成为二进制为0b01001101,0b01100001,0b01101110。根据前面的规则,那么第一个编码字节就为0b00 010011,第二个编码字节为0b00 01 0110,第三个编码字节为0b 00 0001 01,第四个编码字节为0b00 101110,把编码字节转化为int分别为19,22,5,46,然后查询Base64编码表,可知这些int对应的字符为TWFu。因此我们可以得出Base64(Man)=TWFu

截屏2021-05-28 上午11.18.23.png

实现

下面我们以C++为例来实现一个简单的Base64编解码器。

确定接口

为了专注于算法实现,我们使用string来承载数据,使用namespace来组织代码,一个简单的使用例子如下:

 std::string encodedString = Base64::encode("海客谈瀛洲,烟涛微茫信难求");
 std::cout<<"Encoded : "<<encodedString<<std::endl;
 std::cout<<"Decoded : "<<Base64::decode(encodedString)<<std::endl;

编码实现

在编码的过程中,我们要做的事情其实就是:

  • 把字节数组按照每三个一组读出来,然后填充到四个字节的空间里面,每个字节都只使用低六位。
  • 不足三个字节的全部补0
字节拆分

为了统一代码风格,我们使用std::array来替代原始数组。整个代码的逻辑就是:目标数组的第一个字节采用原始数组第一个字节的高6位,因此先使用0b11111100这个掩码做与运算获取高6位,然后向右移动2位,这样就使得原始数组第一个字节的前6位移动到了目标数组的第一个字节。剩下的情况依次类推。

 void mergeArray(array<char,4>& out,const array<char,3>& in){
        out[0] = ( 0b11111100 & in[0]) >> 2;
        out[1] = ((0b00000011 & in[0]) << 4) + ((0b11110000 & in[1]) >> 4);
        out[2] = ((0b00001111 & in[1]) << 2) + ((0b11000000 & in[2]) >> 6);
        out[3] =   0b00111111 & in[2];
}
补齐

当readCount大于0的时候,说明我们读到了字节,但是这个字节不够3个,因此需要补充到3个。补充到三个之后需要按照之前的方式来拆分字节。例如读到了一个字节,那么需要填充两个0,读到的这个字节会被拆分到两个字节里面,然后我们把这两个字节(readCount + 1)进行输出,剩下的两个字节都是0,用来补位的,在Base64的算法里面这些0需要被替换成'='。因此,我们可以总结一个规律,剩余1一个字节,需要补两个== ,剩余两个字节需要补一个 = 。

if(readCount > 0){
            while(index < 3){
                rawBuffer[index] = '\0';
                index++;
            }
            mergeArray(encodeBuffer,rawBuffer);
            for(int j = 0;j < readCount + 1;j++){
                result.push_back(CHAR_ARRAY[encodeBuffer[j]]);
            }
            for(int i=0;i< 3 - readCount ;i++){
                result.push_back('=');
            }
        }

完整的代码如下

namespace Base64{
    using namespace std;
    const array<char,64> CHAR_ARRAY = 
        {'A','B','C','D','E','F','G','H','I','J','K','L',
         'M','N','O','P','Q','R','S','T','U','V','W','X',
         'Y','Z','a','b','c','d','e','f','g','h','i','j',
         'k','l','m','n','o','p','q','r','s','t','u','v',
         'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'};
    
    string encode(const string &);
    string decode(const string &);
    inline void mergeArray(array<char,4>& ,const array<char,3>& );
    inline void splitArray(array<char,3>&,const array<char,4>&);
    inline bool isValidChar(char);
    inline unsigned getCharIndex(char);
    
    //implementation
    string encode(const string & data){
        string result;
        string::const_iterator startIt = data.cbegin();
        string::const_iterator endIt = data.cend();
        array<char,3> rawBuffer;
        array<char,4> encodeBuffer;
        int readCount = 0;
        while(startIt != endIt ){
            rawBuffer[readCount] = *startIt;
            startIt++;
            readCount++;
            if(readCount == 3){
                mergeArray(encodeBuffer,rawBuffer);
                for(auto& item : encodeBuffer ){
                    result.push_back(CHAR_ARRAY[item]);
                }
                readCount = 0;
            }
            
        }
        int index = readCount;
        if(readCount > 0){
            while(index < 3){
                rawBuffer[index] = '\0';
                index++;
            }
            mergeArray(encodeBuffer,rawBuffer);
            for(int j = 0;j < readCount + 1;j++){
                result.push_back(CHAR_ARRAY[encodeBuffer[j]]);
            }
            for(int i=0;i< 3 - readCount ;i++){
                result.push_back('=');
            }
        }
        return result;
    }
    string decode(const string & data){
        string result;
        string::const_iterator startIt = data.cbegin();
        string::const_iterator endIt = data.cend();

        array<char,4> rawBuffer;
        array<char,3> decodeBuffer;
        int readCount = 0;

        while(startIt != endIt && isValidChar(*startIt) && (*startIt) != '='){
            rawBuffer[readCount] = *startIt;
            startIt ++;
            readCount ++;
            if(readCount == 4){
                for(auto & item:rawBuffer){
                    item = (char)getCharIndex(item);
                }
                splitArray(decodeBuffer,rawBuffer);
                for(auto & item:decodeBuffer){
                    result.push_back(item);
                }
                readCount = 0;
            }
        }
        if(readCount > 0){
            int index = readCount;
            while(index < 4){
                rawBuffer[index] = 0;
                index ++;
            }
            for(auto & item:rawBuffer){
                    item = (char)getCharIndex(item);
            }
            splitArray(decodeBuffer,rawBuffer);
            for(int j=0;j<readCount - 1;j++){
                 result.push_back(decodeBuffer[j]);
            }
        }
        return result;

    }

    void mergeArray(array<char,4>& out,const array<char,3>& in){
        out[0] = ( 0b11111100 & in[0]) >> 2;
        out[1] = ((0b00000011 & in[0]) << 4) + ((0b11110000 & in[1]) >> 4);
        out[2] = ((0b00001111 & in[1]) << 2) + ((0b11000000 & in[2]) >> 6);
        out[3] =   0b00111111 & in[2];
    }
    void splitArray(array<char,3>& out,const array<char,4>& in){
        out[0] = ((0b00111111 & in[0]) << 2) + ((0b00110000 & in[1]) >> 4);
        out[1] = ((0b00001111 & in[1]) << 4) + ((0b00111100 & in[2]) >> 2);
        out[2] = ((0b00000011 & in[2]) << 6) +  (0b00111111 & in[3]);
    }
    bool isValidChar(char c){
        return isalnum(c) || c == '+' || c == '/';
    }

    const unsigned PLUS_INDEX = 62;
    const unsigned QU_INDEX = 63;
    const unsigned NUM_START_INDEX = 52;
    const unsigned BIG_AL_INDEX = 0;
    const unsigned LOW_AL_INDEX = 26;
    unsigned getCharIndex(char c){
        return (c == '+') ? PLUS_INDEX : (
            (c == '/') ? QU_INDEX :(
                (c - '0' <= 9 ) ? NUM_START_INDEX + (c - '0') : (
                    (c < 'a') ? BIG_AL_INDEX + ( c - 'A') : LOW_AL_INDEX + ( c - 'a')
                )
            )
        );
    }
}