前言
所谓热升级,实际上就是在程序/服务不停止的前提下,通过增加、修改、删除相关功能模块,达到功能升级的目的。
为什么要热升级
举个例子,我们可能都有这样一个经历,正在操作一个软件,可能是个重要的工作,这个时候软件发现有新的功能更新,需要升级程序,弹出一个看似很人性化的提示:请重新启动程序以完成升级!但是,问题是,升级的功能可能跟我们当前工作所用的功能完全没有关系,却要我们丢弃辛辛苦苦做了半天的工作,就为了一个不相关的功能重做!我们当然也可不理,继续做我们的工作,直到完成后重启完成升级。但这显然不是我们理想的方式,如果软件是以敏捷开发模式做出的,几乎不可避免的要频繁升级程序,那么可以想象这会让用户多么烦恼!
特别是对于服务来说,我们总是希望保持稳定,希望7*24小时永不停止。所以,我们希望能有这样一种方式,能够直接更新相应的模块,在不停止进程的情况下,保持服务为最新版本。下面我介绍两种实现方式。
应用程序域(AppDomain)
在正式介绍前,我们还要在这里重新温习一下一个重要的概念,应用程序域。
所谓应用程序域,.Net引入的一个概念,指的是一种边界,它标识了代码的运行范围,在其中产生的任何行为,包括异常都不会影响到其他应用程序域,起到安全隔离的效果。也可以看成是一个轻量级的进程。
一个进程可以包含多个应用程序域,各个域之间相互独立。如下是一个.net进程的组成(图片来自网络)
进程启动后,会首先建立两个应用程序域,一个叫公共程序域(Domain-Neutral),其中加载的所有类型可以供其他所有应用程序域使用;另一个叫默认程序域,它加载了我们自己的应用程序,默认程序域中运行的代码可能导致整个进程崩溃。为了使我们的进程能够稳定运行,就可以新建一个应用程序域来加载一些我们认为可能会导致问题的程序集,而在新的应用程序域中运行的代码不会对默认程序域造成影响,保证了进程的稳定。
使用AppDomain类提供的静态方法,可进行应用程序域的创建与卸载
//创建应用程序域 public static AppDomain CreateDomain(string friendlyName); //卸载应用程序域 public static void Unload(AppDomain domain);
其中,创建应用程序域方法CreateDomain还有其他多个重载,为我们提供丰富的创建新应用程序域所用的配置。
卸载应用程序域时,CLR将清理该应用程序域使用的所有资源,包括加载的程序集,未释放的非托管资源等。但公共程序域和默认程序域无法卸载。
通常,我们封装的功能都是以程序集的形式存在的,而程序集只有在应用程序域卸载以后才能释放。这就是为什么在程序运行的过程中无法直接进行修改程序文件的原因,程序运行后,相关的程序集文件被加载到了默认应用程序域中,而默认应用程序域又无法卸载,导致我们不得不关闭进程才能修改相应文件。
而我们在程序启动的时候,把这个程序集加载一个我们新建的应用程序域中,需要更新文件的时候只要卸载这个域,在不关闭进程的情况下,不就可以对文件进行更新了么?这个也就是我们能够进行“热升级”的理论基础。
方式一
在新的应用程序域中加载功能模块,需要更新时,卸载应用程序域,再重新加载。
下面用一个简单具体的解决方案说明一下。
解决方案如下所示:
其中,MainServer是主程序,Module1是程序中要使用的功能模块,实现了CommonLib项目中ICalculater接口
public interface ICalculater { int Calc(int a, int b); }
public class Calculater : MarshalByRefObject, ICalculater { public int Calc(int a, int b) { int res = a + b; Console.WriteLine("Add {0} and {1}, result: {2} [run in {3}]", a, b, res, AppDomain.CurrentDomain.FriendlyName); return res; } }
当前功能是计算两个整数之和,后面我将通过修改该计算的实现,达到功能升级的目的。
注意:如果要在新的应用程序域中调用,类型必须继承MarshalByRefObject,请网上参考-按引用封送和按值封送-相关文章。
首先,我们来看如何创建新的程序域,并调用域内方法
static void Main(string[] args) { Console.WriteLine("current domain: {0}", AppDomain.CurrentDomain.FriendlyName + Environment.NewLine); Console.WriteLine("input two data to calc: "); Console.Write("a: "); int a = Convert.ToInt32(Console.ReadLine()); Console.Write("b: "); int b = Convert.ToInt32(Console.ReadLine()); AppDomain ad_Calc = AppDomain.CreateDomain("domin #calc"); ICalculater calc = (ICalculater)ad_Calc.CreateInstanceAndUnwrap("Module1", "Module1.Calculater"); calc.Calc(a, b); }
其实代码很简单,通过调用方法CreateDomain创建新的应用程序域并给域命名为domain #calc后,调用CreateInstanceAndUnwrap,输入程序集名和要创建的实例类型名后,即可获得一个该类型的一个引用代理。面向接口的方式使我们可以直接通过强制转换调用相应的方法(如果不面向接口的话,就要使用反射的方式调用方法,相对来说麻烦一些,速度也会慢一些)。执行过程中,让程序打印出当前所在域的名称,可以直观地看到当前代码是在哪里执行的。如下是运行结果
结果中我们能很清晰的看到,没有调用Calc方法时,当前运行所在域是MainServer.exe(也即是默认应用程序域的名称),调用Calc方法时,实际上是运行在domain #calc域中的。
既然实现了在新域中运行,接下来我们看下怎么对功能进行升级。
继续在Main函数中添加代码,在域创建并加载Module1成功后,尝试直接删除Module1.dll文件
string cmd; while ((cmd = Console.ReadLine()).ToLower() != "quit") { switch (cmd.ToLower()) { case "del_calc": TryDelete("Module1.dll"); Console.WriteLine(); break; case "unload_calc": UnloadDomain(ad_Calc); Console.WriteLine(); break; case "reload_and_run_calc": AppDomain domainNew; if (ReloadDomain(out domainNew)) { ICalculater calcNew = (ICalculater)domainNew.CreateInstanceAndUnwrap("Module1", "Module1.Calculater"); calcNew.Calc(a, b); } Console.WriteLine(); break; } }
结果显然是,无法删除,Module1.dll文件句柄还在程序域中没有释放。
卸载程序域,再删除试试呢
成功了!也就是卸载程序域后,该程序域加载的文件句柄也相应被释放,这时候可以自由地操作文件了,当然也就可以把我们新版本的功能替换上去了。
这时候我们修改Calculater中的Calc方法,返回a,b两参数之积,并生成新的dll文件
public class Calculater : MarshalByRefObject, ICalculater { public int Calc(int a, int b) { int res = a * b; Console.WriteLine("Multiply {0} and {1}, result: {2} [run in {3}]", a, b, res, AppDomain.CurrentDomain.FriendlyName); return res; } }
重新创建应用程序域,为了和之前创建的程序域区分,命名为domin reload #calc ,并加载Module1,执行Calc方法
比较前后两次调用的结果,功能升级成功!
这里只是介绍了作为功能提供模块或没有界面的模块的升级方法,而对于需要作为主界面的子窗体的模块如何实现热升级?我将在下一篇进行介绍。
本节完整的代码请 点击下载