图解 Base64 实现原理并使用 js 实现一个简单的 Base64 编码器 🤔🤔🤔

发布于:2024-05-05 ⋅ 阅读:(24) ⋅ 点赞:(0)

为什么会出现 Base64

Base64 编码出现的主要原因是为了解决在不同系统之间传输二进制数据时可能遇到的问题。下面是一些导致 Base64 编码广泛使用的具体原因。

不同系统的数据处理差异

早期的通信系统和协议设计主要是基于文本数据的传输。这些系统可能无法正确处理非文本数据(如二进制文件),因为某些二进制值可能会被解释为控制字符(例如,行结束符、回车符等),这会干扰数据的正确传输和解释。

操作系统差异(如 Windows、macOS、Linux)会影响文件管理和系统调用的方式。例如,文件路径表示方法在 Windows 系统中通常使用反斜杠(\),而在 UNIX-like 系统(如 Linux 和 macOS)中使用正斜杠(/)。此外,文件系统(如 NTFS、HFS+、EXT4)的差异也会影响文件属性的支持、文件大小限制和性能表现。

假设我们要发送这样一个带有两行的文本信息:

hello
Moment

如果我们将这些字符串转换为 ASCII,它将如下所示:

104 101 108 108 111 10 77 111 109 101 110 116 65281 10;

然而数字 10 在 ASCII 表中代表的是换行符(LF, Line Feed),在不同的操作系统中,它的处理方式可能会导致问题,特别是在文本文件的换行表示上。以下是主要操作系统间的差异:

  • Windows 系统中,文本文件的换行通常使用回车符(CR, Carriage Return)和换行符(LF)的组合,也就是 13 10 或 CR LF。这种方式在 ASCII 表中由 CR(回车符,ASCII 为 13)和 LF(换行符,ASCII 为 10)组合表示。当你在 Windows 系统中创建文本文件时,每一行的末尾通常会自动添加这种字符组合。

  • Unix/Linux 系统中,文本文件的换行仅使用换行符(LF, ASCII 为 10)。这是由历史原因决定的,因为 Unix 设计者选择使用单一的换行符来简化系统的处理。在这些系统中,如果文本文件仅包含换行符作为行结束标记,通常不会出现问题。

  • 在 MacOS X 以前的旧系统(如 MacOS 9),换行符是使用回车符(CR, ASCII 为 13)。但从 MacOS X 开始,它转变为使用 Unix 风格的换行符(LF)。

这种不兼容主要是由于不同系统对换行符的解释和处理方式不同所引起的。为了解决这个问题,很多现代文本编辑器和开发环境提供了设置选项,允许用户选择使用哪种风格的换行符,以保证文件在不同操作系统间的兼容性。

所以我们就可以将这些字节作为 Base64 字符串进行 Base64 编码:

aGVsbG8KTW9tZW5077yBCg==

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

邮件系统的限制

电子邮件最初是基于 ASCII 文本设计的,这意味着它们只能处理 7 位的数据。然而,大多数现代数据(如图片和文档文件)通常是以 8 位字节形式存储的二进制数据。在不对这些数据进行编码的情况下直接通过电子邮件传输,会导致信息损失或错误。

ASCII(美国信息交换标准代码)是一种字符编码标准,旨在标准化电子设备和计算机系统中文本的表示。ASCII 使用 7 位二进制数来表示字符,能够编码 128 个不同的符号,包括英文字母、数字、标点符号以及控制字符(如回车和换行符)。这种编码标凈因其简单和广泛的兼容性而在早期计算系统中得到了广泛应用。

如下表是一个包括常用 ASCII 字符的表,展示了从 0 到 127 的 ASCII 值,以及它们对应的字符:

ASCII(美国标准信息交换码)是一种字符编码标准,用于表示文本数据。它包括控制字符、可打印字符以及数字。下面是一个包括常用 ASCII 字符的表,展示了从 0 到 127 的 ASCII 值,以及它们对应的字符:

20240505171607

这张表提供了从 0 到 127 的所有 ASCII 码及其对应的字符。其中包括控制字符(例如 NUL, SOH, STX 等,它们用于控制文本处理的各种方面)和可打印字符(例如字母、数字和标点符号)。

在 1970 年代,当电子邮件系统(如 ARPANET 上的邮件系统)被创建时,其设计原则是基于 ASCII 文本。这主要是因为:

兼容性:ASCII 是当时广泛支持的标准,几乎所有的计算机和传输设备都能处理 ASCII 文本,使得跨系统通信变得可行。 简单性:使用 ASCII 文本意味着邮件内容可以在不同的计算机系统和终端上无需特殊处理地显示,简化了邮件系统的实现。 网络效率:ASCII 文本由于只需 7 位就能表示一个字符,相比较其他可能的编码方式(如使用 8 位或更多位的编码)在当时的网络条件下更加高效。

在 1970 年代,当电子邮件系统(如 ARPANET 上的邮件系统)被创建时,其设计原则是基于 ASCII 文本。这主要是因为:

  1. 兼容性:ASCII 是当时广泛支持的标准,几乎所有的计算机和传输设备都能处理 ASCII 文本,使得跨系统通信变得可行。

  2. 简单性:使用 ASCII 文本意味着邮件内容可以在不同的计算机系统和终端上无需特殊处理地显示,简化了邮件系统的实现。

  3. 网络效率:ASCII 文本由于只需 7 位就能表示一个字符,相比较其他可能的编码方式(如使用 8 位或更多位的编码)在当时的网络条件下更加高效。

由于最初的电子邮件是基于 ASCII 文本设计的,这导致了一些限制:

  1. 二进制数据和非英文字符的处理问题:早期的电子邮件系统不支持直接发送二进制文件(如图片、文档等)或包含非英文字符的邮件,因为这些内容无法用标准 ASCII 编码直接表示。

  2. 内容格式限制:邮件内容基本限于简单文本,不支持复杂格式或样式(如字体变化、颜色等)。

  3. 随着技术的发展和用户需求的增加,电子邮件标准也逐渐演进,增加了如 MIME(多用途互联网邮件扩展)这样的标准来支持多媒体内容、多种字符集和复杂的邮件格式,解决了早期基于 ASCII 文本设计的电子邮件系统的限制。

什么是 Base64

Base64 是一种基于 64 个可打印字符来表示二进制数据的编码方法。它常用于在不支持二进制数据的环境中传输、存储或处理数据。例如,通过电子邮件发送图片或其他媒体文件时,通常会用 Base64 编码转换成文本格式,因为传统的邮件系统仅支持文本内容。

Base64 的使用场景主要有以下几个方面:

  1. 电子邮件:邮件协议原生只支持文本内容。使用 Base64 可以安全地发送二进制文件(如图片、PDF 文档等)。

  2. 数据 URL:在 HTML 或 CSS 中直接嵌入小型媒体文件,而无需外部文件链接。

  3. Web API:在 Web APIs 中安全传输数据,特别是在 URL、Cookie 或其他限制只能携带文本的环境中。

  4. 图片编码:将一些体积不大的图片进行 Base64 编码,直接内嵌到网页源码里面。

Base64 编码的结果由简单的 ASCII 字符组成,适合在文本系统中显示和处理,几乎所有的系统和网络传输协议都支持 ASCII 字符,使得 Base64 广泛应用于多种情况。

但是 Base64 编码后的数据比原始二进制数据大约增加 33%,这会增加存储和传输的负担,它看起来可能难以直接阅读,但它不是加密工具,不能提供数据的安全保护。对安全性有要求的场合应使用真正的加密方法。

总之,Base64 是一种广泛使用的数据编码方法,主要解决文本环境下的二进制数据传输问题。它简单、通用,但增加了数据体积,且不能提供加密保护。

Base64 原理

Base64 编码使用一组 64 个字符:大写字母 A-Z、小写字母 a-z、数字 0-9,加上两个符号(通常是 +/)。

它的索引表如下所示:

索引 字符 索引 字符 索引 字符 索引 字符
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 /

编码过程中,会将原始二进制数据每三个字节划分为一组,总计 24 位。然后,这 24 位被分成 4 个 6 位的小组,每个 6 位小组会映射到 Base64 字符集中的一个字符。

如果原始数据的字节不是 3 的倍数,Base64 编码过程会在数据末尾添加一到两个等号(=)作为填充符,以确保数据长度是 3 的倍数。这样做是为了在解码时能正确恢复原始数据。

假设我们有字符串 "Man",在 ASCII 中对应的二进制表示为:

  • M = 77 (01001101)

  • a = 97 (01100001)

  • n = 110 (01101110)

这些字节合在一起形成 24 位的二进制串:01001101 01100001 01101110。

接下来,将这 24 位分为 4 组,每组 6 位:

  • 010011 (十进制 19) -> T

  • 010110 (十进制 22) -> W

  • 000101 (十进制 5) -> F

  • 101110 (十进制 46) -> u

因此,"Man" 用 Base64 编码后为 "TWFu"。

接下来我们用图标的形式来展示:

20240505173130

这样可能不太直观,举个例子就容易理解了。比如我们对 cat 进行编码:

20240505173711

可以看到我们的 cat 编码后变成了 Y2F0:

20240505173751

如果待编码内容的字节数不是 3 的整数倍,那需要进行一些额外的处理。如果最后剩下 1 个字节,那么将补 4 个 0 位,编码成 2 个 Base64 字符,然后补两个 =:

20240505174301

如果最后剩下 2 个字节,那么将补 2 个 0 位,编码成 3 个 Base64 字符,然后补一个 =:

20240505174028

如何实现一个简易的 Base64 编码器

接下来我们将实现一个简答的 Base64 编码器,如下代码所示:

function encodeToBase64(input) {
  // 定义Base64字符集
  const chars =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

  // 将输入字符串转换为字节数组
  let str = input
    .split("")
    .map((c) => c.charCodeAt(0).toString(2).padStart(8, "0"))
    .join("");

  // 将位字符串分割成6位一组
  const chunks = str.match(/.{1,6}/g) || [];

  // 将每个6位二进制数转换为对应的Base64字符
  let base64 = chunks
    .map((bin) => {
      if (bin.length < 6) {
        // 如果不足6位,在末尾添加0
        bin = bin.padEnd(6, "0");
      }
      return chars[parseInt(bin, 2)];
    })
    .join("");

  // 根据Base64编码规则,输出长度必须是4的倍数,不足的地方使用`=`填充
  while (base64.length % 4 !== 0) {
    base64 += "=";
  }

  return base64;
}

const originalString = "Man";
const base64Encoded = encodeToBase64(originalString);

console.log("源字符串:", originalString);
console.log("Base64 编码 字符串:", base64Encoded);

这些输出结果和我们上面讲到的内容一模一样。

20240505175038

为什么 Base64 编码后的数据比原始二进制数据大约增加 33%,

Base64 编码导致数据增大约 33%的原因在于其编码过程本身以及所使用的字符集。这种增加主要是因为 Base64 将原始数据按照每 3 个字节为一组进行编码,而每组编码后转换为 4 个字符,这就改变了数据的存储效率。以下是详细的解释和计算:

在前面的内容都知道我们的一个二进制到 Base64 的转换主要经过如下步骤

  • 原始二进制数据通常按字节(每字节 8 位)存储。

  • Base64 编码是将每 3 个字节的 24 位二进制数据分为 4 组,每组 6 位。

  • 每组 6 位二进制数据转换为一个 Base64 字符。

也就是说每 3 个字节转换后变为 4 个 Base64 字符。因此,Base64 编码后的字符数比原始字节数增加了 1/3(即原始数据的 133.33%)。

计算

假设有 N 字节的原始数据:

  • 原始数据的位数:N × 8 位。
  • 按 Base64 编码,每 3 字节分为一组,变为 4 个字符,所以 N 字节数据需要
    N 3 × 4 \lceil \frac{N}{3} \rceil \times 4
    个字符表示。

如果 N 是 3 的倍数,编码后的数据大小 M 是:

M = 3 4 N M = \frac{3}{4}N

所以,编码后的数据大小是原始大小的 133.33%,即增加了约 33.33%。

Base64 使用 64 个字符(A-Z, a-z, 0-9, +, /)加上填充符号(=),而原始二进制数据只用 0 和 1 两种状态。Base64 的每个字符需要 6 位二进制来表示

2 6 = 64 2^6 = 64

但原始的每个字节是 8 位。这种从 8 位到 6 位的转换需要额外空间来保持相同的信息量。

因为这种效率的减少,Base64 不适合用于大规模数据存储或传输效率非常关键的应用。它更多地用于编码非文本信息,如在 URL 中传输小量的二进制数据或在电子邮件中发送图片等。

如何保存 base64 字符串为文件

假设我们有这样的场景,后端把图片转换成了 base64 编码并以 json 的格式返回给前端,那么我们前端应该如何将这个 base64 编码转换成图片然后实现下载功能呢?

首先我们编写一个 node 代码来实现将文件转化为 base64 编码先,如下代码所示:

const fs = require("fs");

function encodeImageToBase64(filePath, outputFilePath) {
  fs.readFile(filePath, (err, data) => {
    if (err) {
      console.error(err);
      return;
    }
    // 转换为Base64字符串
    const base64Image = data.toString("base64");

    fs.writeFile(outputFilePath, base64Image, (err) => {
      if (err) {
        console.error("Error writing file:", err);
        return;
      }
      console.log("File written successfully:", outputFilePath);
    });
  });
}

encodeImageToBase64("./moment.webp", "./test.md");

最终输出结果如下图所示:

20240505181759

然后我们编写我们的 html 文件,如下代码所示:

20240505182055

当我们打开浏览器的时候,发现他自动给我们下载了图片:

20240505182142

大功告成!!!

参考资料

总结

Base64 是一种编码方法,用于将二进制数据转换成 ASCII 字符集的文本格式。这种编码主要用于在不支持二进制数据的媒介上传输数据,如在电子邮件或 Web 数据中。Base64 编码后的数据比原始数据体积大约增加 33%,因为它使用四个字符来表示每三个字节的二进制数据。

在网页设计中,如果使用 HTTP 形式的 URL 加载图片,每张图片都会触发一个单独的 HTTP 请求,从而增加页面的总请求次数并可能降低加载速度。相反,通过将图片编码为 Base64 格式的字符串并直接嵌入 HTML 中,可以随 HTML 内容一起加载,有效减少 HTTP 请求的数量,这是提升网页加载性能的有效策略。