使用 .NET 和後臺智能傳輸服務 API 來編寫自動更新應用程序。

使用 .NET 和後臺智能傳輸服務 API 來編寫自動更新應用程序

發佈日期: 11/26/2004 | 更新日期: 11/26/2004

Jason Clark

本文假設您熟悉 C# 和 Visual Basic .NET

下載本文的代碼: BITS.exe (363KB)

摘要

.NET Framework 和 Windows 都具有一些非常有趣的 API,這些 API 可用來創建能夠自動通過網絡進行自我更新的應用程序。編寫能夠像 Windows Update 一樣自我更新的應用程序有很多好處,其中包括爲用戶提供了便利,因爲這使從維護到網絡管理都變得更容易。自動更新需要注意發現、安全性以及文件替換等因素。 在本文中,作者介紹了 BITS API 以及 .NET Framework 的許多處理自動更新(使用與 Windows Update 相同的功能)的這些方面的功能。

*
本頁內容
困難 困難
BITS 基礎知識 BITS 基礎知識
BITS、COM 和託管代碼 BITS、COM 和託管代碼
將 BITS 與自動更新應用程序結合起來使用 將 BITS 與自動更新應用程序結合起來使用
關於 BITS 的一些考慮事項 關於 BITS 的一些考慮事項
.NET Framework 和安全性 .NET Framework 和安全性
強名稱和安全綁定 強名稱和安全綁定
更新打包和替換 更新打包和替換
文件的替換和提取 文件的替換和提取
AutoUpdateApp.exe 示例應用程序 AutoUpdateApp.exe 示例應用程序
自動更新應用程序的功能 自動更新應用程序的功能

我必須承認,我喜歡 Windows® Update 功能。 在我的計算機開機的時間中,大約有 85% 的時間都連接到了 Internet 上,但是,像許多人一樣,我當然不會這麼多時間都在使用網絡。 Windows XP 利用了這一未用的帶寬,將網上可用的最新服務包和修補程序與我計算機上已安裝的服務包和修補程序進行比較。如果它找到了我需要的更新,便會在後臺將它們下載下來。 完成後,Windows 會通知我計算機上有需要安裝的新軟件包。

如果可以選擇的話,我希望讓客戶端上的每一個應用程序都像 Windows 一樣允許自動更新。 現在有許多有利的條件以及現成的連接來實現這一功能。 如果要使應用程序自動進行自我更新,必須編寫代碼來處理髮現、下載、安全性以及替換等方面的問題。

爲了處理實際的下載,我將介紹 Windows 的一項新功能:後臺智能傳輸服務 (BITS)。 在討論此功能之後,我將介紹 .NET Framework 中可用來解決自動更新應用程序的安全性和替換問題的功能。

請注意,雖然 BITS 1.5 可重新發布軟件可以很好地工作在 Windows 2000 和 Windows XP 上,但是 Microsoft 不打算在 Windows 9x 或 Windows Me 上支持 BITS API。 BITS 有望作爲 Windows 的將來版本的一個組件提供。

但是,在開始下面的內容之前,需要指出的重要一點是:要使您的應用程序能夠使用本文所介紹的技術,需要在 .NET 下對它進行管理。 BITS 自身是操作系統的一部分,而這裏介紹的 .NET Framework 技術可以用於非託管的應用程序的自動更新功能。

困難

爲了找到遠程服務器上的更新,您的應用程序需要一種查詢網絡的手段。 這需要網絡代碼以及使應用程序和服務器可以用來彼此通信的簡單協議。 我將在本文的後面繼續探討“發現”這個話題。

接下來您還必須能夠下載軟件包。 由於 .NET 在網絡方面駕輕就熟,因此下載可能看上去不像是一個大問題,但是下載用戶所請求的文件僅僅是一個方面。另一個方面是在未經用戶同意的情況下下載大型文件。禮貌的自動更新應用程序僅僅使用殘留的帶寬來下載更新。 這聽起來很不錯,但是,您會看到,它帶來了一個相當困難的技術問題。幸運的是,相應的解決方案已存在。

安全可能是首當其衝的一個問題。 請花一點時間想一想 Windows Update 功能。它的一個主要目的是獲取安全修補程序。 想想,如果 Windows Update 自身都不能確認它安裝的是否是安全的代碼,情況會怎麼樣?顯而易見,從 Internet 上下載代碼並執行該代碼的任何應用程序都必須將安全性作爲一個首要問題。因此,我將探討如何確保自動更新應用程序的安全性。

最後一個需要考慮的因素是使用應用程序自身的新版本來替換應用程序的過程。 此問題之所以引人關注,是因爲它要求代碼在將自身從系統中刪除時能夠運行。 處理這一技巧有許多種方法。

關於這些困難,最慶幸的一件事情是在 .NET Framework 與 Windows 之間,所有工具都是現成的,可以立刻用來解決這些問題。

BITS 基礎知識

BITS 是 Windows 中新增的一項非常實用的文件傳輸功能,它通過 HTTP 異步地從遠程服務器上下載文件。 BITS 可以專門利用空閒帶寬來處理多個用戶的多個下載任務。 儘管 BITS 不僅僅限於自動更新應用程序使用,但它是 Windows Update 使用的基礎 API。 並且,由於它可用於任何應用程序,因此可用來完成創建自動更新應用程序的過程中所涉及到的大部分非常困難的工作。

下面是基本思想。 應用程序請求 BITS 處理一個或一組文件的下載。 BITS 將該任務添加到自己的隊列中,並將該任務與應用程序所運行在的用戶上下文關聯。 只要用戶登錄,BITS 就會利用空閒帶寬在網絡中細細搜尋 (Drizzle) 這些文件。 事實上,BITS 技術的代號名稱就是 Drizzle,這個詞被證明是 BITS 所執行的工作的一個非常恰當的描述。

那麼它到底是怎樣工作的呢? 這項技術實際是一項非常複雜的技術。 首先,BITS 是作爲一個 Windows 服務實現的,它維護組織成一組優先級隊列的任務集合。 優先級包括前臺、高、普通和低。它按照循環法的原則,通過大約五分鐘的時間片爲同一優先級的每個任務分配帶寬。 一旦隊列中沒有剩餘的任務,便立即檢查下一個優先級隊列中的任務。

前臺隊列中的任務使用儘可能多的網絡帶寬,由於這一原因,前臺優先級應只供響應用戶請求的代碼使用。 餘下的優先級(高、普通和低)遠遠比前臺優先級更引人關注,因爲它們全都是後臺優先級,也就是說,它們僅僅使用未用的網絡帶寬。

爲了實現此後臺功能,BITS 監視網絡數據包,並忽略不屬於它的數據包。 餘下的數據包被認爲是計算機帶寬上的活動負載。 BITS 使用活動負載信息以及連接速度和其他某些統計信息來確定應繼續下載文件還是暫停(以增加活動用戶的吞吐量)。 因此,用戶不會遇到帶寬問題。

能夠立即中斷所做的工作對於 BITS 是非常重要的。 在許多情況下,BITS 不得不在僅僅下載了文件的一部分的情況下放棄與網絡的連接甚至完全斷開連接。 但是,下載了一部分的文件會保存起來,這樣,當 BITS 抓住了與網絡連接的片刻機會時,便能夠從中斷的位置繼續下載。 這種恢復功能也有一些副作用。

請記住 BITS 是用於從 HTTP 服務器傳輸文件的。 要使 BITS 能夠工作,服務器應與 HTTP 1.1 兼容,或者至少支持 GET 方法中的 Range 頭。 這是因爲 BITS 需要能夠請求文件的一部分。 此外,下載的內容必須是靜態內容,例如,標記文件、代碼文件、位圖或聲音。當請求動態內容(例如,CGI、ISAPI 或 ASP.NET 產生的內容)時,包含 Range 頭的 GET 請求沒有意義。

當前, BITS 有兩個版本: 1.0 和 1.5。 BITS 1.0 隨同 Windows XP 一起提供,它具有下列功能: 可與對話框及其他 UI 元素一起使用的可中斷後臺文件下載、下載優先級、任務完成及出錯時的可選通知以及可選的進度通知。 BITS 1.5 隨同 Windows .NET Server 一起提供。 除了 BITS 1.0 中包含的功能外,1.5 版還具有可中斷的後臺文件上載以及使用“基本”、“摘要”、“NTLM”、“協商”(Kerberos) 或“Passport”對連接進行身份驗證的功能。 BITS 1.5 還作爲與 Windows 2000 及更高版本兼容的可重新發布軟件提供(請參閱後臺智能傳輸服務)。

BITS 1.0 中的全部功能對於編寫自動更新應用程序已夠用,但是使用 BITS 1.5 功能可以執行更復雜的任務,例如,出售更新或處理應用程序與服務器之間的交互。

BITS、COM 和託管代碼

BITS API 是作爲 COM 對象實現的,正因爲此,所以不能編寫 .NET Framework 版的 API。 幸運的是,BITS API 非常簡單易用。 本文所包含的示例應用程序是使用 C# 編寫的,因此爲了使用 BITS API,我不得不硬着頭皮使用 .NET Framework 的 COM Interop 功能。 我在這裏不深入探討運行庫可調用包裝 (RCW) 及相關的內容,而僅僅說明我使用此 API 所遵循的過程。

如果我使用的是 C++,那麼我的 BITS 代碼的開頭部分可能如下所示:

IBackgroundCopyManager* pBCM = NULL;
hr = CoCreateInstance(__uuidof(BackgroundCopyManager), NULL,
CLSCTX_LOCAL_SERVER, __uuidof(IBackgroundCopyManager), (void**) &pBCM);
if (SUCCEEDED(hr))
{
// Party-on with the pBCM interface pointer
}

等效的 C# 代碼使用 new 關鍵字創建 BackgroundCopyManager 對象,然後通過轉換獲得對 IBackgroundCopyManager 接口的引用,而不是通過調用像 CoCreateInstance 或 QueryInterface 這樣的方法。 下面的代碼片段是 C# 示例,它的作用是獲取 IBackgroundCopyManager 接口:

IBackgroundCopyManager bcm = null;
// Create BITS object
bcm = (IBackgroundCopyManager)new BackgroundCopyManager();

此代碼的簡單性有一點誤導的作用,因爲需要做更多的工作來分別將託管的 BackgroundCopyManager 和 IBackgroundCopyManager 類型與底層的 COM 對象和接口關聯。 .NET Framework 通過 RCW 來處理與 COM 對象的交互。 要將託管類型與 RCW 關聯,必須使用屬性。 圖 1 中的代碼說明了如何聲明 BackgroundCopyManager 類和 IBackgroundCopyManager 接口,以使它們表示 BITS COM 對象。

圖 1 中的代碼有很多的屬性(ComImportAttribute、GuidAttribute、MarshalAsAttribute 等等),這使真正的代碼看起來不明顯。 事實上,沒有真正的代碼。 接口和類的定義實際上僅僅是元數據形式的封送佔位符,公共語言運行庫 (CLR) 用它來直接調用 BITS COM API。

代碼示例(請見本文頂部的鏈接)所包含的 InteropBits.cs 文件提供了與 BITS API 一起使用的所有接口、枚舉類型及結構的 C# Interop 代碼。儘管示例僅僅使用了幾個方法,但是我提供了完整的實現,因爲它有助於您探索此 API 的更多功能。 值得注意的一點是,InteropBits.cs 文件中的代碼不是託管 API,而是 COM API 實現之上的 Interop API。 Microsoft 終有一天會發布通過與 .NET Framework 類庫的其餘部分一致的方法向 .NET Framework 代碼公開 BITS 功能的 API。

圖 1 中的代碼以及 InteropBits.cs 示例文件中的完整實現是手動創建的。 .NET Framework SDK 自帶一個名爲 TlbImp.exe 的工具。如果您有一個描述所針對的 COM API 的 TLB 文件,可以使用此工具創建類似的代碼,並將其編譯爲託管程序集。 BITS API 不自帶 TLB 文件,但是,Platform SDK 中包含一個名爲 Bits.idl 的接口定義文件,該文件中有關於接口的描述。

我使用 MIDL.exe 工具(也隨同 Platform SDK 一起提供)基於 Bits.idl 創建了一個 TLB 文件。然後使用 TlbImp.exe 創建了一個表示該 TLB 文件的託管程序集。 我使用 ILDasm.exe 將 TlbImp.exe 產生的程序集反彙編成中間語言。 最後,我將中間語言作爲準則產生了 C# 代碼,並在適當的地方對代碼進行了調整,使它更好用更正確。 這種將 COM Interop 與 C# 結合起來使用的方法無疑是很乏味的,但是它可以對最終結果進行非常全面的控制。

將 BITS 與自動更新應用程序結合起來使用

BITS 服務以任務的形式來管理文件下載。 應用程序創建傳輸任務,然後將一個或多個文件添加到該任務中。 一旦任務的文件列表建立,該任務就會繼續,因爲任務的初始狀態是掛起。 任務用來管理優先級、身份驗證以及錯誤管理等細節。 應用程序可以隨時取消任務。

一旦 BITS 完成了任務中所有文件的傳輸,應用程序便會通過調用某個方法來完成該任務。 Complete 方法將所有文件複製到其最終目的地。儘管 BITS 文檔將任務完成稱爲“複製”文件,但實際是在目標目錄中創建臨時的隱藏文件,Complete 方法僅僅重命名隱藏文件並使其可查看。

即便只使用最少的 BITS API(僅僅使用 IBackgroundCopyManager 和 IBackgroundCopyJob 接口),也可以實現自動更新應用程序所需要的一切。 如果需要任務枚舉或者完成和錯誤通知等功能,則必須使用更多的接口。

示例應用程序 AutoUpdater.exe 使用 BITS 來實現更新的發現和下載。 爲此,應用程序定義了連續更新名稱的模式:對於本示例,我選擇的是 Update1.dll、Update2.dll,依此類推。 儘管這絕不是發現更新的唯一方法,但是它非常適合於 BITS 功能。 爲了存儲更新狀態,示例應用程序維護一個 XML 文件。 該文件中的兩段相關信息是下一個更新編號以及描述當前 BITS 下載任務(如果有正在進行的任務)的 GUID。

應用程序每次運行時,都打開該 XML 文件查看下一個更新是否已下載。 如果沒有,並且 XML 文件中沒有任務 GUID,那麼應用程序將啓動一個 BITS 任務以下載序列中的下一個更新。 下面的代碼顯示了啓動下載任務所必需的方法:

IBackgroundCopyJob job=null;
// Create a bits job to download the next expected update
bcm.CreateJob("Application Update",
    BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, out jobID, out job);

// Add the file to the job
job.AddFile(updateUrl, localLocation);
job.Resume(); // start the job in action

對 job.AddFile 的調用傳遞將下載的文件的位置,以及任務完成時最終文件應存儲在其中的完整本地路徑和文件名。 請注意任務是在掛起狀態下啓動的,因此,在 BITS 服務使用任務來執行任何工作之前,任務必須已繼續。

如果 XML 文件中有 GUID,則說明 BITS 任務已隨着自動更新應用程序上一次啓動而啓動。 因此該 GUID 傳遞給 IBackgroundCopyManager.GetJob 方法,以查看安排的前一個下載任務是否已產生結果。 下一步取決於任務的狀態。

圖 2 中可以看出,如果任務處於錯誤狀態,那麼應用程序將完成任務(將失敗的任務從隊列中清除),然後繼續爲同一個更新文件創建新的任務。 通常情況下,導致此錯誤狀態的原因僅僅是該名稱的更新尚未存在。 至此已完成了示例應用程序的發現部分。

使用 BITS 輪詢更新的最大好處是 BITS 在後臺工作。 因此,儘管輪詢並不是那麼盡如人意,但是 BITS 下載的非強制特徵使它非常適於用來發現更新。

如果任務處於已傳輸狀態,那麼應用程序將通過調用 IBackgroundCopyJob.Complete 來完成該任務。這會導致文件被放入目標目錄中,從而使其可隨時更新。 然後應用程序返回,至此,這個難題中有關下載的部分就解決了。

最後,在圖 2 中的默認情況下,示例應用程序僅僅返回任務,而不對它進行任何處理。 此任務可能處於兩種狀態中的某一種:正在傳輸狀態(意味着任務正在進行)或暫時的錯誤狀態。 這兩種情況都被認爲是可能成功的情況,因此應用程序不對它進行任何處理。 如果 BITS 任務被置於暫時的錯誤狀態,BITS 服務便會認爲此錯誤有可能恢復,因此將重試。 最後,處於暫時錯誤狀態的任務要麼成功,要麼被 BITS 置於錯誤狀態。

令人吃驚的是,除了錯誤恢復邏輯外,這還涉及到 AutoUpdater.exe 示例應用程序的整個發現和下載部分。至於下載過程的高級功能,其實現看起來與網絡通信幾乎沒有任何相似之處,倒是與文件複製有着更多的共同點。 但是,在生產應用程序中,您可能會發現 BITS API 的其他功能的各種應用,例如,自動式傳輸通知、任務優先級處理以及上載任務。

關於 BITS 的一些考慮事項

設計 BITS 從未以啓動網絡連接爲目的。 這是最佳考慮,因爲您不會希望未連接的應用程序撥叫 ISP 僅僅是爲了輪詢更新。 但是,在共享 Internet 連接方面,BITS 1.0 和 1.5 都存在侷限性。 如果系統 A 共享系統 B 的 Internet 連接,那麼當應用程序啓動系統 A 上的 BITS 任務時,可能導致系統 B 啓動撥號。 這是因爲 BITS 服務的當前實現未將連接共享這一情況考慮在內。

這是一個非常棘手的問題,也許 BITS 的將來版本會提供相應的解決辦法。 Windows Update 功能也面臨同樣的問題。 就這一點(在共享連接上啓動撥號)的表現而言,使用 BITS 的應用程序與 Windows 沒有什麼不同。

關於 BITS,最後一個需要關注的問題是任務文件的一致性。 如果下載任務中包含多個文件,那麼在所有文件都可用之前,BITS 不會認爲任務已完成,也不會認爲文件已到達其最終目的地。 但是,BITS 無法知道服務器上的某個文件是否在任務中的其他文件下載後發生更改。因此,在服務器上進行的更新會影響到客戶端上的下載一致性。

有兩種方法可以處理這一問題。 第一種是將 BITS 下載分成多個單文件任務,這是我在示例應用程序中採用的方法。 第二種方法是分別將每個任務的所有文件存儲在服務器上的每個單獨的目錄中。當要對文件進行更新時,將在服務器上爲這組新文件創建一個新目錄。 這樣,之前的某個任務處於中間傳輸狀態的客戶端將不會受到這組新文件的影響。此解決方案所帶來的問題是,當服務器端的文件位置改變時,客戶端需要一種途徑來了解在哪裏找到新文件。

如果您發現需要解決 BITS 代碼中所存在的問題,沒有比 BitsAdmin.exe 更適用的工具了。此工具在 Windows XP CD 的 Support/Tools 子目錄中提供。 爲了避免運行工具的安裝以節省時間,我手動提取 Support.cab 文件中的 .exe。

隨着您逐漸開始熟悉 BITS 下載,會發現 BitsAdmin.exe 工具非常實用。 通過此命令行工具中的開關幾乎可以訪問到 API 的每一項功能。此外,還可以枚舉任務、檢查任務狀態以及查看詳細的日誌和錯誤信息。 可以啓動、掛起、繼續和完成下載任務。 最後,還可以從 BitsAdmin.exe 中更改任務優先級以及取消任務。

很少有附帶的工具能夠如此方便地訪問 Windows API。事實上,像批處理文件這樣的腳本可以通過 BitsAdmin.exe 工具來充分地使用 BITS。 BITS 是可用於發現和下載更新的一個了不起的解決方案,對於大多數應用程序而言,它的缺點微不足道。 現在讓我們來看一看安全性。

.NET Framework 和安全性

爲了用在代碼中,.NET Framework 引入了一些非常有意思的安全結構。 例如,Code Access Security 就是一個這樣的結構。 另一個安全基礎結構是與託管程序集的強名稱綁定。 爲了能夠更安全地將代碼從服務器傳輸到客戶端,我選擇使用此功能。這裏的問題在於自動更新應用程序通過 Internet 與服務器聯繫並下載文件(包括將在客戶端上安裝的代碼)。必須對這些新文件加以保護,以防止它們被客戶端與服務器之間懷有惡意的某一方所操縱。

此問題的一種解決辦法是通過 HTTPS 連接來執行整個數據交換。 BITS 服務能夠通過 HTTPS 協議來查找資源。HTTPS 僅僅是發生在加密的 SSL/TLS 通道中的 HTTP 通信。 但是,這種解決方案存在幾個缺陷。 首先是導致性能下降。

Web 服務器在發送靜態內容(如 HTML 文檔或 EXE 文件)方面表現出色。 當 Microsoft Internet 信息服務 (IIS) 收到對靜態文件的請求時,可以向 TransmitFile Win32?API 發出一個調用。 TransmitFile 在驅動程序這一級將文件逐個比特地從文件系統發送到網絡流中,從而提供了非常卓越的性能。 在通過 HTTPS 引進加密的同時,也大大降低了性能,因爲文件的每個字節都必須加密。 這增加了與更新服務器關聯的成本。

使用 HTTPS 的另一個問題是它使 Web 服務器成爲了安全基礎結構中一個非常易於受到攻擊的節點。 如果 Web 服務器被攻破,那麼客戶端將盲目地下載並安裝被安裝在服務器上的惡意內容。

但是,使用 .NET Framework 的一個非常簡單的功能,可以在實現更安全的更新文件傳輸的同時避免所有這些缺點。 首先,需要一個加密的 SSL/TLS 通道(以使 Web 服務器的吞吐量不會受到影響)。接下來,由於來自被攻破的服務器的重大安全威脅是對客戶端的拒絕服務,因此本解決方案將阻止服務器在客戶端上運行惡意代碼。 .NET Framework 的這一功能稱爲強名稱綁定,它是程序集加載器的一部分。

強名稱和安全綁定

.NET Framework 緩解的一個長期 DLL 問題是弱名稱方案,也就是僅使用文件名來標識可重用代碼。 這個問題已導致了所有類型的應用程序問題都統稱爲“DLL 地獄”。 解決方案就是強名稱。

強名稱包含四部分信息: 文件名、版本號、區域信息以及公鑰。 .NET Framework 使用強名稱來強制在組件之間實現更嚴格的綁定。此框架還使用公鑰/密鑰對在加載強名稱程序集之前先對其進行驗證。 對於強名稱程序集,.NET Framework 語言編譯器(C# 或 Visual Basic® .NET) 直接將公鑰生成爲 DLL 文件。除了公鑰外,編譯器還對文件內容進行哈希計算,並使用私鑰(由生成組織安全保管)對結果進行加密。

加密的哈希也稱爲數字簽名。 要檢查數字簽名,需要將公鑰從程序集文件中提取出來並用它來解密哈希。 然後,重新對文件的內容進行哈希計算,並將結果與簽名哈希進行比較。 如果程序集的內容發生了任何改變,簽名驗證都將失敗,因爲哈希值不匹配。

您可能會猜測使用程序集中的公鑰來確認完全相同的程序集似乎不太安全,確實如此。但是,如果執行驗證的代碼有一種方法可以確認公鑰就是衆所周知的或者是所期待的公鑰,那麼您便擁有了更完整的安全解決方案。 .NET Framework 通過使用公鑰的哈希(稱爲公鑰標記或初始公鑰)來實現此功能。

將程序集加載到應用程序中時,可以指定公鑰標記。 只有當程序集具有強名稱,並且強名稱的公鑰與傳遞給 Assembly.Load 方法的公鑰標記匹配時,.NET Framework 纔會加載該程序集。

接下來,我將逐步演示使用強名稱命名程序集並使用初始公鑰來加載它的過程。 下面的 C# 代碼可以用來生成強名稱程序集。 該程序集除了展示強名稱綁定以外,無法執行其他任何功能:

using System.Reflection;
[assembly:AssemblyKeyFile(@"Keys.snk")]

要生成此代碼,必須首先創建包含公鑰/私鑰對的密鑰文件 (Keys.snk)。 可以使用 .NET Framework SDK 自帶的 Sn.exe 工具從命令行來完成此操作,命令如下:

>sn -k Keys.snk

現在,可以使用 C# 編譯器將強名稱程序集編譯成 .NET Framework 程序集 DLL,命令如下:

>csc -t:library StrongName.cs

所產生的程序集將命名爲 StrongName.dll。要找到此強名稱產生的公鑰標記,可以使用帶 /T 開關的 Sn.exe,如圖 3 所示。


圖 3 提取程序集的公鑰標記

圖 3 中的示例顯示的是公鑰標記爲 d586e46fd39d13b5(十六進制)的 DLL。 要使用強名稱驗證來加載此程序集,只需下面的代碼:

// Load the update
String name = Path.GetFileNameWithoutExtension(patchName);
name = "StrongName.dll, PublicKeyToken=d586e46fd39d13b5");
Assembly update = Assembly.Load(name);

此代碼驗證簽名。如果成功,將返回對程序集對象的引用;如果失敗,Assembly.Load 方法將引發 FileLoadException。 無論在哪一種情況下,使用 Assembly.Load,此代碼都能夠驗證程序集自生成以來未發生過絲毫改變。

自動更新應用程序可以使用 BITS 下載更新文件,而無需考慮服務器端的安全性。 然後,在客戶端上安裝並使用更新之前,可以先使用 Assembly.Load 來驗證程序集。唯一的先決條件是初始應用程序是使用現有的公鑰/密鑰對(從公鑰標記這個角度而言)安裝的。使用匹配的密鑰對生成的任何後期更新都可以先在客戶端上經過驗證,才能再安裝和使用。其間,用於生成更新的密鑰對文件可以存儲在任何計算機上,甚至是未連接到網絡的計算機上。

這一安全機制非常了不起,但它也帶來了兩個問題。 例如,可以更新像位圖和數據文件這樣的非代碼文件嗎? 是否必須強名稱生成所有的 DLL 和 EXE 文件才能使用此機制? 這兩個問題的回答分別是“是”和“否”。

更新打包和替換

如果自動更新解決方案不能用於非代碼文件,或者如果它要求所有的 DLL 和 EXE 文件都是託管的並且具有強名稱,那麼它對於我將沒有那麼大的吸引力。 因此,我需要找到一種當不僅僅包含 .NET Framework 程序集文件的更新時也能夠使用 .NET Framework 強名稱綁定的方法。 答案是將更新文件像程序集資源一樣嵌入到程序集文件中。這不僅使得任何文件類型都可以通過強名稱保護起來,而且還使所有更新任務都採用單文件下載的形式,從而簡化了 BITS 任務的創建過程。一個大文件實際是一個包含許多文件的包。 事實上,在一個包中,可能包含 CAB 文件甚至是 MSI 腳本 — 這種可能性是沒有止境的。

所有 .NET Framework 語言編譯器都允許將任意文件嵌入到程序集文件中。 我將向您展示的是如何使用 C# 編譯器來完成此項工作,但其原理同樣適用於 Visual Basic .NET 或託管的 C++。 下面的命令行將 Code.dll、Bitmap.gif 和 Data.xml 等文件生成爲 Update2.dll(前述三個文件各作爲一個嵌入式資源包含在 Update2.dll 中):

csc /res:Code.dll /res:Bitmap.gif /res:Data.xml /out:Update2.dll 
    /t:library

請注意 C# 編譯器非常靈活,無需 .cs 代碼文件即可生成 DLL 程序集。 此 DLL 僅僅包含嵌入式資源。 但是,如果您希望 DLL 包含強名稱,那麼還應包含一個簡短的 .cs 文件(包含與 StrongName.cs 類似的代碼)。

找到嵌入式資源並將其重新複製到文件系統中所需的代碼顯示在圖 4 中。 此代碼使用 Assembly.GetManifestResourceName 和 Assembly.GetManifestResourceStream 方法來找到資源並將其中的字節複製到文件系統中的文件中。 圖 4 中的代碼不在文件系統中保留原始文件的日期,但是如果要這樣做,也可以很容易地對代碼進行修改。 請注意提取過程僅僅在 Assembly.Load 驗證了容器程序集的簽名之後才發生。

文件的替換和提取

在應用程序更新過程中將文件提取到應用程序目錄中是一個非常重要的問題,即便文件具有強名稱和程序集資源也是如此。問題的癥結在於應用程序在執行過程中將文件複製到其自己的文件上。 更復雜的是,所運行的應用程序可能是像記事本這樣的應用程序。此類應用程序同時運行多個實例是很常見的。

如果您的應用程序是這一類應用程序,那麼全能型解決方案幾乎是不可能的。與其規定單一的解決方案,不如來看一看可以用來處理替換問題的功能。 首先,顯而易見,在不同於主應用程序的單獨進程中運行自動更新功能是可以實現的。如果您的應用程序是非託管的,並且您希望對它進行擴展以便可以使用本文所介紹的技術,那麼這個方法尤其有用。 更新進程可以處理所有 BITS 邏輯以及文件提取。 更新進程甚至可以等到主應用程序的所有實例都已終止,然後再嘗試提取文件。

在單獨的進程中運行提取代碼主要有三個缺點: 首先,它可能使得很難修補自動更新代碼本身。 第二,此方法在服務器端更不具優勢,因爲服務器不太可能關閉。 第三,如果主進程由 Windows Job 對象(不要與 BITS 任務混淆)管理,那麼,當主進程退出時,自動更新進程也將自動被 Windows 終止。

AutoUpdateApp.exe 示例應用程序在主應用程序邏輯所在的進程運行自動更新邏輯。 如果您也選擇這樣做,將會遇到問題,因爲進程正在使用的 DLL 和 EXE 文件將被刪除和替換。 對於 DLL 或 EXE 被刪除的問題,一個不太提倡的技巧是將文件重命名爲臨時文件名,然後將新文件複製到舊文件名上。然後可以使用 MoveFileEx Win32 API 來記錄要在系統重新啓動時刪除的文件,或者在應用程序中包含將搜索每個臨時文件並將其刪除的邏輯。

即便使用重命名技巧,仍然不得不考慮這樣一種可能性:當更新正在進行時,用戶運行了應用程序的另一個實例,這樣它將加載一半舊文件一半新文件。 有兩種方法可以解決這種情況。 一種是使用 CLR 中的應用程序域的影像複製功能。

CLR 在名爲應用程序域或 AppDomain 的邏輯應用程序容器中運行託管代碼。 多個 AppDomain 可以運行在一個 Windows 進程中。當 AppDomain 創建出來後,可以將這一新域標記爲對文件進行影像複製。這意味着當您的應用程序加載程序集時,程序集文件複製到隱藏的臨時目錄中,並從影像位置加載。 .NET Framework 中的 ASP.NET 類使用此功能來避開應用程序文件,使其可以被 ASP.NET 代碼生成器更新。

AppDomains 還可以用於應用程序重執行。理想情況下,當有更新可用時,應用程序會提示用戶確定是否希望應用此更新。 如果用戶的回答是“是”,那麼應用程序將僅僅在適當的位置使用新功能運行。對於某些應用程序而言,在新進程中重新啓動已更新的應用程序是可接受的。 在其他情況下,則最好讓新代碼在執行更新的 EXE 所在的進程中立即開始工作。

使用 AppDomain 可以很容易地重新執行託管 EXE。執行此功能的代碼示例可以在本文所包含的示例代碼(見本文頂部的鏈接)的 RelaunchExe 方法中找到。 RelaunchExe 方法新建一個 AppDomain,然後重新執行可執行文件,並將在第一個位置啓動應用程序時所使用的相同命令行參數傳遞給該文件。

AutoUpdateApp.exe 示例應用程序

本文的代碼示例不僅包含自動更新邏輯,而且還生成應用程序的兩個更新版本,以便您可以動態地看到整個解決方案。 現在讓我們來看一看這個示例。

儘管 Visual Studio? .NET 是功能強大的編碼環境,但有時您也不得不創建自己的批生成。 我的項目是很難作爲 Visual Studio 項目生成的多版本生成的一個示例。 因此我將示例作爲源文件和批文件的集合發佈,以便可以從命令行執行生成。

要試用此應用程序,請從本文頂部的鏈接下載存檔文件,並將它解壓縮到空目錄中。 然後從命令行運行 Build.bat。要使批文件成功,csc.exe(C# 命令行編譯器)和 sn.exe(強名稱實用工具)必須都位於路徑環境變量中。 csc.exe 位於 .NET Framework 安裝區域的以下目錄中: C:/Windows/Microsoft.Net/Framework/。Sn.exe 工具位於 .NET Framework SDK 目錄中,該目錄通常安裝在下面兩個位置中的某一個位置: C:/Program Files/Microsoft.NET 或 C:/Program Files/Microsoft Visual Studio .NET。

使用 Build.bat 生成項目後,會創建兩個重要的目錄: app 和 updates。 請注意,將出現一個 sln 文件,以便您可以輕鬆地使用 Visual Studio .NET 編輯代碼文件(但是,應繼續使用批處理文件來生成項目)。

第一個目錄 app 包含應用程序安裝。 將此目錄更改爲應用程序目錄,然後運行 AutoUpdateApp.exe 以試用它。 此應用程序的實際功能是顯示它在啓動目錄中找到的第一個位圖(見圖 5)。


圖 5 自動更新簡單應用程序

爲了動態地查看更新功能,創建一個名爲 updates 的 IIS 虛擬根目錄(授予匿名下載權限)。 其目的是使 HTTP://localhost/updates 成爲用來發布所有更新的工作位置。 然後將第二個目錄 updates 的內容複製到虛擬根目錄中。

如果一切都設置妥當,那麼您的應用程序現在應該能夠找到位於 HTTP://localhost/updates 的 Update1.dll 和 Update2.dll。 從命令行執行和關閉 AutoUpdateApp.exe 若干次。大約在第二次或第三次啓動時,應用程序應該通知您可以應用第一個更新。 第一個更新僅僅將新的 GIF 文件添加到目錄中。 第二個更新真正地安裝 AutoUpdateApp.exe 的新副本,該副本現在有一項功能可以運行啓動目錄中的所有 GIF 文件的幻燈片放映。

首次啓動 AutoUpdateApp.exe 時,應用程序創建一個名爲 UpdateState.xml 的數據文件。此文件包含控制 AutoUpdateApp.exe 的功能的信息,其中包括應用程序在其中查找更新的網絡位置。 如果您不希望使用 HTTP://localhost/ 上的位置,可以更改 UpdateState.xml 中的 URL 以使用另一個更新服務器。

自動更新應用程序的功能

現在,有一些很棒的功能可用來編寫自動更新的應用程序。 也就是說,Microsoft 開發人員當前正在努力使自動更新更簡單,併成爲應用程序開發的一個天然部分。 .NET Framework 的 1.0 版已提供了這裏介紹的一些固有的基本功能,但沒有涉及部署功能(見.NET Zero Deployment: Security and Versioning Models in the Windows Forms Engine Help You Create and Deploy Smart Clients)。 最後,需要說明的一點是,您編寫的應用程序安裝和更新起來越容易,該應用程序就越成功。

爲了發現和替換更新,Microsoft 計劃在 Windows 的將來版本中引入內置服務。 BITS 服務將依然是基礎的下載機制,但將推出更多的 API 抽象以簡化下載和安全保證過程。 此外,還將擴展 BITS,以包括用於管理應用程序更新且簡單易用的 UI。

我盼望着有一天,啓動應用程序與安裝應用程序之間的分隔線完全消失。 但是,至少今天我已經能夠編寫一安裝就將自我更新的應用程序。 這是非常喜人的第一步。

相關文章,請參閱:


Using Windows XP Background Intelligent Transfer Service (BITS) with Visual Studio .NET


.NET Interop: Get Ready for Microsoft .NET by Using Wrappers to Interact with COM-based Applications


Applied Microsoft .NET Framework Programming by Jeffrey Richter (Microsoft Press, 2002)

背景信息,請參閱:


.NET and COM: The Complete Interoperability Guide by Adam Nathan (SAMS, 2002)

Jason Clark 爲 Microsoft 和 Wintellect (HTTP://www.Wintellect.com) 提供培訓和諮詢,並且是開發 Windows NT 和 Windows 2000 Server 的前輩。 他是 Programming Server-Side Applications for Microsoft Windows 2000 (Microsoft Press, 2000) 的合著者。 可以通過 [email protected] 與 Jason 聯繫。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章