基石#1 | Base64 编码

250 阅读8分钟

这是 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 的索引表。

数值 字符 数值 字符 数值 字符 数值 字符
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 的不同标准

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

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-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. zh.wikipedia.org/wiki/字符编码

  2. zh.wikipedia.org/wiki/Base64

  3. segmentfault.com/a/119000000…

  4. www.ruanyifeng.com/blog/2008/0…

  5. en.wikipedia.org/wiki/Base64…

  6. developer.mozilla.org/zh-CN/docs/…

  7. tools.ietf.org/html/rfc239…

  8. ieeexplore.ieee.org/document/60…

  9. my.oschina.net/wadelau/blo…

  10. ufqi.com/dev/base62x…

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

  12. tools.ietf.org/html/rfc215…