脑洞大开去理解Base64

144 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情

前言

以前写过一篇关于Base64的分析, www.jianshu.com/p/1875a7ffe… ,这次打算从编码的角度,重新去看一次Base64,从中能get到一些编码的点。

Base64是什么

Base64是网络上最常见的用于传输8Bit字节码的编码方式之一,Base64就是一种基于64个可打印字符来表示二进制数据的方法。
简单来说就是把你的需要编码的串转换为 A-Z、a-z、0-9、+、/ 和 =
所有的数据都能用64个字符来表示,就像16进制能做到所有数据都能用16个字符来表示一样。

2e6ff6ba63519cf5329f166f7f9f5c8.png

进行Base64编码

这里具体来看看Base64是怎么做的。因为Base64是64个字符,所以可以使用6bit表示,就是2的6次方。

比如最大的二进制数 111111,以8bit来看比较直观,所以前两位补零00111111,转成10进制就是63,对应表就是 / ,所以最多只能有6位来表示,比如10111111 这样就没办法找对对应的索引。

那么进行Base64编码的方式就很简单了,假如有一个字符串,我们先将字符串转换为 2进制,这样的二进制一般是8位,再从左往右取6位,转换成10进制再对应索引表就行。
记住,3个字节一组,为什么3个字节一组,因为3x8=4x6

讲了这么多道理,可能有点绕,实践一些就知道了,比如下面的栗子(抄网上的)

字符串      a       b        c
ASCII      97      98       99
8bit   01100001 01100010 01100011
6bit   011000   010110   001001   100011
十进制      24      22        9        35
对应编码    Y        W        J        j

一看就知道了,都不用解释是吧,随便直接再看看补位图,如果分割之后剩余的不足,也就是上面说的3x8=4x6 , 没凑够3,那就会在后面补上 =

bf1716f09bff841f3913e22eb5f57a9.png

这个也不用解释吧, 3x8=4x6 我觉得这个等式就能说明一切。

特殊字符串进行编码

上面的例子看似没问题,但是会有个误导,那就是ASCII码,并不是一定要有转成ASCII码这一步,我们只需要最终转成二进制就行,不然比如emoji 比如汉字这些是没办法转成ASCII码的。

比如我现在要将 “卧槽” 进行Base64编码要怎么做?
(1)首先需要将“卧槽”转成2进制
将“卧槽”转成2进制,这又和编码方式有关,我们用的不是ASCII码,不同的编码转成的二进制不同,得到的Base64编码的结果也不同,这点需要注意
在UTF-8下:111001011000110110100111 111001101010011110111101
在Unicode下:00000000000000000101001101100111 00000000000000000110100111111101
PS:我们就以UTF-8为例。
(2)对2进制进行 3x8=4x6

按3个分组  11100101 10001101 10100111       11100110 10100111 10111101
6bit             111001 011000 110110 100111      111001 101010 011110 111101
十进制        57   24   54   39        57   42   30   61
对应编码    5Y2n    5qe9

我们验证一下,可以在随便在网上找个在线编码的

3b228bde07f242c133a21913f87c460.png

好,这就对上了,如果没对上了也没关系,因为我们上面有说过,不同的编码得到的结果不同,说不定你用的是UTF-8,但是在线编码的网站用的是Unicode
既然汉字能解决了,那设么emoji啊,其他国家的语言啊,自然不在话下

Base64解码

既然有编码,那自然有解码。 先看我用java写的一个Demo(binary是转二进制的方法):

        String baseStr = "卧槽";
        byte[] baseBytes = baseStr.getBytes();
        Log.v("mmp","编码前的二进制:"+binary(baseBytes, 2));
        String encodedString = Base64.getEncoder().encodeToString(baseBytes);
        Log.v("mmp","编码之后的结果:"+encodedString);

        byte[] decodedBytes = Base64.getDecoder().decode(encodedString);
        Log.v("mmp","解码前的二进制:"+binary(decodedBytes, 2));
        String decodedString = new String(decodedBytes);
        Log.v("mmp","解码之后的结果:"+decodedString);

655aee0b1eb3ef5deb5b9b448d84696.png

可以看出解码就是一个逆向的过程,使用原本的二进制进行编码,对这个Base64串解码之后会得到原本的二进制。而解码的过程也没什么好说的了,就是上面编码的过程逆过来就行。

注意

需要注意的是编码之后的 + / = 3个符号都是比较常用的符号,所以在传递数据时为了防止出错,可以将这3个符号替换成其它的符号。

反推Base64的设计

什么时候需要用到Base64呢?可能加密的时候是用到最多的,当你进行加密操作之后,通俗的讲就是会得到一串二进制数组。想想这时候你要怎么传输,直接传二进制数组?还是转成UTF-8去传?当然不是,这时候就是用Base64去传(当然写到文件传文件也行)。

那么你这样想一想,你不做Base64,自己内部定义一套编码方式行不行?

表面一看。当然可以,Base64是用6bit来划分,你说6bit划分最终让结果很长,我不愿意,而且我不想让别人一看就知道是用了Base64,(当然一般不会有这种奇怪的操作)。你说你想用7bit来划分,然后你就要定义出一个表。

6bit的Base64需要64个字符代替,也就是2的6次方。所以7bit需要的字符就是2的7次方,也就是128。现在你想一想,能不能做?做不了,为什么,你去哪有偷128个字符,你自己看看你的键盘,你觉得能弄出来吗?

我举这个例子就是为了说明,当时设计的时候,为什么要设计成Basa64而不是Base128。 那反着想,7bit划分不行,那我按5bit划分行不行?
其实还真行,5bit划分就是32,你只需要找出32个符号来代替就行。 但是,就是我上面也有说过,缺点就是生成的字符串比Base64更长,所以为什么设计成64就是这样。当然,如果确实能找出128个基础符号,那么128当然比64好

再想想,脑洞再大开一点,找不出128个符号真就实现不了128吗?我觉得是可以的,只需要多找出一个,没错,我可以用两个维度去表示,当我把一个维度变成两个维度之后,我就能用65去表示128。也就是说我能做到按7bit来划分的Base65。

比如我用一个问号"?"来表示变维度,假如A表示0,那?A就表示65。当然这不会这么简单,比如说多一个维度编解码的时候就更耗时,而且还要考虑补位问题等等。

但我做过一个有意思的操作,同样是按6bit来划分的64,我把表打乱,我这不叫Base64,我比如叫Kylin64,也是64个符号,只不过不是Base64的对照表。然后有人抓包捕获你数据,然后他自信的一看,这数据格式明显就是Base64,我想想他拿数据去解解不出的样子,想想就很开心。当然一般也不会这么做,数据安全的话用加密也就可以了,这只不过是可以当成一个玩笑。