简介:这套基于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 version="1.0" encoding="utf-8"?> <updateInfo> <version>1.3.2</version> <minVersion>1.0.0</minVersion> <downloadUrl>http://yourdomain.com/update/app_1.3.2.zip</downloadUrl> <fileHash>D2A4F0B1E8C93D7E6A5F4B3C2D1E0F9A8B7C6D5E</fileHash> <releaseNotes> - 修复了导出报表时偶尔崩溃的问题 - 优化了数据加载速度 - 新增了批量操作功能 </releaseNotes> <forceUpdate>false</forceUpdate> </updateInfo>version字段是服务器端当前最新版本号,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写这种程序,其实没什么高深的技术,关键是把边界情况想全了,把用户可能会干的各种"奇怪操作"都考虑进去,然后做好对应的兜底处理。代码好不好是其次,稳不稳才是核心。你要是照着上面的思路撸一遍,做一个能用的更新程序,一个下午基本就够了。剩下的是后续长期使用中,不断遇到问题、不断修正的过程。
本文还有配套的精品资源,点击获取