﻿# 利用 WebClient 類別模擬 HTTP POST 表單送出的注意事項

我們都知道 [WebClient 類別](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.aspx)是個簡單易用的東西，不只可以用作 HTTP 用途，[連 FTP 都能用](http://blog.darkthread.net/blogs/darkthreadtw/archive/2010/01/10/3916.aspx)，想偷懶時很快就能寫出一些網路資料上傳、下載的程式，像我在寫一些測試程式時經常會使用 [WebClient 類別](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.aspx)，但大多情況都用來「下載網頁」居多，少有模擬表單上傳資料的情況，但利用 [WebClient 類別](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.aspx)在「傳送表單資料」時要小心使用，否則遠端接不到資料又很難除錯時哪就麻煩了。

與「上傳資料」有關的方法有以下四種 (不考慮非同步模式的方法)

-   byte\[\] [**UploadData**](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.uploaddata.aspx)  
    將位元組陣列傳送至資源，並傳回含有任何回應的 [Byte](http://msdn.microsoft.com/zh-tw/library/system.byte.aspx) 陣列。
-   byte\[\] [**UploadFile**](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.uploadfile.aspx)  
    將本機檔案傳送至資源，並傳回含有任何回應的 [Byte](http://msdn.microsoft.com/zh-tw/library/system.byte.aspx) 陣列。
-   byte\[\] [**UploadValues**](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.uploadvalues.aspx)  
    將 [NameValueCollection](http://msdn.microsoft.com/zh-tw/library/system.collections.specialized.namevaluecollection.aspx) 傳送至資源，並傳回含有任何回應的 [Byte](http://msdn.microsoft.com/zh-tw/library/system.byte.aspx) 陣列。
-   string [**UploadString**](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.uploadstring.aspx)  
    將 [String](http://msdn.microsoft.com/zh-tw/library/system.string.aspx) 傳送至資源。

如果是你，你會選用哪一種呢？你會不會覺得 string 比較友善呢？ ^\_\_^

像我們之前就這樣寫，以為可以順利的將資料上傳（錯誤示範）：

```csharp
using (WebClient wc = new WebClient())
{
  try
  {
	  wc.Encoding = Encoding.UTF8;

	  string resultXML = wc.UploadString(Config.PostURL, 
		  String.Format("id={0}&pw={1}", user.ID, user.PW));
  }
  catch (WebException ex)
  {
	  throw new Exception("無法連接遠端伺服器");
  }
}
```

透過這段程式所送出的 HTTP Request Header 如下：

```
POST /Home/Echo HTTP/1.1Host: my.example.com:52215Content-Length: 13Connection: Keep-Aliveid=aaa&pw=bbb
```

雖然一樣是 POST 方法，但是缺少了最重要的 Content-Type 標頭，也就是：

```
Content-Type: application/x-www-form-urlencoded
```

以我們常用 ASP.NET MVC 為例，你不傳送標準的 Content-Type 過去，就無法正確抓到所有表單資料，也無法透過 Model Binder 自動繫結到 Action 參數中！

雖然你可以利用 [wc.Headers 屬性](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.headers.aspx)自行將缺少的 Header 加上去，例如：

```
wc.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
```

但後來我們改用 [**UploadValues**](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.uploadvalues.aspx) 方法，程式碼範例如下：

```csharp
using (WebClient wc = new WebClient())
{
  try
  {
      wc.Encoding = Encoding.UTF8;
      
      NameValueCollection nc = new NameValueCollection();
      nc["id"] = "aaa";
      nc["pw"] = "bbb";
      
      byte[] bResult = wc.UploadValues(Config.PostURL, nc);
      
      string resultXML = Encoding.UTF8.GetString(bResult);
  }
  catch (WebException ex)
  {
      throw new Exception("無法連接遠端伺服器");
  }
}
```

雖然多了幾行 Code，但是其實語法更容易理解，而且也不用自己組 POST 資料中類似 QueryString 的字串，還會自動做 URLEncode 動作，只是最後多一個步驟要將 byte\[\] 轉回 string 而已，這段程式最後會送出的 HTTP Request Header 如下：

```
POST /Home/Echo HTTP/1.1Content-Type: application/x-www-form-urlencodedHost: my.example.com:52215Content-Length: 13Connection: Keep-Aliveid=aaa&pw=bbb
```

但是，使用 [WebClient 類別](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.aspx)其實有許多缺點，所以我文章一開頭就說過「想偷懶時」，主要的缺點有：

-   無法指定 Timeout，所以當網路連不上時，網頁或程式會整個停在那裡很久！
-   不適合用來下載大量的檔案，高負載的網站也不適合這樣用，即便你用非同步的方式撰寫，也會讓 [WebClient](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.aspx) 因為佔據過多 Threads 而導致效能降低，壹台電腦的 Threads 數是有限制的。

對於較正式的場合，還是建議改用 [HttpWebRequest 類別](http://msdn.microsoft.com/zh-tw/library/system.net.httpwebrequest.aspx)處理。

**相關連結**

-   [WebClient 類別 (System.Net)](http://msdn.microsoft.com/zh-tw/library/system.net.webclient.aspx)
-   [CODE-使用C#程式從網站下載檔案](http://blog.darkthread.net/blogs/darkthreadtw/archive/2008/11/26/3660.aspx)
-   [CODE-FTP上傳檔案的精簡寫法](http://blog.darkthread.net/blogs/darkthreadtw/archive/2010/01/10/3916.aspx)
-   [Asynchronous I/O with WebClient](http://msdn.microsoft.com/en-us/magazine/cc700359.aspx)
