0%

Base64编码及其实现

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

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

原理

文本文件和二进制文件

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

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

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

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

这样,我们得到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 公钥证书