概念
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里面这个字符表如下:
但是这就要求二进制的长度要被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
实现
下面我们以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')
)
)
);
}
}
复制代码