C# 檔案下載 : WinINet

sparkdev發表於2016-11-11

在 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。不過在筆者看來,更重要的是我們可以選用不同的方式去處理下載問題。

Demo 下載

相關文章