这是 Cornerstone 基石系列的第一篇,主题是 Base64 编码。
¶什么是 Base64 编码
根据维基百科,字符编码(英语:Character encoding)、字集码是把字符集中的字符编码为指定集合中某一对象(例如:比特模式、自然数序列、8位组或者电脉冲),以便文本在计算机中存储和通过通信网络的传递[1]。
简而言之,编码就是使用一种数据格式来表达另一种数据格式的一个一一对应。
不过需要注意的是,Base64 编码中的“编码”二字并不符合上文中的字符编码的定义。恰恰相反,Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法[2]。与 Base64 编码类似的二进制数据文本化方法还有 uuencode、BinHex 等。
由于 ,每个 Base64 编码字符可以表示 6 个比特(bit),而一个字节有 8 个比特(Byte),所以一个 Base64 编码字符可以表示
个字节。由此我们可以知道,Base64 使用四个字节来编码三个字节,即被 Base64 编码过后的二进制数据会增大约
倍。
用作 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)。具体原因很简单,这里略过不表,请读者自己思考。
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 的不同标准
¶Base64 应用:Data URLs
Data URLs,即前缀为
data:协议的 URL,其允许内容创建者向文档中嵌入小文件[6]。
Data URLs 的形式为:
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%,区间为 ,比 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,学到了很多。再接再厉吧,我们下一篇见!
(马上就要考离散了咕咕咕咕咕咕