在开发中,Base64编码会经常使用到,平时也就是使用,没有去真正了解过Base64的原理,今天开发的时候,使用key、value的方式保存Base64编码之后的字符串,文件中的形式为key=value,但是Base64字符串本身就有等于号(=),所以,我担心这会不会出问题啊?我使用的是Properties类来保存key/value的,我发现Properties会自动对保存的字符串中出现的等于号做转义,示例如下:
weOcndflNzFLAyseO/JcBA\=\==bjWJ6o9guMCPxAjgdpF2hA\=\=
\= 就是转义的,Properties读到这两个符号的时候,会把它当成一个普通字符,所以对于:\=\== ,这里前面的两个等于号是普通字符,第三个等于号就是key/value的分隔符了,Properties会把第三个等号前面的字符当作key,后面的字符当作value,所以不会有问题。
但是我突然又想到,万一Base64的字符串里面本身就包含有反斜杠呢?这会不会导致读取出错啊,于是乎这时需要我去了解一下Base64都有哪些字符。
在百度百科里面就有很详细的解释,Base64,顾名思义,就是任意的内容,都可以使用64个字符来进行编码表示,如下:
总结64个字符为:26个大写字母(A - Z)、26个小字字母(a - z)、10个数字(0 - 9)、两个字符(+ /),共64个字符。所以Base64的字符串中不会出现反斜杠等一些特殊符号,方便进行显示,且这64个符号都是可打印符号,显示肯定是没问题的。
看到这里,我在想,Base64怎么这么神奇呢?它是怎么做到64个字符就可以表示一切的呢?比如,一张图片可以用Base64表示,一段音频可以用Base64表示。。。所有的数据都可以编码为Base64,然后还可以还原回原来的内容,就是这么神奇。
当我了解了Base64的原理之后,感觉简单到不行,一点也不神奇了!其实计算机上所有的数据(文本、图片、音乐、视频等等)都是二进制,即0101010110这样的二进制数据,Base64就是把这些二进制进行编码而已,编码规则如下:
下面举例说明:
如上图,有3个字节,使用Base64对这3个字节进行编码,则Base64会把这3个字节化分成4个字节,如下:
为了美观,我们把转换后的Base64字节重新画图,如下:
Ok,这样就把3个字节(共24位)分成了4个字节,但是计算机里的一个字节是占8个位的,所以上图中的Base64字节还需要补上两个0,以填充满8个位,如下:
到这里,Base64最核心的编码规则就了解的差不多了,可以看到,原始字节是3个字节,使用Base64编码后变成了4个字节,所以,使用Base64编码,需要的存储空间比原来多33.33%(1 / 3 = 33.33%),即按原始字节保存是保存3个字节,Base64编码后需要保存4个字节,如下图:
前面我们说了,Base64会把3个字节转换为4个字节,所以Base64的最小长度为4个字节,也就是说Base64字符串的长度最短为4个字符。这时我就想了,那如果我想编码的数据只有1个字节呢(计算机存储的最小单位为字节)?很简单,1个原始字节可以编码为两个Base64字节:分为6位 + 2位(因为每6个位要拆分为1个新的字节),Base64最少要4个字节,那还差两个字节,所以使用等于号填充,示例如下:
把0b0000_0100编码为Base64,结果是怎样的呢?我们先用代码运行一下结果:
( 注:jdk1.7及以上版本可以使用0b开头表示二进制,且数值之间可以使用下划线分隔)
fun main() {
val bytes = byteArrayOf(0b0000_0100)
println(Base64.getEncoder().encodeToString(bytes))
}
输出结果如下:
BA==
0b0000_0100对应的十进制为4,4编码为Base64后结果为:BA==,接下来,我们就一步步分解,如下:
如上图,可以看到,1个原始字节分成了两个Base64字节,我们把Base64字节补齐8位(在高位补0),如下:
Ok,这样就把0000_0100编码为了两个Base64字节了,分别为:0000_0001、0000_0000,这两个字节对应的十进制值为1和0,然后我们到Base64符号表中查找,数字1对应的符号为B,数字0对应的符号为A。我们说Base64最少要4个字节,这里只有两个字节,所以需要使用两个等于号填充,所以最终显示结果为:BA==
上面是标准的Base64,了解了原理之后,其实我们自己也可以写一个类似的编码,即使用不同的符号来表示,实现起来并不难,所以市面上也出现了不同的变种,下面引用一下百度百科的内容:
标准的Base64并不适合直接放在URL里传输,因为URL编码器会把标准Base64中的“/”和“+”字符变为形如“%XX”的形式,而这些“%”号在存入数据库时还需要再进行转换,因为ANSI SQL中已将“%”号用作通配符。
为解决此问题,可采用一种用于URL的改进Base64编码,它在末尾填充’='号,并将标准Base64中的“+”和“/”分别改成了“-”和“”,这样就免去了在URL编解码和数据库存储时所要作的转换,避免了编码信息长度在此过程中的增加,并统一了数据库、表单等处对象标识符的格式。
另有一种用于正则表达式的改进Base64变种,它将“+”和“/”改成了“!”和“-”,因为“+”,“*”以及前面在IRCu中用到的“[”和“]”在正则表达式中都可能具有特殊含义。
此外还有一些变种,它们将“+/”改为“-”或“.”(用作编程语言中的标识符名称)或“.-”(用于XML中的Nmtoken)甚至“:”(用于XML中的Name)。
0 ~ 63,共64个数字,分别使用64个字符来显示,那我们就来验证一下。
前面我们知道,1个字节会被分成两个字节,差两个字节用等于号填充。
0000_0100编码为Base64,首先会把前6个位(0000_01)编码为1个字节,后两位(00)也编码为1个字节,这里我就不使用后两位了,我想办法让前6位的值从0 ~ 63变化(很简单,让前6位每次加1即可),然后看结果是否正好对应上Base64中的64个字符,这样的话,结果为:XA==,其中X是未知符号,A==是固定符号,因为后两位为0,0对应的Base64符号为A,差两字节所以补两个等于号,所以A==是已知符号,接下来我们就实现前6位二进制对应的符号变化,代码如下:
fun main() {
var number = -0b0000_0100
repeat(64) {
number += 0b0000_0100
val bytes = byteArrayOf(number.toByte())
val high6Bit = number ushr 2 // 取出高6位的值
// 索引小于10的在前面补0
val high6BitString = "${if (high6Bit < 10) "0" else ""}$high6Bit"
println("$high6BitString(${toBinaryString(high6Bit)}) : ${Base64.getEncoder().encodeToString(bytes)}")
}
}
/** 将指定的十进制数转换为对应的二进制表示形式 */
private fun toBinaryString(number: Int): String {
var binaryString = Integer.toBinaryString(number)
repeat(6 - binaryString.length) {
// 不够6位的在前面补0,我们知道传进来的数为0 ~ 63,6个比特位就可以表示这64个数字了。
binaryString = "0$binaryString"
}
return binaryString
}
为什么是加0b0000_0100,因为1是要加到前6位的位置,所以是加0b0000_0100,这样后两位永远是00,运行结果如下:
00(000000) : AA==
01(000001) : BA==
02(000010) : CA==
03(000011) : DA==
04(000100) : EA==
05(000101) : FA==
06(000110) : GA==
07(000111) : HA==
08(001000) : IA==
09(001001) : JA==
10(001010) : KA==
11(001011) : LA==
12(001100) : MA==
13(001101) : NA==
14(001110) : OA==
15(001111) : PA==
16(010000) : QA==
17(010001) : RA==
18(010010) : SA==
19(010011) : TA==
20(010100) : UA==
21(010101) : VA==
22(010110) : WA==
23(010111) : XA==
24(011000) : YA==
25(011001) : ZA==
26(011010) : aA==
27(011011) : bA==
28(011100) : cA==
29(011101) : dA==
30(011110) : eA==
31(011111) : fA==
32(100000) : gA==
33(100001) : hA==
34(100010) : iA==
35(100011) : jA==
36(100100) : kA==
37(100101) : lA==
38(100110) : mA==
39(100111) : nA==
40(101000) : oA==
41(101001) : pA==
42(101010) : qA==
43(101011) : rA==
44(101100) : sA==
45(101101) : tA==
46(101110) : uA==
47(101111) : vA==
48(110000) : wA==
49(110001) : xA==
50(110010) : yA==
51(110011) : zA==
52(110100) : 0A==
53(110101) : 1A==
54(110110) : 2A==
55(110111) : 3A==
56(111000) : 4A==
57(111001) : 5A==
58(111010) : 6A==
59(111011) : 7A==
60(111100) : 8A==
61(111101) : 9A==
62(111110) : +A==
63(111111) : /A==