16.網絡《果殼中的c#》

網絡體系

16.1 網絡體系結構

System.Net.* 命名空間包含各類支持標準網絡協議的通訊。html

  • WebClient 外觀類:支持通訊HTTP或FTP執行簡單的下載/上傳操做。
  • WebRequestWebResponse 類:支持更多的客戶端HTTP或FTP操做。
  • HttpListener 類:可用來編寫HTTP服務器。
  • SmtpClient類:支持經過 SMTP 建立和發送電子郵件。
  • DNS類:支持域名和地址之間的轉換。
  • TcpClientUdpClientTcpListenerSocket類:支持傳輸層和網絡層的直接訪問。

網絡體系結構

16.2 地址與端口

IPv4web

  • 目前主流,32位。用點號分隔的4個十進制數(101.102.103.104)。地址多是惟一,也多是一個特定子網中的惟一(例如,企業網絡)。

IPv6c#

  • 128位地址。這些地址用冒號分隔的十六進制數(例如,[3EA0:FFFF:198A:E4A3:4FF2:54fA:41BC:8D31])。.NET FrameWork要求加方括號。

System.Net 命名空間的 IPAddress 類是採用其中一種協議地址。它有一個構造函數能夠接收字節數組,以及一個靜態的 Parse 方法接收正確字符串:數組

IPAddress a1 = new IPAddress(new byte[] { 101, 102, 103, 104 });
    IPAddress a2 = IPAddress.Parse("101.102.103.104");
    Console.WriteLine(a1.Equals(a2));                               //true
    Console.WriteLine(a1.AddressFamily);                            //InterNetwork
    IPAddress a3 = IPAddress.Parse("[3EA0:FFFF:198A:E4A3:4FF2:54fA:41BC:8D31]");
    Console.WriteLine(a3.AddressFamily);                            //InterNetworkV6

TCP和UDP協議將每個IP地址劃分爲65535個端口,從而容許一臺計算機在一個地址上運行多個應用程序,每個應用程序使用一個端口。許多應用程序都分配有標準端口,例如,HTTP使用端口80,SMTP使用端口25。瀏覽器

49152到65535的TCP和UDP端口官方保留,它們只用於測試和小規模部署。服務器

IP地址和端口組合在.NETFramwork 使用 IPEndPoint 類表示:cookie

IPAddress a = new IPAddress(new byte[] { 101, 102, 103, 104 });
            IPEndPoint ep = new IPEndPoint(a, 222);    //端口:222
            Console.WriteLine(ep.ToString());          //101.102.103.104:222

16.3 URI

URI描述了一個 Internet 或 LAN 的資源,例如網頁。文件或電子郵件地址。
URI通常氛圍三個元素:協議(scheme)、權限(authority)和路徑(path)。System.Uri 類正是採用這種劃分方式,爲每一種元素提供對應的屬性。
URI屬性
在構造函數中傳入如下字符串之一,就能夠建立一個 Uri 對象:網絡

  • URI字符串,例如http://www.ebay.com 或 file://janespc/sharedpics/dolphin.jpg
  • 硬盤中一個文件的絕對路徑,例如 c:\myfiles\data.xls
  • LAN中一個文件的 UNC路徑,例如 \janespc\sharedpics\dolphin.jpg

文件和UNC路徑會自動轉換爲URI:添加協議 "file:",反斜槓會轉換爲斜槓。Uri的構造函數在建立Uri以前也會對傳入的字符串執行一些基本的清理操做,包括將協議和主機名轉換爲小寫、刪除默認端口和空端口號。若是傳入一個不帶協議的URI字符串,例如"www.test.com",那麼會拋出一個 UriFormatException 異常。併發

Uri 有一個 IsLoopback 屬性,它表示 Uri 是否引用本地主機(IP地址爲127.0.0.1),以及一個 IsFile 屬性,它表示Uri是否引用一個本地或UNC(IsUnc)路徑。若是 IsFile 返回 true,LocalPath 屬性會返回一個符合本地操做系統習慣的 AbsolutePath(帶反斜槓),而後能夠用它來調用 File.OPenapp

Uri 的實例有一些只讀屬性。要修改一個 Uri,咱們須要實例化一個 UriBuilder 對象,這是一個可寫屬性,它能夠經過 Uri 屬性轉換爲 Uri。

Uri info = new Uri("http://www.domain.com:80/hosting/");
        Uri page = new Uri("http://www.domain.com/hosting/page.html");

        Console.WriteLine(info.Host);             //www.domain.com
        Console.WriteLine(info.Port);             //80
        Console.WriteLine(page.Port);             //80

        Console.WriteLine(info.IsBaseOf(page));      //True
        Uri relative = info.MakeRelativeUri(page);
        Console.WriteLine(relative.IsAbsoluteUri);   //Flase
        Console.WriteLine(relative.ToString());      //page.html

URI例子

Uri 一些靜態方法:

Uri.EscapeDataString(url);
UriHostNameType type =  Uri.CheckHostName(url);
bool isScheme =  Uri.CheckSchemeName(url);

Uri靜態方法

16.4 客戶端類

WebRequest 和 WebRespone是管理 HTTP 和 FTP 客戶端活動及 "file:" 協議的通用基類。

WebClient 是一個便利的門面類。它負責調用 WebRequestWebRespone,能夠節省不少編碼。WebClient 支持字符串、字節數組、文件和流,而 WebRequestWebRespone 只支持流。可是,WebClient不是萬能的,由於它不支持某些特性(如cookie)。

HttpClient 是另外一個基於 WebRequestWebRespone 的類(更準確說基於HttpWebRequest和HttpWebResponse),Framework 4.5 引入。

WebClient主要做爲請求/響應類之上薄薄的一層,而 HttpClient 則增長更多功能,可以處理基於HTTP的Web API、基於REST的服務和自定義驗證模式。

WebClientHttpClient都支持以字符串或字節數組方式處理簡單的文件下載/上傳操做。它們都擁有一些異步方法,可是隻有 WebClient 支持進度報告。

  請求/響應類 引入 異步進度報告
WebClient 薄薄一層 -
HttpClient 處理基於HTTP的Web API等 Framework 4.5

提示:WinRT應用程序不能使用 WebClient,必須使用WebRequest/WebResponseHttpClient(用於HTTP鏈接)

16.4.1 WebClient

WebClient 使用步驟:

  1. 實例化一個 WebClient 對象。
  2. 設置 Proxy 屬性值。
  3. 在須要驗證時設置 Credentials 屬性值。
  4. 使用響應的 URI 調用 DownliadXXXUploadXXX 方法。

下載方法有:

public void DownloadFile(string address, string fileName);
public string DownloadString(string address);
public byte[] DownloadData(string address);
public Stream OpenRead(string address);

每個方法都有重載,接收 URI 對象代替字符串地址的參數。上傳相似,返回包含服務器響應的值:

public byte[] UploadFile(string address, string fileName);
public byte[] UploadFile(Uri address, string fileName);
public byte[] UploadFile(string address, string method, string fileName);
public byte[] UploadFile(Uri address, string method, string fileName);
...
public string UploadString(string address, string data);
public byte[] UploadData(string address, byte[] data);
public Stream OpenWrite(string address);
...

UploadValues 方法可用於以 POST 方法參數提交一個 HTTP 表單的值。WebClient 還包含一個 BaseAddress 屬性,可用於爲全部地址添加一個字符串前綴,如http://www.mysite.com/data。

public byte[] UploadValues(string address, NameValueCollection data);
...

例子,下載 http://www.cnblogs.com/artwl/archive/2012/03/07/2382848.html 並以 code111.htm 保存

System.Net.WebClient webClient = new System.Net.WebClient();
            webClient.Proxy = null;
            webClient.DownloadFile("http://www.cnblogs.com/artwl/archive/2012/03/07/2382848.html", "code111.htm");
            System.Diagnostics.Process.Start("code111.htm");

從 Framework 4.5 開始,WebClient 提供了長任務方法的異步版本,他們會返回能夠等待的任務

await webClient.DownloadFileTaskAsync("http://www.qq.com", "wegpage.htm");

這些方法使用"TaskAsync"後綴,不一樣於使用"Async"後綴的EAP舊異步方法。可是,新方法不支持取消操做和進步報告的標準「TAP」模式。相反,在處理延續時,必須調用 WebClient 對象的 CanceAsync 方法;而處理報告時,須要處理 DownloadProgressChanged/UploadProgressChanged 事件。

下面例子下載一個網頁並顯示進度報告,若是下載超過5秒,則取消下載。

//winform.
        private void button1_Click(object sender, EventArgs e)
        {
            Test();
        }

        async Task Test()
        {
            var wc = new WebClient();
            wc.DownloadProgressChanged += (sender, args) =>
                //                獲取異步任務的進度百分比
                Console.WriteLine(args.ProgressPercentage + "% 完成");

            Task.Delay(5000).ContinueWith(ant => wc.CancelAsync());

            await wc.DownloadFileTaskAsync("http://www.qq.com", "wegpage.htm");
        }

提示: 當請求取消時,程序會拋出一個 WebException 異常,它的 Status 屬性是 WebExceptionStatus.RequestCanceled.(歷史緣由不拋出 OpeerationCanceledException 異常。)

捕捉與進度相關的事件,將它們提交到激活的同步上下文,因此它們的處理器不須要使用 Dispatcher.BeginInvoke,就能夠更新UI控件。

警告: 若是須要使用取消操做或進度報告,要避免使用同一個 WebClient 對象依次執行多個操做,由於這樣造成競爭條件。

16.4.2 WebRequest 和 WebResponse

比 WebClient 複雜,可是更靈活。
1.使用一個 URI 調用 WebRequest.Create,建立一個 Web 請求實例。
2.設置 Proxy 屬性。
3.若是須要身份驗證,設置 Credentials 屬性。

若是要上傳數據,則:
4.調用請求對象的 GetRequestStream,而後在流中寫入數據。若是須要處理響應,則轉到第5步。

若是要下載數據,則:
5.調用請求對象的 GetResponse,建立一個 Web 響應實例。

6.調用響應對象的 GetResponseStream,而後(可使用 StreamReader)從流中讀取數據。

WebRequest和WebResponse流程

下面例子演示如何下載和顯示一個示例網頁

//同步
        public static void Test1()
        {
            WebRequest req = WebRequest.Create("http://www.baidu.com");  //1.
            req.Proxy = null;                                            //2.
            using (WebResponse res = req.GetResponse())                  //5.下載
            {
                using (Stream rs = res.GetResponseStream())              //6.
                {
                    using (FileStream fs = File.Create("code.html"))
                    {
                        rs.CopyTo(fs);
                    }
                }
            }
        }

異步方式

//異步
        public static async void AsyncTest()
        {
            WebRequest req = WebRequest.Create("http://www.qq.com");
            req.Proxy = null;
            using (WebResponse res = await req.GetResponseAsync())
            {
                using (Stream rs = res.GetResponseStream())
                {
                    using (FileStream fs = File.Create("code2.html"))
                    {
                        await rs.CopyToAsync(fs);
                    }
                }
            }
        }

靜態方法 Create 會建立一個 WebRequest 類型的子類實例,如 HttpWebRequestFtpWebRequest
它選擇的子類取決 URI 的前綴。

前綴 Web請求類型
http:或https: HttpWebRequest
ftp: FtpWebRequest
file: FileWebRequest

此外,調用 WebRequest.RegisterPrefix,能夠註冊自定義前綴。

WebRequest 包含一個 Timeout 屬性,單位爲毫秒。若是出現超時,那麼會拋出一個 WebException 異常,其中包含一個 Status 屬性,WebExceptionStatus.Timeout。HTTP默認超時時間爲100秒,而FTP超時時間爲無限。
超時

WebRequest 對象不能回收並用於處理多個請求——每一個實例只適用於一個做業。

16.4.3 HttpClient

HttpClient 在 HttpWebRequest 和 HttpWebRespone 之上提供了另外一層封裝。它的設計是爲了支持愈來愈多的 Web API 和 REST 服務,在處理比獲取網頁等更復雜的協議時實現比 WebClient 更佳的體驗。

  • 一個 HttpClient 就能夠支持併發請求。(使用 WebClient 處理併發請求,須要爲每個併發線程建立一個新實例,須要自定義請求頭、cookies和驗證模式,所以很麻煩)
  • HttpClient 可用於編寫和插入自定義消息處理器。這樣能夠建立單元測試樁函數,以及建立自定義管道(用於記錄日誌、壓縮、加密等)調用WebClient的單元測試代碼則很難編寫。
  • HttpClient 包含豐富的可擴展的請求頭與內容類型系統。

    HttpClient 不能徹底替代 WebClient,由於它不支持進度報告。WebClient 也有一個有點,它支持 FTP、file://和自定義URI模式,適應全部Framework版本。

簡單 HttpClient 使用方法建立一個實例,而後使用 URI 調用 其中一個 Get* 方法:

string html = await new HttpClient().GetStringAsync("http://linqpad.net");

(另外還有 GetByteArrayAsync和GetStreamAsync)HttpClient的全部 I/O 密集型方法都是異步的(沒有同步版本)。

與 WebClient 不一樣,想獲取最佳性能 HttpClient,必須重用相同的實例。HttpClient 容許併發操做,因此下面語句合法。同時下載兩個網頁:

var client = new HttpClient();
    var tast1 = client.GetStringAsync("http://www.linqpad.net");
    var tast2 = client.GetStringAsync("http://www.albahari.com");
    Console.WriteLine(await task1);
    Console.WriteLine(await task2);

HttpClinet 包含一個 TimeOut 屬性和一個 BaseAddress 屬性,爲每個請求添加一個 URI 前綴。
HttpClient 在必定程度上就是一層實現:一般使用的大部分屬性都定義在另外一個類中,即 HttpClientHandler。要訪問這個類,先建立一個實例,而後將這個實例傳遞給 HttpClient 的構造方法:

var handler = new HttpClientHandler{UseProxy = false};
    var  client = new HttpClient(handler);
    ...

這個例子在處理器中禁用了代理支持。此外,還有其餘一些屬性可用於控制 cookies、自動重定向、驗證等(後續看16.5「HTTP訪問」)

1.GetAsync與響應消息

GetStringAsync、GetByteArrayAsync和GetStreamAsync方法是更經常使用的 GetAsync 方法的快捷方法。GetAsync方法會返回一個響應消息:

var client = new HttpClient();
        //GetAsync 方法也接受一個 CancellationToken
        HttpResponseMessage response = await client.GetAsync("http://www.baidu.com");
        response.EnsureSuccessStatusCode();
        string html = await response.Content.ReadAsStringAsync();
        Console.WriteLine(html);

HttpResponseMessage 包含一些訪問請求頭,和HTTP StatusCode 的屬性,與 WebClient 不一樣。除非顯示地調用 EnsureSuccessStatusCode,不然返回不成功狀態(如404,資源未找到)不會拋出異常,然而,通訊或DNS錯誤會拋出異常。

HttpResponseMessage 包含一個 CopyToAsync 方法,它能夠將數據寫到一個流中,適用於將輸入寫到一個文件中:

using (var fileStream =File.Create("linqpad.htm"))
{
    await response.Content.CopyToAsync(fileStream);
}

GetAsync 是與 HTTP 的4種動做相關的4個方法之一(PostAsync,PutAsync,DeleteAsync)

2.SendAsync與請求消息

這是訪問全部數據的惟一低層方法,要使用這個類,首先要建立一個 HttpRequestMessage

var client = new HttpClient(); 
    var request = new HttpRequestMessage(HttpMethod.Get, "http://www.weibo.com");
    HttpResponseMessage response = await client.SendAsync(request);
    response.EnsureSuccessStatusCode();
    ...

建立一個 HttpResponseMessage 對象,意味着能夠自定義請求的屬性,如請求頭和內容自己,它們可用於上傳數據。

3.上傳數據與HttpContent

建立一個 HttpRequestMessage 對象之後,設置它的 Content 屬性,就能夠上傳內容。這個屬性類型是抽象類 HttpContent。
Framework包含下面子類,對應不一樣類型。

  • ByteArrayContent
  • StreamContent
  • FormUrlEncodedContent
  • StreamContent
var client = new HttpClient(new HttpClientHandler() {UseProxy = false});
    var request = new HttpRequestMessage(HttpMethod.Post, "http://www.albahari.com/EchoPost.aspx");
    request.Content = new StringContent("This is a test");
    HttpResponseMessage response = await client.SendAsync(request);
    response.EnsureSuccessStatusCode();
    Console.WriteLine(await  response.Content.ReadAsStringAsync());

4.HttpMessageHandler

前面說過,大多數自定義請求屬性不在 HttpClient 中定義,而在 HttpClientHandler 中定義。後者實際是 HttpMessageHandler 的子類,

public abstract class HttpMessageHandler : IDisposable
{
    protected internal abstract Task<HttpResponseMessage> SendAsync
    (HttpRequestMessage request, CancellationToken cancellationToken);

    public void Dispose();
    protected virtual void Dispose(bool disposing);
}

SendAsync 方法是在 HttpClient 的 SendAsync 方法中調用。
HttpMessageHandler 很是容易繼承,同時提供 HttpClient 的擴展點。

5.單元測試與樁處理器

建立 HttpMessageHandler 的子類,就能夠建立一個幫助進行單元測試的樁處理器:
...

6.使用實現處理器鏈

建立 DelegatingHandler 的子類,就能夠建立一個調用其餘消息處理器的消息處理器(造成處理器鏈)。這種方法適用於實現自定義身份驗證、壓縮和加密協議。
例子,簡單的日誌處理器:

class LoggingHandler : DelegatingHandler
    {
        public LoggingHandler(HttpMessageHandler nextHandler)
        {
            InnerHandler = nextHandler;
        }

        protected async override Task<HttpResponseMessage> SendAsync
            (HttpRequestMessage request, CancellationToken cancellationToken)
        {
            Console.WriteLine("Requesting:" + request.RequestUri);
            var response = await base.SendAsync(request, cancellationToken);
            Console.WriteLine("Got response:" + response.StatusCode);
            return response;
        }
    }

這裏重寫 SendAsync 時保持異步性。在重載任務的方法時添加 async 修飾符是絕對合法的如這個例子。
相對於將信息直接寫到控制檯,更好的方法是給構造方法傳入某種日誌記錄對象。最好是接受一對 Action<T> 代理,而後讓他們記錄請求和響應對象的日誌信息。

16.4.4 代理

16.4.5 身份驗證

建立一個 NetworkCredential 對象,...

16.4.6 異常處理

16.5 HTTP訪問

16.5.1 請求頭

WebClient、WebRequest 和 HttpClient 均可以添加自定義HTTP請求頭,以及在響應中列舉請求頭信息。
請求頭只是一些鍵/值對,其中包含相應的元數據,如消息內容類型或服務器軟件。

例子演示如何在請求中添加自定義請求頭信息,而後在 HttpClient 的響應消息中列舉全部請求頭信息:

WebClient wc =new WebClient();
        wc.Proxy = null;
        wc.Headers.Add("CustomHeader","JustPlaying/1.0");
        wc.DownloadString("http://www.cnblogs.com");

        foreach (string name in wc.ResponseHeaders.Keys)
        {
            Console.WriteLine(name + "=" + wc.ResponseHeaders[name]);
        }

相反,HttpClient 包含一些強類型集合,其中包含與標準HTTP頭信息相對應的屬性。
DefaultRequestHeaders 屬性包含適用於每個請求的頭信息:

var client = new HttpClient(handler);   //handler = new HttpClientHandler() { UseProxy = false };
        client.DefaultRequestHeaders.UserAgent.Add(
            new ProductInfoHeaderValue("VisualStudio","2013")
            );

        client.DefaultRequestHeaders.Add("CustomHeader", "VisualStudio/2013");

HttpRequestMessage 類的 Headers 屬性則包含請求頭特有的頭信息。

16.5.2 查詢字符串

?key1=value1&key2=value2&key3=value3...
WebClient 包含一個字段風格的屬性,它能夠簡化查詢字符串的操做。
例子,在百度搜索單詞 "WebClient" ,而後顯示結果:

WebClient wc = new WebClient();
    wc.Proxy = null;
    wc.QueryString.Add("wd", "WebClient");     // Search for "WebClient"
    //wc.QueryString.Add("hl", "fr");           // 原本想法語,貌似錯的,這個是谷歌規則
    wc.DownloadFile("http://www.baidu.com/s", "results.html");
    System.Diagnostics.Process.Start("results.html");

若是要使用 WebRequest 或 HttpClient 實現相同效果,那麼必須手工賦值給請求URL正確格式字符串:

string requestURI = "https://www.baidu.com/s?wd=webclient";

若是查詢中包含符號或空格,那麼使用 URI 的 EscapeDataString 方法:

string search = Uri.EscapeDataString("(WebClient OR HttpClient)");
            string language = Uri.EscapeDataString("fr");
            string requestURI = "http://www.google.com/search?q=" + search +
                                "&hl=" + language;

結果爲 http://www.google.com/search?q=(WebClient%20OR%20HttpClient)&hl=fr

16.5.3 上傳表單數據

WebClent 的 UploadValues 方法能夠把HTML表單的方式提交數據:

WebClient wc = new WebClient();
    wc.Proxy = null;
    
    var data = new System.Collections.Specialized.NameValueCollection();
    data.Add("Name", "Joe Albahari");
    data.Add("Company", "O'Reilly");
    
    byte[] result = wc.UploadValues("http://www.albahari.com/EchoPost.aspx",
                                     "POST", data);
    
    Console.WriteLine(Encoding.UTF8.GetString(result));

結果:
You posted=Name=Joe+Albahari&Company=O'Reilly

NameValueCollection 中的鍵(如 searchtextbox 和 searchMode) 與HTML表單的輸入框相對應。使用 WebRequest 上傳表單數據操做更復雜。(若是須要使用 cookies等特性,則必須採用這種方法)

下面是具體操做過程:

  1. 將請求的 ContentType 設置爲 "application/x-www.form-urlencoded",將它的方法設置爲「POST」。
  2. 建立一個包含上傳數據的字符串,而且將其編碼爲:
    name1=value1&name2=value2&name3=value3...
  3. 使用 Encoding.UTF8.GetBytes 將字符串轉爲字節數組。
  4. 將 Web 請求的 ContentLength 屬性設置爲字節組的長度。
  5. 調用 Web 請求的 GetRequestStream,而後寫入數據數組。
  6. 調用 GetResponse,讀取服務器的響應。
//WebRequest 上傳表單數據
var req = WebRequest.Create ("http://www.albahari.com/EchoPost.aspx");
req.Proxy = null;
req.Method = "POST";
req.ContentType = "application/x-www-form-urlencoded";

string reqString = "Name=Joe+Albahari&Company=O'Reilly";
byte[] reqData = Encoding.UTF8.GetBytes (reqString);
req.ContentLength = reqData.Length;

using (Stream reqStream = req.GetRequestStream())
  reqStream.Write (reqData, 0, reqData.Length);

using (WebResponse res = req.GetResponse())
using (Stream resSteam = res.GetResponseStream())
using (StreamReader sr = new StreamReader (resSteam))
  Console.WriteLine (sr.ReadToEnd());

若是使用 HttpClient,則要建立和生成 FormUrlEncodedContent 對象,而後再傳遞到 PostAsync 方法,或者設置到請求的 Content 屬性上。

//HttpClient 上傳表單數據
string uri = "http://www.albahari.com/EchoPost.aspx";
var client = new HttpClient();
var dict = new Dictionary<string,string> 
{
    { "Name", "Joe Albahari" },
    { "Company", "O'Reilly" }
};
var values = new FormUrlEncodedContent (dict);
var response = await client.PostAsync (uri, values);
response.EnsureSuccessStatusCode();
Console.WriteLine (await response.Content.ReadAsStringAsync());

16.5.4 Cookies

Cookies是名稱/值字符串對,它是HTTP服務器經過響應頭髮送到客戶端。Web瀏覽器客戶端通常會記住cookie,而後在終止以前,後續請求都會將它們重複發送給服務器(相同地址)。
Cookie 使服務器知道它是否正在鏈接以前鏈接過的相同客戶端,從而不須要在URI重複添加負責查詢字符串。

默認狀況,HttpWebRequest 會忽略從服務器接收的任意 cookie。爲了接收 cookie,必須建立一個 CookieCotainer 對象,而後分配到WebRequest。而後,就能夠列舉響應中接收到的 cookie:

CookieContainer cc = new CookieContainer();

var request = (HttpWebRequest) WebRequest.Create ("http://www.baidu.com");
request.Proxy = null;
request.CookieContainer = cc;
using (var response = (HttpWebResponse) request.GetResponse())
{
  foreach (Cookie c in response.Cookies)
  {
    Console.WriteLine (" Name:   " + c.Name);
    Console.WriteLine (" Value:  " + c.Value);
    Console.WriteLine (" Path:   " + c.Path);
    Console.WriteLine (" Domain: " + c.Domain);
  }
  // Read response stream 讀取響應流...
}

結果
若是使用 HttpClient,則須要建立一個 HttpClientHandler 實例:

var cc = new CookieContainer();
var handler = new HttpClientHandler();
handler.CookieContainer = cc;  //設置
var client = new HttpClient (handler);

WebClient 不支持 cookie。
若是須要在未來的請求上接收到的 cookie,則只須要給每個新的 WebRequest 對象設置相同的 CookieContainer 對象,或者在 HttpClient 中使用相同對象發送請求。
CookieContainer 是可序列化的類,因此它能夠寫到磁盤中。此外,能夠先使用一個新的 CookieContainer,而後再按照下面的方法手動添加cookie:

CookieContainer cc = new CookieContainer();
        ....
        Cookie c = new Cookie("BAIDUID","00DD40CC4B46....."
        ,"/",".baidu.com");
        cc.Add(c);

16.5.5 表單驗證

16.4.5 身份驗證 介紹如何使用 NetworkCredentials 對象實現其中一些類型的驗證,如Basic或NTLM(在 Web瀏覽器彈出一個對話框)。然而,大多數驗證的網站都使用某種基於表單的方法。
用戶在文本框輸入用戶名和密碼,單擊按鈕提交數據,而後在成功驗證以後接收到 cookie,這個 cookie 屬於所訪問網頁的私有數據。經過 WebRequest 或 HttpClient,就能夠實現。

<form action="http://www.somesite.com/login" method="post">
   <input type="text" name="username" id="user">
   <input type="password" name="password" id="pass">
   <button type="submit" id="login-btn">Log In</button>
</form>

下面演示 WebRequest/WebResponse 登陸網站:

string loginUri = "http://www.somesite.com/login";
string username = "username";   // (Your username)
string password = "password";   // (Your password)
string reqString = "username=" + username + "&password=" + password;
byte[] requestData = Encoding.UTF8.GetBytes (reqString);

CookieContainer cc = new CookieContainer();
var request = (HttpWebRequest)WebRequest.Create (loginUri);
request.Proxy = null;
request.CookieContainer = cc;
request.Method = "POST";

request.ContentType = "application/x-www-form-urlencoded";
request.ContentLength = requestData.Length;

using (Stream s = request.GetRequestStream())
  s.Write (requestData, 0, requestData.Length);

using (var response = (HttpWebResponse) request.GetResponse())
  foreach (Cookie c in response.Cookies)
    Console.WriteLine (c.Name + " = " + c.Value);

//咱們如今已經成功登陸,只要給後續的 WebRequest 對象添加 cc,就能夠保持已驗證用戶的狀態

使用 HttpClient 實現的方法:

string loginUri = "http://www.somesite.com/login";
string username = "username";
string password = "password";

CookieContainer cc = new CookieContainer();
var handler = new HttpClientHandler { CookieContainer = cc };

var request = new HttpRequestMessage (HttpMethod.Post, loginUri);
request.Content = new FormUrlEncodedContent (new Dictionary<string,string>
{
  { "username", username },
  { "password", password }
});

var client = new HttpClient (handler);
var response = await client.SendAsync (request);
response.EnsureSuccessStatusCode();

16.5.6 SSL

16.6 編寫HTTP服務器

咱們可使用 HttpListener 類編寫自定義 HTTP 服務器,下面是一個監聽端口 51111 的簡單服務器,它會等待一個客戶端請求,而後返回一行回覆。

static void Main()
{
  ListenAsync();                           // Start server
  WebClient wc = new WebClient();          // Make a client request.
  Console.WriteLine (wc.DownloadString
    ("http://localhost:51111/MyApp/Request.txt"));
}

async static void ListenAsync()
{
  HttpListener listener = new HttpListener();
  listener.Prefixes.Add ("http://localhost:51111/MyApp/");  // Listen on
  listener.Start();                                         // port 51111.

  // Await a client request:
  HttpListenerContext context = await listener.GetContextAsync();

  // Respond to the request:
  string msg = "You asked for: " + context.Request.RawUrl;
  context.Response.ContentLength64 = Encoding.UTF8.GetByteCount (msg);
  context.Response.StatusCode = (int) HttpStatusCode.OK;

  using (Stream s = context.Response.OutputStream)
  using (StreamWriter writer = new StreamWriter (s))
    await writer.WriteAsync (msg);

  listener.Stop();
}

輸出You asked for: /MyApp/Request.txt

HttpListener 內部並步使用 .net Socket 對象,相反,它調用 Windows HTTP Server API。支持計算機中多個應用程序監聽相同的IP地址和端口,前提是每個應用程序都註冊不一樣的地址前綴。

調用 GetContext 時,HttpListener 會等待下一個客戶端請求,這個方法會返回一個對象,其中包含 Request 和 Response 屬性。每個屬性都與 WebRequest 和 WebResponse 對象相似,可是它們屬於服務器。例如,咱們能夠讀寫請求和響應對象的頭信息和 Cookie,使用的方法與客戶端很是相似。

咱們能夠基於預期客戶端類型選擇如何支持 HTTP 協議特性。至少,每個請求都須要設置內容長度和狀態碼。

下面這個簡單網頁服務器,最多支持50併發請求:

using System;
using System.IO;
using System.Net;
using System.Threading.Tasks;

class WebServer
{
  HttpListener _listener;
  string _baseFolder;      // Your web page folder.

  public WebServer (string uriPrefix, string baseFolder)
  {
    _listener = new HttpListener();
    _listener.Prefixes.Add (uriPrefix);
    _baseFolder = baseFolder;
  }

  public async void Start()
  {
    _listener.Start();
    while (true)
      try 
      {
        var context = await _listener.GetContextAsync();
        Task.Run (() => ProcessRequestAsync (context));
      }
      catch (HttpListenerException)     { break; }   // Listener stopped.
      catch (InvalidOperationException) { break; }   // Listener stopped.
  }

  public void Stop() { _listener.Stop(); }

  async void ProcessRequestAsync (HttpListenerContext context)
  {
    try
    {
      string filename = Path.GetFileName (context.Request.RawUrl);
      string path = Path.Combine (_baseFolder, filename);
      byte[] msg;
      if (!File.Exists (path))
      {
        Console.WriteLine ("Resource not found: " + path);
        context.Response.StatusCode = (int) HttpStatusCode.NotFound;
        msg = Encoding.UTF8.GetBytes ("Sorry, that page does not exist");
      }
      else
      {
        context.Response.StatusCode = (int) HttpStatusCode.OK;
        msg = File.ReadAllBytes (path);
      }
      context.Response.ContentLength64 = msg.Length;
      using (Stream s = context.Response.OutputStream)
        await s.WriteAsync (msg, 0, msg.Length);
    }
    catch (Exception ex) { Console.WriteLine ("Request error: " + ex); }
  }
}

下面是啓動程序的 Main 方法:

static void Main()
{
  // Listen on port 51111, serving files in d:\webroot:
  var server = new WebServer ("http://localhost:51111/", @"d:\webroot");
  try
  {
    server.Start();
    Console.WriteLine ("Server running... press Enter to stop");
    Console.ReadLine();
  }
  finally { server.Stop(); }
}

可使用任何一個 Web 瀏覽器做爲客戶端進行測試,這裏的URI是 http://localhost/ 加上網站名稱。

警告: 若是其餘軟件佔用同一個端口,那麼 HttpListener 不會啓動(除非這個軟件也使用Windows HTTP Server API)

使用異步函數能夠實現服務器的可擴展性和提升運行效率。然而,從UI線程開始均可能會影響可擴展性,由於每個請求中,執行過程都會在每一次等待以後返回UI線程。這種等待毫無心義,由於沒有共享的狀態。因此UI中,最好去掉UI線程,或者這樣:
Task.Run(start);
或者在調用 GetContextAsync 以後調用 ConfigureAwait(false)

注意,即便這些方法是異步的,但咱們仍然在 Task.Run 中調用 ProcessRequestAsync。這樣就尅容許調用者立刻可以處理另外一個請求,而不須要同步等待方法執行結束(一直到第一個 await)。

相關文章
相關標籤/搜索