了不起的Base64

2023-11-08 15:44:32 浏览数 (2)

❝不要乱说话。话说出去之前我们还是话的主人,话说出去之后我们就成了话的奴隶。 ❞

大家好,我是「柒八九」

前言

在我们项目开发中,Base64想必大家都不会很陌生,Base64是将「二进制数据」转换为文本的一种优雅方式,使存储和传输变得容易。但是,作为一个合格的程序员,我们应该有一种打破砂锅问到底的求助欲望。

所以,今天我们来讲讲在各种语言中出镜率都高的离谱的Base64算法。今天,我们就用我们在初高中语文老师教我们的描述一个事物的三大步骤:1. 是什么,2. 如何工作,3. 为什么它很重要。来讲讲Base64算法。

好了,天不早了,干点正事哇。

1. 前置知识点

「前置知识点」,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。「如果大家对这些概念熟悉,可以直接忽略」 同时,由于阅读我文章的群体有很多,所以有些知识点可能「我视之若珍宝,尔视只如草芥,弃之如敝履」。以下知识点,请「酌情使用」。 ❞

RFC

RFC,全称为Request for Comments,是一种用于定义「互联网标准和协议」的文件系列。 ❞

RFC最早由互联网工程任务组(IETF)创建,用于记录和传播互联网协议、方法和最佳实践的提案、规范和讨论。

「每个 RFC 都有一个唯一的编号」,通常以RFC开头,后面跟着一个数字,例如RFC 791RFC 2616等。RFC文档通常包含了协议规范、技术说明、最佳实践、标准化提案等,以促进互联网技术的发展和互操作性。

我们可以在IETF-datatracker[1]中输入指定的编号或者查找的关键字进行搜寻。

以下是一些常见的RFC文档,大家可以翻阅自己想了解的技术点:

  1. RFC 791 - Internet Protocol (IP): 定义了 IPv4,是互联网上最基本的协议之一。
  2. RFC 793 - Transmission Control Protocol (TCP): 定义了 TCP,一种重要的传输协议,用于可靠的数据传输。
  3. RFC 2616 - Hypertext Transfer Protocol -- HTTP/1.1: 定义了 HTTP 协议,用于在 Web 上传输超文本的基础通信协议。
  4. RFC 2326 - Real Time Streaming Protocol (RTSP): RTSP 用于流媒体传输,如音频和视频流的控制。
  5. RFC 5246 - The Transport Layer Security (TLS) Protocol Version 1.2: 定义了 TLS 1.2,用于安全地传输数据,如 HTTPS 协议中使用的加密通信。
  6. RFC 4648[2] - 这是咱们今天的主角,Base64的相关内容

Latin-1 字符集

Latin-1,也称为ISO-8859-1,是一种由国际标准化组织(ISO)认可的「8 位字符集」,代表了「西欧语言的字母表」。正如其名称所示,「它是ISO-8859的一个子集」,该标准还包括用于写作系统如西里尔文、希伯来文和阿拉伯文的其他相关字符集。它被大多数Unix系统以及Windows系统使用。

Latin-1有时被不太准确地称为「扩展 ASCII」。 ❞

这是因为其字符集的前 128 个字符与美国 ASCII 标准相同。其余字符集包含了带重音的字符和符号。

关于更详细的Latin-1的表格,可以参考Latin-1-table[3]


btoa

btoaJavaScript 中的一个内置函数,用于将二进制数据(通常是 8 位字节)编码为 Base64 字符串。它的名称是 binary to ASCII 的缩写,用于将二进制数据转换为文本字符串,以便在文本协议中传输或存储。

用法:

btoa 函数接受一个字符串参数,该字符串包含二进制数据。它将该二进制数据转换为 Base64 编码的字符串。

代码语言:javascript复制
const binaryData = "front789";
const base64String = btoa(binaryData);
console.log(base64String);

这段代码将 front789 这个字符串转换为 Base64 编码的字符串并将结果打印到控制台。

限制:

尽管 btoa 是一个有用的函数,但它有一些限制:

  1. 「只能编码字符串:」 btoa 函数只接受字符串作为参数,而不接受其他类型的数据(如二进制数组)。如果需要编码二进制数据,需要先将其转换为字符串。
  2. 「字符集限制:」 btoa 函数仅支持 Latin-1 字符集,这意味着它只能编码包含在 Latin-1 字符集内的字符。如果字符串包含超出 Latin-1 字符集的字符,那么会导致编码失败。
  3. 「不适合加密:」Base64 编码不是加密,它只是一种编码方式,不提供安全性。如果需要加密数据,应该使用专门的加密算法而不是仅仅进行 Base64 编码。
  4. 「数据大小增加:」 Base64 编码会增加数据大小。通常情况下,Base64 编码后的数据会比原始二进制数据更大,这可能会对数据传输和存储造成额外开销。

Data URL

Data URL 是一种统一资源标识符(URI)方案,用于将数据嵌入到文档中,而不是从外部文件加载数据。Data URL 允许我们将数据(如文本、图像、音频等)直接包含在网页或文档中,而不需要额外的 HTTP 请求。这种方式对于小型资源或需要避免外部请求的情况非常有用。

Data URL 的基本结构如下:

代码语言:javascript复制
data:[<mediatype>][;base64],<data>

其中:

  • <mediatype> 是可选的媒体类型(例如,text/plainimage/png),用于描述数据的类型。如果被省略,则默认值为 text/plain;charset=US-ASCII
  • ;base64 是可选的,表示数据以 Base64 编码方式包含。如果省略了 ;base64,则数据将以纯文本方式包含。
  • <data> 包含实际的数据,可以是文本或二进制数据。

以下是 Data URL 的一些常见用途和示例:

「嵌入图像:」 Data URL 可用于将图像直接嵌入 HTMLCSS 中,而不需要外部图像文件。例如,将一张 PNG 图像嵌入 HTML 中:

代码语言:javascript复制
<img
  src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wAABgAB/OGirwAAAABJRU5ErkJggg=="
  alt="Embedded Image"
/>

「内联 CSS:」 Data URL 可用于内联 CSS 样式表,以减少外部 CSS 文件的请求。例如,将 CSS 样式表嵌入 HTML 中:

代码语言:javascript复制
<style>
  body {
    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/wAABgAB/OGirwAAAABJRU5ErkJggg==);
  }
</style>

「嵌入字体:」 Data URL 可用于嵌入自定义字体,以确保字体在不同设备上显示一致。例如,嵌入一个字体文件:

代码语言:javascript复制
@font-face {
  font-family: "CustomFont";
  src: url(data:application/font-woff;base64,d09GRgABAAAA...) format("woff");
}

「内联脚本:」 Data URL 可用于内联小型 JavaScript 脚本,以减少外部脚本文件的请求。例如,内联一个简单的 JavaScript 函数:

代码语言:javascript复制
<script>
  let greeting = "前端柒八九";
  alert(greeting);
</script>

2. 为什么会出现 Base64 编码

要理解为什么需要 Base64 编码,我们需要了解一些计算机历史。

计算机以二进制(01)进行通信,但人们通常希望使用更丰富的数据形式进行通信,如文本图像「为了在计算机之间传输数据,首先必须将其编码为 0 和 1,然后再解码」。以文本为例,有许多不同的编码方式。如果我们都能就一个单一的编码方式达成一致,那将会简单得多,但很遗憾,这并不是事实。针对这块的内容,可以参考了不起的 Unicode

最初创建了许多不同的编码方式(例如 Baudot 编码),每种方式「使用不同数量的比特来表示一个字符」,直到最终 ASCII 成为一个标准,「每个字符使用 7 位」。然而,大多数「计算机将二进制数据存储为每个字节由 8 位组成的数据」,因此 ASCII 不适合传输这种类型的数据。一些系统甚至会删除最高位。

为解决这些问题,引入了 Base64 编码。这允许我们「将任意字节编码为已知不会损坏的字节」(ASCII 字母数字字符和一些符号)。缺点是使用 Base64 对消息进行编码会增加其长度 - 「每 3 个字节的数据编码为 4 个 ASCII 字符」

要可靠地发送文本,我们可以首先使用自己选择的文本编码(例如 UTF-8)将其编码为字节,然后将结果的二进制数据使用 Base64 编码为可安全传输的 ASCII 文本字符串。接收者反转此过程以恢复原始消息。当然,这需要接收者知道使用了哪种编码,通常需要单独发送这些信息。

我们来看一个示例:

我希望发送一个带有两行的文本消息:

代码语言:javascript复制
Hello
world!

如果我将其发送为 ASCII(或 UTF-8),它将如下所示:

代码语言:javascript复制
72 101 108 108 111 10 119 111 114 108 100 33

某些系统会破坏字节 10,所以我们可以将这些字节作为 Base64 字符串进行 Base64 编码:

代码语言:javascript复制
SGVsbG8Kd29ybGQh

这里的所有字节都是已知的安全字节,所以很少有机会使任何系统损坏此消息。我可以发送这个消息而不是我的原始消息,然后让接收者反转此过程以恢复原始消息。


2. 什么是 Base64 编码?

Base64编码将二进制数据转换为文本,具体来说是ASCII文本。生成的文本仅包含A-Za-z0-9以及符号 /这些字符。

而在之前我们在了不起的 Unicode中介绍过ASCII的。

由于字母表中有 26 个字母,我们有26 26 10 2(64)个字符。因此,这种编码被命名为Base64。这 64 个字符被认为是「安全」的,也就是说,与字符<>n等不同,「它们不会被旧计算机和程序误解」

下面是经过 Base64 编码的文本front789的样子:ZnJvbnQ3ODk=

还有一点需要注意,如果在使用JS对某一个文本进行准换时,如果该文本包含非Latin1字符的字符串,会报错,所以我们需要对其进行准换处理。

代码语言:javascript复制
// 原始文本字符串,包含非Latin1字符
const text = "前端柒八九";

// 创建一个 TextEncoder 对象,用于将文本编码为字节数组
const encoder = new TextEncoder();

// 使用 TextEncoder 对象将文本编码为字节数组
const data = encoder.encode(text);

// 使用 String.fromCharCode 和展开运算符 (...) 将字节数组转换为字符串
// 然后使用 btoa 函数将字符串转换为 Base64 编码
const base64 = btoa(String.fromCharCode(...data));

// 打印 Base64 编码后的结果
console.log(base64); //5YmN56uv5p S5YWr5Lmd

我们在这里并没有加密文本。给定Base64编码的数据,非常容易将其转换回(解码)原始文本。我们「只是改变了数据的表示」,即编码

❝在本质上,Base64编码使用一组特定的、减少的字符来「编码二进制数据」,以防止数据损坏。 ❞

Base64字母表

由于只有64个字符可用于编码,我们可以仅使用6位来表示它们,因为2^6 = 64。每个Base64数字表示6位数据。一个字节中有8位,而 86「最小公倍数」24。因此,「24 位,或 3 个字节,可以用四个 6 位的 Base64 数字表示」

4. Base64 使用案例

我们可能在HTML文档中使用了<img src="789.jpeg">标签来包含图像。其实,我们可以直接将「图像数据」嵌入到 HTML 中,而不必使用外链!数据URL可以做到这一点,它们使用Base64编码的文本来内联嵌入文件。

代码语言:javascript复制
<img src="data:image/gif;base64,xxxxbase64encodedtextxxxx" />

data:[<mime type
  >][;charset=<charset>][;base64],<encoded data></encoded></charset
></mime>

另一个常见的用例是当我们需要在网络上传输或存储一些二进制数据,而网络只能处理文本或ASCII数据时。这确保了数据在传输过程中保持不变。还有就是在 URL 中传递数据时,当数据包含不适合 URL 的字符时,此时Base64就有了用武之地。

Base编码还在许多应用程序中使用,因为它使得可以使用文本编辑器来操作对象。

我们还可以使用 Base64 编码「将文件作为文本传输」

  • 首先,获取文件的字节并将它们「编码为 Base64」
  • 然后传输 Base64 编码的字符串,然后在接收端「解码为原始文件内容」

5. Base64 编码算法

以下是将一些文本转换为 Base64 的简单算法。

  1. 将文本转换为其二进制表示
  2. 比特位分组为每组6位
  3. 将每个组转换为0到63的十进制数。它不能大于 64,因为每组只有 6 位。
    • 如果转换为十进制数的数字大于 64,我们可以将其取模64 例如:151 % 64 = 23
  4. 使用Base64字母表将此十进制数转换为等效的Base64字符

通过上述操作我们会得到一个Base64编码的字符串。如果最后一组中的比特位不足,可以使用===作为填充。

让我们以front7作为范例,来模拟上述操作。

通过首先将每个字符转换为其对应的 ASCII 数字,然后将该十进制数转换为二进制,(使用ASCII 转二进制工具[4])将文本front7转换为二进制:

代码语言:javascript复制
01100110 01110010 01101111 01101110 01110100 00110111

f        r        o        n        t        7

将比特位分组为每组6位

代码语言:javascript复制
011001 100111 001001 101111 011011 100111 010000 110111

将每个组转换为 0 到 63 之间的十进制数:

代码语言:javascript复制
011001 100111 001001 101111 011011 100111 010000 110111

25     23     9     47     27     23      16       27
  • 这步中如果数据超过 64,需要对其 64 取模

现在使用 Base64 字母表将每个十进制数转换为其 Base64 表示:

代码语言:javascript复制
25  23   9   47  27  23  16  27

Z    n   J   v   b   n   Q   3

然后我们完成了。名字front7在 Base64 中表示为ZnJvbnQ3

乍一看,Base64 编码的好处并不是很明显。

想象一下,如果我们有一张图片或一个「敏感文件」(PDF、文本、视频等),而不是简单的字符串,我们想将它存储为文本。我们可以首先将其转换为二进制,然后进行 Base64 编码,以获得相应的 ASCII 文本。

现在我们可以将该文本发送或存储在任何地方,以任何我们喜欢的方式,而不必担心一些旧设备、协议或软件会错误解释原始二进制数据以损坏我们的文件。

6. 如何进行 Base64 编码和解码

所有编程语言都支持将数据编码为 Base64 格式以及从 Base64 格式解码数据。

JS 中处理

代码语言:javascript复制
// 简单字符串
const text1 = "front789";
bota(text1); // ZnJvbnQ3ODk=

// 超出`Latin-1`字符的字符串
const text2 = "前端柒八九";
const encoder = new TextEncoder();
const data = encoder.encode(text);
const base64 = btoa(String.fromCharCode(...data));
console.log(base64); //5YmN56uv5p S5YWr5Lmd

Rust 中处理

Rust的话,我们可以直接用 base64 crate。

Cargo.toml 文件中添加以下内容:

代码语言:javascript复制
[dependencies]
base64 = "0.21.5"
代码语言:javascript复制
use base64::{Engine as _, engine::general_purpose};

let orig = b"data";
let encoded: String = general_purpose::STANDARD_NO_PAD.encode(orig);
assert_eq!("ZGF0YQ", encoded);
assert_eq!(orig.as_slice(), &general_purpose::STANDARD_NO_PAD.decode(encoded).unwrap());

// or, URL-safe
let encoded_url = general_purpose::URL_SAFE_NO_PAD.encode(orig);

想了解更多关于Rust如何处理Base64,可以查看Rust base64[5]

此外,终端也内置支持 Base64 编码。在终端中尝试以下命令:

代码语言:javascript复制
echo "前端柒八九" | base64
5YmN56uv5p S5YWr5LmdCg==

$ echo "5YmN56uv5p S5YWr5LmdCg==" | base64 -d
前端柒八九

后记

「分享是一种态度」

「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」

Reference

[1]

IETF-datatracker: https://datatracker.ietf.org/

[2]

RFC 4648: https://datatracker.ietf.org/doc/html/rfc4648?ref=writesoftwarewell.com

[3]

Latin-1-table: https://kb.iu.edu/d/aepu

[4]

ASCII 转二进制工具: https://www.rapidtables.com/convert/number/ascii-to-binary.html

[5]

Rust base64: https://docs.rs/base64/latest/base64/

0 人点赞