在 C# 中,除了 WebClient 我們還可以使用一組 WindowsAPI 來完成下載任務。這就是 Windows Internet,簡稱 WinINet。本文通過一個 demo 來介紹 WinINet 的基本用法和一些實用技巧。
接口介紹
相比 WebClient 的用法,Win32API 在使用時可能會煩瑣一些。所以先把用到的 API 簡單介紹一下。
資源的初始化和釋放
InternetOpen
這是需要調用的第一個方法,它會初始化內部數據結構,為后面的調用做准備。
InternetCloseHandle
這個方法用來關閉使用中打開的 Internet 句柄,釋放資源。
建立到服務器的連接
InternetOpenUrl
這是一個通用的函數,應用程序可以用它來請求數據(只要是 WinINet 支持的協議就可以)。尤其是當我們僅僅想要通過一個 URL 獲取數據,而不關心通信協議相關的內容時,這個接口就特別合適。該方法會解析參數中的 URL 字符串,然后建立到服務器的連接,並准備下載由 RUL 標識的數據。
檢查響應信息
HttpQueryInfo
檢索與 HTTP 請求相關的報頭信息。主要是查看請求是否成功。
讀取響應內容
InternetReadFile
從 InternetOpenUrl 打開的句柄中讀取數據。
下載過程
這里我們只介紹下載過程中的關鍵環節,完整的過程請參考本文的 demo。
InternetOpenUrl
當請求與服務器建立連接時,我們重點考慮三個問題:請求的 url,是否使用 RELOAD 標識, 客戶端是否支持 gzip 壓縮。
請求的 url 不用多說,這里直接請求一個 http url。
我們不希望拿到客戶端緩存中的數據,所以希望每次請求都能夠從服務器重新下載。此時需要為 InternetOpenUrl 方法傳入 INTERNET_FLAG_RELOAD 標識。
當前絕大多數的 web 服務器都是支持 gzip 壓縮的,我們的客戶端當然也要能夠解壓縮服務器傳回來的 gzip 格式的數據。所以我們要在請求中告訴服務器,客戶端是能夠處理 gzip 數據的。只有這樣,服務器才會主動的返回 gzip 格式的數據。
代碼如下:
string referer = "Referer: xxxxxx\nAccept-Encoding: gzip"; // INTERNET_FLAG_RELOAD -> 0x80000000 // 跳過緩存,強制從原始的服務器下載數據 hInetFile = NativeMethods.InternetOpenUrl(this._hInet, uri.AbsoluteUri, referer, referer.Length, 0x80000000, IntPtr.Zero);
HttpQueryInfo
接下來我們開始檢查前面發送的請求返回的 header 中的信息。主要是:請求的資源是否存在,返回的數據有多長,返回的文件的原始名稱是什么,返回的數據是以什么格式被壓縮的。
我們先要通過檢查返回的狀態碼來確定請求是否成功,也就是返回的是不是 200。
byte[] content = new byte[BufferSize]; int count = BufferSize; int temp = 0; NativeMethods.HttpQueryInfo(hInetFile, 19, content, out count, out temp) statuscode = Encoding.Unicode.GetString(content, 0, count);
正確返回時,statuscode 應該是“200”。
不要對 HttpQueryInfo 的第二個參數感到奇怪,為了獲得請求的返回狀態我們就得傳入 19。你可以參考Query Onfo Flags 。
用類似的方法可以得到返回數據的長度,原始的文件名稱,返回數據的格式。
InternetReadFile
前面一切順利的話就可以讀取數據了。這個方法本身沒什么可說的,但出於簡化操作的目的,筆者對 InternetReadFile 進行了簡單的封裝。創建了一個繼承自 Stream 的類 MyInternetReadStream。在重寫的 Read 方法中調用 InternetReadFile,並且添加了一個回調方法用來計算下載進度等信息。下面是代碼概要,完整代碼請參考 demo。
public override int Read(byte[] buffer, int offset, int count) { int dwNumberOfBytesToRead = Math.Min(BufferSize, count); int length = 0; NativeMethods.InternetReadFile(this._hInetFile, this._localBuffer, dwNumberOfBytesToRead, out length) Array.Copy(this._localBuffer, 0, buffer, offset, length); this._bytesReadCallback(length, this._contentLength); return length; }
Gzip stream
前面我們提到,服務器可能返回的是經過 gzip 壓縮的數據,這就需要我們先檢查數據的格式。如果是 gzip 格式的數據就需要把它解壓縮。其實這在 C# 中是很簡單的,我們只要把剛才創建的 MyInternetReadStream 的實例傳給 GZipStream 的構造函數,創建一個新的 GZipStream 實例就可以了。
private Stream GetInternetStream(IntPtr hInetFile) { //檢查數據是不是gzip格式 string contentEncoding = MyWinInet.GetContentEncoding(hInetFile); if (contentEncoding.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) != -1) { return new GZipStream(this.ForGZipReadStream(hInetFile), CompressionMode.Decompress, false); } … } private Stream ForGZipReadStream(IntPtr hInetFile) { return new MyWinInet.MyInternetReadStream(hInetFile, new MyWinInet.MyInternetReadStream.BytesReadCallback(this.BytesReadCallback)); }
至於計算下載進度,實時的下載速度的實現和 《C# 文件下載 : WebClient》中的實現基本相同,請參考上文,或者直接看本文的 demo。
小結
相比 WebClient,使用 WinINet 接口要煩瑣不少。當然也有一定的優勢,比如《C# 文件下載 : WebClient》中提到的代理問題,WinINet 的默認設置就能處理好 Credentials。不過在筆者看來,更重要的是我們可以選用不同的方式去處理下載問題。