这是 Cornerstone 基石系列的第一篇,主题是 Base64 编码。
¶什么是 Base64 编码
根据维基百科,字符编码(英语:Character encoding)、字集码是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递[1]。
简而言之,编码就是使用一种数据格式来表达另一种数据格式的一个一一对应。
不过需要注意的是,Base64 编码中的“编码”二字并不符合上文中的字符编码的定义。恰恰相反,Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法[2]。与 Base64 编码类似的二进制数据文本化方法还有 uuencode、BinHex 等。
由于 $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 的索引表。
数值 | 字符 | 数值 | 字符 | 数值 | 字符 | 数值 | 字符 |
---|---|---|---|---|---|---|---|
0 | A | 16 | Q | 32 | g | 48 | w |
1 | B | 17 | R | 33 | h | 49 | x |
2 | C | 18 | S | 34 | i | 50 | y |
3 | D | 19 | T | 35 | j | 51 | z |
4 | E | 20 | U | 36 | k | 52 | 0 |
5 | F | 21 | V | 37 | l | 53 | 1 |
6 | G | 22 | W | 38 | m | 54 | 2 |
7 | H | 23 | X | 39 | n | 55 | 3 |
8 | I | 24 | Y | 40 | o | 56 | 4 |
9 | J | 25 | Z | 41 | p | 57 | 5 |
10 | K | 26 | a | 42 | q | 58 | 6 |
11 | L | 27 | b | 43 | r | 59 | 7 |
12 | M | 28 | c | 44 | s | 60 | 8 |
13 | N | 29 | d | 45 | t | 61 | 9 |
14 | O | 30 | e | 46 | u | 62 | +* |
15 | P | 31 | f | 47 | v | 63 | /* |
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]。
一个来自维基百科的例子:
文本 | M | a | n | |||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ASCII 编码 | 77 | 97 | 110 | |||||||||||||||||||||
二进制位 | 0 | 1 | 0 | 0 | 1 | 1 | 0 | 1 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 1 | 1 | 0 | 1 | 1 | 1 | 0 |
索引 | 19 | 22 | 5 | 46 | ||||||||||||||||||||
Base64 编码 | T | W | F | u |
另一个来自维基百科的例子(包含 padding =
):
文本(1 Byte) | A | |||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
二进制位 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
二进制位(补 0) | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Base64 编码 | Q | Q | = | = | ||||||||||||||||||||
文本(2 Byte) | B | C | ||||||||||||||||||||||
二进制位 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
二进制位(补 0) | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Base64编码 | Q | k | M | = |
¶用 JavaScript 实现 Base64 编码与解码
目前主流的浏览器中都实现了全局方法 atob()
和 btoa()
用于 Base64 的编码与解码。
因为 Base64 原理比较简单,我就顺便实现了一下,作为参考。因为只是随手的实现,没有仔细考虑效率和阅读他人代码,可能会有 bug,请不要用在实际用途中。
同时这份代码使用了 Node.js 中的 Buffer
,这也将会是基石系列第二篇的主题。
要补充的一点是,在实际使用 Base64 时,=
padding 可以视情况而省略,不影响数据的完整性(事实上,在这份代码的解码函数一开始就去掉了原数据的 padding)。具体原因很简单,这里略过不表,请读者自己思考。
1 | function base64encode(data, { |
¶Base64 的不同标准
¶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-9
、A-Z
、a-w
、x1
、x2
、x3
、y
、z
作为编码集,并且省略 padding。其中 x1
、x2
、x3
被称为 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]。
¶写在最后
本来打算清明就写完的又咕咕咕了(笑
呼,终于写完了第一篇,虽然比之前计划的篇幅还是小了点(删了点比较复杂而且无关的细节,可以以后再单独写)。读了很多博客、wiki 和 RFC,学到了很多。再接再厉吧,我们下一篇见!
(马上就要考离散了咕咕咕咕咕咕