计算机基础

如何用通俗易懂的语言解释 BASE64?

这么说吧:在对计算机底层有一定了解的人眼里,计算机内部其实分表里两层世界。


“里世界” 是更本质的,也是CPU以及偏中底层的各种编程语言直接打交道的那个世界。

正因其本质,因此绝大多数的编程语言、大多数的通讯协议,全程运行于里世界;只有需要向用户展示什么时,才会临时做个转换——这就是为什么如果你学C,可能很久很久都沉浸在黑框框里出不来的原因:表世界太表面了,不值得花太多精力在它上面。

在“里世界”,一切数据都是一串串的二进制字节流

比如你敲个大写字母’A’,它在里世界其实是数字65;敲个小写字母’a’,里世界对应的数字是97……

这个对应关系是ASCII码表规定的:

事实上,里世界也没有什么65、97之类,它只有二进制。你敲6,它在里世界其实是十进制数字54或者十六进制数字0x36对应的那个二进制数字。

甚至,你敲11001011,这下总是二进制了吧?

并不是。’1’在里世界是49,’0’在里世界是48。所以你敲的其实是4949484849484949!

哦,里世界,一组数字的最小单位是字节。所以49其实在里世界的表示是”00110001″ ——在电路上,就是一组8根导线,电压分别是“低低高高低低低高”。

总之,里世界只有高高低低的电压;我们把一组八个电压称为一个字节;它的内容究竟是什么,需要通过ASCII码表转换,然后才能正确解释。


实际上,我们的转换表有无数种。只有通过ASCII编码输入的数据,才可以通过ASCII表转换回表世界表示。

比如说,我们熟悉的、C/C++以及Java等诸多语言,它们都有个对初学者来说非常“可怕”的东西,就是变量类型——使用一个变量之前,你需要先声明它的类型;后续引用这个变量时,你还得保证它和初始声明的变量类型一致。

这个变量类型,实质上就是“转换表类型”。

比如,我声明变量A是short int,意思是我在表世界写出来的虽然是一串ASCII码表示的数字,但实际存储到里世界时,我希望把它当成二进制数字解释,存到两个字节(16位二进制)里面!

同样的,声明变量B是string,意思是把我写的一串字母按ASCII码规则翻译,每个字母翻译到一个字节的ASCII码机内二进制表示上去。

总之,通过“变量类型声明”,我就为自己在“表世界”敲入的一串字符指定了一个“里世界”存储、运算规则,从而确保计算机按我的意图处理信息。

较新的很多脚本语言可以自动分析程序员在表世界敲入的东西“更可能是什么”,从而为它自动选择一个合适的表示——如果这个分析不正确,那么接下来的某个运算规则可能就无法成立;此时又要自动转换内部表示,使得规则成立。

举例来说,我在python里面声明了一个变量x,指定初始值为“13512342234”;解释器很聪明,觉得这应该是一个数字,于是自动替我转换成二进制表示,存储到8个字节里面。

没想到我其实是存了一串电话号码。所以后面我就做了个字符串加法:

“我是流落在外的尼日利亚王子,如果你能给我100块钱,让我买张车票回家,继承王位后必有厚报。有意者请拨打电话:”+ x

字符串参与的+实际上是字符串拼接;结果x存成二进制数字了,没法拼接。所以python手忙脚乱的又把8个字节的二进制数字转换回来,转成了11个字节的ASCII字符串“13512342234”,然后才能正确拼接。

类似的,一副图片,也可以按像素的顺序,在里世界以每个像素RGB三个字节的格式存储。这就是BMP图像——再给它加个文件头,知道一行多少个像素,就可以在“里世界表示”和“表世界表示”之间来回转换了。

同样的,一首歌曲,采样率48KHZ,双声道,也可以按采样顺序把每次采样的电压值两个两个字节的顺序存储在里世界。将来重放时,把这个数字序列按采样间隔送到DAC转换回模拟波形、再驱动音箱/耳机发声,你在表世界就听到优美的旋律了。

总之,你在“表世界”看到的、听到的一切一切,在里世界都是一串数字。

为了让你在不知道“里世界”存在的前提下,仍然能操控电脑、工作学习和娱乐,程序需要时刻准备着,帮你把里世界的数字表示转换成表世界的文本、图片、音乐,甚至3D打印机喷头的移动、车床夹具/刀具的精确定位……


有了这个基础,我们终于可以谈谈base64了。

谈base64之前,我们得先谈谈HTTP。

在程序员眼中,HTTP是一个“奇葩”。

它在本质上,其实是一个“完全用表世界表示”传输各种信息的协议,也就是所谓的“文本协议”;但与之同时,它的功能越发强大、涉及领域越来越广,也就越来越需要“沉”进里世界才能完成任务——但它偏偏是一个完全的表世界协议。

比如,如前所述,对计算机来说,文本数字需要先转换“里世界表示”才能解读其意义——123456只是ASCII码49 50 51 52 53 54而已,要正确读出它的含义“十二万三千四百五十六”,就要先把49转换成里世界的1,然后1X10,加上50对应的里世界2,整体再乘以10……

也就是:1X10^5+2X10^4+3X10^3+4X10^2+5X10+6

通过这么一串复杂的转换和计算,ASCII字符串123456的含义才能正确解读。

更可怕的,比如图片、音乐等信息,它们本应是里世界的一大堆数字;现在却不得不表示成表世界的ASCII文本——然后,文本再翻译回里世界的数字表示,这才能正确解释其含义;最后,再把里世界的二进制表示转换成表世界的图片、音乐……

换句话说,HTTP是一个完全在表世界运行的文本协议;但它却又无时无刻不在和里世界打交道!

这就带来了海量的反复转换,效率很低。

这没办法改变,为了方便人阅读排错嘛,自然不可能不付出代价。

但是,把图片、音乐等信息转换成十进制、十六进制,这实在是太蠢了。

这是因为,每位十进制/十六进制数字,编码成ASCII码后都要占据8个二进制位;而8个二进制位要编码成十进制/十六进制数字呢,起码要三个十进制数字或者两个十六进制数字!

换句话说,数据量膨胀了两三倍!如果说“在表世界表示和里世界表示之间反复转换”只是累了用户的电脑/手机,没什么大不了的——反正它们性能膨胀,闲着也是闲着——那么,一幅图片通过HTTP协议传输,10M的数据成了30M、10G的数据成了30G……这就不好玩了:无论是网站运营商的服务器还是死贵死贵的商业线路,这都是个太大的压力。

此外,很多数据,不仅仅是图片、视频和音乐,它们同样是二进制的,不能直接当文本数据传输——否则会被当做ASCII码控制字符解释的,一不小心就把http协议本身干扰了。

比如,http用“回车”来分割不同字段(于是一行就是一个数据);而如果你的AutoCAD文档里面恰好有个13,这个13就会被解释为“回车”——于是,浏览器觉得,AutoCADFile: XXXX\n CADFileContent:XXXXXXX 这几个字段的设置已经结束了,剩下是另一回事……哎呀,格式怎么乱成这样了?

总之,为了避免干扰http协议、同时尽可能提高“里世界表示”到“表世界表示”的编码效率,我们需要设计一套新的编码方案。这就是base64编码。

base64编码不使用ASCII控制字符以及经常在http/html协议里使用的<、>等字符,这就避免了干扰各种文本协议;同时,它的每位符号尽可能的多,顾名思义,base64嘛,以64个可打印字符弄出的64进制字符串,于是每个ASCII码字符携带的信息量就尽可能的增加了,编码后的数据量膨胀就不会太过可怕了。

总之,经过种种努力,在“表世界文本”中携带“里世界数据”终于不再成为难题。

事实上,有些数据,比如URL,它虽然不是“里世界数据”;但因为格式太过随意,也可能破坏页面内容。怎么办呢?简单,也用base64把它“装箱”,就不怕它像里世界数据那样污染表世界了。

反正浏览器能自动识别URL中的base64编码,可以自动把它从base64箱子里取出来,不耽误使用。

但是,请注意,base64和加密无关。它仅仅是“以表世界文本表示里世界数据”的一套编码协议而已;虽然编码后的东西你看不懂,但本质上仍然是明文,任何人都可以通过一个逆变换轻易还原它。


BASE64就像是一种翻译工具,可以将一些电脑懂但人看不懂的二进制数据,翻译成人类可读的文本格式。

举个例子,假如你要在网上买东西,需要填写信用卡号,但是为了保护你的信用卡信息不被黑客窃取,网站通常会使用BASE64编码,将你的信用卡号转换成一串文本格式的字符串。这样,你输入的信息就不是明文传输,即使被截获也无法轻易识别出你的信用卡号。

再比如你需要通过电子邮件发送一张照片给你的朋友,但是电子邮件只能传输文本信息,不能直接传输图片。那么你可以将这张图片用BASE64编码方式转换成文本格式,然后将这段文本复制到邮件正文中发送给你的朋友。你的朋友收到邮件后,可以将这段文本用BASE64解码,还原出原始的二进制数据,就可以得到你发送的图片了。这样,你就可以方便地通过电子邮件发送图片了。