免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

VB.NET实现桌面应用自动更新:版本校验、下载替换与回滚

VB.NET实现桌面应用自动更新:版本校验、下载替换与回滚 简介这套基于VB.NET实现的软件自动更新程序面向桌面应用开发者解决客户端版本分发与升级维护难题。资源完整包含自动更新客户端、Web服务端及配置文件覆盖从版本检查、更新包下载到本地替换安装的核心流程。压缩包共40个文件以vb源码、dll动态库、exe可执行程序为主辅以resx资源文件和配置文件结构清晰便于二次开发与部署。已有705人学习下载适合希望掌握更新机制或快速集成更新功能的.NET开发者。通过本套程序可深入理解System.Net网络通信、XML数据解析、文件操作、多线程进度反馈及版本比较等关键技术是一份可直接参考的实战代码方案。1. 更新程序的整体设计思路1.1 为什么需要独立的更新流程做客户端软件的兄弟应该都有体会程序写完了、功能测试通过了最头疼的反而不是开发本身而是怎么把新版本送到用户手里。让人家重新下载安装包一次两次还行频繁更新用户就会烦而且很多用户根本不看官网公告你发再多更新说明他永远在用老版本。所以自动更新这个功能不是锦上添花而是桌面应用的基本配置。拿我这次写的vb.net自动更新程序来说解决的痛点是主程序exe在运行时文件会被系统锁定你没法直接覆盖替换。所以更新流程必须独立出来让一个专门的更新程序去处理下载、校验、替换、重启这一串动作。这跟安装包升级是两回事安装包是重装自动更新是热替换前者要用户手动介入后者是程序自己完成。用vb.net来写这个功能最大的优势是语法结构清晰、上手快。尤其是代码阅读性比某些脚本语言好理解得多。再加上Windows平台上vb.net跟系统组件的交互能力很强文件操作、网络请求、进程管理这些活写起来非常顺手。1.2 更新程序的核心工作流程整个更新程序跑起来以后就是一个标准的状态机检查更新 -- 下载安装包 -- 校验文件 -- 替换文件 -- 重启主程序。每个环节都要考虑到失败的情况比如网络断了、服务器返回404、文件下载了一半、目标文件被占用等等。我设计的具体流程是这样的主程序启动的时候通过一个后台线程调用更新程序updater.exe这个更新程序先访问服务器上的版本配置文件拿到服务器端最新版本号跟本地版本号比对。如果需要更新就弹出更新界面显示当前版本、新版本、更新内容用户点确认后开始下载更新包。下载完成后自动校验哈希值然后关掉主进程解压/替换文件最后重新拉起主程序。整个链路里每一步失败都有对应的提示和回退机制。补充一点我见过有人把更新逻辑直接写进主程序里其实这样坑很大。主程序一旦更新到一半崩溃下次启动可能就是一个不完整的程序连更新功能都跑不起来了。独立更新程序的方案虽然多一个exe文件但稳定性完全不在一个层次。这算是我做这个项目时第一个确认下来的设计决策。2. 更新配置与版本校验的实现2.1 服务器端清单文件设计更新程序要知道有没有新版本靠的是一个清单文件。我在服务器上放了一个version.xml结构很简单但字段设计上是有讲究的?xml version1.0 encodingutf-8? updateInfo version1.3.2/version minVersion1.0.0/minVersion downloadUrlhttp://yourdomain.com/update/app_1.3.2.zip/downloadUrl fileHashD2A4F0B1E8C93D7E6A5F4B3C2D1E0F9A8B7C6D5E/fileHash releaseNotes - 修复了导出报表时偶尔崩溃的问题 - 优化了数据加载速度 - 新增了批量操作功能 /releaseNotes forceUpdatefalse/forceUpdate /updateInfoversion字段是服务器端当前最新版本号minVersion是强制更新阈值downloadUrl指向更新包的下载地址fileHash是更新包的SHA1哈希releaseNotes给用户展示更新说明forceUpdate标记这次更新是否强制。这里有个关键点文件必须单独存哈希不能只靠文件名判断。因为很多时候开发环境打包出来的版本号没变但代码变了。有一点提醒一下downloadUrl尽量用域名不要用IP。因为以后如果换服务器IP更新包链接全挂了用户端的更新程序会一直提示检查更新失败排查起来很被动。我第一版就犯了这个错误后来统一的静态资源域名再也没出过这个问题。2.2 版本比较与更新判断清单文件拿到之后最关键的一步是版本号比较。这里有一个常见的坑版本号不能当字符串比。比如你本地是1.9.0服务器是1.10.0字符串比较的话1.9.0大于1.10.0因为字符串按字符逐位比较9比1大。结果就是永远检测不到新版本。用vb.net的话最稳妥的做法是直接用System.Version类型。这个类型专门为版本号设计天然支持分段比较Dim localVer As New Version(Application.ProductVersion) Dim serverVer As New Version(serverVersionString) If serverVer localVer Then 有新版本进入更新流程 If serverVer.Major localVer.Major 1 Then 大版本跨度过大建议强制更新 End If Else 已经是最新版本 End IfVersion类的构造函数会自己解析1.3.2这种格式然后(大于)运算符会依次比较Major、Minor、Build、Revision四个分段不会出现上面说的9比10大的问题。这里要注意如果版本号格式不规范Version.Parse会抛异常。所以我实际开发时会包一层Try-Catch解析失败就视为未知版本直接跳过更新并写日志。对于minVersion的判断也一样如果服务端的minVersion大于本地版本那就不管用户愿不愿意强制走更新流程——这种场景通常是服务器端数据格式变更老版本连不上新协议不更新就用不了。3. 下载与文件更新实现3.1 使用WebClient下载更新包更新包通常是zip压缩包里面包含需要替换的程序文件、依赖dll、配置文件。下载这一步vb.net里有好几种方案WebClient、HttpWebRequest、HttpClient。很多老项目喜欢用WebClient因为它代码最简洁几行就能搞定文件的下载和进度事件。Using client As New WebClient() AddHandler client.DownloadProgressChanged, AddressOf OnDownloadProgress AddHandler client.DownloadFileCompleted, AddressOf OnDownloadCompleted 设置超时时间避免网络异常时卡死 client.Headers.Add(User-Agent, AutoUpdater/1.0) 同步方式下载配合后台线程使用 client.DownloadFile(downloadUrl, tempFilePath) End UsingDownloadProgressChanged事件能拿到当前下载进度用于界面上的进度条展示Private Sub OnDownloadProgressChanged(sender As Object, e As DownloadProgressChangedEventArgs) e.ProgressPercentage是0-100的整数直接给进度条用 progressBar.Value e.ProgressPercentage labelStatus.Text String.Format(正在下载... {0}% ({1:F1} MB / {2:F1} MB), e.ProgressPercentage, e.BytesReceived / 1024 / 1024, e.TotalBytesToReceive / 1024 / 1024) End Sub下载完成的事件里要检查一下e.Error看看是不是因为断网或者超时失败。我测试的时候发现WebClient的Timeout属性其实不太好使它的默认值是100秒但对DownloadFileAsync来说超时控制有时候不那么精确。所以我后来在DownloadFileCompleted事件里判断错误加上秒表计时超过预期时间就主动CancelAsync。另外下载路径建议用Path.GetTempPath()下的临时目录不要直接放程序目录或者桌面防止杀毒软件和权限问题的干扰。下载到临时目录的好处还在于即使下载到一半失败也不会污染正在运行的主程序文件更新失败还可以重新下载。3.2 文件校验与备份回滚下载完成不等于可以直接覆盖必须校验完整性。这一步是在DownloadFileCompleted之后单独做的方法。我用SHA1对下载好的zip进行哈希计算跟清单文件里的fileHash比对。这里有个逻辑细节如果校验失败直接弹窗报更新包损坏请重试然后删除临时文件结束更新流程。不要想着反正文件大小差不多凑合用吧压缩包只要错一个字节解压就可能失败强行继续的结果是主程序文件被替换成残缺版本启动不了比不更新更糟。Private Function CheckFileHash(filePath As String, expectedHash As String) As Boolean Try Using sha1 As New Security.Cryptography.SHA1CryptoServiceProvider() Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read) Dim hashBytes As Byte() sha1.ComputeHash(fs) Dim actualHash As String BitConverter.ToString(hashBytes).Replace(-, ).ToLower() Return actualHash.Equals(expectedHash.Trim().ToLower()) End Using End Using Catch ex As Exception Return False End Try End Function哈希校验通过后进入文件替换阶段。我不会直接删老文件然后解压新的而是在程序目录下建一个backup文件夹把即将被覆盖的文件先复制一份过去。这样更新中途出了岔子还能从备份恢复。 备份当前版本的文件 Dim backupDir As String Path.Combine(Application.StartupPath, backup, DateTime.Now.ToString(yyyyMMddHHmmss)) Directory.CreateDirectory(backupDir) For Each file In Directory.GetFiles(Application.StartupPath, *.exe) File.Copy(file, Path.Combine(backupDir, Path.GetFileName(file)), True) Next这个备份目录在下次正常启动后可以自动清理掉我建议保留最近2-3个版本的备份万一新版本出现严重问题还能手动回滚。4. 更新过程的界面反馈与交互4.1 进度提示的整体设计更新过程的界面是最容易被低估的一部分。很多开发者觉得反正就一个进度条没多大技术含量。但实际用起来才会发现如果界面提示做得含糊用户会在更新过程中反复杀进程觉得程序卡死了。我做了一个独立的更新窗口FormUpdater上面有三个核心元素当前操作状态的文字提示比如正在检查更新...正在下载更新包 45%正在安装更新...、进度条、以及一个动态动画。进度条这块有个细节就是不同阶段的进度语义不一样。下载阶段进度条跟着DownloadProgressChanged走这是真实的网络进度但到了解压/替换阶段没有现成的进度事件只能自己处理。我的做法是把100%的进度拆成两段下载阶段占0-80%安装阶段占80%-100%。这样用户看到的进度条是持续增长的不会在安装中卡住很久显得像死机。4.2 用正弦曲线做动态等待动画的过程这里要说到vb.net的Math.Sin了。安装阶段没法量化进度的时候进度条是定在那儿不动的容易让用户误判。我在窗口的底部放了一个自定义绘制的Panel画一条不断流动的正弦波来表示程序还在干活别关我。这个思路其实是早期网页加载动画的常见玩法但桌面程序里很多人忘了用。Private Sub DrawWave(sender As Object, e As PaintEventArgs) Dim g As Graphics e.Graphics g.SmoothingMode Drawing2D.SmoothingMode.AntiAlias Dim wavePen As New Pen(Color.FromArgb(78, 140, 210), 2.5F) Dim yOffset As Integer wavePanel.Height / 2 Dim amplitude As Integer 12 Dim phase As Double wavePhase Dim points As New List(Of PointF)() For x As Integer 0 To wavePanel.Width Step 2 核心y sin(x * 频率 相位差) * 振幅 中心偏移 Dim y As Single CSng(yOffset Math.Sin(x * 0.03 phase) * amplitude) points.Add(New PointF(x, y)) Next g.DrawLines(wavePen, points.ToArray()) wavePen.Dispose() End Sub然后启一个Timer每20毫秒把wavePhase加0.15然后让Panel重绘。正弦函数在这里的作用就是生成一条平滑的上下起伏的曲线它的周期和振幅决定了波浪的形状。如果不用正弦而是用简单的锯齿波或者方波画面看起来就会很突兀——正弦波的优势在于它是一条连续可导的曲线视觉上最自然。这段动画的代码量不大但对整个更新体验的提升非常明显。界面上我还会加一个更新详情的折叠区域里面显示当前正在写入哪个文件名方便技术背景的用户判断是不是卡住了。普通用户不会去看这个区域但一旦遇到问题截图发过来我就能很快定位是替换到了哪个文件。5. 常见问题与排查技巧实录5.1 文件被占用导致替换失败这个问题大概率是更新程序最常见的坑了。虽然更新之前我会先尝试关闭主程序进程但用户可能同时开着好几个程序的实例或者有某个进程把exe/dll锁住了。处理办法我在代码里做了三重保障第一重替换前先调用Process.GetProcessesByName找到主程序进程并杀掉这是在保存好用户数据之后。第二重如果杀进程没杀干净有时候进程一会儿自动重启就等下300毫秒再试一次。第三重如果还是失败就写一个restart.bat批处理到临时目录把替换操作延后到系统重启时进行同时提示用户需要重启电脑完成更新。Private Sub KillMainProcess() Dim procs As Process() Process.GetProcessesByName(MyApp) For Each p As Process In procs Try p.Kill() p.WaitForExit(1000) Catch ex As Exception 没有权限或者进程已退出忽略 End Try Next Threading.Thread.Sleep(500) 等待文件句柄释放 End Sub这个方案实测下来能解决九成以上的文件占用问题。剩下的那一成一般是杀毒软件在后台扫描exe文件导致锁死——这时候bat脚本的方案就派上用场了。5.2 杀毒软件误报的排查更新程序、注册表操作、下载执行文件这些行为天生容易触发杀毒软件的敏感监控。我自己遇到过的情况是更新程序编译后第一次运行时Windows Defender提示检测到可疑行为。排查下来是因为更新程序里用到了下载并执行文件的逻辑杀毒软件把它判成了Downloader类型。解决思路有几个方向一是代码签名买一个代码签名证书给exe签名这个能解决大多数杀毒软件的误报尤其是360、腾讯管家这类国内软件对未签名程序的信任度很低。二是不做下载后立即执行这种模式改为主程序启动时校验更新文件分步执行降低行为特征上的风险。三是如果只是内网工具可以在部署文档里写明添加白名单的步骤。签名证书一年几百块对于正式发布的产品来说这个成本省不了。对个人开发者或者开源项目来说可以考虑用免费证书自签但注意自签证书仍然会有警告只是在部分杀毒软件中信任度会稍高一些。我的实际项目后来上了代码签名误报率基本降到零。5.3 更新后主程序启动失败的自动回滚这个问题虽然不常发生但一旦发生就是事故级别。我做了一个lastsuccess.log文件主程序每次正常启动后都会更新这个文件的时间戳。更新程序在执行替换后把旧版本文件留在backup目录然后等待主程序启动。主程序启动后如果30秒内没有正常初始化通过命名管道或者文件标志通知更新程序更新程序就自动从backup目录恢复旧版本并提示用户新版本启动失败已回滚到旧版本。这个自动回滚逻辑我建议一定要做。因为有些bug只在用户的特定环境下才会触发开发环境根本测不出来。没有回滚机制的话用户卡在一个起不来的版本上只能手动重装体验非常糟糕。实际开发中我还加了一个尝试次数限制最多自动回滚两次如果两次都失败就停止尝试并弹窗提示用户联系技术支持避免无限循环重启。5.4 更新包空格和中文文件名的问题小坑但真的很常见。服务器上放更新包的时候文件名里带了空格或者中文下载时URL编码处理不好就会出现404或者下载损坏。我的做法是服务器上的文件名统一用不含空格、不含中文的格式比如app_1_3_2.zip版本号里的点换成下划线。然后在版本配置文件里直接写全URL更新程序下载时原样使用。这样最省事也最不容易出错。如果你一定要用带空格的URL记得用Uri.EscapeUriString()转义一下但我个人建议别折腾改文件名最简单。6. 更新代码执行效率和稳定性的一些建议6.1 使用后台线程避免界面卡顿更新程序如果直接在UI线程里做网络下载和文件操作界面一定会卡死用户会以为程序没响应然后手动关掉。vb.net里我用BackgroundWorker或者Task.Run来处理这些耗时操作。注意在更新界面初始化的时候就把更新中不允许关闭窗口的逻辑做了——通过FormClosing事件里判断状态防止用户误关导致更新流程中断。Private Sub FormUpdater_FormClosing(sender As Object, e As FormClosingEventArgs) Handles MyBase.FormClosing If isUpdating Then e.Cancel True labelStatus.Text 正在更新中请勿关闭窗口... End If End Sub如果用户真的强杀进程更新到一半也不怕——因为我们有临时目录和备份机制下次启动时更新程序检测到临时文件存在会先清理未完成的部分再重新下载。6.2 断点续传与失败重试策略更新包小的时候几MB到几十MB断点续传不太需要。但如果你要更新的软件包含大量资源文件更新包可能到几百MB这时候下载过程中网络一抖全部重新下载体验就很差。我给大文件更新场景加了一个简单的重试逻辑下载失败后自动重试3次每次间隔2秒、4秒、8秒指数退避。断点续传实现相对复杂需要服务端支持Range请求如果没时间做重试3次基本也能扛过临时网络波动。另外下载的时候把临时文件先写成.part格式下载完成后改名为正式zip这样即使在临时目录里看到半截文件也能快速识别是否是有效的更新包。6.3 增量更新的思路扩展如果更新包体积特别大全量更新确实会消耗大量带宽和时间。增量更新的思路是只下载两个版本之间发生变更的文件。具体做法是在清单文件里加一个files节点列出每个文件的相对路径、新版本号、文件哈希。更新程序比对本地文件列表找出需要更新的文件只下载这些。这样更新包就从整个软件变成变更文件集合体量往往缩小到原来的十分之一甚至更少。增量更新的复杂度在于需要维护历史版本的文件清单但如果你的软件已经做了一年以上更新频率一个月一次这个投入是值得的。我这边现在的方案是混合式小版本走增量更新大版本比如跨大版本的功能重构走全量更新。判断依据就是变更文件数量和总体积超过50MB就全量否则增量。7. 一些额外的细节最后再分享一个我踩过的坑更新程序本身也要考虑自身的更新。一个常年不更新的更新程序如果有一天主程序换了启动方式、引用了新的依赖或者系统环境变化更新程序本身可能就该升级了。我的做法是更新程序启动时先检查自身的版本如果需要更新先把自己更新好再更新主程序。这一步逻辑不复杂但很容易被忽略。还有一个小细节是更新日志的记录。自动更新不像手动安装用户不知道中间发生了什么。我会在更新程序所在目录下维护一个update_log.txt记录每次更新的时间、旧版本号、新版本号、下载耗时、替换结果。这样用户报问题时直接看日志就能定位是哪个环节出了问题。如果你把自动更新做成一个独立可复用的模块考虑作为一个公共服务供多个项目共用你会发现收益会持续放大。这也可以是一种思路把这个vb.net更新程序沉淀成一个独立的项目以后新项目启动时直接引用省掉反复开发的时间。代码架构上尽量保持独立性界面逻辑跟业务逻辑分离引入的时候只需要改改URL和主程序名剩下的通用。从我个人的实际经验来看vb.net写这种程序其实没什么高深的技术关键是把边界情况想全了把用户可能会干的各种奇怪操作都考虑进去然后做好对应的兜底处理。代码好不好是其次稳不稳才是核心。你要是照着上面的思路撸一遍做一个能用的更新程序一个下午基本就够了。剩下的是后续长期使用中不断遇到问题、不断修正的过程。本文还有配套的精品资源点击获取
返回列表