初开 阅读(45) 评论(0)

一、概览

在本章节,我们将从编码的概念开始,谈谈常见的一些编码类型,再接着谈谈信息论中两个最重要的编码过程-压缩和加密,分析它们在网站中的应用,最后深入理解在分布式系统中的编码过程-序列化。

对信息论有足够理解的朋友可以快速略过前4节,阅读后面的序列化及思考部分。

二、什么是编码

让我们回到数百年前的古代,在那个时候可没有电话,如果有人想给远方的朋友传个信,那就只能修书一封,飞鸽传书了。

我们分析下这个过程中,某人将想说的话(语言),写到(存储到)纸上(文字),然后飞鸽传书(传输)。古代识字率比较低,万一朋友不识字,还得请隔壁的教书先生念信,将文字表述为语言。

在这个场景中,我们看到了一句话经历了语言到文字,然后文字再到语言的转化,这个过程在信息论中被称为编码。因此,广义上的编码是信息在不同形式下的表示和转换的过程。

编码有两个重要的组成部分,一是多种表现形式,比如语言和文字,二是不同编码形式间的对应关系,比如“编码”读作“[biān mǎ]”,写作“编码”。而编码的主要应用场景是传输和存储。特别的,逆向的编码过程我们称为解码。

当然,编码本身是一个大主题,我们今天谈到的编码,将是狭义上的编码,指计算机中,特别是软件开发中,不同数据格式的表示和转化过程。

三、常见的编码

(一) 数制与二进制

现代计算机通常是基于二进制的,用0和1表状态,二进制属于进位计数制一种,另一种十六进制也在计算机中经常使用,而生活中常用的是十进制。

之所以使用二进制,是因为电子计算机利用的是通电、断电(或高电平、低电平)两种状态。这两种状态可以表示为1/0,正好与二进制相对应,一个状态表示一个二进制位,单位称为bit,然后便可以编码表示任一数字,比如7=111。

再进一步,如果让第一位表示符号,即正负,那么负数就可以编码了;如果约定前n位表整数位,后m位表小数位,这就是浮点数了。当然在实际中,负数我们是通过补码的方式表示,浮点也有单精度和双精度之分,本文不做进一步阐述。

另一方面,0、1也与布尔代数理论中的两种逻辑值“True”、“False”对应,再结合逻辑电路,这就为计算机提供了逻辑基础。

Niklaus Wirth写过一本书《算法 + 数据结构 = 程序》,那么在这里,逻辑运算+数值编码=现代计算机

(二) 字符编码

光有数字的编码是不够的,在过去的几千年,人类留下来的历史的信息大多是靠文字记录,数字从0到9不过10个,而汉字则以十万计,还不包括一些繁体和变体,其他语言文字就更别说了。因此,在计算机中,我们也需要一种特定的编码规则来表示文字。

1.ASCII码

ASCII码是英语字符与二进制位的对应的编码机制,采用8个二进制位来表示一个字符(8bit),8bit可以表示从0000000到11111111的数字,共256位。而英文大小写共52个,再加上数字字符,空格,换行的特殊符号,8bit完全足够了。而实际上ASCII码只规定了128个字符的编码,只用了8bit的后7位,第一位统一为0。这就有了另一个单位字节(byte),即1byte占8bit。

2.非ASCII码

随着计算机的推广,其他国家有自己的语言和文字,它们也需要在计算机中表示,ASCII只用了后7位,所以最开始一些欧洲的语言比如法语,通过扩展ASCII码到8位来表示对应语言的字符,这个字符集便是ISO-8859-1。

ISO-8859-1容量还是256位,其他语言的文字,比如数以万计的汉字就无法加入了,因此1981年中国推出了汉字字符编码GB2312,为了兼容ASCII,英文字母还是用1byte表示,汉字则用两个字节表示。

既然汉字有对应编码了,还有日,韩等诸多文字也得编码。再者,如果我们用汉字写邮件发往英文地区,也会有编码不同无法识别的问题,这些就需要一个统一的解决方案了。

3.Unicode字符集

不同国家使用不同编码自然就会导致信息沟通的困难,我们自然就想到了需要一个统一的标准来编码这个世界上所有的字符,于是Unicode就诞生了。

Unicode之所被称为字符集,因为它并不是一种编码规则,仅仅只规定了字符与数值的对应关系-码点(Code Point),但没有规定具体实现,比如用几个字节存储。

常见的实现有如下几种:

  • UTF-8,可变长度编码,对ASCII码在内的字符使用单字节,对汉字等字符用3字节。
  • UTF-16,统一用2字节编码。
  • UTF-32,统一用4字节编码。

一般来说,国外的网站普遍用ISO-8859-1,国内通常使用GBK,面向国际的网站通常基于UTF-8。

编码的兼容性

本小节我们谈到了多种字符编码,很多编码都只适用于特定语言,最后UNICODE的出现才统一编码标准,这里就体现了编码需要考虑的第一个方面-兼容性,在制定编码时需要评估跨语言,跨系统的场景。

(三)文件编码

在计算机中普遍数据的存储方式是通过文件存放的,所以,各种格式的文件本身就是一种编码方式,一般来说我们可以通过后缀名来区分,比如.txt,.doc,*.gif等等,当然后缀是可以被篡改,严格一点可以根据文件头信息来分别。

常见的文件类型比如:

文本文件(*.txt)

文本文件都是以2.2节谈到字符编码方式存储的,在Windows下,用记事本打开txt文件,点击文件->另存为,可以看到可选的编码方式,默认是ANSI(这是一种Windows用来兼容不同语言的编码方式),也可以改为Unicode等等。

音频文件(*.mp3)

在生活中,我们通常用MP3来指代音频文件,实际上MP3只是音频文件的一种编码方式,常见的音频编码类型有MP3、WMA、OGG…,它们在音质,存储,兼容性等方面各有所长。

其他还有视频文件,压缩文件等等,在计算机中,每一种文件的格式都是一种编码格式。

四、压缩

(一)游程编码

考虑到这么一段大写的数据AAABBB,长度为6。

如果我们定义一种编码机制:

AAA=A3
BBB=B3

那么上面的数据可以表示为A3B3,长度仅为4。同样的编码,再给这一段信息D3C3,你也能轻松转义成DDDCCC。我们可以看到后一种表示方式的长度更小。这种编码方式叫游程编码。

将数据通过特定的编码方式减少数据包大小的机制我们称之为压缩,编码机制即指压缩算法,而上面的AAA=A3这一对应关系,称之为字典,它的解码过程自然是解压了。

在信息论中,压缩被称为信源编码,这是一种对输入信息进行编码,优化信息和压缩信息的编码方式。

我们对比下编码与压缩:

(二) Huffman算法

我们再定义一个字典,用01来编码字符串。

AA=0
BB=1
CC=01

那么就有如下编码:

AACCCC=00101

如果我们换一种编码方式:

AA=0
BB=01
CC=1

那么就有

AACCCC=011

因为CC出现了两次,如果我们用较短的编码来表示出现频率高的字符,就可以得到更好的压缩比(压缩前后数据大小的比例)。

那么,如何计算出这样一个字典?这就有了非常经典的压缩算法-霍夫曼编码。

Huffman算法是一种依据字符出现频率来计算最优压缩字典的算法。

算法摘要如下

  1. 将信源符号按照概率大小从大到小排列;
  2. 把概率最小的两个信源符号分成一组,其中,上面一个编码为0,下面一个编码为1,并将这两个符号的概率加起来,其结果再与尚未处理过的符号重新按照大小排序;
  3. 重复步骤2,直到所有的信源符号都处理完毕;
  4. 从右至左按照编码路径返回,即可得到各个码字。

比如以下频率

编码可表示为

其他压缩算法还有LZ77,以及基于类似原理的一些变种算法,本文不再阐述。

在基于这些压缩算法的常见程序有gzip,winrar,7z等等,我们将在3.3节介绍压缩的应用场景。

(三) 有损压缩

如下是两张图片,都是通过一张图处理得来的,它们的色彩,清晰度可有差异?

肉眼是很难看出,实际上右图的像素比左图少5%,并通过放大到同等大小的。

上一节谈到的Huffman算法是一种无损压缩算法,也就是说,压缩后的数据可以通过逆向过程,解压还原出原数据的。

而像图片这种允许一部分数据丢失但不影响效果的场景,我们就可以用有损压缩的算法降低文件大小。

有损压缩是利用了人类视觉,听觉等方面不敏感的特性,允许压缩过程中损失一定的信息;虽然不能完全恢复原数据,但可以获得更大的压缩比。因此,常见的有损压缩往往应用在图片,音频,视频等文件编码上。

(四)压缩的应用

最后,我们谈谈压缩的一些应用场景。

谈编码的概念时,介绍了编码是用在传输和存储中的,我们就从这两个角度谈谈压缩的应用。

1.传输

在前端部分,诸如Nginx,Tomcat等Web服务器都可以配置gzip压缩,浏览器在发送请求时通过添加Accept-Encoding: gzip;请求头,从而实现各类资源的压缩传输。

文件压缩也是常用场景,我们曾遇到过由于图片没有压缩,导致一个网页需要传输上百M数据的问题,严重影响用户体验。

在后端的各类分布式系统中,通常会根据特定的数据格式自定义压缩规则,我们会在第5节谈谈序列化的例子。

2.存储

在数据库和日志系统中都提供了压缩功能,比如MySQL就提供了压缩表选项,可以在建表时进行配置。腾讯的TMySQL和阿里的AliSQL等还提供针对列压缩的特性,为blob/text等大字段进行灵活的压缩配置。

在分布式文件系统中,文件的压缩应用就广泛了,像用户上传的图片,很多网站都会进行统一的格式转换,针对不同的场景,压缩到不同的分辨率,比如缩略图就是通过高压缩比的有损压缩而来。特别的,HDFS(Hadoop Distributed File System)还提供了合并压缩存储的方案,我们会在本书第二部分,分布式文件系统一文中谈谈这个话题。

编码的效率

我们知道,衡量算法复杂度分为时间复杂度和空间复杂度,其实说的是算法的效率从空间和时间两个方面来衡量。而压缩正是提高了编码的空间效率,因此,效率是编码需要考虑的第二个方面。

五、加密

回到本文开头谈到了寄信场景,假设我们寄的不是家书,而是一名将军要下达给部队的命令函,那这封信就不能白纸黑字的写清楚了,万一信使被抓就泄密了,我们需要做一些加密的手段。

(一) 密码本

密码本是最古老的保密方式之一,在北宋的军事著作《武经总要》记载着一种名为“字验”的加密手段。

他们收集了军队中常用的40种战斗情况,如请弓、请粮料、都将病等等,然后战前约定一首五言律诗编码,五言律诗共8行,每行5字,正好40字,每个字对应一种战斗情况。这样传令时只需传一个字就可以完成通讯了。

如王维的使至塞上:

单车欲问边,属国过居延。
征蓬出汉塞,归雁入胡天。
大漠孤烟直,长河落日圆。
萧关逢候骑,都护在燕然。

(二) 对称加密

回到计算机中,我们为文本消息定义一种编码,称为字符x+5,每个字符用它后面的第5个字符代替,如:

A->F
B->G

通过这种方式,只要x+5这个规则不被破解,就起到了加密作用,而接收者只需要知道x+5的值,就可以解码出x的值。由于加密和解密都用到了同一个密钥,所以这个方式我们称为对称加密

当然,实际应用中的加密算法比这个复杂的多,通常密钥长度在几十到数百位不等。常见的对称加密算法有DES、IDEA、RC等等。

我们对比下编码与加密:

(三)非对称加密

由于对称加密只有一个密钥,一旦公开所有人都可以解密信息,在多方传输中存在安全风险。因此也就有了非对称加密,这种方式将加密密钥(公钥)和解密密钥(私钥)分开,任何发信人都可以用公钥发送加密信息,但只有收信人有私钥解密。

网上有这么一段形象的解释:

非对称加密算法就是别人想要发信息给你。你先造一个保险箱。保险箱关上是不用钥匙的。把这个保险箱开着递给别人。别人发信息就把信息放进去然后“砰”一下关上还给你。只有你有钥匙信息可以查看信息,除非别人有其它方式破解。

我们来简单描述下非对称加密算法RSA的编解码过程。

RSA基于这样一个事实,将两个大素数相乘是很容易,但从结果中分解出它们则非常耗时。选取大素数可长达数千位,当然这样的素数选取出来也不容易,通常是用Miller Rabin等素数测试算法来检测素数,会存在一定的误判。

  • 1.随机选取相同的两个大素数相乘,我们这里用A=37,B=23两个小素数代替。

N=A×B
T=(A-1)×(B-1)

N=37×23=851
T=(37-1)×(23-1)=36×22=792
  • 2.选取第三个随机数(E)作为公钥,它必须与T互质,则私钥(D)满足

    D×E mod T = 1

如选E(公钥)=5

D×5 mod 792=1
D(私钥)=317
  • 3.把明文(M)加密为密文(C):

    C=M^E mod N

  • 4.解密为

    M=C^D mod N

如 M(明文)=7

C(密文)=7^5 mod 851 = 638
M(明文)=638^317 mod 851 =7

(四) 安全Hash

Hash函数是一种将任意长度的消息压缩到某一固定长度的消息摘要的函数,可以充当原数据的映射,用于快速查找和校验。

一般情况下由于输出Hash值的空间通常远小于输入的空间,不同的输入可能会计算出相同的Hash值,所以无法从Hash来唯一的确定输入值,会出现值冲突,难以求逆。但是,通过精心设计的Hash算法,如MD5SHA,其值域极大,在现有计算机条件实现了无冲突,同时求逆也更加不可能。因此,对于一些无需解密的数据,这些安全Hash算法就能用来充当加密工具。

1.彩虹表

虽然Hash本身不可逆,但并非不可破解,如字符串123456,我们用MD5算法计算出它的Hash值为7e8feb2276322ecddd4423b649dfd4d9,这样,只要看到是这段hash值的地方,我们就可以认为这段数据是123456。这就有了一种暴力破解方式-彩虹表。

彩虹表是预先将数以百亿级的各种字符串,特别是有规律的字符串的Hash计算出来,整个表的大小通常在数百G以上,然后通过碰撞的方式破解Hash值。

2.加盐

既然一般字符串的Hash表是可以被计算的,我们可以通过往输入项里面加一段信息,我们称之为盐(salt),比如123456改为salt123456,然后再计算Hash值。之后每次查询的时候,都在输入项前面加上salt,然后再查询,只要不被人知道盐值,用普通的彩虹表是无法破解信息的。

3.慢Hash

前面谈到各种加密方式并非无法破解,只是以现有计算机的运算能可能需要十几甚至上百年的时间。这就有了密码学领域的另一个通识,就是加密方式并非要做到绝对无法破解,很多信息是有时效性的,只需要在一定年限内无法破解,过期之后的价值就不大了,这也是很多国家机密会设置保密期的原因。

因此,就有了另一种对抗破解的方式-慢Hash,慢哈希是指执行这个哈希函数非常慢,这样暴力破解需要枚举遍历所有可能结果时,就需要花上非常非常长的时间。由于慢Hash耗计算量,我们往往会把这些计算量转嫁到客户端,让客户端计算完Hash传给服务端校验。

(五)加密的应用

我们也从传输和存储方面谈谈加密的应用。

1. 传输

常见的加密传输有HTTPS,SSH等协议,它们往往是非对称加密与对称加密算法相结合制定的,我们会在第二章通信协议中进一步谈谈HTTPS的内容。

2. 存储

存储方面最重要是密码存储了,通常基于安全Hash,附加加盐、慢hash等手段进行复杂的加密。

编码的安全性

在这一节,我们谈到了多种加密方式,这些方式都是为了保证数据在存储和传输过程中的安全,那么加密自然就是体现了编码的安全性

六、序列化的本质

在现在的分布式系统中,特别是以微服务为代表的分布式服务技术,序列化是其中的关键技术点之一。

在深入理解序列化之前,我们先来准确描述下它的概念。

序列化是将数据结构或对象转换为可存储(如在文件或内存缓冲区)或传输(如通过网络)的格式并稍后重建(可能在不同的计算机中)的过程。

数据结构和对象这个不用解释,但可存储或传输的格式最常见的自然是字节了,除此之外还有基于字符串的JSON。
比如,有这么一个对象

class Person {
    String name="Jim";
}

可以用JSON表示为{ name:"Jim"}。因此,一些JSON框架,比如Java中的FastJson,也可以归类到序列化框架之中。至于重建的过程便是反序列化了。

我们对比下编码的概念:编码是信息在不同形式下的表示和转换的过程。在这里,数据结构、对象、字节、json是不是信息的表示方式?反序列化是不是对应解码?序列化的过程是不是一个编码过程?

理解这一点,我们就可以回答出,序列化的本质是编码。

既然这样,我们就可以从编码的角度来分析序列化了。前面的文章,我们从字符集中谈到编码的兼容性,从压缩中谈到编码的效率,从加密中谈到编码的安全性,那边这里我们就从这三个方面谈谈序列化。

(一) 序列化的兼容性

序列化的兼容性有这么三个方面。

1. 语言及平台的兼容性

开发语言有JAVA,C++等等很多种,如果涉及到多语言应用之间的交互,那么序列化框架必须有多种语言的实现版本。当然,如果一个公司的内部应用全是用同一语言开发的,自然就无需引入这种兼容性。

平台方面,比如浏览器和服务器这两个平台,常见JSON格式就能很好的兼容二者,所以我们往往提供基于JSON的API。

2. 复杂对象兼容性

以JAVA为例,JAVA的复杂对象场景包括继承、组合、泛型、循环依赖等等。

以循环依赖为例,循环依赖是指两个以上对象相互引用,如

class A {
    B b;
}
class B {
    A a;
}

一旦它们相互引用,一些序列化框架在序列化时,序列化A发现里面包含B,然后又序列化B,发现B又包含A,又序列化A…,这样就出现死循环了。

通常在一些JSON序列化框架中会遇到这种问题,JSON格式本身也难以体现循环依赖关系。

现实场景往往更复杂,我们就遇到过一个接口包含数百个对象,上千字段,各种继承、泛型,在测试时,四五种序列化框架,仅一种能应对这种复杂场景。

着重提一句,兼容性是一个序列化框架最重要的指标,没有之一,必要的时候我们可以牺牲效率,但序列化本身不能出错,特别是核心流程上,因为序列化一旦出错,整个业务逻辑就直接崩溃了。

3. 数据结构不一致的兼容性

序列化的场景通常是用于不同服务器间的数据传输,在JAVA中,我们会通过jar的方式,让client和server引入同一个类。

如果有一天因为业务需要,要在某个类中增减字段,但我们无法做到client和server同时升级,这样,client和server的序列化结构就不一致了,一些依赖字段顺序的序列化框架这时就往往会出现问题,所以,能够兼容这种不一致场景也是评估一个序列化框架
的指标之一。

(二)序列化的效率

我们谈效率无非就是时间和空间。

序列化的时间效率自然就是序列化与反序列化的耗时,在不考虑硬件的场景下,通常取决于语言效率,数据量等等。比如在JAVA,基于反射的方法调用和普通调用是有性能差异的,而序列化往往基于反射操作,因此对这一块进行优化就能提升一定的效率。

空间效率通常就是压缩了,比如一个boolean值True,将它序列化成True,和序列化成T,两者之间长度是不一样的,很多序列化框架就是通过这种自定义的压缩字典来处理数据。

(三) 序列化的安全性

序列化通常是用于传输场景,传输的安全性在内网下基本是无需考虑的,对于提供出去的外网服务,比如与客户端APP的通信,其安全性往往是基于传输协议本身来实现的,比如基于HTTPS来传输。

另一个安全风险来自于注入攻击,注入攻击往往是因为本应用来查询或保存的字符串被当成命令代码执行了。特别是基于JSON的序列化框架,由于JSON本身是基于字符串的,如果里面的字符串被当作代码执行了,就会产生比较严重的后果。

七、 思考

我们从了解编码是信息的转换过程开始,从兼容性、效率及安全性三个方面分析了编码的原理。然后,谈到了分布式系统中的序列化,而序列化的本质是编码,因此,也从同样的三个方面分析了序列化。

那么,我们可以进一步:

  • 1.如果给你某种压缩或加密机制,你会如何分析?
  • 2.如果让你制定一种新的编码机制,你会考虑哪些问题?
  • 3.如果给你统一场景下的多种编码机制,你会如何比较和选择?

再进一步:

  • 1.对于解决同一问题的多个框架,你会如何比较?
  • 2.对于任意一个程序,你会如何分析?

还再进一步:

  • 1.对于在生活中遇到的任何一个事物,比如一个饮水杯,你会如何分析和评价它的价值?

如:

  1. 兼容性:这个杯子是否耐冷,耐热,抗摔。
  2. 效率:这个杯子相比其他产品能带来哪方面的效率提示提升?
  3. 安全性:这个杯子材料是否有毒害物质,有没有异味?

回答完这些问题,感觉像是造了一把锤子,然后所有的东西当钉子,但这并不是我的初衷,回到本书的序中写到的,这本书真正要写的是我对第一性原理,结构化思维,系统思维这些的思维方式的应用。

既然讲了底层的逻辑思维了,其实本文最底层的逻辑是用到了逻辑思维的两种方式-归纳和演绎。我们从字符,压缩,加密的编码中归纳出了一套分析编码的结构,然后演绎出序列化的第一性原理是编码,用编码的结构去分析了序列化,框架,甚至现实生活中的产品,最后完成了这一篇系统化的文章。

附本文的导图。

以上就是编码的相关内容,我们会在后面的协议,服务化框架等文章里面将本章内容关联起来,进一步完善整个编码的知识体系。

八、参考文献

作者:初开

发表于:博客园

本文基于 知识共享-署名-非商业性使用-禁止演绎 4.0 国际许可协议发布,转载必须保留署名及链接。