- A+
引言
本来博主想偷懒使用AutoUpdater.NET组件,但由于博主项目有些特殊性和它的功能过于多,于是博主自己实现一个轻量级独立自动更新组件,可稍作修改集成到大家自己项目中,比如:WPF/Winform/Windows服务。大致思路:发现更新后,从网络上下载更新包并进行解压,同时在 WinForms 应用程序中显示下载和解压进度条,并重启程序。以提供更好的用户体验。
1. 系统架构概览
自动化软件更新系统主要包括以下几个核心部分:
- 版本检查:定期或在启动时检查服务器上的最新版本。
- 下载更新:如果发现新版本,则从服务器下载更新包。
- 解压缩与安装:解压下载的更新包,替换旧文件。
- 重启应用:更新完毕后,重启应用以加载新版本。
组件实现细节
独立更新程序逻辑:
1. 创建 WinForms 应用程序
首先,创建一个新的 WinForms 应用程序,用来承载独立的自动更新程序,界面就简单两个组件:添加一个 ProgressBar
和一个 TextBox
控件,用于显示进度和信息提示。
2. 主窗体加载事件
我们在主窗体的 Load
事件中完成以下步骤:
- 解析命令行参数。
- 关闭当前运行的程序。
- 下载更新包并显示下载进度。
- 解压更新包并显示解压进度。
- 启动解压后的新版本程序。
下面是主窗体 Form1_Load
事件处理程序的代码:
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(() => 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}"); } }
3. 解析命令行参数
我们需要从命令行接收下载地址、启动程序名称和当前运行程序的名称。以下是解析命令行参数的代码:
查看代码
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 < args.Length; i++) { if (args[i].StartsWith("--url=")) { downloadUrl = args[i].Substring("--url=".Length); } else if (args[i] == "--url" && i + 1 < args.Length) { downloadUrl = args[++i]; } else if (args[i].StartsWith("--launch=")) { programToLaunch = args[i].Substring("--launch=".Length); } else if (args[i] == "--launch" && i + 1 < args.Length) { programToLaunch = args[++i]; } else if (args[i].StartsWith("--current=")) { currentProgram = args[i].Substring("--current=".Length); } else if (args[i] == "--current" && i + 1 < args.Length) { currentProgram = args[++i]; } } return !string.IsNullOrEmpty(downloadUrl) && !string.IsNullOrEmpty(programToLaunch) && !string.IsNullOrEmpty(currentProgram); }
4. 下载更新包并显示进度
使用 HttpClient
下载文件,并在下载过程中更新进度条:
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[8192]; 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(() => progressBar.Value = progress)); } } } } } }
5. 解压更新包并显示进度
在解压过程中跳过 Updater.exe
文件(因为当前更新程序正在运行,大家可根据需求修改逻辑),并捕获异常以确保进度条和界面更新:
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(() => 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(() => progressBar.Value = progress)); } catch (Exception ex) { _ = Invoke(new Action(() => textBoxInformation.Text = $"解压失败:{entry.FullName}, 错误: {ex.Message}")); continue; } } } }
6. 启动解压后的新程序
在解压完成后,启动新版本的程序,并且关闭更新程序:
查看代码
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}"); } }
检查更新逻辑
1. 创建 UpdateChecker
类
创建一个 UpdateChecker
类,对外提供引用,用于检查更新并启动更新程序:
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) && !string.IsNullOrEmpty(downloadUrl) && 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}"); } } }
2. 服务器配置XML
服务器上存放一个XML文件配置当前最新版本、安装包下载地址等,假设服务器上的 XML 文件内容如下:
<?xml version="1.0" encoding="utf-8"?> <update> <version>1.0.2</version> <url>https://example.com/yourfile.zip</url> </update>
主程序调用更新检查
主程序可以通过定时器或者手动调用检查更新的逻辑,博主使用定时检查更新:
查看代码
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(); } }
思考:性能与安全考量
在实现自动化更新时,还应考虑性能和安全因素。例如,为了提高效率,可以添加断点续传功能;为了保证安全,应验证下载文件的完整性,例如使用SHA256校验和,这些博主就不做实现与讲解了,目前的功能已经完成了基本的自动更新逻辑
结论
自动化软件更新是现代软件开发不可或缺的一部分,它不仅能显著提升用户体验,还能减轻开发者的维护负担。通过上述C#代码示例,你可以快速搭建一个基本的自动化更新框架,进一步完善和定制以适应特定的应用场景。
本文提供了构建自动化软件更新系统的C#代码实现,希望对开发者们有所帮助。如果你有任何疑问或建议,欢迎留言讨论!