基石#1 | Base64 编码

这是 Cornerstone 基石系列的第一篇,主题是 Base64 编码。


什么是 Base64 编码

根据维基百科,字符编码(英语:Character encoding)、字集码是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递[1]

简而言之,编码就是使用一种数据格式来表达另一种数据格式的一个一一对应。

不过需要注意的是,Base64 编码中的“编码”二字并不符合上文中的字符编码的定义。恰恰相反,Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法[2]。与 Base64 编码类似的二进制数据文本化方法还有 uuencodeBinHex 等。

由于 $64=2^6$,每个 Base64 编码字符可以表示 6 个比特(bit),而一个字节有 8 个比特(Byte),所以一个 Base64 编码字符可以表示 $\frac{3}{4}$ 个字节。由此我们可以知道,Base64 使用四个字节来编码三个字节,即被 Base64 编码过后的二进制数据会增大约 $\frac{4}{3}$ 倍。

用作 Base64 编码的字符集包括大写字母 A-Z、小写a-z,数字 0-9共 62 个,加上两个视不同 Base64 规范而不同的可打印字符(标准 Base64 规范使用的字符是 +/)。

以下为 Base64 的索引表。

数值字符数值字符数值字符数值字符
0A16Q32g48w
1B17R33h49x
2C18S34i50y
3D19T35j51z
4E20U36k520
5F21V37l531
6G22W38m542
7H23X39n553
8I24Y40o564
9J25Z41p575
10K26a42q586
11L27b43r597
12M28c44s608
13N29d45t619
14O30e46u62+*
15P31f47v63/*

padding: =
* 视不同 Base64 标准而不同

为什么我们需要 Base64

MIME 格式的电子邮件中,只能使用 ASCII 码的可打印字符。所以,人们需要发明一种编码方式,以 ASCII 可打印字符表示非 ASCII 码字符,以在邮件中嵌入图片、音频等二进制数据,也即 Base64 编码。事实上,Base64 编码是作为 MIME 多媒体电子邮件标准的一部分开发的[3]

同时,在阮一峰老师的博客[4]中也提到 Base64 编码的其他意义:

  • 所有的二进制文件,都可以因此转化为可打印的文本编码,使用文本软件进行编辑;
  • 能够对文本进行简单的加密。

(尽管 Base64 作为一种简单的固定替换只能称作广义上的加密(与凯撒密码类似)。)

同时,Base64 编码可以用作在 URL 中传递二进制数据或非 URL-friendly 的数据,也可以用作以 data: URL 形式在 HTML 等文本文件中内嵌图片等二进制数据。当然,由于 +/ 是非 URL-friendly 的,我们需要使用一种用于 URL 的改进 Base64 编码(不在末尾填充 = 号,并将 +/ 替换为 -_[2]

Base64 编码过程

将 3 字节的数据,先后放入一个 24 位的缓冲区中,先来的字节占高位。数据不足 3 字节的话,于缓冲器中剩下的比特用 0 补足。每次取出 6 比特,按照其值对应索引表中的字符作为编码后的输出,直到全部输入数据转换完成。

若原数据长度不是 3 的倍数时且剩下 1 个输入数据,则在编码结果后加 2 个=;若剩下 2 个输入数据,则在编码结果后加 1 个 =[2]

一个来自维基百科的例子:

文本Man
ASCII 编码7797110
二进制位010011010110000101101110
索引1922546
Base64 编码TWFu

另一个来自维基百科的例子(包含 padding =):

文本(1 Byte)A
二进制位010000010000000000000000
二进制位(补 0)010000010000000000000000
Base64 编码QQ==
文本(2 Byte)BC
二进制位010000100100001100000000
二进制位(补 0)010000100100001100000000
Base64编码QkM=

用 JavaScript 实现 Base64 编码与解码

目前主流的浏览器中都实现了全局方法 atob()btoa()用于 Base64 的编码与解码。

因为 Base64 原理比较简单,我就顺便实现了一下,作为参考。因为只是随手的实现,没有仔细考虑效率和阅读他人代码,可能会有 bug,请不要用在实际用途中。

同时这份代码使用了 Node.js 中的 Buffer,这也将会是基石系列第二篇的主题。

要补充的一点是,在实际使用 Base64 时,= padding 可以视情况而省略,不影响数据的完整性(事实上,在这份代码的解码函数一开始就去掉了原数据的 padding)。具体原因很简单,这里略过不表,请读者自己思考。

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
function base64encode(data, {
char62 = '+',
char63 = '/',
padding = '=',
} = {}) {
const c = Buffer.from(data);

const base64EncodeTab = `ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789${char62}${char63}`;

let str = '';

for (let i = 0; i < Math.ceil(c.byteLength / 3); i++) {
const p = 3 * i;
str += base64EncodeTab[c[p] >> 2];
str += base64EncodeTab[((c[p] << 4) & 0x3F) | (c[p + 1] >> 4)];
const remains = c.byteLength - p;
if (remains === 1) break;
str += base64EncodeTab[((c[p + 1] << 2) & 0x3F) | (c[p + 2] >> 6)];
if (remains === 2) break;
str += base64EncodeTab[c[p + 2] & 0x3F];
}

return str.padEnd(Math.ceil(str.length / 4) * 4, padding);
}

function base64decode(data, {
char62 = '+',
char63 = '/',
padding = '=',
encoding = 'utf-8',
} = {}) {
data = data.split(padding)[0];

const base64DecodeTab = ((chars) => {
let tmp = {};
for (let i = 0; i < chars.length; i++) tmp[chars[i]] = i;
return tmp;
})(`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789${char62}${char63}`);

const c = Buffer.alloc(Math.ceil(data.length / 4) * 3 + ((data.length + 2) % 4) - 2);

for (let i = 0; i < Math.ceil(data.length / 4); i++) {
const p = 4 * i;
c[3 * i] = (base64DecodeTab[data[p]] << 2) | (base64DecodeTab[data[p + 1]] >> 4);
if (p + 2 >= data.length) break;
c[3 * i + 1] = ((base64DecodeTab[data[p + 1]] << 4) & 0xFF) | (base64DecodeTab[data[p + 2]] >> 2);
if (p + 3 >= data.length) break;
c[3 * i + 2] = ((base64DecodeTab[data[p + 2]] << 6) & 0xFF) | base64DecodeTab[data[p + 3]];
}

return c.toString(encoding);
}

Base64 的不同标准

参见 维基百科(English only) [5]

Base64 应用:Data URLs

Data URLs,即前缀为 data: 协议的 URL,其允许内容创建者向文档中嵌入小文件[6]

Data URLs 的形式为:

1
data:[<mediatype>][;base64],<data>

其中,<mediatype> 为 MIME 类型的字符串,其默认值为 text/plain;charset=US-ASCII 。如果 <data> 为二进制类型,则需将其进行 Base64编码,并加上 [;base64] 选项。

具体的例子网上很多,这里就略过不表了。Data URLs 定义在 RFC 2397 [7]中,有兴趣的读者可以仔细阅读一下。

Base62x

为了克服 Base64 由于输出内容中包括两个以上“符号类”字符(+/= 等)而带来的互不兼容多变种问题,一种输出内容无符号的 Base62x 编码方案被引入软件工程领域,Base62x 被视为无符号化的 Base64 改进版本。[2]

是国人提出的哦。

具体的 Base62x 描述可见 Base62x: An alternative approach to Base64 for non-alphanumeric characters[8]

基本思路为,使用 0-9A-Za-wx1x2x3yz 作为编码集,并且省略 padding。其中 x1x2x3 被称为 tag,在解码过程中遇见字符 x 则与下一个字符共同解码。

使用 Base62x 编码的长度平均为原消息的 138%,区间为 $[\frac{4}{3},\frac{8}{3}]$,比 Base64 略大[8]。而在实际使用中,根据这篇文章(来自 Base62x 作者),我们可以认为 Base62x 的编码效率比 Base64 略高[9]

更多有关 Base62x 的资源以及其 Demo 可参见其官方网站[10]

UTF-7

由于在过去 SMTP 的传输仅能接受 7 比特的字符,而当时 Unicode 并无法直接满足既有的 SMTP 传输限制,在这样的背景下 UTF-7 被提出。严格来说 UTF-7 不能算是 Unicode 所定义的字符集之一,较精确的来说,UTF-7 是提供了一种将 Unicode 转换为 7 比特 US-ASCII 字符的转换方式[11]

详见维基百科[11]RFC 2152[12]

写在最后

本来打算清明就写完的又咕咕咕了(笑

呼,终于写完了第一篇,虽然比之前计划的篇幅还是小了点(删了点比较复杂而且无关的细节,可以以后再单独写)。读了很多博客、wiki 和 RFC,学到了很多。再接再厉吧,我们下一篇见!

(马上就要考离散了咕咕咕咕咕咕


  1. https://zh.wikipedia.org/wiki/字符编码

  2. https://zh.wikipedia.org/wiki/Base64

  3. https://segmentfault.com/a/1190000004533485

  4. http://www.ruanyifeng.com/blog/2008/06/base64.html

  5. https://en.wikipedia.org/wiki/Base64#Variants_summary_table

  6. https://developer.mozilla.org/zh-CN/docs/Web/HTTP/data_URIs

  7. https://tools.ietf.org/html/rfc2397

  8. https://ieeexplore.ieee.org/document/6020065

  9. https://my.oschina.net/wadelau/blog/1591374

  10. https://ufqi.com/dev/base62x/

  11. https://zh.wikipedia.org/wiki/UTF-7

  12. https://tools.ietf.org/html/rfc2152

如果这篇文章对你有帮助,那么不妨?
0%