二进制的数据, 每个字符的取值范围都是 [0, 255] , 作为 ascii 码解析时, 只有部分可打印.
比如, 我用文本编辑器 VIM 打开一张 jpeg 图片, 会发现内容是乱码. 以下是头两行数据:
- ÿØÿà^@^PJFIF^@^A^A^@^@^A^@^A^@^@ÿÛ^@C^@^F^D^E^F^E^D^F^F^E^F^G^G^F^H
- ^P
我在 VIM 下输入 :%!xxd , 可以查看二进制数据对应的十六进值. 以下是头两行数据:
- 00000000: ffd8 ffe0 0010 4a46 4946 0001 0100 0001 ......JFIF......
- 00000010: 0001 0000 ffdb 0043 0006 0405 0605 0406 .......C........
注意, 下面的两行和上面的两行内容长度不一定是相同的. 上面的数据是遇到 ascii 码为换行时换行, 下面的数据是每 16 个字节换行.
比较上下两份数据, 可以看到 JFIF 在上面也打印了, 在下面的右半部分也打印了. 说明他们的数据源确实是同一份, 只是展示方式不同.
显然, 如果我想肉眼看这份二进制数据, 或者说作为文本拷贝这份数据, 16 进制的格式要优于二进制 ascii 码格式.
但是, 16 进制表示法, 需要两个字节才能表示表示原始数据的一个字节. 比如 4a464946 表示 JFIF . 即大小增加了一倍.
有点大? 于是, 有人发明了 base64 算法. 它保持了编码后可打印特性的同时, 大小只增加 1/3 .
base64 算法原理
base64 将原始二进制数据每三个字节 (也即 24 位) 看成一个单元, 然后将 24 位按每 6 位进行一次切割, 切割成 4 个字节. 切割后每个字节的范围是 [0, 63] .
由于 [0, 63] 的 ascii 码也并不都是可打印的, 于是将 [0, 63] 再一一对应换算成一个可打印的字符.
标准文档 RFC 4648 对该映射关系定义如下:
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
也即 0 对应 A,1 对应 B,63 对应 / .
换算之后, 三个字节就变成可打印的四个字节的内容了.
解码方的逻辑, 先将每个可打印的字符按刚才的映射表反算出对应的值. 然后每 4 个字节看成一个单位, 按位运算还原出原始的 3 个字节. 比如: 解码前第 1 个字节的低 6 位作为解码后第 1 个字节的高 6 位, 解码前第 2 个字节的低 6 位的前 2 位作为解码后第 1 个字节的低 2 位, 这样就得到了解码后的第 1 个字节. 后续依次按顺序推算.
还有一个问题, 原始数据长度如果不是 3 的倍数怎么办?
那么无非是两种情况, 一种是最后剩 1 个字节, 另一种是剩两个字节:
== =
即 base64 保证了编码后的字符串长度为 4 的倍数.
作为 url 参数时有什么问题
有的人会把二进制数据用 base64 编码后, 放入 url 的参数中, 这么做有一个问题, base64 编码后可能会出现 +/= 三个字符, 而这三个字符会影响到整个 url 串的解析.
举个例子, url 串 https://pengrl.com/all?key=value , 如果将其中的 value 设置成 1/2= , 则 url 串变成 https://pengrl.com/all?key=1/2= . 其中的 / 和 = 是不是有点傻傻分不清楚呢?
那么如何解决呢?
编码后的 + 和 / 两个符号来源于上面给出的那张映射表. 于是标准文档 RFC 4648 中给出另一张映射表, 将其中的 + 替换成了中划线 - , / 替换成了下划线 _ .
这里额外说一句, base64 并不适合用来做文本加密, 因为算法是公开的, 并且它只是一种简单的查表映射, 有经验的 web 开发者, 甚至看到末尾的 = 都能猜到是 base64 编码. 即使编码和解码都使用自定义的映射表, 根据文本规律, 也很容易破解出映射表.
剩下 = , 上面也说过, 是由于原始数据长度不是 3 的倍数填充得到的, 解决方法也很简单, 编码时不填充, 解码时剩余的不够 4 字节的数据按顺序解就好.
上面的两种做法, 都需要保证, 编码端和解码端是能对上号的.
base64 主要还是对二进制做可打印编码, 如果是处理 url 参数, 最好还是使用 urlencode.urlencode 是啥, 下回再聊.
来源: http://www.tuicool.com/articles/ae2y6zE