首页 / 资讯中心 / 文章详情

VB.NET桌面软件自动更新程序实现:从版本检查到文件替换

VB.NET桌面软件自动更新程序实现:从版本检查到文件替换 ★ FEATURED ARTICLE
简介面向桌面软件自动升级需求的 VB.NET 更新程序方案适合.NET桌面开发者或需要为软件增加在线更新能力的团队。资源以VS解决方案形式提供包含AutoUpdate客户端与WsAutoUpdate上传/发布服务端可解决用户无需手动下载安装即可自动获取新版本的核心问题覆盖版本检测、HTTP下载、XML/JSON解析、文件覆盖、进度显示、异常处理与多线程更新等关键技术点。包体为RAR压缩包共40个文件约79KB以vb源码工程为主另有exe可执行文件、dll类库、pdb调试符号、sln/vbproj工程配置以及xml、asmx网络服务文件结构完整便于按模块对照学习。已有708人学习下载。通过工程可快速掌握VB.NET开发自动更新机制的完整流程尤其能理解客户端与服务端交互、配置管理、权限处理及更新回退等实操细节。1. 自动更新程序是干什么的桌面软件发版不用再群发压缩包了每个维护过桌面软件的开发大概都经历过这种尴尬新版改了一个关键bug但用户装的一直是三个月前的包你只能一个群一个群发消息、传压缩包、远程指挥对方解压覆盖。这套用 vb.net 写的自动更新程序就是为了解决这个事。它是一个独立的更新器集成到 WinForms 或 WPF 工程之后主程序启动时自动请求更新服务器有新版本就下载、校验、替换、重启全程用户只看到一个进度条。适合团队内部工具、中小型桌面客户端也适合作为模块嵌进已有 .NET 工程。核心逻辑不依赖任何第三方库拿过来就能改。2. 技术选型与整体架构为什么是 VB.NET、HTTP 下载加文件清单2.1 为什么用 VB.NET 写更新器VB.NET 和 C# 编译出来的 IL 是一致的.NET Framework 下能用的类库几乎完全重叠。选 VB.NET 来做这个更新器通常只有两个现实原因项目本身就是 VB.NET 老工程接入更新器不用给团队引入第二种语言习惯或者维护者只熟 VB.NET 语法希望源码拿来自己能立刻改得动。我看了这套代码它的实现完全依赖标准库System.Net 做 HTTP 请求System.Security.Cryptography 做哈希校验System.IO 做文件操作没有引入 NuGet 包。目标框架是 .NET Framework 4.5 往上放到 4.6.2、4.7.2 也可以直接编译。有一点需要提醒VB.NET 默认 Option Strict 是关闭的也就是允许隐式类型转换从 C# 转过来的人会不习惯。实际改代码前我建议你先把 Option Strict On能提前暴露一堆类型隐患。2.2 更新流程拆成四个阶段一次完整更新从用户启动客户端开始拆成检查、下载、校验、替换四段。检查阶段读本地版本号请求服务器上的版本清单下载阶段把清单里列出的文件逐个拉到临时目录下载过程中上报进度校验阶段对拉下来的文件做哈希对比防止文件损坏或中间被劫持替换阶段先停掉主程序进程备份旧文件再覆盖新文件最后拉起主程序。这四个阶段的顺序不能乱。跳过校验直接替换是很多自动更新程序“更新完程序反而打不开”的根因。跳过备份直接覆盖一旦新版文件有问题用户连后悔药都没有。所以这套代码把每个阶段拆成了独立方法任意一步失败都不会进入下一步。2.3 目录结构与文件职责AutoUpdater/ ├── AutoUpdater.sln ├── AutoUpdater/ │ ├── AutoUpdater.vbproj │ ├── FormMain.vb 更新进度窗口 │ ├── UpdateManager.vb 更新调度核心 │ ├── FileChecker.vb 文件哈希校验 │ ├── ConfigManager.vb 服务器地址等配置读取 │ └── app.config 服务器地址、超时等参数 └── ServerSample/ ├── version.xml 版本清单示例 └── packages/ 更新包文件目录FormMain 只负责显示进度条和按钮状态里面不写业务逻辑。UpdateManager 是核心调度器它调 FileChecker 做校验调下载方法拉文件调替换方法应用更新。这样拆的好处是以后想改成静默更新、强制更新只需要在 UpdateManager 上加开关UI 层几乎不用动。如果你打算接手来改建议先从 UpdateManager 读起它相当于整个更新的状态机。3. 核心模块拆解版本检查、断点续传、哈希校验与备份替换3.1 版本检查用 System.Version 而不是字符串比较版本检查是更新流程的入口。这套代码没有读程序集版本号而是用了一个独立的 version.txt 文件好处是发版时不用重新编译主程序也能改版本号。 UpdateManager.vb —— 版本检查部分 Public Function NeedUpdate() As Boolean Dim localVer As Version GetLocalVersion() If localVer Is Nothing Then Return True Dim remoteVer As Version GetRemoteVersion() If remoteVer Is Nothing Then Return False Return remoteVer localVer End Function Private Function GetLocalVersion() As Version Dim vFile As String Path.Combine(Application.StartupPath, version.txt) If Not File.Exists(vFile) Then Return Nothing Return New Version(File.ReadAllText(vFile).Trim()) End Function Private Function GetRemoteVersion() As Version Dim url As String m_ServerBaseUrl version.xml Dim request As HttpWebRequest DirectCast(WebRequest.Create(url), HttpWebRequest) request.Method GET request.Timeout 5000 Using response As HttpWebResponse request.GetResponse() If response.StatusCode HttpStatusCode.OK Then Return Nothing Using reader As New StreamReader(response.GetResponseStream()) Dim xml As XDocument XDocument.Parse(reader.ReadToEnd()) Return New Version(xml.Root.Element(version).Value.Trim()) End Using End Using End Function这里最关键的是用了 System.Version 类型而不是直接用字符串比大小。System.Version 会把 1.2.3.4 解析成主版本、次版本、Build、Revision 四个段RemoteVer localVer 是比较四个段的数值。字符串比较在这里是个经典坑后面避坑章节会专门说。GetLocalVersion 返回 Nothing 的情况通常是部署时漏了 version.txt代码会保守地认为需要更新让流程继续走宁可多下一次也不漏更新。GetRemoteVersion 里网络异常会被上层 Try/Catch 捕获完整实现里会区分超时和连接失败分别写日志。3.2 下载模块带断点续传的文件拉取下载是整个更新过程最耗时的部分。初学者通常会用 WebClient.DownloadFile但这套代码用了 HttpWebRequest 加 Range 头支持断点续传。下载中断时不用从头再来对体积比较大的程序集来说体验差别明显。 UpdateManager.vb —— 支持断点续传的下载方法 Public Function DownloadWithResume(url As String, localPath As String) As Boolean Dim fileSize As Long 0 If File.Exists(localPath) Then fileSize New FileInfo(localPath).Length End If Dim request As HttpWebRequest DirectCast(WebRequest.Create(url), HttpWebRequest) request.AddRange(fileSize) request.Headers(Accept-Encoding) identity request.Timeout 10000 Dim result As Boolean False Using response As HttpWebResponse request.GetResponse() Dim statusCode As Integer CInt(response.StatusCode) If statusCode 206 OrElse statusCode 200 Then Using fs As New FileStream(localPath, FileMode.Append, FileAccess.Write) Using ns As Stream response.GetResponseStream() Dim buffer(8190) As Byte Dim readCount As Integer ns.Read(buffer, 0, buffer.Length) While readCount 0 fs.Write(buffer, 0, readCount) readCount ns.Read(buffer, 0, buffer.Length) End While End Using End Using result True End If End Using Return result End Function说明几个参数。AddRange(fileSize) 是告诉服务器“我本地已有 fileSize 字节从这之后继续给我”服务器支持的话返回 206 Partial Content不支持的话会返回 200 和全量内容。所以代码里同时接受 206 和 200 两种状态码如果是 200就说明服务器忽略 Range 头而 FileMode.Append 会把全量内容追加到已下载部分的后面导致文件损坏这种情况要靠后面的哈希校验兜底。设置 Accept-Encoding 为 identity 是为了防止服务器对响应做 gzip 压缩。如果服务器返回压缩内容下载下来的字节长度和文件实际大小对不上断点续传的分段逻辑会彻底乱套。服务器端如果要开压缩你这边的下载逻辑也要跟着处理解压常见的做法是更新包文件本身已经压缩过HTTP 层不再二次压缩。3.3 文件校验本地哈希和清单对比文件下载完不能直接用要先算一遍哈希和 version.xml 里记录的哈希对比。这套代码用的是 MD5实现很短但对于内部工具来说够用不是高安全场景没必要上 SHA256计算速度还慢一些。 FileChecker.vb —— 计算文件 MD5 Public Function GetMD5(filePath As String) As String Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read) Using md5 As MD5 MD5.Create() Dim hash As Byte() md5.ComputeHash(fs) Return BitConverter.ToString(hash).Replace(-, ).ToUpper() End Using End Using End Function UpdateManager.vb —— 校验下载结果 Public Function VerifyFile(localPath As String, expectMD5 As String) As Boolean If Not File.Exists(localPath) Then Return False Dim actual As String GetMD5(localPath) Return String.Equals(actual, expectMD5, StringComparison.OrdinalIgnoreCase) End FunctionGetMD5 用 FileStream 打开文件避免一次性把整个文件读进内存。对大文件来说File.ReadAllBytes 的写法会让内存峰值冲到几百 MB流式计算哈希只占几百 KB。VerifyFile 对比时用了 OrdinalIgnoreCase忽略大小写避免服务器生成清单时用了小写、本地算出来是大写这类问题。需要注意MD5 只防传输错误不防恶意篡改。如果更新服务器是公网 IP且通讯没有走 HTTPS中间人可以同时改 version.xml 和更新包MD5 对比形同虚设。所以生产环境部署时更新服务器地址务必用 HTTPS 协议。3.4 替换与重启先杀进程、再备份、后覆盖替换阶段最容易翻车。Windows 下文件被占用时 File.Copy 直接抛 IOException所以在覆盖之前代码会先按进程名杀掉主程序。数据库类程序要注意杀进程前有没有未保存的数据更新器没法判断只能靠主程序在退出前做保存这个逻辑要写在主程序里。 UpdateManager.vb —— 停止目标进程 Private Sub KillProcess(processName As String) For Each p As Process In Process.GetProcessesByName(processName) p.Kill() p.WaitForExit(3000) Next End Sub UpdateManager.vb —— 带备份的覆盖替换 Public Function ApplyUpdate(packageDir As String, targetDir As String, fileList As List(Of String)) As Boolean For Each relPath In fileList Dim srcFile As String Path.Combine(packageDir, relPath) Dim dstFile As String Path.Combine(targetDir, relPath) Dim backupFile As String dstFile .bak If File.Exists(dstFile) Then If File.Exists(backupFile) Then File.Delete(backupFile) File.Move(dstFile, backupFile) End If Try 确保目标目录存在 Dim dir As String Path.GetDirectoryName(dstFile) If Not Directory.Exists(dir) Then Directory.CreateDirectory(dir) File.Copy(srcFile, dstFile, True) Catch 复制失败则回滚备份 If File.Exists(backupFile) Then If File.Exists(dstFile) Then File.Delete(dstFile) File.Move(backupFile, dstFile) End If Return False End Try If File.Exists(backupFile) Then File.Delete(backupFile) Next Return True End Function备份和回滚的逻辑是这套代码里最值得抄的部分。每个文件覆盖前先移动成 .bak新文件复制成功后删掉 .bak中途失败就把 .bak 移回来。注意回滚的顺序要先删掉已写入的半成品再移动 .bak否则会提示目标文件已存在。整个过程在 Try/Catch 内完成任何一个文件失败已经处理过的文件不自动回滚这个策略选择了“尽量把能更新的更新完失败文件单独记录”适合更新包文件数不多、且失败后重跑完整的场景。KillProcess 里的 WaitForExit(3000) 是等进程退出最多 3 秒进程没响应就继续循环下一个不阻塞更新流程。实际部署时杀毒软件可能会同时给主程序和更新器加锁导致替换失败这种情况靠重试容错捕 IOException 后最多重试五次每次间隔一秒是常见的处理方式。4. 运行与部署编译、服务器清单和主程序两种接入方式4.1 编译配置与目标框架在 Visual Studio 里打开 AutoUpdater.sln右键项目进入属性把目标框架选为 .NET Framework 4.6.2 或 4.7.2。如果你的客户端机器系统较老建议选 4.5兼容性更广代价是少了部分新 API。用命令行编译的话msbuild AutoUpdater.sln /p:ConfigurationRelease /p:Platformx86平台目标必须和主程序一致。如果主程序是 x86 编译的更新器也要 x86否则更新器在 64 位系统上会被当成 64 位进程启动访问的文件路径没区别但拉起主程序时位数不一致会有兼容问题。Release 输出目录下会得到 AutoUpdater.exe这个 exe 要和主程序放在同一目录部署。4.2 服务器端版本清单配置服务器只需要一个静态文件服务IIS、Nginx 或者对象存储静态桶都行。把 ServerSample 目录里的 version.xml 和 packages 文件夹传上去配置好 MIME 类型.xml 要能通过 HTTP 直接访问。version.xml 的结构?xml version1.0 encodingutf-8 ? update version1.2.3.0 files file pathMyApp.exe size1024000 md5A1B2C3D4E5F67890ABCDEF1234567890 / file pathMyApp.dll size2048000 md5B2C3D4E5F67890ABCDEF1234567890AB / file pathconfig.json size2048 md5C3D4E5F67890ABCDEF1234567890ABC / /files /updateupdate 节点是版本号等价于前面版本检查里读到的 remoteVer。files 节点里每项对应一个要更新的文件。path 是相对主程序目录的路径不要带盘符不要写绝对路径也不要写 ../ 这种跨目录引用。更新器会把 path 拼到服务器地址后面得到 http://服务器地址/update/MyApp.exe。size 字段实际上在下载逻辑里不是必须的但它能用来在下载前做空间预检也能在下载完后快速比对文件长度发现长度不符可以直接判失败不用等哈希计算。MD5 字段是校验的核心。清单必须用打包脚本生成不能手写。我自己的习惯是在打包电脑上跑一个批处理对 packages 目录下所有文件算 MD5自动生成 version.xml人工手写几乎必漏文件。4.3 主程序接入启动器模式和后台检查模式主程序接入更新器有两种常见模式。第一种是启动器模式把用户真正双击的 exe 换成更新器主程序改名为你自己的名字更新器检查完再拉起主程序。第二种是后台检查模式主程序正常启动空闲时发起检查发现新版本弹窗问用户确认后由更新器接管。模式侵入性用户体验适用场景启动器模式主程序代码零改动启动时先看进度条略拖慢启动老项目、内部工具后台检查模式主程序需加调用逻辑不打断当前操作正式对外产品、频繁使用工具这套源码默认按启动器模式设计主程序不需要调用更新器的代码用户双击 AutoUpdater.exe 后更新器负责下载替换最后 Process.Start 拉起主程序。如果你改成后台检查模式主程序里会有一段类似的代码 主程序中的调用示例后台检查模式 Private Sub CheckUpdate() Dim p As New Process() p.StartInfo.FileName Path.Combine(Application.StartupPath, AutoUpdater.exe) p.StartInfo.Arguments --check p.Start() End Sub主程序调用更新器后自身要退出否则更新器在替换阶段杀不掉主进程。退出前要保证用户数据已经保存这是主程序要做的事。更新器拿到 --check 参数时会先下载并替换文件再启动主程序最后退出自己整个衔接靠进程调度完成。5. 避坑记录自动更新最常见的五个翻车现场5.1 替换时提示“另一个程序正在使用此文件”现象更新器运行到 File.Copy 时抛出 IOException界面卡在替换阶段。原因主程序没有完全退出或者主程序杀掉了但子进程还在运行。也可能是杀毒软件在安装或扫描 exe 文件短暂占用句柄。解决替换前按进程名 Kill 主程序并 WaitForExit如果主程序会拉子进程把子进程也列入等待列表。杀毒软件造成的占用无法预判更新器需要加一个重试循环捕获 IOException 后延时 1 秒重试最多 5 次再失败才报错。5.2 断点续传下载完成但 MD5 对不上现象下载进度显示 100%VerifyFile 返回 False反复重试都一样。原因服务器不支持 Range 请求时返回 200 和全量内容但本地已有部分文件代码用 FileMode.Append 打开文件把全量内容接到了旧数据后面。另一种可能是服务器开启了 gzip 压缩下载内容不是原始字节。解决在 DownloadWithResume 里先判断 HTTP 状态码是否 206如果是 200 且本地文件长度大于 0先删除临时文件再重新全量下载。同时已经在请求头里指定了 Accept-Encoding: identity部署服务器时也尽量关掉对 .exe 的动态压缩。5.3 版本号 1.10.0 被判成小于 1.2.0现象客户端装的是 1.2.0服务器已经发到 1.10.0更新器却提示已是最新版本。原因代码里用了字符串比较。字符串按字符顺序逐位比较“1.10.0”的第三个字符是“1”“1.2.0”的第三个字符是“2”所以得出 1.10.0 更旧的错误结论。解决版本号必须用 System.Version 类型解析和比较。Version 会把四段全部转成数值再比较10 2 自然成立。检查一下你的代码里有没有把版本号当 String 直接比较这是更新逻辑里最不起眼也最容易翻车的地方。5.4 杀毒软件把更新器当成木马隔离现象部分机器部署后报毒AutoUpdater.exe 被直接隔离主程序起不来。原因更新器行为特征就是“下载程序文件 杀进程 覆盖文件”和某些木马行为高度重合。没有数字签名的 exe 误报率相当高。解决给主程序和更新器都加代码签名证书Windows Defender 会降低拦截概率。内部工具没有证书就在部署文档里写明加白名单。不建议用加壳工具规避查杀加壳会让误报率更高还会影响文件哈希校验。5.5 用户本地配置被更新包覆盖丢失现象版本升级后用户的窗口布局、账号配置、缓存文件全部被还原成初始状态。原因更新包打包时把整个程序目录都打进去了version.xml 里没有排除 config、cache 这类用户数据路径。替换阶段按照清单把所有文件都覆盖了一遍包括用户自己的配置文件。解决在文件清单里排除需要保留的目录或者在 ApplyUpdate 时判断当前文件路径是否属于用户数据区属于的跳过。更好的做法是把用户数据放到 %AppData% 下的独立目录程序目录只放 exe 和 dll更新器永远不碰 AppData这样更新和用户数据彻底隔离。6. 二次开发加静默更新参数和抽离更新核心类库6.1 给更新器加一个 --silent 参数团队内部工具做强制静默更新很常见场景是要求用户下次启动时自动更新不弹任何窗口。改造点在 FormMain 的构造函数里解析命令行参数有 --silent 就隐藏主窗口更新完成后照常拉起主程序。Public Sub New() InitializeComponent() Dim args As String() Environment.GetCommandLineArgs() m_Silent args.Contains(--silent) If m_Silent Then Me.ShowInTaskbar False Me.Opacity 0 Me.WindowState FormWindowState.Minimized End If End Sub窗口隐藏不代表逻辑停了UpdateManager 的流程照走。唯一要处理的是失败时的提示静默模式下没有弹窗错误日志必须写到文件否则用户只看到“更新失败”四个字原因无从查起。我一般会在静默模式下把异常堆栈写到程序目录下的 update_error.log方便远程排查。6.2 把更新逻辑抽成独立类库如果你想在多个产品里复用这套更新器建议把 UpdateManager、FileChecker、ConfigManager 从 exe 工程里挪到一个独立的类库工程比如 AutoUpdater.Core。UI 工程只引用类库通过事件接收进度和状态变化。这样主程序也可以在后台线程里直接调用类库完成更新不必借助外部 exe 进程。 主程序内联调用示例 Dim updater As New UpdateManager(http://your-server/update/) AddHandler updater.ProgressChanged, Sub(percent As Integer) statusLabel.Text percent.ToString() % End Sub updater.DoUpdate()抽成类库有个隐藏收益主程序和更新器可以共用同一份连服务器配置不会出现“服务器地址换了一批机器忘记同步”的情况。从那以后我每次打包新版本都会强制走一遍完整流程清空临时目录、重新生成 version.xml、在一台干净机器上从旧版本跑一次更新、确认 MD5 一致、再确认主程序能正常拉起。这套动作看起来繁琐但比起让用户装到一半看到“校验失败”的红叉值得。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站