首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# 實作應用程式線上自動更新功能完整架構設計

     編輯:關於C語言

架構概覽

採用 Client-Server 架構:客戶端定期或啟動時呼叫 Web Service 檢查版本,若有新版本則下載檔案至暫存目錄,驗證無誤後覆蓋本地檔案並重啟應用程式。核心元件:

  • 版本資訊 XML:伺服器端維護,記錄各模組版本號、下載 URL、檔案雜湊值。
  • Web Service (ASMX/WCF/Web API):提供 GetVersionInfo()、DownloadFile() 端點。
  • 客戶端更新引擎:版本比對、檔案下載、備份與覆蓋、重啟邏輯。

版本資訊 XML 結構

<?xml version="1.0" encoding="utf-8"?>
<Modules>
  <Module>
    <Name>MyApp.exe</Name>
    <Version>2.1.0.4</Version>
    <Url_Intranet>https://intranet/update/MyApp.exe</Url_Intranet>
    <Url_Internet>https://cdn.example.com/update/MyApp.exe</Url_Internet>
    <Hash_SHA256>A3F2...</Hash_SHA256>
    <IsCritical>true</IsCritical>
  </Module>
  <Module>
    <Name>CoreLib.dll</Name>
    <Version>1.5.2</Version>
    <Url_Intranet>https://intranet/update/CoreLib.dll</Url_Intranet>
    <Url_Internet>https://cdn.example.com/update/CoreLib.dll</Url_Internet>
    <Hash_SHA256>B7E9...</Hash_SHA256>
  </Module>
</Modules>

客戶端核心程式碼

資料模型

public class ModuleInfo
{
    public string Name { get; set; }
    public Version Version { get; set; }
    public string UrlIntranet { get; set; }
    public string UrlInternet { get; set; }
    public string HashSha256 { get; set; }
    public bool IsCritical { get; set; }
}

public class LocalConfig
{
    public Dictionary<string, Version> InstalledVersions { get; set; } = new();
}

更新引擎主流程

public class AutoUpdater
{
    private readonly string _appPath;
    private readonly string _tempPath;
    private readonly IUpdateService _service; // Web Service Proxy
    private readonly bool _useIntranet;

    public AutoUpdater(string appPath, IUpdateService service, bool useIntranet = true)
    {
        _appPath = Path.GetFullPath(appPath);
        _tempPath = Path.Combine(_appPath, "temp_update");
        _service = service;
        _useIntranet = useIntranet;
    }

    public async Task<UpdateResult> CheckAndUpdateAsync()
    {
        // 1. 取得伺服器版本資訊
        var serverModules = await _service.GetVersionInfoAsync();
        if (serverModules == null || serverModules.Count == 0)
            return UpdateResult.NoUpdate;

        // 2. 讀取本地版本紀錄
        var localConfig = LoadLocalConfig();

        // 3. 比對版本,建立需更新清單
        var toUpdate = new List<ModuleInfo>();
        foreach (var srv in serverModules)
        {
            if (!localConfig.InstalledVersions.TryGetValue(srv.Name, out var localVer) ||
                localVer < srv.Version)
            {
                toUpdate.Add(srv);
            }
        }

        if (toUpdate.Count == 0) return UpdateResult.NoUpdate;

        // 4. 使用者確認 (UI 層處理)
        // if (!UserConfirm(toUpdate)) return UpdateResult.UserCancelled;

        // 5. 下載檔案至暫存目錄
        Directory.CreateDirectory(_tempPath);
        foreach (var mod in toUpdate)
        {
            string url = _useIntranet ? mod.UrlIntranet : mod.UrlInternet;
            string dest = Path.Combine(_tempPath, mod.Name);
            await DownloadWithRetryAsync(url, dest, 3);
            
            // 6. 驗證雜湊值
            if (!string.IsNullOrEmpty(mod.HashSha256) &&
                !VerifyHash(dest, mod.HashSha256))
                throw new InvalidOperationException($"檔案雜湊驗證失敗: {mod.Name}");
        }

        // 7. 備份並覆蓋 (需關閉主程式,通常由啟動器處理)
        BackupAndReplace(toUpdate);

        // 8. 更新本地版本紀錄
        foreach (var mod in toUpdate)
            localConfig.InstalledVersions[mod.Name] = mod.Version;
        SaveLocalConfig(localConfig);

        return UpdateResult.Success;
    }

    private async Task DownloadWithRetryAsync(string url, string dest, int maxRetry)
    {
        using var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
        for (int i = 0; i < maxRetry; i++)
        {
            try
            {
                using var stream = await client.GetStreamAsync(url);
                using var fs = File.Create(dest);
                await stream.CopyToAsync(fs);
                return;
            }
            catch (Exception ex) when (i < maxRetry - 1)
            {
                await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i))); // 指數退避
            }
        }
        throw new Exception($"下載失敗超過重試次數: {url}");
    }

    private bool VerifyHash(string filePath, string expectedHash)
    {
        using var sha = SHA256.Create();
        using var fs = File.OpenRead(filePath);
        var hash = Convert.ToHexString(sha.ComputeHash(fs));
        return string.Equals(hash, expectedHash, StringComparison.OrdinalIgnoreCase);
    }

    private void BackupAndReplace(List<ModuleInfo> modules)
    {
        string backupDir = Path.Combine(_appPath, $"backup_{DateTime.Now:yyyyMMdd_HHmmss}");
        Directory.CreateDirectory(backupDir);

        foreach (var mod in modules)
        {
            string src = Path.Combine(_tempPath, mod.Name);
            string dst = Path.Combine(_appPath, mod.Name);
            
            if (File.Exists(dst))
                File.Move(dst, Path.Combine(backupDir, mod.Name), true); // 覆蓋備份
            
            File.Move(src, dst, true);
        }
        // 刪除暫存目錄
        Directory.Delete(_tempPath, true);
    }

    private LocalConfig LoadLocalConfig()
    {
        string configFile = Path.Combine(_appPath, "UpdateConfig.xml");
        if (!File.Exists(configFile)) return new LocalConfig();
        try { return XmlSerializer.DeserializeFromFile<LocalConfig>(configFile); }
        catch { return new LocalConfig(); }
    }

    private void SaveLocalConfig(LocalConfig config)
    {
        XmlSerializer.SerializeToFile(config, Path.Combine(_appPath, "UpdateConfig.xml"));
    }
}

public enum UpdateResult { NoUpdate, Success, UserCancelled, Error }

啟動器模式 (解決檔案鎖定問題)

主程式執行中無法覆蓋自身 EXE/DLL,需採用 獨立啟動器:

  1. Updater.exe:唯一常駐程式,啟動時檢查更新、執行覆蓋、啟動 MyApp.exe。
  2. MyApp.exe:實際業務邏輯,不包含更新邏輯。
  3. 更新時 Updater.exe 下載新版 MyApp.exe 覆蓋,再重新啟動子進程。
// Updater.exe 邏輯簡化
static async Task Main()
{
    var updater = new AutoUpdater(AppDomain.CurrentDomain.BaseDirectory, new UpdateServiceClient());
    var result = await updater.CheckAndUpdateAsync();
    
    if (result == UpdateResult.Success || result == UpdateResult.NoUpdate)
    {
        // 啟動主程式
        Process.Start(new ProcessStartInfo("MyApp.exe") { UseShellExecute = true });
    }
    // 錯誤時顯示訊息並結束
}

Web Service 端點設計 (ASP.NET Core Web API 範例)

[ApiController]
[Route("api/update")]
public class UpdateController : ControllerBase
{
    [HttpGet("version")]
    public ActionResult<List<ModuleInfo>> GetVersionInfo()
    {
        // 從資料庫/設定檔讀取最新版本資訊
        return Ok(_updateRepository.GetAllModules());
    }

    [HttpGet("download/{fileName}")]
    public IActionResult DownloadFile(string fileName)
    {
        var path = _fileStorage.GetFilePath(fileName);
        if (!System.IO.File.Exists(path)) return NotFound();
        return PhysicalFile(path, "application/octet-stream", fileName);
    }
}

安全性與健全性考量

風險對策
中間人攻擊竄改檔案HTTPS 強制傳輸 + SHA256 雜湊驗證 + 程式碼簽章驗證
惡意版本資訊誘導下載版本 XML 數位簽章、伺服器憑證釘選
更新過程中斷導致損毀啟動器模式 + 完整備份 + 原子性移動 (MoveFileEx)
舊版本相容性問題最低支援版本檢查、資料庫遷移腳本版本化
頻寬佔用影響業務節流下載、背景智慧傳輸服務 (BITS)、差分更新

差分更新進階 (選用)

僅下載二進位差異 (如 bsdiff/Courgette 算法),大幅縮減下載量。需伺服器端預先產生 Patch 檔,客戶端套用 Patch 重組新檔案。

測試清單

  • [ ] 正常更新流程 (單一/多模組)
  • [ ] 網路中斷重試與續傳
  • [ ] 雜湊驗證失敗處理
  • [ ] 磁碟空間不足
  • [ ] 權限不足 (Program Files 需管理員)
  • [ ] 並發更新鎖定 (Mutex 單一實例)
  • [ ] 版本回滾機制
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved