所谓热升级,实际上就是在程序/服务不中止的前提下,经过增长、修改、删除相关功能模块,达到功能升级的目的。html
举个例子,咱们可能都有这样一个经历,正在操做一个软件,多是个重要的工做,这个时候软件发现有新的功能更新,须要升级程序,弹出一个看似很人性化的提示:请从新启动程序以完成升级!可是,问题是,升级的功能可能跟咱们当前工做所用的功能彻底没有关系,却要咱们丢弃辛辛苦苦作了半天的工做,就为了一个不相关的功能重作!咱们固然也可不理,继续作咱们的工做,直到完成后重启完成升级。但这显然不是咱们理想的方式,若是软件是以敏捷开发模式作出的,几乎不可避免的要频繁升级程序,那么能够想象这会让用户多么烦恼!安全
特别是对于服务来讲,咱们老是但愿保持稳定,但愿7*24小时永不中止。因此,咱们但愿能有这样一种方式,可以直接更新相应的模块,在不中止进程的状况下,保持服务为最新版本。下面我介绍两种实现方式。网络
在正式介绍前,咱们还要在这里从新温习一下一个重要的概念,应用程序域。dom
所谓应用程序域,.Net引入的一个概念,指的是一种边界,它标识了代码的运行范围,在其中产生的任何行为,包括异常都不会影响到其余应用程序域,起到安全隔离的效果。也能够当作是一个轻量级的进程。函数
一个进程能够包含多个应用程序域,各个域之间相互独立。以下是一个.net进程的组成(图片来自网络)post
进程启动后,会首先创建两个应用程序域,一个叫公共程序域(Domain-Neutral),其中加载的全部类型能够供其余全部应用程序域使用;另外一个叫默认程序域,它加载了咱们本身的应用程序,默认程序域中运行的代码可能致使整个进程崩溃。为了使咱们的进程可以稳定运行,就能够新建一个应用程序域来加载一些咱们认为可能会致使问题的程序集,而在新的应用程序域中运行的代码不会对默认程序域形成影响,保证了进程的稳定。ui
使用AppDomain类提供的静态方法,可进行应用程序域的建立与卸载url
//建立应用程序域 public static AppDomain CreateDomain(string friendlyName); //卸载应用程序域 public static void Unload(AppDomain domain);
其中,建立应用程序域方法CreateDomain还有其余多个重载,为咱们提供丰富的建立新应用程序域所用的配置。spa
卸载应用程序域时,CLR将清理该应用程序域使用的全部资源,包括加载的程序集,未释放的非托管资源等。但公共程序域和默认程序域没法卸载。.net
一般,咱们封装的功能都是以程序集的形式存在的,而程序集只有在应用程序域卸载之后才能释放。这就是为何在程序运行的过程当中没法直接进行修改程序文件的缘由,程序运行后,相关的程序集文件被加载到了默认应用程序域中,而默认应用程序域又没法卸载,致使咱们不得不关闭进程才能修改相应文件。
而咱们在程序启动的时候,把这个程序集加载一个咱们新建的应用程序域中,须要更新文件的时候只要卸载这个域,在不关闭进程的状况下,不就能够对文件进行更新了么?这个也就是咱们可以进行“热升级”的理论基础。
在新的应用程序域中加载功能模块,须要更新时,卸载应用程序域,再从新加载。
下面用一个简单具体的解决方案说明一下。
解决方案以下所示:
其中,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方法
比较先后两次调用的结果,功能升级成功!
这里只是介绍了做为功能提供模块或没有界面的模块的升级方法,而对于须要做为主界面的子窗体的模块如何实现热升级?我将在下一篇进行介绍。
本节完整的代码请 点击下载