北尘 發表於 2024-6-5 09:02:00

.NET C# 程序自动更新组件

<h2>引言</h2>
<p>本来博主想偷懒使用AutoUpdater.NET组件,但由于博主项目有些特殊性和它的功能过于多,于是博主自己实现一个轻量级独立自动更新组件,可稍作修改集成到大家自己项目中,比如:WPF/Winform/Windows服务。大致思路:发现更新后,从网络上下载更新包并进行解压,同时在 WinForms 应用程序中显示下载和解压进度条,并重启程序。以提供更好的用户体验。</p>
<h2>1. 系统架构概览</h2>
<p>自动化软件更新系统主要包括以下几个核心部分:</p>
<ul data-spm-anchor-id="5176.28103460.0.i11.297c3da2eWOQxl">
<li><strong>版本检查</strong>:定期或在启动时检查服务器上的最新版本。</li>
<li><strong>下载更新</strong>:如果发现新版本,则从服务器下载更新包。</li>
<li><strong>解压缩与安装</strong>:解压下载的更新包,替换旧文件。</li>
<li data-spm-anchor-id="5176.28103460.0.i10.297c3da2eWOQxl"><strong>重启应用</strong>:更新完毕后,重启应用以加载新版本。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/2696180/202406/2696180-20240604194140486-1028873006.png" alt="" height="572" width="1145"></p>
<p id="1717501304585"></p>
<h2>组件实现细节</h2>
<h2>独立更新程序逻辑:</h2>
<h3>1. 创建 WinForms 应用程序</h3>
<p>首先,创建一个新的 WinForms 应用程序,用来承载独立的自动更新程序,界面就简单两个组件:添加一个 <code>ProgressBar</code> 和一个 <code>TextBox</code> 控件,用于显示进度和信息提示。</p>
<h3>2. 主窗体加载事件</h3>
<p>我们在主窗体的 <code>Load</code> 事件中完成以下步骤:</p>
<ul>
<li>解析命令行参数。</li>
<li>关闭当前运行的程序。</li>
<li>下载更新包并显示下载进度。</li>
<li>解压更新包并显示解压进度。</li>
<li>启动解压后的新版本程序。</li>
</ul>
<p>下面是主窗体 <code>Form1_Load</code> 事件处理程序的代码:</p>
<pre class="language-csharp highlighter-hljs"><code>private async void Form1_Load(object sender, EventArgs e)
{
    // 读取和解析命令行参数
    var args = Environment.GetCommandLineArgs();
    if (!ParseArguments(args, out string downloadUrl, out string programToLaunch, out string currentProgram))
    {
      _ = MessageBox.Show("请提供有效的下载地址和启动程序名称的参数。");
      Application.Exit();
      return;
    }
    // 关闭当前运行的程序
    Process[] processes = Process.GetProcessesByName(currentProgram);
    foreach (Process process in processes)
    {
      process.Kill();
      process.WaitForExit();
    }
    // 开始下载和解压过程
    string downloadPath = Path.Combine(Path.GetTempPath(), Path.GetFileName(downloadUrl));

    progressBar.Value = 0;
    textBoxInformation.Text = "下载中...";

    await DownloadFileAsync(downloadUrl, downloadPath);

    progressBar.Value = 0;
    textBoxInformation.Text = "解压中...";

    await Task.Run(() =&gt; ExtractZipFile(downloadPath, AppDomain.CurrentDomain.BaseDirectory));

    textBoxInformation.Text = "完成";

    // 启动解压后的程序
    string programPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, programToLaunch);
    if (File.Exists(programPath))
    {
      _ = Process.Start(programPath);
      Application.Exit();
    }
    else
    {
      _ = MessageBox.Show($"无法找到程序:{programPath}");
    }
}</code></pre>
<h3>3. 解析命令行参数</h3>
<p>我们需要从命令行接收下载地址、启动程序名称和当前运行程序的名称。以下是解析命令行参数的代码:</p>
<details>
<summary>查看代码</summary>
<pre class="language-csharp highlighter-hljs"><code>      private bool ParseArguments(string[] args, out string downloadUrl, out string programToLaunch, out string currentProgram)
      {
            downloadUrl = null;
            programToLaunch = null;
            currentProgram = null;

            for (int i = 1; i &lt; args.Length; i++)
            {
                if (args.StartsWith("--url="))
                {
                  downloadUrl = args.Substring("--url=".Length);
                }
                else if (args == "--url" &amp;&amp; i + 1 &lt; args.Length)
                {
                  downloadUrl = args[++i];
                }
                else if (args.StartsWith("--launch="))
                {
                  programToLaunch = args.Substring("--launch=".Length);
                }
                else if (args == "--launch" &amp;&amp; i + 1 &lt; args.Length)
                {
                  programToLaunch = args[++i];
                }
                else if (args.StartsWith("--current="))
                {
                  currentProgram = args.Substring("--current=".Length);
                }
                else if (args == "--current" &amp;&amp; i + 1 &lt; args.Length)
                {
                  currentProgram = args[++i];
                }
            }

            return !string.IsNullOrEmpty(downloadUrl) &amp;&amp; !string.IsNullOrEmpty(programToLaunch) &amp;&amp; !string.IsNullOrEmpty(currentProgram);
      }</code></pre>
</details>
<h3>4. 下载更新包并显示进度</h3>
<p>使用 <code>HttpClient</code> 下载文件,并在下载过程中更新进度条:</p>
<pre class="language-csharp highlighter-hljs"><code>private async Task DownloadFileAsync(string url, string destinationPath)
{
    using (HttpClient client = new HttpClient())
    {
      using (HttpResponseMessage response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead))
      {
            _ = response.EnsureSuccessStatusCode();

            long? totalBytes = response.Content.Headers.ContentLength;

            using (var stream = await response.Content.ReadAsStreamAsync())
            using (var fileStream = new FileStream(destinationPath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true))
            {
                var buffer = new byte;
                long totalRead = 0;
                int bytesRead;

                while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length)) != 0)
                {
                  await fileStream.WriteAsync(buffer, 0, bytesRead);
                  totalRead += bytesRead;

                  if (totalBytes.HasValue)
                  {
                        int progress = (int)((double)totalRead / totalBytes.Value * 100);
                        _ = Invoke(new Action(() =&gt; progressBar.Value = progress));
                  }
                }
            }
      }
    }
}</code></pre>
<h3>5. 解压更新包并显示进度</h3>
<p>在解压过程中跳过 <code>Updater.exe</code> 文件(因为当前更新程序正在运行,大家可根据需求修改逻辑),并捕获异常以确保进度条和界面更新:</p>
<div class="dark bg-gray-950 rounded-md border- border-token-border-medium">
<div class="flex items-center relative text-token-text-secondary bg-token-main-surface-secondary px-4 py-2 text-xs font-sans justify-between rounded-t-md">
<div class="flex items-center">&nbsp;</div>
</div>
</div>
<pre class="language-csharp highlighter-hljs"><code>private void ExtractZipFile(string zipFilePath, string extractPath)
{
    using (ZipArchive archive = ZipFile.OpenRead(zipFilePath))
    {
      int totalEntries = archive.Entries.Count;
      int extractedEntries = 0;

      foreach (ZipArchiveEntry entry in archive.Entries)
      {
            try
            {
                // 跳过 Updater.exe 文件
                if (entry.FullName.Equals(CustConst.AppNmae, StringComparison.OrdinalIgnoreCase))
                {
                  continue;
                }
                string destinationPath = Path.Combine(extractPath, entry.FullName);

                _ = Invoke(new Action(() =&gt; textBoxInformation.Text = $"解压中... {entry.FullName}"));

                if (string.IsNullOrEmpty(entry.Name))
                {
                  // Create directory
                  _ = Directory.CreateDirectory(destinationPath);
                }
                else
                {
                  // Ensure directory exists
                  _ = Directory.CreateDirectory(Path.GetDirectoryName(destinationPath));
                  // Extract file
                  entry.ExtractToFile(destinationPath, overwrite: true);
                }

                extractedEntries++;
                int progress = (int)((double)extractedEntries / totalEntries * 100);
                _ = Invoke(new Action(() =&gt; progressBar.Value = progress));
            }
            catch (Exception ex)
            {
                _ = Invoke(new Action(() =&gt; textBoxInformation.Text = $"解压失败:{entry.FullName}, 错误: {ex.Message}"));
                continue;
            }
      }
    }
}</code></pre>
<h3>6. 启动解压后的新程序</h3>
<p>在解压完成后,启动新版本的程序,并且关闭更新程序:</p>
<details>
<summary>查看代码</summary>
<pre class="language-csharp highlighter-hljs"><code>&nbsp;private void Form1_Load(object sender, EventArgs e)
{
    // 省略部分代码...

    string programPath = Path.Combine(extractPath, programToLaunch);
    if (File.Exists(programPath))
    {
      Process.Start(programPath);
      Application.Exit();
    }
    else
    {
      MessageBox.Show($"无法找到程序:{programPath}");
    }
}</code></pre>
</details>
<h2>检查更新逻辑</h2>
<h3>1. 创建 <code>UpdateChecker</code> 类</h3>
<p>创建一个 <code>UpdateChecker</code> 类,对外提供引用,用于检查更新并启动<span style="background-color: rgba(230, 126, 35, 1)">更新程序</span>:</p>
<pre class="language-csharp highlighter-hljs"><code>public static class UpdateChecker
{
    public static string UpdateUrl { get; set; }
    public static string CurrentVersion { get; set; }
    public static string MainProgramRelativePath { get; set; }

    public static void CheckForUpdates()
    {
      try
      {
            using (HttpClient client = new HttpClient())
            {
                string xmlContent = client.GetStringAsync(UpdateUrl).Result;
                XDocument xmlDoc = XDocument.Parse(xmlContent);

                var latestVersion = xmlDoc.Root.Element("version")?.Value;
                var downloadUrl = xmlDoc.Root.Element("url")?.Value;

                if (!string.IsNullOrEmpty(latestVersion) &amp;&amp; !string.IsNullOrEmpty(downloadUrl) &amp;&amp; latestVersion != CurrentVersion)
                {
                  // 获取当前程序名称
                  string currentProcessName = Process.GetCurrentProcess().ProcessName;

                  // 启动更新程序并传递当前程序名称
                  string arguments = $"--url \"{downloadUrl}\" --launch \"{MainProgramRelativePath}\" --current \"{currentProcessName}\"";
                  _ = Process.Start(CustConst.AppNmae, arguments);

                  // 关闭当前主程序
                  Application.Exit();
                }
            }
      }
      catch (Exception ex)
      {
            _ = MessageBox.Show($"检查更新失败:{ex.Message}");
      }
    }
}</code></pre>
<h3>2. 服务器配置XML</h3>
<p>服务器上存放一个XML文件配置当前最新版本、安装包下载地址等,假设服务器上的 XML 文件内容如下:</p>
<pre class="language-xml highlighter-hljs"><code>&lt;?xml version="1.0" encoding="utf-8"?&gt;
&lt;update&gt;
    &lt;version&gt;1.0.2&lt;/version&gt;
    &lt;url&gt;https://example.com/yourfile.zip&lt;/url&gt;
&lt;/update&gt;</code></pre>
<h2>主程序调用更新检查</h2>
<p>主程序可以通过定时器或者手动调用检查更新的逻辑,博主使用定时检查更新:</p>
<details>
<summary>查看代码</summary>
<pre class="language-csharp highlighter-hljs"><code>&nbsp;internal static class AutoUpdaterHelp
{
      private static readonly System.Timers.Timer timer;
      static AutoUpdaterHelp()
      {
          UpdateChecker.CurrentVersion = "1.0.1";
          UpdateChecker.UpdateUrl = ConfigurationManager.AppSettings["AutoUpdaterUrl"].ToString();
          UpdateChecker.MainProgramRelativePath = "Restart.bat";
          timer = new System.Timers.Timer
          {
            Interval = 10 * 1000//2 * 60 * 1000
          };
          timer.Elapsed += delegate
          {
            UpdateChecker.CheckForUpdates();
          };
      }

      public static void Start()
      {
          timer.Start();
      }

      public static void Stop()
      {
          timer.Stop();
      }
}</code></pre>
</details>
<h2 data-spm-anchor-id="5176.28103460.0.i14.297c3da2eWOQxl">思考:性能与安全考量</h2>
<p>在实现自动化更新时,还应考虑性能和安全因素。例如,为了提高效率,可以添加断点续传功能;为了保证安全,应验证下载文件的完整性,例如使用SHA256校验和,这些博主就不做实现与讲解了,目前的功能已经完成了基本的自动更新逻辑</p>
<h2 data-spm-anchor-id="5176.28103460.0.i12.297c3da2eWOQxl">结论</h2>
<p>自动化软件更新是现代软件开发不可或缺的一部分,它不仅能显著提升用户体验,还能减轻开发者的维护负担。通过上述C#代码示例,你可以快速搭建一个基本的自动化更新框架,进一步完善和定制以适应特定的应用场景。</p>
<hr>
<p data-spm-anchor-id="5176.28103460.0.i13.297c3da2eWOQxl">详情请看:.NET C# 程序自动更新组件 - 极客Bob - 博客园</p>
<p data-spm-anchor-id="5176.28103460.0.i13.297c3da2eWOQxl">本文提供了构建自动化软件更新系统的C#代码实现,希望对开发者们有所帮助。如果你有任何疑问或建议,欢迎留言讨论!</p>
<hr>
<p data-spm-anchor-id="5176.28103460.0.i13.297c3da2eWOQxl">&nbsp;</p><br><br>
来源:https://www.cnblogs.com/Bob-luo/p/18231510
頁: [1]
查看完整版本: .NET C# 程序自动更新组件