实现C/S程序的自动更新

时间:2022-06-26 15:23:35

最近在做一个.Net C/S的系统,需要实现自动更新。MS已经提供了ClickOnce,很方便,但是用起来不太习惯,还是决定自己写一个简单的。

自动更新无非文件比较、下载、启动程序几个步骤,其中文件比较可以通过手动在配置文件中维护版本号,也可以比较文件的MD5值,或者在.Net里还可以用Assembly或文件的版本号。因为怕麻烦,手动维护不考虑,剩下两者各有所长,都提供了以供选择。下载就比较简单了,http、ftp、WebService都可以选择。启动程序一般用System.Diagnostics.Process.Start就可以,我用的是AppDomain.ExecuteAssembly。

要自动生成文件版本信息,需要有个服务端,可以是WebService等等,我采用的是自己实现IHttpHandler,提供文件的版本信息和下载。

下面是UpdateHelper的类图和定义:

 

实现C/S程序的自动更新

 

GetUpdateInfos、CheckIfNeedUpdate、DownloadFile分别是获取文件版本信息、比较文件是否是最新的、下载文件的作用,很简单。

  1.     public static class UpdateHelper
  2.     {
  3.         public static UpdateInfo[] GetUpdateInfos()
  4.         {
  5.             HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Properties.Settings.Default.Server);
  6.             request.Method = "GET";
  7.             HttpWebResponse response = (HttpWebResponse)request.GetResponse();
  8.             if (response.StatusCode == HttpStatusCode.OK)
  9.             {
  10.                 using (Stream output = request.GetResponse().GetResponseStream())
  11.                 {
  12.                     XmlSerializer xml = new XmlSerializer(typeof(UpdateInfo[]));
  13.                     return (UpdateInfo[])xml.Deserialize(output);
  14.                 }
  15.             }
  16.             else
  17.             {
  18.                 throw new Exception("Exception occurs on the server.");
  19.             }
  20.         }
  21.         public static bool CheckIfNeedUpdate(UpdateInfo update)
  22.         {
  23.             if (!File.Exists(update.FileName))
  24.             {
  25.                 return true;
  26.             }
  27.             else
  28.             {
  29.                 switch (update.VersionType)
  30.                 {
  31.                     case VersionType.FileVersion:
  32.                         return FileVersionInfo.GetVersionInfo(update.FileName).FileVersion != update.Version;
  33.                     case VersionType.MD5:
  34.                         using (FileStream file = File.OpenRead(update.FileName))
  35.                         {
  36.                             return Convert.ToBase64String(MD5.Create().ComputeHash(file)) != update.Version;
  37.                         }
  38.                     defaultreturn false;
  39.                 }
  40.             }
  41.         }
  42.         public static void Update()
  43.         {
  44.             UpdateInfo[] updates = UpdateHelper.GetUpdateInfos();
  45.             List<Thread> downloadsThreads = new List<Thread>();
  46.             foreach (UpdateInfo update in updates)
  47.             {
  48.                 if (UpdateHelper.CheckIfNeedUpdate(update))
  49.                 {
  50.                     Thread thread = new Thread(delegate(object param) { UpdateHelper.DownloadFile((string)param); });
  51.                     thread.Start(update.FileName);
  52.                     downloadsThreads.Add(thread);
  53.                 }
  54.             }
  55.             foreach (Thread thread in downloadsThreads) thread.Join();
  56.         }
  57.         public static void DownloadFile(string fileName)
  58.         {
  59.             HttpWebRequest request = (HttpWebRequest)WebRequest.Create(Properties.Settings.Default.Server + "?download=" + fileName);
  60.             request.Method = "GET";
  61.             HttpWebResponse response = (HttpWebResponse)request.GetResponse();
  62.             if (response.StatusCode == HttpStatusCode.OK)
  63.             {
  64.                 byte[] buffer = new byte[8096];
  65.                 using (Stream output = request.GetResponse().GetResponseStream())
  66.                 {
  67.                     using (FileStream file = File.Open(fileName, FileMode.OpenOrCreate))
  68.                     {
  69.                         int length = output.Read(buffer, 0, buffer.Length);
  70.                         while (length != 0)
  71.                         {
  72.                             file.Write(buffer, 0, length);
  73.                             length = output.Read(buffer, 0, buffer.Length);
  74.                         }
  75.                     }
  76.                 }
  77.             }
  78.             else
  79.             {
  80.                 throw new Exception("Exception occurs on the server.");
  81.             }
  82.         }
  83.     }
  84.     public class UpdateInfo
  85.     {
  86.         public UpdateInfo() { }
  87.         public UpdateInfo(string filename, VersionType versionType, string version)
  88.         {
  89.             FileName = filename;
  90.             VersionType = versionType;
  91.             Version = version;
  92.         }
  93.         public string FileName;
  94.         public VersionType VersionType;
  95.         public string Version;
  96.     }
  97.     public enum VersionType
  98.     {
  99.         FileVersion,
  100.         MD5
  101.     }

UpdateServiceHandler定义:

  1.  public class UpdateServiceHandler : IHttpHandler
  2.     {
  3.         #region IHttpHandler Members
  4.         public bool IsReusable
  5.         {
  6.             get { return true; }
  7.         }
  8.         public void ProcessRequest(HttpContext context)
  9.         {
  10.             if (String.Equals(context.Request.RequestType, "GET", StringComparison.OrdinalIgnoreCase))
  11.             {
  12.                 string filename = context.Request.QueryString["download"];
  13.                 UpdateInfoSection updateInfoSection = GetUpdateConfig();
  14.                 if (String.IsNullOrEmpty(filename))
  15.                 {
  16.                     context.Response.ContentEncoding = Encoding.UTF8;
  17.                     context.Response.ContentType = "text/xml";
  18.                     foreach (UpdateInfo update in updateInfoSection.UpdateFiles.Values)
  19.                     {
  20.                         switch (update.VersionType)
  21.                         {
  22.                             case VersionType.FileVersion:
  23.                                 update.Version = FileVersionInfo.GetVersionInfo(update.Path).FileVersion;
  24.                                 break;
  25.                             case VersionType.MD5:
  26.                                 using (FileStream file = File.OpenRead(update.Path)) update.Version = Convert.ToBase64String(MD5.Create().ComputeHash(file));
  27.                                 break;
  28.                         }
  29.                     }
  30.                     XmlSerializer xml = new XmlSerializer(typeof(List<UpdateInfo>));
  31.                     xml.Serialize(context.Response.Output, new List<UpdateInfo>(updateInfoSection.UpdateFiles.Values));
  32.                 }
  33.                 else
  34.                 {
  35.                     UpdateInfo update;
  36.                     if (updateInfoSection.UpdateFiles.TryGetValue(filename, out update))
  37.                     {
  38.                         context.Response.ContentEncoding = Encoding.UTF8;
  39.                         context.Response.ContentType = "application/x-msdownload";
  40.                         context.Response.AppendHeader("Content-Disposition""attachment;filename=" + filename);
  41.                         context.Response.WriteFile(update.Path);
  42.                     }
  43.                     else
  44.                     {
  45.                         context.Response.ContentEncoding = Encoding.UTF8;
  46.                         context.Response.ContentType = "text/html";
  47.                         context.Response.Output.WriteLine("File {0} not fount.", filename);
  48.                     }
  49.                 }
  50.             }
  51.         }
  52.         private UpdateInfoSection GetUpdateConfig()
  53.         {
  54.             UpdateInfoSection updateInfoSection = (UpdateInfoSection)ConfigurationManager.GetSection("ClientUpdateInfo");
  55.             if (!updateInfoSection.Initialize)
  56.             {
  57.                 foreach (UpdateInfo update in updateInfoSection.UpdateFiles.Values)
  58.                 {
  59.                     if (String.IsNullOrEmpty(update.Path)) update.Path = Path.Combine(updateInfoSection.DefaultForlder, update.FileName);
  60.                     update.Path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, update.Path);
  61.                 }
  62.                 updateInfoSection.Initialize = true;
  63.             }
  64.             return updateInfoSection;
  65.         }
  66.         #endregion
  67.     }
  68.     public class UpdateInfoSection : ConfigurationSection
  69.     {
  70.         public string DefaultForlder;
  71.         public Dictionary<string, UpdateInfo> UpdateFiles = new Dictionary<string, UpdateInfo>();
  72.         public bool Initialize = false;
  73.         protected override void DeserializeSection(System.Xml.XmlReader reader)
  74.         {
  75.             while (reader.Read())
  76.             {
  77.                 if (reader.NodeType == System.Xml.XmlNodeType.Element)
  78.                 {
  79.                     switch (reader.Name)
  80.                     {
  81.                         case "File":
  82.                             string versionType = reader.GetAttribute("VersionType");
  83.                             UpdateFiles.Add(reader.GetAttribute("FileName"),
  84.                             new UpdateInfo(
  85.                                 reader.GetAttribute("FileName"),
  86.                                 String.IsNullOrEmpty(versionType) ? VersionType.FileVersion : (VersionType)Enum.Parse(typeof(VersionType), versionType, true),
  87.                                 reader.GetAttribute("Version"),
  88.                                 reader.GetAttribute("Path")));
  89.                             break;
  90.                         case "DefaultFolder":
  91.                             DefaultForlder = reader.ReadString();
  92.                             break;
  93.                     }
  94.                 }
  95.             }
  96.         }
  97.     }
  98.     public class UpdateInfo
  99.     {
  100.         public UpdateInfo() { }
  101.         public UpdateInfo(string filename, VersionType versionType, string version, string path)
  102.         {
  103.             FileName = filename;
  104.             VersionType = versionType;
  105.             Version = version;
  106.             Path = path;
  107.         }
  108.         public string FileName;
  109.         [XmlIgnore]
  110.         public string Path;
  111.         public VersionType VersionType;
  112.         public string Version;
  113.     }
  114.     public enum VersionType
  115.     {
  116.         FileVersion,
  117.         MD5
  118.     }

 

注意下其中UpdateInfo多了一个Path属性,表示文件的实际路径,不需要显示给客户端,因此加了XmlIgnore标记。

在服务端需要配置UpdateInfoSection项和UpdateServiceHandler的地址映射。

在<system.web>节<httpHandlers>下添加一项:

  1.     <httpHandlers>
  2.       <add verb="*" path="update.aspx" type="MyService.UpdateServiceHandler"/>
  3.     </httpHandlers>

其中MyService.UpdateServiceHandler需要替换为UpdateServiceHandler的完整名称。

然后添加UpdateInfoSection项:

  1.   <configSections>
  2.     <section name="ClientUpdateInfo" type="MyService.UpdateInfoSection"/>
  3.   </configSections>
  4.   <ClientUpdateInfo>
  5.     <DefaultFolder>bin</DefaultFolder>
  6.     <File FileName ="log4net.dll"/>
  7.     <File FileName ="Castle.Core.dll"/>
  8.     <File FileName ="Castle.DynamicProxy2.dll"/>
  9.     <File FileName ="Client.dll" Path ="bin/Client.exe" />
  10.     <File FileName ="client.config" VersionType="MD5"/>
  11.   </ClientUpdateInfo>

DefaultFolder是在没有为文件提供Path时默认的包含文件的目录,配置比较简单。

好了,现在可以打开update.aspx页面看到文件的版本信息

实现C/S程序的自动更新

在update.aspx后加上参数的话,例如?download=log4net.dll,就可以下载文件了。

实现C/S程序的自动更新

 

最后,客户端只需要更新并运行程序就可以了,当然还需要设置一下服务端的地址(Properties.Settings.Default.Server)

  1.  static class Program
  2.     {
  3.         /// <summary>
  4.         /// The main entry point for the application.
  5.         /// </summary>
  6.         [STAThread]
  7.         static void Main()
  8.         {
  9.             UpdateHelper.Update();            
  10.             AppDomain domain = AppDomain.CreateDomain("client");
  11.             domain.SetData("APP_CONFIG_FILE""client.config");
  12.             domain.ExecuteAssembly("Client.dll", AppDomain.CurrentDomain.Evidence, Environment.GetCommandLineArgs());
  13.         }        
  14.     }