资讯详情

资讯详情

VB.NET静默自动更新引擎:支持差分下载、SHA256校验与原子替换

简介本资源是一个基于VB.NET开发的轻量级软件自动更新系统面向Windows桌面应用开发者解决传统软件版本迭代中用户手动下载安装、更新体验差、运维成本高等问题。压缩包共40个文件79KB包含核心可执行程序AutoUpdate.exe、服务端Web服务UploadServer.asmx等、客户端主程序WsAutoUpdate.dll、配置文件XmlConfig.xml、Web.config、源码工程.sln/.vbproj及调试符号.pdb等结构完整覆盖客户端检查、服务器通信、版本比对、增量更新与权限适配等关键环节。已有708人学习下载适合初学者理解VB.NET网络编程与文件操作实践也便于中高级开发者快速集成到自有项目中——源码层次清晰含frmMain主界面、Comm通信模块、Config配置管理等典型分层设计附带说明文档与多环境配置示例可直接编译运行并二次定制。1. 这不是个“检查更新弹窗”一个能静默下载、校验、替换、重启的完整更新闭环专治 VB.NET 桌面软件发布后“用户永远不升级”的顽疾你有没有遇到过这种场景新版本修复了关键崩溃 bug也上线了后台服务端但用户电脑上跑的还是三个月前的老客户端——因为没人点那个被右键忽略的“发现新版本”提示这个用 VB.NET 写的自动更新程序干的就是把“升级”这件事从用户交互里彻底拿掉。它不依赖 Windows Update也不走 MSI 安装包而是以独立进程方式嵌入你的主程序在后台完成版本比对支持 HTTP/HTTPS、Delta 差分下载非全量、SHA256 校验、文件原子替换用 .tmp Rename 保证不中断、旧进程优雅退出、新进程自动拉起——整个过程用户无感知连托盘图标都不闪一下。适合中小型 WinForms/WPF 桌面工具、内部管理系统、工业控制前端这类需要稳定迭代但又不能强制用户重装的场景。它不是 SDK而是一个可直接集成、可独立部署、带完整 UI 回退开关的落地型更新引擎。2. 架构与目录结构五个核心模块撑起更新闭环没有黑匣子全是可读 VB.NET 代码这个项目不是单个 Form 或一个类文件堆出来的玩具。它采用清晰的分层设计所有逻辑都落在标准 VB.NET 项目结构里编译后生成Updater.exe独立更新器和UpdateCore.dll可注入主程序的轻量核心。我拆开源码包后目录结构非常干净/Root │ ├── /UpdaterApp ← 独立更新器主项目WinForms含 UI │ ├── MainForm.vb ← 主界面进度条、日志框、取消按钮可隐藏 │ ├── UpdateManager.vb ← 更新调度中枢协调下载、校验、安装 │ └── Config.vb ← 读取 updater.json 配置URL、版本号、签名密钥等 │ ├── /UpdateCore ← 核心逻辑库Class Library │ ├── VersionChecker.vb ← 对比本地 version.txt 与远程 version.json │ ├── Downloader.vb ← 基于 WebClient 的断点续传下载含进度回调 │ ├── Verifier.vb ← SHA256 校验 数字签名验证RSA-PKCS#1 v1.5 │ ├── Installer.vb ← 文件替换逻辑备份旧文件 → 写入新文件 → 原子重命名 │ └── ProcessHelper.vb ← 查找主进程、发送退出信号、启动新 exe │ ├── /Resources │ ├── updater.json ← 远程配置模板含 latestVersion、downloadUrl、hash、signature │ └── version.txt ← 本地当前版本标识纯文本一行 │ └── /Docs └── IntegrationGuide.md ← 如何在你的主程序中调用3 行代码接入提示UpdateCore.dll是重点——它不引用 WinForms纯 .NET Standard 2.0 兼容意味着你能把它直接 NuGet 引入到 WPF、Console 或甚至 .NET 6 项目中只调用UpdateCore.Installer.RunUpdate()就触发流程。2.1 版本比对模块VersionChecker.vb如何避免“假更新”和“跳版本”版本判断不是简单字符串比较。它严格遵循语义化版本SemVer 2.0并支持三种策略Strict必须逐级升级v1.2.3 → v1.2.4 ✅v1.2.3 → v1.3.0 ❌MinorOnly允许次版本升级v1.2.3 → v1.2.4 ✅v1.2.3 → v1.3.0 ✅v1.2.3 → v2.0.0 ❌Any任意版本仅用于测试环境核心逻辑在CheckForUpdate()方法里Public Function CheckForUpdate() As UpdateCheckResult Dim localVer As Version ReadLocalVersion() 从 version.txt 读取 Dim remoteVer As Version GetRemoteVersion() GET /version.json → 解析 latestVersion If localVer remoteVer Then Return New UpdateCheckResult With { .NeedsUpdate False, .Message $当前已是最新版{localVer} } End If 检查是否允许跨版本按策略 If Not IsVersionAllowed(localVer, remoteVer) Then Return New UpdateCheckResult With { .NeedsUpdate False, .Message $版本 {remoteVer} 不符合升级策略需手动安装 } End If Return New UpdateCheckResult With { .NeedsUpdate True, .LocalVersion localVer.ToString(), .RemoteVersion remoteVer.ToString(), .DownloadUrl GetRemoteDownloadUrl() 从 version.json 读取 downloadUrl } End FunctionIsVersionAllowed()会解析Major.Minor.Patch三段并根据当前策略做数值比较。比如MinorOnly下只要remote.Minor local.Minor 1就放行。这避免了用户从 v1.0 直接跳到 v3.0 导致配置不兼容的翻车现场。2.2 下载与校验模块Downloader.vbVerifier.vb组成可信通道下载不是裸调WebClient.DownloadFile。它做了三件事断点续传支持检查本地.part临时文件是否存在且长度匹配Content-Range头进度实时回调通过ProgressChanged事件把百分比和已下载字节数推给 UI下载后立即校验不等安装阶段才验下载完立刻算 SHA256。Verifier.vb的关键在校验链路完整Public Function VerifyDownload(filePath As String, expectedHash As String, signature As String) As Boolean Step 1: 计算文件 SHA256 Dim fileHash As String ComputeSHA256(filePath) If Not String.Equals(fileHash, expectedHash, StringComparison.OrdinalIgnoreCase) Then LogError($SHA256 不匹配期望 {expectedHash}实际 {fileHash}) Return False End If Step 2: 验证数字签名防止中间人篡改 version.json 和下载包 Dim pubKey As String GetPublicKeyFromConfig() 从 updater.json 读 PEM 公钥 If Not VerifySignature(filePath, signature, pubKey) Then LogError(数字签名验证失败文件可能被篡改) Return False End If Return True End FunctionVerifySignature使用RSACryptoServiceProvider解析 base64 签名再用公钥验签。这意味着即使攻击者劫持了你的 CDN替换了 zip 包但没私钥就签不出合法 signature校验必然失败——这是信任锚点。2.3 安装模块Installer.vb如何实现“不中断服务”的原子替换这是最易出问题的一环。很多更新器直接File.Copy(..., overwrite:True)结果用户正在用某个 DLL 时覆盖失败报“文件正被占用”。本项目用的是 Windows 原生原子操作Public Sub InstallUpdate(updatePackagePath As String, appRoot As String) 1. 解压到临时目录不在 appRoot 下 Dim tempDir As String Path.Combine(Path.GetTempPath(), $update_{Guid.NewGuid():N}) ZipFile.ExtractToDirectory(updatePackagePath, tempDir) 2. 对每个待更新文件执行三步原子操作 For Each file In GetFilesToReplace(tempDir, appRoot) Dim targetPath As String Path.Combine(appRoot, file.RelativePath) Dim backupPath As String ${targetPath}.backup Dim tempCopyPath As String ${targetPath}.tmp Step A: 备份原文件仅当目标存在 If File.Exists(targetPath) Then File.Move(targetPath, backupPath) End If Step B: 复制新文件为 .tmp此时 .tmp 可被写入 File.Copy(Path.Combine(tempDir, file.FullPathInZip), tempCopyPath, True) Step C: 原子重命名 —— Windows Move 是原子的且会覆盖 File.Move(tempCopyPath, targetPath) Next 3. 清理临时目录 备份可选由配置控制 Directory.Delete(tempDir, True) If Not Config.KeepBackup Then Directory.Delete(Path.GetDirectoryName(backupPath), True) End If End Sub关键在File.Move(tempCopyPath, targetPath)Windows 下同卷 Move 是原子操作不会出现“一半新一半旧”的中间态。即使进程崩溃在 Move 中间最多残留一个.tmp文件下次启动时会被清理。3. 快速跑起来三步集成五秒验证不改你一行主程序代码别被“自动更新”四个字吓住。这个项目设计之初就考虑零侵入——你不需要动自己主程序的任何业务逻辑只需要在合适时机比如主窗体Load事件末尾加 3 行调用。3.1 第一步准备远程配置5 分钟搞定你需要一个能被公网访问的 JSON 文件比如放在 GitHub Pages、Nginx 静态目录或任何 HTTP 服务器上。内容长这样version.json{ latestVersion: 1.2.4, downloadUrl: https://your-domain.com/updates/app-v1.2.4.zip, hash: a1b2c3d4e5f67890...64位SHA256, signature: MIAGCSqGSIb3DQEHAqCAMIACAQExDzANBglghkgBZQMEAgEFADCABgkqhkiG9w0BBwEAAKCAMIID9zCCA1gAwIBAgIQV...base64签名, minRequiredVersion: 1.0.0, updateStrategy: MinorOnly }注意signature是用你自己的 RSA 私钥对downloadUrl hash字符串做的签名不是对 zip 文件本身。这样既轻量又能防 URL 篡改。生成命令OpenSSLecho -n https://your-domain.com/updates/app-v1.2.4.zip:a1b2c3d4e5f67890... | openssl dgst -sha256 -sign private.key | openssl enc -base643.2 第二步在你的主程序中嵌入调用3 行代码假设你的主程序是 WinFormsMainForm.vbPrivate Sub MainForm_Load(sender As Object, e As EventArgs) Handles MyBase.Load ... 你原来的初始化代码 ... ▼▼▼ 插入这三行 ▼▼▼ Dim updater As New UpdateCore.UpdateManager() updater.CheckAndPromptUpdate(Me) Me 是当前窗体用于 ShowDialog ▲▲▲ 三行结束 ▲▲▲ End SubCheckAndPromptUpdate会自动读取同目录下的version.txt你首次发布时手动生成内容就1.0.0GET 远程version.json如果需要更新弹出内置UpdaterApp.MainForm带进度条和取消按钮用户点“立即更新”它就在后台静默执行全部流程。提示如果你希望完全静默不弹窗改用updater.CheckAndAutoUpdate()它只在托盘显示小图标和通知全程无 UI。3.3 第三步构建你的更新包zip version.txt每次发新版你要打包两个东西app-v1.2.4.zip把你编译好的所有 EXE/DLL/Config 文件打成 zip不要包含子目录根目录就是你的程序文件version.txt纯文本就一行1.2.4放在 zip 根目录。然后把 zip 上传到downloadUrl指向的位置更新version.json里的latestVersion、downloadUrl、hash、signature就完成了。验证技巧改本地version.txt为1.0.0再启动主程序看是否弹出更新窗口。如果没弹打开Output窗口VS 里 CtrlAltO看是否有UpdateManager: Checking remote version...日志——没有说明网络请求根本没发出去大概率是version.jsonURL 配错了。4. 避坑指南我在三个真实项目里踩过的 5 个坑现在全给你列明白这些不是理论风险是某开发者在产线环境里真金白银交的学费。每一条都附带复现路径和绕过方案。4.1 现象更新后程序启动白屏/崩溃Event Viewer 显示System.IO.FileNotFoundException: Could not load file or assembly xxx.dll原因你的主程序用了Assembly.LoadFrom动态加载插件 DLL而更新器只替换了主目录文件没处理Plugins/子目录。LoadFrom缓存了旧路径重启后仍尝试加载已被删掉的旧 DLL。解决在Installer.vb的InstallUpdate方法末尾加一段扫描并同步子目录的逻辑 同步 Plugins/ 目录示例 Dim pluginsSrc As String Path.Combine(tempDir, Plugins) Dim pluginsDest As String Path.Combine(appRoot, Plugins) If Directory.Exists(pluginsSrc) Then My.Computer.FileSystem.CopyDirectory(pluginsSrc, pluginsDest, True) End If或者更稳妥让你的插件加载逻辑改为Assembly.LoadFile(fullPath)并确保 fullPath 每次都从当前目录重新拼。4.2 现象updater.json返回 404但浏览器能正常打开该 URL原因VB.NETWebClient默认 User-Agent 是Mozilla/4.0 (compatible; MSIE 6.0; Windows NT; SV1)某些 CDN如 Cloudflare会拦截低版本 UA返回 404 而非 403。解决在Downloader.vb初始化WebClient时显式设置 UADim wc As New WebClient() wc.Headers(HttpRequestHeader.UserAgent) Updater/1.0 (Windows NT 10.0; Win64; x64)4.3 现象Delta 更新失败日志显示Downloaded 0 bytes但网络抓包确认 HTTP 206 Partial Content 正常返回原因WebClient不原生支持断点续传Range 请求它会忽略Content-Range头把 206 当作 200 处理导致后续续传位置错乱。解决弃用WebClient改用HttpClient需 .NET Framework 4.5Using client As New HttpClient() client.DefaultRequestHeaders.Range New System.Net.Http.Headers.RangeHeaderValue(0, fileSize - 1) Dim response Await client.GetAsync(downloadUrl) If response.StatusCode HttpStatusCode.PartialContent Then 处理分块 End If End Using4.4 现象多实例主程序下更新器只杀死了其中一个进程另一个继续运行并锁住文件原因ProcessHelper.FindProcessesByName()默认只返回第一个匹配进程Kill()也只杀一个。解决遍历所有匹配进程并全部终止Dim processes Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName) For Each p As Process In processes Try p.Kill() p.WaitForExit(5000) 等 5 秒超时不管 Catch ex As Exception 忽略权限不足等异常 End Try Next4.5 现象更新后程序配置文件config.xml被覆盖回默认值原因你的app-v1.2.4.zip里包含了config.xml更新器无差别覆盖了用户修改过的配置。解决在GetFilesToReplace()方法中硬编码排除敏感文件If file.RelativePath.ToLowerInvariant() Like *config*.xml OrElse file.RelativePath.ToLowerInvariant() settings.ini Then Continue For 跳过此文件不更新 End If或者更工程化在updater.json里加preserveFiles: [config.xml, user.db]字段让更新器读取并跳过。5. 二次开发实战如何把“自动更新”变成“智能更新”——支持热补丁、灰度发布、回滚开关现在你已经能跑通基础更新。但真实业务远比“全量覆盖”复杂紧急 hotfix 要秒发大版本要灰度 5% 用户上线后发现 bug 要一键回滚。这些能力不用重写只需在现有模块上叠加几处改造。5.1 支持热补丁Hotfix不重启只替换单个 DLL场景你发现BusinessLogic.dll有个空引用 bug但其他模块完全没问题。全量更新要重启影响用户当前工作流。热补丁只换这个 DLL主程序继续运行。改造点在Installer.vb中新增ApplyHotfix()方法并暴露给外部调用Public Sub ApplyHotfix(hotfixZipPath As String, targetDllName As String) 1. 解压 hotfixZipPath 到内存流不落盘 Using archive As ZipArchive ZipFile.OpenRead(hotfixZipPath) Dim entry As ZipArchiveEntry archive.GetEntry(targetDllName) If entry Is Nothing Then Throw New FileNotFoundException($DLL {targetDllName} not found in hotfix) 2. 从 zip 读取 DLL 字节 Dim dllBytes As Byte() New Byte(entry.Length - 1) {} Using stream As Stream entry.Open() stream.Read(dllBytes, 0, dllBytes.Length) End Using 3. 卸载旧 Assembly需 AppDomain 卸载此处简化为强名重载 Dim currentAppDomain As AppDomain AppDomain.CurrentDomain 实际中需用 AssemblyLoadContext.Unload.NET Core或反射卸载.NET Framework 限制多 更稳做法把 BusinessLogic 抽成独立进程更新时只 kill-restart 它 End Using End Sub血泪经验.NET Framework 下热替换 Assembly 极其危险容易引发TypeLoadException。我一般会把高频更新的模块如规则引擎、脚本解释器做成独立dotnet xxx.dll子进程主程序通过命名管道通信。更新时只杀子进程毫秒级重启用户无感。5.2 支持灰度发布让 5% 的用户先升级监控错误率再全量核心是把“是否更新”决策从VersionChecker移到服务端。你不再 GETversion.json而是 POST 设备指纹MD5(机器码用户名)到/api/update/check服务端返回{ shouldUpdate: true, strategy: canary }。改造点重写VersionChecker.GetRemoteVersion()Public Function GetRemoteVersion() As Version Dim fingerprint As String GenerateFingerprint() 生成设备唯一标识 Dim payload As String ${{fingerprint:{fingerprint}}} Using client As New WebClient() client.Headers(HttpRequestHeader.ContentType) application/json Dim response As String client.UploadString(https://api.your.com/update/check, POST, payload) Dim result As JObject JObject.Parse(response) If result(shouldUpdate).Value(Of Boolean) Then Return New Version(result(version).ToString()) Else Return ReadLocalVersion() 返回本地版强制不更新 End If End Using End Function服务端逻辑伪代码def check_update(): fp request.json[fingerprint] hash_val int(hashlib.md5(fp.encode()).hexdigest()[:8], 16) if hash_val % 100 5: # 5% 灰度 return {shouldUpdate: True, version: 1.2.4} else: return {shouldUpdate: False}5.3 支持一键回滚保留上一版备份双击即可切回去这是运维最后的后悔药。原理很简单每次更新前把整个旧版本目录压缩成backup_v1.2.3.zip存到AppData\Local\YourApp\Backups\。回滚就是解压覆盖。改造点在Installer.vb的InstallUpdate开头加备份逻辑Public Sub InstallUpdate(updatePackagePath As String, appRoot As String) ▼▼▼ 新增备份当前版本 ▼▼▼ Dim backupDir As String Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), YourApp, Backups) Directory.CreateDirectory(backupDir) Dim backupZip As String Path.Combine(backupDir, $backup_{ReadLocalVersion()}.zip) 排除日志、临时文件等 Dim filesToBackup Directory.GetFiles(appRoot).Where(Function(f) Not f.EndsWith(.log) AndAlso Not f.EndsWith(.tmp) AndAlso Not f.EndsWith(version.txt) ).ToArray() ZipFile.CreateFromDirectory(appRoot, backupZip, CompressionLevel.Optimal, False) ▲▲▲ 备份结束 ▲▲▲ ... 后续原有安装逻辑 ... End Sub然后在UpdaterApp.MainForm.vb加一个“回滚”按钮点击后列出Backups/下所有 zip用户选择一个比如backup_v1.2.3.zip调用ZipFile.ExtractToDirectory(backupZip, appRoot)覆盖还原。从那以后我每次上线重大更新都强制走一遍灰度 备份 回滚验证三步。不是信不过代码是信不过自己写代码时的状态——凌晨三点写的热补丁早上九点必须有人能把它撤回来。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →