直到Windows 8 之前,微软都没有了像苹果一样的提供一个AppStore,所以在这个平台上开发跟使用软件都是有一定门槛的:对于普通用户来说,专门跑去电子市场买一套办公或者娱乐软件的光盘并不是所有人都喜欢的事情,而即使是在软件发展环境不太健康的中国市场,到各大软件网站下载到无毒的软件也不是一件容易的事情;对于开发者而言更是如此,不仅要考虑完成软件开发所预期的各种功能,还要处理诸如打包、防破解、注册流程等等一系列的附加工作,这些事情对于所有运行于Windows平台上的软件都会遇到,它们的解决方案也大同小异,但Windows并没有帮助开发者们处理这些问题。
我们今天要讨论的一个主题——“升级”,也涵盖在其中。如果您有多年使用Windows操作系统的经验,那么应该会发现一个现象,目前的常用软件都是这样升级的:在帮助菜单中放置一个叫做“软件升级”的菜单项,点击后便会启动一个查询最新版本的loading界面,一旦查询到新版本,便会提示用户是否要下载并安装。
这个功能的实现逻辑非常简单,但对于很多需要持续运营的软件而言,它是一个重要的“生命线”。因为只有让用户感受到这个团队的努力,这个团队才有维系下去的意义,而这就需要用户能够通过升级拿到新的版本。
然而,即使很多团队将“查询是否是最新版本”这个逻辑做成了软件启动后的“第一要务”,依旧只有一部分热心用户会选择点击“确定”按钮,我们来看看他们的顾虑:
(1) 新功能我都用不上,没有升级的必要。
(2) 新版本可能不稳定。
(3) 我是盗版用户,升级了,破解补丁就无效了
(4) 升级一次下载一个很大的安装包,要等很久,还要安装,太折腾了。
前三个问题对于不同的产品都没有通用的解决方案,相比于技术的问题,它们更像是产品本身的定位问题,或者开发团队的素质问题,需要具体情况具体分析。但是对于第四种情况,的确是技术人员可以尝试去挑战的。
【安全补丁】
记得就在几年前,整个中国的网络环境远没有现在那么舒适,当时除了部分高校内部的校园网有比较高的速度,1M bps的“小水管”还是占绝大多数。我们姑且称这个时间段为“窄带时代”,一个在线看视频只能依靠P2P加速的时代。
而与龟速的网络不相协调的是,安装包的体积往往都是十几兆甚至几十兆,在这个环境下,等待一个新版本安装包的下载完成是非常考验耐心的,相信我们绝大多数人都有在缓慢的downloading界面点击“取消”按钮的经历。
看来,简单的下载新版本安装包并执行安装流程的方案,只适应于忠诚的用户和特定的场景,那我们来关注下微软的开发团队采用了什么办法?要知道Windows操作系统的开发团队每天都会接到不止一个有关Windows的安全漏洞,而这些问题的修复不仅仅频繁,而且紧急。
面对带宽占用和安装耗时的问题,微软采用了安全补丁的办法:所谓的安全补丁,本质上就是携带新逻辑一个或多个新版本程序文件,比如原来abc.dll中暴露了一个缓冲区溢出的漏洞,那我们就发布一个已经做过保护的abc.dll 2.0版,包装成可以独立安装的安全补丁,然后下发给用户,在用户重启Windows操作系统时完成替换(Windows在真正的初始化完成后,是很难替换其系统文件的,因为系统要面对占用问题,缓解的方法之一是hotfix技术,但是绝大多数场景下,依然需要用户重启后安装)。
这个过程看似简单,而且体积苗条的安全补丁不会在下载和安装上浪费什么时间,按理来说是个很理想的措施。但实际在运营中却是代价巨大的,甚至不是一般的团队可以承担的起的:
首先,一个足够复杂的软件,其各个模块之间的关系必然是很紧密的,2.0版本的abc.dll往往需要一个2.0版本的“ddd.dll”、“eee.dll”… 除非一个dll中封装的模块非常非常独立,否则不可能存在一个dll可以随意替换到以往发布的任何一个版本中。这就需要一个高成本的工作——“定制”。言下之意,如果一款软件在服役期的版本多达十几个,我们需要提供十几个abc.dll 2.0版,给特定的外部版本推送特定的补丁。
其次,任何补丁的发布都需要严格的测试,一个版本的测试可能要花费若干天的时间,那么十几个版本的测试呢?代价可想而知。(例如Windows开发团队每发布一批安全补丁都需要几周的内部测试和外部测试,所以有专门的团队来负责此事,而且规模还非常可观)
图1: 补丁升级
只需用户一个确认,整个过程便在后台慢慢地执行(诚然,不厌其烦的重启提示确实有些骚扰)。虽然这么多年来,微软一直在维系着这种安全、有效并且易于被用户接受的方案,但不得不说,对于一般的开发团队,这套方案显得过于昂贵了。
【保持最新】
但我们也不必拘泥于这样一种思路,要知道,微软采用对特定版本推特定补丁的做法是有着它自身立场的一些考虑的,比如:
(1) 微软的补丁程序在执行逻辑上其实很复杂,远不止单纯的文件替换这么简单,这是一个操作系统复杂到一定阶段后的产物。但对于一般的应用软件,简单的文件比对和替换基本能满足绝大多数场景。
(2) 微软肯定不希望XP或者Vista用户无缘无故的升级到Win7,但对于普通的免费软件,或者购买license后可以享受终身升级的付费软件,这件事情未尝不可。
如果抛弃上诉的两个包袱,那么我们现在就可以设计一个方案,一切的一切,都始于一个简单的想法,让用户本地的程序文件跟随服务器的最新版本,我们称之为“文件保持最新”。而且,这个方案的思想非常的简单,就是由客户端程序将自己的版本号上传给升级服务器,由升级服务器确定当前用户版本到最新版本需要更新那些文件,将这些需要更新的文件组织成一个file-list下发给用户,而后由客户端在运行过程中将file-list中罗列的文件逐个下载到本地,并在下次启动程序前完成替换。
如果再进行一些更细致的考虑,我们还要关注一些编译上的知识,比如我们知道一个PE文件(dll、exe等)头部会携带一些本次编译特有的信息,比如时间戳等等,而对于升级而言,只有数据段和代码段的变化才是我们关注的,所以在进行版本间的文件比较时,我们需要去除掉这部分的干扰信息。
下图是对这个简单想法的描述:
图2: 保持最新的思路
这个方案看似比较合理,但是我们都清楚一个事实,现在Windows上的应用软件千千万,但没有几个是采用上述思想进行升级的,为什么呢?
因为这个方案太过于理想化了:一个软件如果是基于类似C++的编译型语言开发,那么即使过滤掉头信息的变化,两个不同版本的程序文件也没有多少是相同的,甚至一个都没有。要解释清楚这个现象,我们也要像上面一样,再关注一些编译和链接上的知识:
我们来看引发一个PE文件的差异的原因,其实主要源自三种因素(详情可以参考http://www.daemonology.net/papers/bsdiff.pdf):
(1) 头信息变化:上面以及提及,这是编译器每次生成二进制文件时都会参杂进去的信息,刚才也说了,这种信息很好规避。
(2) 代码的变化:你修改了一个Project中的部分代码,那么反映到二进制PE文件中就会有一段明显而集中的差异。这是不可避免的。
(3) 间接的影响:我们知道编译出的PE文件中有大量的信息是绝对的地址,如果你改变了一个指针的指向,那么所有引用这个指针的地址都会发生变化,好吧,你要接受一个事实:一行代码的变化会被编译器严重放大,而且一个PE文件的变化可能并非源自于这个Project自己的源代码差异。
如果是一个由几百万行代码组成的庞大系统,单单组成它的Project就多达上百个,各个模块之间的依赖关系异常复杂,那么上面提及的第三种差异(即间接的影响),在决定程序文件的差异度上的作用就会被无限放大。
也就是说,如果采用这种方案,一个普通的应用软件一次升级可能需要耗掉100M以上的带宽。虽然最近几年中国的网络环境越发舒适,但我们都清楚,这个数量级还不是我们能够接受的。所以我们可以回答刚才那个问题了“为什么没有几个软件是采用上述思想进行升级的”。
【差间压缩】
稍微对军事略感兴趣的朋友都知道,20世纪最具创新意义的军事武器,都是诞生于二战后期,战争的需求极大地推动了技术的进步。同样的,现代互联网的很多需求也不断地催生着技术的进步,比如说——游戏。
大型网游其实一直都面临着类似的问题:首先,游戏资源文件大多是被打包在一起并进行加密的,尤其是3D网游,它们的资源包动辄过G。而另一方面,为了增加游戏的互动性已经节日活动,频繁的更新也必不可少,那总不能每次升级都强迫用户下载上G的更新文件吧?
安全的要求和运营的压力本就矛盾,但技术团队找到了一个“曲线救国”的方案,那就是不再只盯着一个个文件,而是缩小比较的粒度,将文件的差异缩小到Bit级,直接关注二进制差异。同样的思路也可以被搬到这里:
如果一个DLL文件在前后两个版本的迭代中发生了变化,那么肯定不是每个Bit都发生了差异,而只是其中一些Bit有所不同。如果我们将这些差异提取出来,做成二进制Patch,那么只需要由升级服务器向客户端下发这些Patch,那么客户端自己就可以根据这些Patch以及旧文件“合成”出新版本的文件。
如果一个5M的DLL发生了差异,往往只是因为修改了其中的一行代码,那么使用上面的思想,我们就可以只产生一个不到1K的差异Patch,将其下发给客户端,由5M到1K,我们节约了99.98%的带宽。
图3:将粒度缩小到bit
那么如何实现呢?
其实方法有很多,但是目前最普遍适用的是一种称为“滑动比较”的方案,简单用图描述一下其思路大致如下:
图4:滑动delta算法
上面说的这种思路,在计算机领用被称之为“delta压缩”,或者“差间压缩”,比较典型的例子就是视频压缩领域的帧间差量,第二帧只保存较第一帧中变化的部分。
【PE文件】
但是对于PE文件,问题又出现了,地址变动引发的差异往往会稀疏得散落于整个文件的各个部分。(最坏的情况下,公共头文件中一行代码的改动,可以在最终生成的二进制文件中产生多大10%的差异。)仔细用Beyond Compare比对一下两个版本的DLL文件,你会发现,每隔几十个字节,一个地址变了,又过了十几个字节,同样的地址又变了。所以“滑动匹配”的方案又不适用,因为采用这种方法算出来的差异补丁往往不比实际文件小多少。
图5:PE文件的差异
这个问题其实是方向性错误,还记得上面提到的PE文件差异原因时的第三类差异吗?我们用了精确匹配的思想去对付一个不能精确比对的问题,就好像基因工程中的相似度比较,人和黑猩猩的基因差异可能不到1%,但是这1%的差异是散落于23对DNA的各个片段中。
图6:寻找其中的规律
我们仔细看一下上面【图6】中二进制变化,就会发现这里似乎存在着一些规律:
■近似区域的差异源自指针地址的变迁
■指向同一地址的指针会发生同样的变化
■临近地址往往发生携同性质的变化
于是乎,我们的算法需要对这些问题进行分类处理了。换句话说,由于代码变化引起的二进制差异可以作为一类进行处理,因为这些差异即集中又明显。而由于指针偏移间接引起的差异则要统计的收敛起来,这些差异往往都是同质的,往往几十处的二进制差异源自一个指针的跳
图7:分而治之的处理
如何在一段二进制程序段中寻找出这些差异并进行合理的压缩,这个需求非常迫切,而且已经有了成熟的方案:
■BsDiff: Linux中的一个开源工具,致力于快速和轻量的更新Linux的操作系统漏洞(跟微软的安全补丁类似),其算法的核心思想是基于统计学规律进行近似匹配,然后通过一系列的变化(比如BWT变换)提高“近似段”的压缩率。
■Courgette: Google Chrome升级系统的核心模块,基于BsDiff,但对其进行了一系列的改进,将平台相关的信息(即x86汇编指令)融入其中,以期望更精确的定位指针,从而避免统计算法在差异明显时候的错误率。
上述两个模块的内部原理还是很有意思的,前者是典型的学术思维,后者则是集大成的工程师风范,但为了避免打消您阅读完此文的动力,我会在稍后的文章中再做补充。
如果不考虑跨平台问题,毫无疑问Google的Courgette是做得更加优秀的,下面是根据Chrome团队官网(http://dev.chromium.org/developers/design-documents/software-updates-courgette)上提供的统计数据制作出的图标:
图8:不错的带宽节省
由于二进制差异对带宽的节省非常惊人,所以用户通过很低的带宽耗用下载到一个升级包,整个过程可以尽可能地减少对用户的骚扰。比如在用户在使用Chrome浏览器时,可以无感的升级到最新版本,尽可能地避免自己的浏览器漏洞不被非法网页利用。
最重要的是,由于整个补丁包的制作过程基于统一的算法,所以整个过程可以进行自动化的收拢,从而将人从其中解放出来。即互谅网上存在该软件很多不同的版本,但我们省去了最开始提到的为每个版本定制不同补丁时的重复工作,一切交给计算机来自动完成。
【回顾总结】
话题到这里基本就结束了,总的来说,本文概括了几种常见的客户端软件升级方案,它们在实现方案上各有不同,但都为是为了解决某一问题而诞生。如所有的领域一样,计算机这个领域的技术的进步就是在一次又一次的挑战中不断前进,不断去尝试,在各种苛刻的需求和棘手的问题面前不断进化,才最终铸造了我现在这个伟大的时代。