手把手带你剥开 Base64 外衣:从原理到手搓实现

226 阅读13分钟

        你是不是只会调用现成的 Base64 接口,却从来不知道它的底层到底在干嘛? 明明只是简单的编码转换,为啥会有 = 号填充?为啥 3 个字节能变成 4 个字符?为啥二进制串要拆成 6 位一组? 别再把 Base64 当成 “黑箱” 了! 今天这篇教程,就手把手带你剥开 Base64 的外衣,从二进制位运算到分组拆分规则,再到边界 case 处理,全程干货无废话,也是对上一篇文章《字符编码轻松学:从 ASCII 到 Unicode,再到 UTF 家族 —— 搞懂它们的 “前世今生”》实战的应用。 不用复杂的依赖,不用高深的知识,跟着步骤一步步敲代码,你也能亲手实现一个完整的 Base64 编码器,彻底吃透它的核心逻辑! 

在进入今天的内容之前,咱们先快速回顾一下上一篇文章里讲过的 UTF-8表示码点范围。

常用汉字 UTF-8 占 3 字节,ASCII 字符占 1 字节;双字节、4 字节规则
因非本文所需,不再展开

接下来,我们再给出 Base64 编码中 0-63 索引对应的完整字符映射表。

有了上面的准备之后我们再来梳理下字符转base64流程,如下所示。

第一步、获取字符的 Unicode 码点

第二步、将码点转换为二进制字节

第三步、用UTF-8对二进制字节进行编码

第四步、按 6 位分组拆分UTF-8编码后的二进制串

第五步、分组映射 Base64 索引字符

第六步、若输出的字符不足4的倍数用等号(=)补充

对于不熟悉Unicode码点、ASCII码和UTF编码的可阅读上一篇文章

接下来,我们将逐一拆解不同类型字符(单个字母、数字、短单词、汉字)转 Base64 的完整过程 —— 涵盖字母 “a”、数字 “2”、单词 “HR”“NBA”,以及汉字 “你们”,从简单到复杂,把每种场景的编码逻辑讲透。

1、字母 a 转base64推导步骤如下

1.1、获取 a 的Unicode码点。方法有很多这里我用shell命令来获取

1.2、字母 “a” 的码点是十六进制 61(十进制 97)

//转二进制后仅占了7位不足8位
1100001

1.3、符合 UTF-8 单字节存储规则(0-127),对应的二进制为:

//左侧补0凑齐8位(只有左侧补0才不影响原二进制值大小)
01100001  

1.4、按6位一组拆分二进制数据、不足6位右侧补0到6位(base64拆分规则)

// 拆分后如下所示
// 第一组 011000 对应十进制值为 24
// 第二组 010000 对应十进制值为 16
011000 
010000  

1.5、查询两组二进制值所对应的base64索引字符(上面👆🏻给出的索引表)

011000 -> 24 -> base64索引字符为:Y 
010000 -> 16 -> base64索引字符为:Q 

1.6、因 YQ 长度非 4 的倍数,补 = 至 4 字符(base64用等号区分该字符是否用到了补位且输出的长度必须为4的倍数)

//最终输出结果为
YQ==

我们再用shell验证,结果如下图所示

推导过程完全正确,再次用表格整理流程如下所示

2、数字 2 转base64推导步骤如下

2.1、获取 2 的Unicode码点

2.2、数字 “2” 的码点是十六进制 32(十进制 50)

//转二进制后不足8位
110010

2.3、符合 UTF-8 单字节存储规则(0-127),对应的二进制为:

//左侧补0凑齐8位(只有左侧补0才不影响原二进制值大小)
00110010 

2.4、按6位一组拆分二进制数据、不足6位右侧补0到6位(base64拆分规则)

//拆分后如下所示
//第一组 001100 对应十进制值为 12
//第二组 100000 对应十进制值为 32
001100 
100000 

2.5、查询两组二进制值所对应的base64索引👆🏻

001100 -> 12 -> base64索引字符为:M 
100000 -> 32 -> base64索引字符为:g 

2.6、因 Mg 长度非 4 的倍数,补 = 至 4 字符(base64用等号区分该字符是否用到了补位且输出的长度必须为4的倍数)

//最终输出结果为
Mg==

3、单词 HR 转base64推导步骤如下

3.1、获取 HR 的Unicode码点。

3.2、HR 的码点分别是十六进制 48和52

//转二进制后不足8位
1001000  //字母 H 十六进制码点48转二进制 
1010010  //字母 R 十六进制码点52转二进制 

3.3、符合UTF-8 单字节存储规则(0x00-0x7F)范围,对应的二进制为:

//左侧补0凑齐8位(只有左侧补0才不影响原二进制值大小)
01001000  //字母 H 十六进制码点48转二进制 
01010010  //字母 R 十六进制码点52转二进制 

3.4、按6位一组拆分二进制数据、不足6位右侧补0到6位(base64拆分规则)

//3.4.1 将HR二进制按顺排列
01001000 01010010
//3.4.2 按照6位一拆分不足6位右侧补0 
010010  //对应十进制 18 
000101  //对应十进制 5
001000  //对应十进制 8

3.5、查询三组二进制值所对应的base64索引👆🏻

010010 -> 18 -> base64索引字符为:S 
000101 -> 5  -> base64索引字符为:F 
001000 -> 8  -> base64索引字符为:I 

3.6、因 SFI 长度非 4 的倍数,补 = 至 4 字符(base64用等号区分该字符是否用到了补位且输出的长度必须为4的倍数)

//最终输出结果为
SFI=

4、单词 NBA 转base64推导步骤如下

4.1、获取 NBA 的Unicode码点。

4.2、NBA 的码点分别是十六进制 4E、42和41

//转二进制后不足8位
1001110  //字母 N 
1000010  //字母 B 
1000001  //字母 A 

4.3、符合UTF-8 单字节存储规则(0x00-0x7F)范围,对应的二进制为:

//左侧补0凑齐8位(只有左侧补0才不影响原二进制值大小)
01001110  //字母 N 
01000010  //字母 B 
01000001  //字母 A 

4.4、按6位一组拆分二进制数据、不足6位右侧补0到6位(base64拆分规则)

//4.4.1 将NBA二进制按顺排列
01001110 01000010 01000001
//4.4.2 按照6位一拆分不足6位右侧补0 
010011  //对应十进制 19
100100  //对应十进制 36
001001  //对应十进制 9
000001  //对应十进制 1

4.5、查询四组二进制值所对应的base64索引👆🏻

010011 -> 19 -> base64索引字符为:T 
100100 -> 36 -> base64索引字符为:k 
001001 -> 9  -> base64索引字符为:J 
000001 -> 1  -> base64索引字符为:B 

5、汉字 “你们” 转base64推导步骤如下

5.1、获取 “你们” 的Unicode码点。

5.2、“你们” 的码点分别是十六进制 4F60 和 4EEC

//转二进制后不足16位2个字节
100111101100000  // 汉字 “你” 码点 4F60 二进制 
100111011101100  // 汉字 “们” 码点 4EEC 二进制 

5.3、符合_UTF-8 三字节存储规则(0x0800-0xFFFF)范围_

// 5.3.1 将字符的二进制长度补齐到2个字节16位 
//左侧补0凑齐16位(只有左侧补0才不影响原二进制值大小)
//根据utf8三字节存储规则字符的二进制长度必须凑够16位所以左侧补1个0
0100111101100000  // 汉字 “你” 
0100111011101100  // 汉字 “们” 
// 5.3.2 根据utf8三字节存储规则进行拆分 汉字 “你” 和 “们” 的二进制
// utf8三字节拆分规则 1110xxxx 10xxxxxx 10xxxxxx (这里涉及utf8三字节储存规则不熟悉的请看上一篇文章’字符编码轻松学:从 ASCII 到 Unicode,再到 UTF 家族 —— 搞懂它们的 “前世今生”‘)
111001001011110110100000  //用utf8三字节编码后 “你” 的二进制长度达到 24位 3 字节
111001001011101110101100  //用utf8三字节编码后 “们” 的二进制长度达到 24位 3 字节

5.4、按6位一组拆分二进制数据、不足6位右侧补0到6位(base64拆分规则)

//5.4.1 将汉字“你们”经过utf8三字节编码后的二进制按顺排列
111001001011110110100000 111001001011101110101100
//5.4.2 按照6位一拆分不足6位右侧补0 
111001  //对应十进制 57
001011  //对应十进制 11
110110  //对应十进制 54
100000  //对应十进制 32
 
111001  //对应十进制 57
001011  //对应十进制 11
101110  //对应十进制 46
101100  //对应十进制 44 

5.5、查询八组二进制值所对应的base64索引

111001 -> 57 -> base64索引字符为:5 
001011 -> 11 -> base64索引字符为:L 
110110 -> 54 -> base64索引字符为:2 
100000 -> 32 -> base64索引字符为:g 
 
111001 -> 57 -> base64索引字符为:5 
001011 -> 11 -> base64索引字符为:L 
101110 -> 46 -> base64索引字符为:u 
101100 -> 44 -> base64索引字符为:s 

        看到这里,大家应该能 get 到 Base64 的核心逻辑了 —— 它针对的是字符编码对应的二进制数据做 6 位分组处理。本文全程基于 UTF-8 编码展开,要注意的是,同一个汉字如果用 GBK 编码再转 Base64,得到的结果和 UTF-8 版本是不一样的。因此大家要留意因编码不同导致base64后不能相互解析问题。 

      上面的流程在精简总结下就是:

获取字符码点 -> UTF-8编码或其它编码 -> 6位一拆分 -> base64索引字符映射。 

      既然大家都看到这里了咱们在上一点难度推导最后一个案例。大家应该看到了我用的终端里面笑脸(😁)表情,怎样将它进行base64呢?

这个和上面的汉字不一样用到了_UTF-8四字节编码存储_。我先抛砖引玉快速推导下大家可以作为参考

printf 'U+%04X\n' \"😁\"
输出:U+1F601

U+1F601(十进制为 128513)超出了 UTF-8 三字节的存储范围(0x800-0xFFFF,十进制 2048~65535),因此需采用 UTF-8 四字节编码存储。

//😁码点U+1F601转二进制
11111011000000001


//utf8四字节存储规则
11110xxx 10xxxxxx 10xxxxxx 10xxxxxx


由utf8四字节存储规则可知有21个x空位需要补入
因此需要将 11111011000000001 补充到21位长度左侧补40 
结果:000011111011000000001


笑脸😁的Unicode码点用utf8四字节表示就是
111100 001001 111110 011000 100000 010000 
6位一拆分后索引与字符
111100 -> 60 -> 8
001001 -> 9  -> J
111110 -> 62 -> +
011000 -> 24 -> Y
100000 -> 32 -> g
010000 -> 16 -> Q

由于字符长度不足4的倍数因此补上2个等号结果为:8J+YgQ==

至此对base64剖析全篇结束。通过这篇文章的引导你是不是对base64有深入的认识了呢?不难看出,若要彻底吃透 Base64,充分掌握 UTF-8 编码 的相关知识是关键前提。

最后问大家一个问题base64为什么要6位一拆分呢?欢迎评论留言讨论。