在C#中,除了webclient我们还可以使用一组WindowsAPI来完成下载任务。这就是Windows Internet,简称 WinINet。本文通过一个demo来介绍WinINet的基本用法和一些实用技巧。
Winform文件下载之WebClient
相比WebClient的用法,Win32API在使用时可能会烦琐一些。所以先把用到的API简单介绍一下。
InternetOpen
这是需要调用的第一个方法,它会初始化内部数据结构,为后面的调用做准备。
InternetCloseHandle
这个方法用来关闭使用中打开的Internet句柄,释放资源。
InternetOpenUrl
这是一个通用的函数,应用程序可以用它来请求数据(只要是WinINet支持的协议就可以)。尤其是当我们仅仅想要通过一个URL获取数据,而不关心通信协议相关的内容时,这个接口就特别合适。该方法会解析参数中的URL字符串,然后建立到服务器的连接,并准备下载由RUL标识的数据。
HttpQueryInfo
检索与HTTP请求相关的报头信息。主要是查看请求是否成功。
InternetReadFile
从 InternetOpenUrl打开的句柄中读取数据。
这里我们只介绍下载过程中的关键环节,完整的过程请参考本文的demo。
当请求与服务器建立连接时,我们重点考虑三个问题:请求的url,是否使用 RELOAD 标识, 客户端是否支持gzip压缩。
请求的url不用多说,这里直接请求一个http url.
我们不希望拿到客户端缓存中的数据,所以希望每次请求都能够从服务器重新下载。此时需要为InternetOpenUrl方法传入INTERNET_FLAG_RELOAD 标识。
当前绝大多数的web服务器都是支持gzip压缩的,我们的客户端当然也要能够解压缩服务器传回来的gzip格式的数据。所以我们要在请求中告诉服务器,客户端是能够处理gzip数据的。只有这样,服务器才会主动的返回gzip格式的数据。
代码如下:
1
2
3
4
5
6
7
|
string
referer =
"Referer: xxxxxx\nAccept-Encoding: gzip"
;
// INTERNET_FLAG_RELOAD -> 0x80000000
// 跳过缓存,强制从原始的服务器下载数据
hInetFile = NativeMethods.InternetOpenUrl(
this
._hInet, uri.AbsoluteUri, referer, referer.Length, 0x80000000, IntPtr.Zero);
|
接下来我们开始检查前面发送的请求返回的header中的信息。主要是:请求的资源是否存在,返回的数据有多长,返回的文件的原始名称是什么,返回的数据是以什么格式被压缩的。
我们先要通过检查返回的状态码来确定请求是否成功,也就是返回的是不是200。
1
2
3
4
5
6
7
8
9
|
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进行了简单的封装。创建了一个继承自Stream的类MyInternetReadStream。在重写的 Read方法中调用InternetReadFile,并且添加了一个回调方法用来计算下载进度等信息。下面是代码概要,完整代码请参考demo。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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,<br> dwNumberOfBytesToRead,
out
length)
Array.Copy(
this
._localBuffer, 0, buffer, offset, length);
this
._bytesReadCallback(length,
this
._contentLength);
return
length;
}
|
前面我们提到,服务器可能返回的是经过gzip压缩的数据,这就需要我们先检查数据的格式。如果是gzip格式的数据就需要把它解压缩。其实这在C#中是很简单的,我们只要把刚才创建的MyInternetReadStream的实例传给GZipStream的构造函数,创建一个新的GZipStream实例就可以了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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, <br>
new
MyWinInet.MyInternetReadStream.BytesReadCallback(
this
.BytesReadCallback));
}
|
至于计算下载进度,实时的下载速度的实现和Winform文件下载之WebClient 中的实现基本相同,请参考上文,或者直接看本文的demo。
总结:相比WebClient,使用WinINet接口要烦琐不少。当然也有一定的优势,比如前文中提到的代理问题,WinINet的默认设置就能处理好Credentials。不过在笔者看来,更重要的是我们可以选用不同的方式去处理下载问题。