Base64是一种基于64个可打印字符来表示二进制数据的方法。

当我们用文本编辑器打开 jpgpdfexe这些文件格式的时候,会看到一大堆的乱码,这是因为二进制文件包含很多无法显示和打印的字符。所以,如果想要让记事本这样的文本编辑器处理二进制数据,就需要一个从二进制到字符串的转换方法。Base64就是一种最常见的二进制编码方法

原理

文本文件和二进制文件

使用计算机时,我们会遇到各种各样的文件格式,有像 txtcppjavamd这样的可以用文本编辑器直接打开的文件格式,我们称之为 文本文件,也有一些文件用编辑器打开时是一堆乱码,比如像上面提到的 jpgwavpdf,这些文件格式称之为 二进制文件

我们知道,所有文件在计算机的存储都是一堆0和1的序列,也就是说不论是文本文件还是二进制文件,在物理上,计算机的存储是都是二进制的。 所谓的文本文件和二进制文件的区别并不是物理上的,而是逻辑上的。广义上来说,文本文件也是二进制文件。二者的区别是因为你看待数据的方式不同而产生的差别,具体的说二者的区别就在于打开这个文件的程序对其内容的解释上。

以文件的读写过程为例,这实际包含了如下的两个转换过程

磁盘 ----> 文件缓冲区 ----> 应用程序内存空间

第一个过程中,文件被应用程序从磁盘读取到文件缓冲区,二者都是一堆的0和1的序列,这个时候文本文件和二进制文件并没有什么区别。

接下来,不同的应用程序,根据面对的不同文件格式,对一堆的0和1序列进行解释。对于文本文件,应用程序直接将其中的数据(也就是这一堆0和1序列按照ASCII或者Unicode编码的方式解释出来),显示成文本的形式。对于其他类型的文件,一般包括了控制信息和内容信息,应用程序按照文件格式,从这些数据中解析出对应的数据内容,比如常见的wav文件解析过程,而这些就是所谓的二进制文件。

Base64的转换

Base64编码选择的64个可打印字符为字母A-Z,a-z、数字0-9,这样共有62个字符,此外还有两个字符在不同的系统中而不同。比如对于MIME格式,其采用的为

1
const static char table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

然后,而二进制数据进行处理,每3个字节一组,一共是 $ 3 × 8 = 24 $ bit,划分为4组,每组正好6个bit。

base64
base64

这样,我们得到4个数字作为索引,然后查表,获得相应的4个字符,就得到编码后的字符串。所以,Base64编码会把3字节的二进制数据编码为4字节的文本数据,长度增加33%,好处是编码后的文本数据可以在邮件正文、网页等直接显示。

如果要编码的二进制数据不是3的倍数,Base64就会在后面补0,每补1个0就在编码出来的字符串后加上1个 = ,解码的时候会自动将 = 去掉。

Python中内置的 base64可以直接进行base64的编解码

1
2
3
4
5
>>> import base64
>>> base64.b64encode(b'hello, world!')
b'aGVsbG8sIHdvcmxkIQ=='
>>> base64.b64decode(b'aGVsbG8sIHdvcmxkIQ==')
b'hello, world!'

C++实现

此处源码可在 base64的C++实现

encode方法

  • 首先计算生成base64字符串的长度
  • 原来的二进制流中,每3个字节为整体,合为一个32位的bit串
  • 将这个32位bit串的低24bit的每6个比特作为索引,得到映射表中对应的字符
  • 如果原来的字节串不是3的倍数,则补零,并且每补一个0加一个 =
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
std::string
encode_base64(const std::vector<uint8_t> unencoded)
{
    std::string encoded;
    const auto size = unencoded.size();
    encoded.reserve(((size / 3) + (size % 3 > 0)) * 4);

    uint32_t value;
    auto cursor = unencoded.begin();
    for (size_t position = 0; position < size / 3; position++)
    {
        value = (*cursor++) << 16;

        value += (*cursor++) << 8;
        value += (*cursor++);
        encoded.append(1, table[(value & 0x00FC0000) >> 18]);
        encoded.append(1, table[(value & 0x0003F000) >> 12]);
        encoded.append(1, table[(value & 0x00000FC0) >> 6]);
        encoded.append(1, table[(value & 0x0000003F) >> 0]);
    }

    switch (size % 3)
    {
        case 1:
            value = (*cursor++) << 16;

            encoded.append(1, table[(value & 0x00FC0000) >> 18]);
            encoded.append(1, table[(value & 0x0003F000) >> 12]);
            encoded.append(2, pad);
            break;
        case 2:
            value = (*cursor++) << 16;

            value += (*cursor++) << 8;

            encoded.append(1, table[(value & 0x00FC0000) >> 18]);
            encoded.append(1, table[(value & 0x0003F000) >> 12]);
            encoded.append(1, table[(value & 0x00000FC0) >> 6]);
            encoded.append(1, pad);
            break;
    }

    return encoded;
}

decode方法

  • 首先判断base64字符串的长度是不是4的倍数
  • 去除 = 这样的 pad,= 只可能是1个或2个
  • 遍历整个base64字符串,每4个为一个单位(对应也就是3个字节的二进制数,以value表示),对于每个字符得到其在映射表中的索引,从而得到value的值,进而解析出每个字节的值

对应代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
bool decode(std::vector<uint8_t>& out, const std::string& in)
{
    const static uint32_t mask = 0x000000FF;

    const auto length = in.length();
    if ((length % 4) != 0)
        return false;

    size_t padding = 0;
    if (length > 0)
    {
        if (in[length - 1] == pad)
            padding++;
        if (in[length - 2] == pad)
            padding++;
    }

    std::vector<uint8_t> decoded;
    decoded.reserve((length / 4) * 3 - padding);

    uint32_t value = 0;
    for (auto cursor = in.begin(); cursor < in.end();)
    {
        for (size_t position = 0; position < 4; position++)
        {
            value <<= 6;
            if (*cursor >= 0x41 && *cursor <= 0x5A)     // A-Z
                value |= *cursor - 0x41;
            else if (*cursor >= 0x61 && *cursor <= 0x7A)    // a-z
                value |= *cursor- 0x47;
            else if (*cursor >= 0x30 && *cursor <= 0x39)    // 0-9
                value |= *cursor + 0x04;
            else if (*cursor == 0x2B)       // +
                value |= 0x3E;
            else if (*cursor == 0x2F)       // /
                value |= 0x3F;
            else if (*cursor == pad)
            {
                // Handle 1 or 2 pad characters.
                switch (in.end() - cursor)
                {
                    case 1:
                        decoded.push_back((value >> 16) & mask);
                        decoded.push_back((value >> 8) & mask);
                        out = decoded;
                        return true;
                    case 2:
                        decoded.push_back((value >> 10) & mask);    // (<< 6)(>> 16)
                        out = decoded;
                        return true;
                }
            }
            else
                return false;

            cursor++;
        }

        decoded.push_back((value >> 16) & mask);
        decoded.push_back((value >> 8) & mask);
        decoded.push_back((value >> 0) & mask);
    }

    out = decoded;
    return true;
}

调用

写一个简单的demo验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include "base64.h"
#include <iostream>

int main()
{
    std::string encoded_str("aGVsbG8sIHdvcmxkIQ==");
    std::cout << "Encoded base64 string: " << encoded_str << std::endl;

    std::vector<uint8_t> orignal_vector;
    if ((decode_base64(orignal_vector, encoded_str)) == false)
    {
        std::cout << "decode error" << std::endl;
        return 0;
    }
    std::string orignal_str(orignal_vector.begin(), orignal_vector.end());
    std::cout << "Original string is " << orignal_str << std::endl;

    std::string encoded = encode_base64(orignal_vector);
    std::cout << "Encoded again: " << encoded << std::endl;
    return 0;
}

编译后得到

1
2
3
4
5
$ gcc -o demo demo.cpp base.cpp
$ ./demo 
Encoded base64 string: aGVsbG8sIHdvcmxkIQ==
Original string is hello, world!
Encoded again: aGVsbG8sIHdvcmxkIQ==

应用

上面说了base64的编码方法,那么为什么要使用base64编码呢,有哪些情景需求?

我们知道在计算机中任何数据都是按ascii码存储的,而ascii码的128~255之间的值是不可见字符。而在网络上交换数据时,比如说从A地传到B地,往往要经过多个路由设备,由于不同的设备对字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。所以就先把数据先做一个Base64编码,统统变成可见字符,这样出错的可能性就大降低了。

HTML内嵌base64编码图片

MIME

X.509 公钥证书