深入理解Base64编码原理及优势

433 阅读7分钟

image.png

Base64的优势

Base64是一种常见的编码方式,主要用于在不同系统之间以文本形式传输二进制数据。其优势如下:

  1. 文本传输:Base64将二进制数据转换为纯文本,因此可以安全地在文本协议中传输,例如在URL、JSON等数据格式中嵌入二进制数据。
  2. 可读性:Base64编码后的数据只包含常见字符(A-Z、a-z、0-9、+、/),不包含控制字符和特殊字符,增加了数据的可读性和可打印性。
  3. 不改变数据:Base64编码不会对原始数据进行加密或压缩,只是一种映射关系,因此不会导致数据丢失。

Base64 编码对照表

下面是一个对照表,注意二进制这一列始终只用到了6位,而不是一个字节8位,这其实就是后面要说的base64会导致数据膨胀4/3的根本原因。

字符Base64码值二进制值字符Base64码值二进制值
A0000000h33100001
B1000001i34100010
C2000010j35100011
D3000011k36100100
E4000100l37100101
F5000101m38100110
G6000110n39100111
H7000111o40101000
I8001000p41101001
J9001001q42101010
K10001010r43101011
L11001011s44101100
M12001100t45101101
N13001101u46101110
O14001110v47101111
P15001111w48110000
Q16010000x49110001
R17010001y50110010
S18010010z51110011
T19010011052110100
U20010100153110101
V21010101254110110
W22010110355110111
X23010111456111000
Y24011000557111001
Z25011001658111010
a26011010759111011
b27011011860111100
c28011100961111101
d29011101+62111110
e30011110/63111111
f31011111=(填充字符)(填充字符)
g32100000

编码逻辑

一张图看懂Base64算法是怎么把cat这个单词变成Base64字符串的

image.png 如上图

  1. cat这个单词需要用3个字节存储,分别是01100011 01100001 01110100, 8 * 3 = 24bit
  2. 把这24bit重新拆分成 6 * 4 = 24bit
  3. 每6位为一组,转成10进制,到上面的对照表中找相应的字符;
  4. 分别找到了Y2F0

这样cat经过Base64编码后就变成了Y2F0,你会发现原来3个字符就可以表达的东西,现在要花四个字符才能表达,这就是数据转成Base64后传输更耗带宽的原因。

末尾补等号怎么回事?

因为8和6最小公倍数是24(3个字节),所以Base64编码时必须3个字节为一组,一组一组的处理。那么数据字节数就不是3的倍数怎么办呢?

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

image.png

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

image.png

代码举例

下面举一个例子,将“我是小菜鸡”这几个字用Base64编码一下

function stringToBase64(str) {
  // Base64字符集
  const base64Chars =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

  let result = "";
  let i = 0;

  // 遍历输入字符串
  while (i < str.length) {
    const char1 = str.charCodeAt(i++);
    const char2 = str.charCodeAt(i++);
    const char3 = str.charCodeAt(i++);

    // 将字符转换为Base64编码的索引值
    const enc1 = char1 >> 2; // 取前6位
    const enc2 = ((char1 & 3) << 4) | (char2 >> 4); // 取后2位 + 下一个字符的前4位
    const enc3 = ((char2 & 15) << 2) | (char3 >> 6); // 取后4位 + 下一个字符的前2位
    const enc4 = char3 & 63; // 取后6位

    // 处理字符是否是末尾字符,并加入到结果中
    if (isNaN(char2)) {
      enc3 = enc4 = 64;
    } else if (isNaN(char3)) {
      enc4 = 64;
    }

    // 根据Base64字符集,将编码后的索引值转换为Base64字符,并拼接到结果中
    result +=
      base64Chars.charAt(enc1) +
      base64Chars.charAt(enc2) +
      base64Chars.charAt(enc3) +
      base64Chars.charAt(enc4);
  }

  return result;
}

// 测试字符串
const text = "我是小菜鸡";
// 使用手动实现的Base64编码函数将字符串转换为Base64
const base64Text = stringToBase64(unescape(encodeURIComponent(text)));
console.log(base64Text); // 输出:5oiR5piv5bCP6I+c6bih

数据膨胀的原因

其实上面也提到了,这里总结一下:

  1. 编码字符集扩大:在原始数据中,每个字节有256个可能的取值(2^8)。而在Base64编码中,每个6位二进制数据只有64个可能的取值(2^6),因为Base64使用的字符集仅包含64个字符。因此,将原始数据中的8位二进制数据转换为6位Base64字符,必然导致数据大小的增加。
  2. 填充字符的引入:在Base64编码中,如果原始数据的字节长度不是3的倍数,会使用等号 "=" 进行填充,确保编码后的数据长度是4的倍数。填充字符会增加编码数据的大小。

为什么有Base64而没有Base256

聪明的你会发现,导致Base64编码后的大小是原来的4/3倍的根本原因是对照表里只有64个明文字符,最多只能表示64种可能,也就是二进制000000 ~ 111111,6个比特。如果对照表里有256个字符,不就可以表示00000000 ~ 11111111 了?就不会浪费了。Base256没有被发明,原因有以下几点:

  1. 可打印性和可读性: Base64编码使用的字符集由64个常见、可打印的ASCII字符组成(A-Z、a-z、0-9、+、/),这使得编码后的数据易于阅读和传输。相比之下,Base256需要使用ASCII字符集中所有的256个字符,其中包含许多不可打印的字符,这使得数据难以阅读和处理。试想一下如cat这个单词如果用base256编码,可能长这样口口口口口,dog这个单词也可能是口口口口口
  2. 传输和存储效率: 虽然Base64编码会导致数据膨胀,但在一些场景中,数据膨胀带来的额外开销可以被接受,而且Base64的可读性和通用性更有价值。相比之下,Base256的数据大小会更接近原始二进制数据,但在文本传输和存储方面并没有明显的优势。

一份简明的 Base64 原理解析