16.網(wǎng)絡(luò)《果殼中的c#》

網(wǎng)絡(luò)體系

16.1 網(wǎng)絡(luò)體系結(jié)構(gòu)

System.Net.* 命名空間包含各種支持標(biāo)準(zhǔn)網(wǎng)絡(luò)協(xié)議的通信驳癌。

  • WebClient 外觀類:支持通信HTTP或FTP執(zhí)行簡(jiǎn)單的下載/上傳操作。
  • WebRequestWebResponse 類:支持更多的客戶端HTTP或FTP操作娃闲。
  • HttpListener 類:可用來(lái)編寫(xiě)HTTP服務(wù)器带斑。
  • SmtpClient類:支持通過(guò) SMTP 創(chuàng)建和發(fā)送電子郵件。
  • DNS類:支持域名和地址之間的轉(zhuǎn)換济锄。
  • TcpClient聊浅、UdpClient餐抢、TcpListenerSocket類:支持傳輸層和網(wǎng)絡(luò)層的直接訪問(wèn)现使。
網(wǎng)絡(luò)體系結(jié)構(gòu)

16.2 地址與端口

IPv4

  • 目前主流,32位旷痕。用點(diǎn)號(hào)分隔的4個(gè)十進(jìn)制數(shù)(101.102.103.104)碳锈。地址可能是唯一,也可能是一個(gè)特定子網(wǎng)中的唯一(例如欺抗,企業(yè)網(wǎng)絡(luò))售碳。

IPv6

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

System.Net 命名空間的 IPAddress 類是采用其中一種協(xié)議地址佃声。它有一個(gè)構(gòu)造函數(shù)可以接收字節(jié)數(shù)組艺智,以及一個(gè)靜態(tài)的 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協(xié)議將每一個(gè)IP地址劃分為65535個(gè)端口,從而允許一臺(tái)計(jì)算機(jī)在一個(gè)地址上運(yùn)行多個(gè)應(yīng)用程序圾亏,每一個(gè)應(yīng)用程序使用一個(gè)端口十拣。許多應(yīng)用程序都分配有標(biāo)準(zhǔn)端口,例如志鹃,HTTP使用端口80夭问,SMTP使用端口25。

49152到65535的TCP和UDP端口官方保留曹铃,它們只用于測(cè)試和小規(guī)模部署缰趋。

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

            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描述了一個(gè) Internet 或 LAN 的資源,例如網(wǎng)頁(yè)陕见。文件或電子郵件地址秘血。
URI一般氛圍三個(gè)元素:協(xié)議(scheme)、權(quán)限(authority)和路徑(path)淳玩。System.Uri 類正是采用這種劃分方式直撤,為每一種元素提供對(duì)應(yīng)的屬性非竿。

URI屬性

在構(gòu)造函數(shù)中傳入以下字符串之一蜕着,就可以創(chuàng)建一個(gè) Uri 對(duì)象:

  • URI字符串,例如http://www.ebay.com 或 file://janespc/sharedpics/dolphin.jpg
  • 硬盤(pán)中一個(gè)文件的絕對(duì)路徑红柱,例如 c:\myfiles\data.xls
  • LAN中一個(gè)文件的 UNC路徑承匣,例如 \janespc\sharedpics\dolphin.jpg

文件和UNC路徑會(huì)自動(dòng)轉(zhuǎn)換為URI:添加協(xié)議 "file:",反斜杠會(huì)轉(zhuǎn)換為斜杠锤悄。Uri的構(gòu)造函數(shù)在創(chuàng)建Uri之前也會(huì)對(duì)傳入的字符串執(zhí)行一些基本的清理操作韧骗,包括將協(xié)議和主機(jī)名轉(zhuǎn)換為小寫(xiě)、刪除默認(rèn)端口和空端口號(hào)零聚。如果傳入一個(gè)不帶協(xié)議的URI字符串袍暴,例如"www.test.com"些侍,那么會(huì)拋出一個(gè) UriFormatException 異常。

Uri 有一個(gè) IsLoopback 屬性政模,它表示 Uri 是否引用本地主機(jī)(IP地址為127.0.0.1)岗宣,以及一個(gè) IsFile 屬性,它表示Uri是否引用一個(gè)本地或UNC(IsUnc)路徑淋样。如果 IsFile 返回 true,LocalPath 屬性會(huì)返回一個(gè)符合本地操作系統(tǒng)習(xí)慣的 AbsolutePath(帶反斜杠)耗式,然后可以用它來(lái)調(diào)用 File.OPen

Uri 的實(shí)例有一些只讀屬性趁猴。要修改一個(gè) Uri刊咳,我們需要實(shí)例化一個(gè) UriBuilder 對(duì)象,這是一個(gè)可寫(xiě)屬性儡司,它可以通過(guò) Uri 屬性轉(zhuǎn)換為 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 一些靜態(tài)方法:

Uri.EscapeDataString(url);
UriHostNameType type =  Uri.CheckHostName(url);
bool isScheme =  Uri.CheckSchemeName(url);
Uri靜態(tài)方法

16.4 客戶端類

WebRequest 和 WebRespone是管理 HTTP 和 FTP 客戶端活動(dòng)及 "file:" 協(xié)議的通用基類。

WebClient 是一個(gè)便利的門面類枫慷。它負(fù)責(zé)調(diào)用 WebRequestWebRespone让蕾,可以節(jié)省很多編碼。WebClient 支持字符串或听、字節(jié)數(shù)組探孝、文件和流,而 WebRequestWebRespone 只支持流誉裆。但是顿颅,WebClient不是萬(wàn)能的,因?yàn)樗恢С帜承┨匦裕ㄈ鏲ookie)足丢。

HttpClient 是另一個(gè)基于 WebRequestWebRespone 的類(更準(zhǔn)確說(shuō)基于HttpWebRequest和HttpWebResponse)粱腻,F(xiàn)ramework 4.5 引入。

WebClient主要作為請(qǐng)求/響應(yīng)類之上薄薄的一層斩跌,而 HttpClient 則增加更多功能绍些,能夠處理基于HTTP的Web API、基于REST的服務(wù)和自定義驗(yàn)證模式耀鸦。

WebClientHttpClient都支持以字符串或字節(jié)數(shù)組方式處理簡(jiǎn)單的文件下載/上傳操作柬批。它們都擁有一些異步方法,但是只有 WebClient 支持進(jìn)度報(bào)告袖订。

請(qǐng)求/響應(yīng)類 引入 異步進(jìn)度報(bào)告
WebClient 薄薄一層 -
HttpClient 處理基于HTTP的Web API等 Framework 4.5 無(wú)

提示:WinRT應(yīng)用程序不能使用 WebClient氮帐,必須使用WebRequest/WebResponseHttpClient(用于HTTP連接)

16.4.1 WebClient

WebClient 使用步驟:

  1. 實(shí)例化一個(gè) WebClient 對(duì)象。
  2. 設(shè)置 Proxy 屬性值洛姑。
  3. 在需要驗(yàn)證時(shí)設(shè)置 Credentials 屬性值上沐。
  4. 使用響應(yīng)的 URI 調(diào)用 DownliadXXXUploadXXX 方法。

下載方法有:

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

每一個(gè)方法都有重載楞艾,接收 URI 對(duì)象代替字符串地址的參數(shù)参咙。上傳類似龄广,返回包含服務(wù)器響應(yīng)的值:

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 方法參數(shù)提交一個(gè) HTTP 表單的值。WebClient 還包含一個(gè) BaseAddress 屬性蕴侧,可用于為所有地址添加一個(gè)字符串前綴蜀细,如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 開(kāi)始奠衔,WebClient 提供了長(zhǎng)任務(wù)方法的異步版本,他們會(huì)返回可以等待的任務(wù)

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

這些方法使用"TaskAsync"后綴塘娶,不同于使用"Async"后綴的EAP舊異步方法归斤。但是,新方法不支持取消操作和進(jìn)步報(bào)告的標(biāo)準(zhǔn)“TAP”模式刁岸。相反脏里,在處理延續(xù)時(shí),必須調(diào)用 WebClient 對(duì)象的 CanceAsync 方法虹曙;而處理報(bào)告時(shí)迫横,需要處理 DownloadProgressChanged/UploadProgressChanged 事件。

下面例子下載一個(gè)網(wǎng)頁(yè)并顯示進(jìn)度報(bào)告酝碳,如果下載超過(guò)5秒矾踱,則取消下載。

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

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

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

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

提示: 當(dāng)請(qǐng)求取消時(shí)疏哗,程序會(huì)拋出一個(gè) WebException 異常呛讲,它的 Status 屬性是 WebExceptionStatus.RequestCanceled.(歷史原因不拋出 OpeerationCanceledException 異常。)

捕捉與進(jìn)度相關(guān)的事件返奉,將它們提交到激活的同步上下文贝搁,所以它們的處理器不需要使用 Dispatcher.BeginInvoke,就可以更新UI控件芽偏。

警告: 如果需要使用取消操作或進(jìn)度報(bào)告雷逆,要避免使用同一個(gè) WebClient 對(duì)象依次執(zhí)行多個(gè)操作,因?yàn)檫@樣形成競(jìng)爭(zhēng)條件污尉。

16.4.2 WebRequest 和 WebResponse

比 WebClient 復(fù)雜膀哲,但是更靈活。
1.使用一個(gè) URI 調(diào)用 WebRequest.Create十厢,創(chuàng)建一個(gè) Web 請(qǐng)求實(shí)例等太。
2.設(shè)置 Proxy 屬性捂齐。
3.如果需要身份驗(yàn)證蛮放,設(shè)置 Credentials 屬性。

如果要上傳數(shù)據(jù)奠宜,則:
4.調(diào)用請(qǐng)求對(duì)象的 GetRequestStream包颁,然后在流中寫(xiě)入數(shù)據(jù)瞻想。如果需要處理響應(yīng),則轉(zhuǎn)到第5步娩嚼。

如果要下載數(shù)據(jù)蘑险,則:
5.調(diào)用請(qǐng)求對(duì)象的 GetResponse,創(chuàng)建一個(gè) Web 響應(yīng)實(shí)例岳悟。

6.調(diào)用響應(yīng)對(duì)象的 GetResponseStream佃迄,然后(可以使用 StreamReader)從流中讀取數(shù)據(jù)。

WebRequest和WebResponse流程

下面例子演示如何下載和顯示一個(gè)示例網(wǎng)頁(yè)

        //同步
        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);
                    }
                }
            }
        }

靜態(tài)方法 Create 會(huì)創(chuàng)建一個(gè) WebRequest 類型的子類實(shí)例贵少,如 HttpWebRequestFtpWebRequest呵俏。
它選擇的子類取決 URI 的前綴。

前綴 Web請(qǐng)求類型
http:或https: HttpWebRequest
ftp: FtpWebRequest
file: FileWebRequest

此外滔灶,調(diào)用 WebRequest.RegisterPrefix普碎,可以注冊(cè)自定義前綴。

WebRequest 包含一個(gè) Timeout 屬性录平,單位為毫秒麻车。如果出現(xiàn)超時(shí),那么會(huì)拋出一個(gè) WebException 異常斗这,其中包含一個(gè) Status 屬性动猬,WebExceptionStatus.Timeout。HTTP默認(rèn)超時(shí)時(shí)間為100秒表箭,而FTP超時(shí)時(shí)間為無(wú)限枣察。

超時(shí)

WebRequest 對(duì)象不能回收并用于處理多個(gè)請(qǐng)求——每個(gè)實(shí)例只適用于一個(gè)作業(yè)。

16.4.3 HttpClient

HttpClient 在 HttpWebRequest 和 HttpWebRespone 之上提供了另一層封裝。它的設(shè)計(jì)是為了支持越來(lái)越多的 Web API 和 REST 服務(wù),在處理比獲取網(wǎng)頁(yè)等更復(fù)雜的協(xié)議時(shí)實(shí)現(xiàn)比 WebClient 更佳的體驗(yàn)虱咧。

  • 一個(gè) HttpClient 就可以支持并發(fā)請(qǐng)求劈愚。(使用 WebClient 處理并發(fā)請(qǐng)求,需要為每一個(gè)并發(fā)線程創(chuàng)建一個(gè)新實(shí)例叽粹,需要自定義請(qǐng)求頭、cookies和驗(yàn)證模式,因此很麻煩)
  • HttpClient 可用于編寫(xiě)和插入自定義消息處理器叛赚。這樣可以創(chuàng)建單元測(cè)試樁函數(shù),以及創(chuàng)建自定義管道(用于記錄日志稽揭、壓縮俺附、加密等)調(diào)用WebClient的單元測(cè)試代碼則很難編寫(xiě)。
  • HttpClient 包含豐富的可擴(kuò)展的請(qǐng)求頭與內(nèi)容類型系統(tǒng)溪掀。

HttpClient 不能完全替代 WebClient事镣,因?yàn)樗恢С诌M(jìn)度報(bào)告。WebClient 也有一個(gè)有點(diǎn)揪胃,它支持 FTP璃哟、file://和自定義URI模式氛琢,適應(yīng)所有Framework版本。

簡(jiǎn)單 HttpClient 使用方法創(chuàng)建一個(gè)實(shí)例随闪,然后使用 URI 調(diào)用 其中一個(gè) Get* 方法:

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

(另外還有 GetByteArrayAsync和GetStreamAsync)HttpClient的所有 I/O 密集型方法都是異步的(沒(méi)有同步版本)阳似。

與 WebClient 不同,想獲取最佳性能 HttpClient铐伴,必須重用相同的實(shí)例撮奏。HttpClient 允許并發(fā)操作,所以下面語(yǔ)句合法当宴。同時(shí)下載兩個(gè)網(wǎng)頁(yè):

    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 包含一個(gè) TimeOut 屬性和一個(gè) BaseAddress 屬性挽荡,為每一個(gè)請(qǐng)求添加一個(gè) URI 前綴。
HttpClient 在一定程度上就是一層實(shí)現(xiàn):通常使用的大部分屬性都定義在另一個(gè)類中即供,即 HttpClientHandler定拟。要訪問(wèn)這個(gè)類,先創(chuàng)建一個(gè)實(shí)例逗嫡,然后將這個(gè)實(shí)例傳遞給 HttpClient 的構(gòu)造方法:

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

這個(gè)例子在處理器中禁用了代理支持青自。此外,還有其他一些屬性可用于控制 cookies驱证、自動(dòng)重定向延窜、驗(yàn)證等(后續(xù)看16.5“HTTP訪問(wèn)”)

1.GetAsync與響應(yīng)消息

GetStringAsync、GetByteArrayAsync和GetStreamAsync方法是更常用的 GetAsync 方法的快捷方法抹锄。GetAsync方法會(huì)返回一個(gè)響應(yīng)消息:

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

HttpResponseMessage 包含一些訪問(wèn)請(qǐng)求頭逆瑞,和HTTP StatusCode 的屬性,與 WebClient 不同伙单。除非顯示地調(diào)用 EnsureSuccessStatusCode获高,否則返回不成功狀態(tài)(如404,資源未找到)不會(huì)拋出異常吻育,然而念秧,通信或DNS錯(cuò)誤會(huì)拋出異常。

HttpResponseMessage 包含一個(gè) CopyToAsync 方法布疼,它可以將數(shù)據(jù)寫(xiě)到一個(gè)流中摊趾,適用于將輸入寫(xiě)到一個(gè)文件中:

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

GetAsync 是與 HTTP 的4種動(dòng)作相關(guān)的4個(gè)方法之一(PostAsync,PutAsync游两,DeleteAsync)

2.SendAsync與請(qǐng)求消息

這是訪問(wèn)所有數(shù)據(jù)的唯一低層方法砾层,要使用這個(gè)類,首先要?jiǎng)?chuàng)建一個(gè) HttpRequestMessage

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

創(chuàng)建一個(gè) HttpResponseMessage 對(duì)象贱案,意味著可以自定義請(qǐng)求的屬性肛炮,如請(qǐng)求頭和內(nèi)容本身,它們可用于上傳數(shù)據(jù)。

3.上傳數(shù)據(jù)與HttpContent

創(chuàng)建一個(gè) HttpRequestMessage 對(duì)象以后铸董,設(shè)置它的 Content 屬性,就可以上傳內(nèi)容肴沫。這個(gè)屬性類型是抽象類 HttpContent粟害。
Framework包含下面子類,對(duì)應(yīng)不同類型颤芬。

  • 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

前面說(shuō)過(guò)悲幅,大多數(shù)自定義請(qǐng)求屬性不在 HttpClient 中定義,而在 HttpClientHandler 中定義站蝠。后者實(shí)際是 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 方法中調(diào)用。
HttpMessageHandler 非常容易繼承菱魔,同時(shí)提供 HttpClient 的擴(kuò)展點(diǎn)留荔。

5.單元測(cè)試與樁處理器

創(chuàng)建 HttpMessageHandler 的子類,就可以創(chuàng)建一個(gè)幫助進(jìn)行單元測(cè)試的樁處理器:
...

6.使用實(shí)現(xiàn)處理器鏈

創(chuàng)建 DelegatingHandler 的子類澜倦,就可以創(chuàng)建一個(gè)調(diào)用其他消息處理器的消息處理器(形成處理器鏈)聚蝶。這種方法適用于實(shí)現(xiàn)自定義身份驗(yàn)證、壓縮和加密協(xié)議藻治。
例子碘勉,簡(jiǎn)單的日志處理器:

    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;
        }
    }

這里重寫(xiě) SendAsync 時(shí)保持異步性。在重載任務(wù)的方法時(shí)添加 async 修飾符是絕對(duì)合法的如這個(gè)例子桩卵。
相對(duì)于將信息直接寫(xiě)到控制臺(tái)验靡,更好的方法是給構(gòu)造方法傳入某種日志記錄對(duì)象。最好是接受一對(duì) Action<T> 代理雏节,然后讓他們記錄請(qǐng)求和響應(yīng)對(duì)象的日志信息胜嗓。

16.4.4 代理

16.4.5 身份驗(yàn)證

創(chuàng)建一個(gè) NetworkCredential 對(duì)象,...

16.4.6 異常處理

16.5 HTTP訪問(wèn)

16.5.1 請(qǐng)求頭

WebClient钩乍、WebRequest 和 HttpClient 都可以添加自定義HTTP請(qǐng)求頭兼蕊,以及在響應(yīng)中列舉請(qǐng)求頭信息。
請(qǐng)求頭只是一些鍵/值對(duì)件蚕,其中包含相應(yīng)的元數(shù)據(jù)孙技,如消息內(nèi)容類型或服務(wù)器軟件。

例子演示如何在請(qǐng)求中添加自定義請(qǐng)求頭信息排作,然后在 HttpClient 的響應(yīng)消息中列舉所有請(qǐng)求頭信息:

        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 包含一些強(qiáng)類型集合,其中包含與標(biāo)準(zhǔn)HTTP頭信息相對(duì)應(yīng)的屬性妄痪。
DefaultRequestHeaders 屬性包含適用于每一個(gè)請(qǐng)求的頭信息:

        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 屬性則包含請(qǐng)求頭特有的頭信息哈雏。

16.5.2 查詢字符串

?key1=value1&key2=value2&key3=value3...
WebClient 包含一個(gè)字段風(fēng)格的屬性,它可以簡(jiǎn)化查詢字符串的操作。
例子裳瘪,在百度搜索單詞 "WebClient" 土浸,然后顯示結(jié)果:

    WebClient wc = new WebClient();
    wc.Proxy = null;
    wc.QueryString.Add("wd", "WebClient");     // Search for "WebClient"
    //wc.QueryString.Add("hl", "fr");           // 本來(lái)想法語(yǔ),貌似錯(cuò)的彭羹,這個(gè)是谷歌規(guī)則
    wc.DownloadFile("http://www.baidu.com/s", "results.html");
    System.Diagnostics.Process.Start("results.html");

如果要使用 WebRequest 或 HttpClient 實(shí)現(xiàn)相同效果黄伊,那么必須手工賦值給請(qǐng)求URL正確格式字符串:

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

如果查詢中包含符號(hào)或空格,那么使用 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;

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

16.5.3 上傳表單數(shù)據(jù)

WebClent 的 UploadValues 方法可以把HTML表單的方式提交數(shù)據(jù):

    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));

結(jié)果:
You posted=Name=Joe+Albahari&Company=O'Reilly

NameValueCollection 中的鍵(如 searchtextbox 和 searchMode) 與HTML表單的輸入框相對(duì)應(yīng)派殷。使用 WebRequest 上傳表單數(shù)據(jù)操作更復(fù)雜还最。(如果需要使用 cookies等特性,則必須采用這種方法)

下面是具體操作過(guò)程:

  1. 將請(qǐng)求的 ContentType 設(shè)置為 "application/x-www.form-urlencoded"毡惜,將它的方法設(shè)置為“POST”拓轻。
  2. 創(chuàng)建一個(gè)包含上傳數(shù)據(jù)的字符串,并且將其編碼為:
    name1=value1&name2=value2&name3=value3...
  3. 使用 Encoding.UTF8.GetBytes 將字符串轉(zhuǎn)為字節(jié)數(shù)組经伙。
  4. 將 Web 請(qǐng)求的 ContentLength 屬性設(shè)置為字節(jié)組的長(zhǎng)度扶叉。
  5. 調(diào)用 Web 請(qǐng)求的 GetRequestStream,然后寫(xiě)入數(shù)據(jù)數(shù)組帕膜。
  6. 調(diào)用 GetResponse辜梳,讀取服務(wù)器的響應(yīng)。
//WebRequest 上傳表單數(shù)據(jù)
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泳叠,則要?jiǎng)?chuàng)建和生成 FormUrlEncodedContent 對(duì)象作瞄,然后再傳遞到 PostAsync 方法,或者設(shè)置到請(qǐng)求的 Content 屬性上危纫。

//HttpClient 上傳表單數(shù)據(jù)
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是名稱/值字符串對(duì)宗挥,它是HTTP服務(wù)器通過(guò)響應(yīng)頭發(fā)送到客戶端。Web瀏覽器客戶端一般會(huì)記住cookie种蝶,然后在終止之前契耿,后續(xù)請(qǐng)求都會(huì)將它們重復(fù)發(fā)送給服務(wù)器(相同地址)。
Cookie 使服務(wù)器知道它是否正在連接之前連接過(guò)的相同客戶端螃征,從而不需要在URI重復(fù)添加負(fù)責(zé)查詢字符串搪桂。

默認(rèn)情況,HttpWebRequest 會(huì)忽略從服務(wù)器接收的任意 cookie盯滚。為了接收 cookie踢械,必須創(chuàng)建一個(gè) CookieCotainer 對(duì)象,然后分配到WebRequest魄藕。然后内列,就可以列舉響應(yīng)中接收到的 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 讀取響應(yīng)流...
}

結(jié)果

如果使用 HttpClient,則需要?jiǎng)?chuàng)建一個(gè) HttpClientHandler 實(shí)例:

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

WebClient 不支持 cookie背率。
如果需要在將來(lái)的請(qǐng)求上接收到的 cookie话瞧,則只需要給每一個(gè)新的 WebRequest 對(duì)象設(shè)置相同的 CookieContainer 對(duì)象嫩与,或者在 HttpClient 中使用相同對(duì)象發(fā)送請(qǐng)求。
CookieContainer 是可序列化的類交排,所以它可以寫(xiě)到磁盤(pán)中划滋。此外,可以先使用一個(gè)新的 CookieContainer埃篓,然后再按照下面的方法手動(dòng)添加cookie:

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

16.5.5 表單驗(yàn)證

16.4.5 身份驗(yàn)證 介紹如何使用 NetworkCredentials 對(duì)象實(shí)現(xiàn)其中一些類型的驗(yàn)證处坪,如Basic或NTLM(在 Web瀏覽器彈出一個(gè)對(duì)話框)。然而都许,大多數(shù)驗(yàn)證的網(wǎng)站都使用某種基于表單的方法稻薇。
用戶在文本框輸入用戶名和密碼嫂冻,單擊按鈕提交數(shù)據(jù)胶征,然后在成功驗(yàn)證之后接收到 cookie,這個(gè) cookie 屬于所訪問(wèn)網(wǎng)頁(yè)的私有數(shù)據(jù)桨仿。通過(guò) WebRequest 或 HttpClient睛低,就可以實(shí)現(xiàn)。

<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 登錄網(wǎng)站:

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);

//我們現(xiàn)在已經(jīng)成功登錄服傍,只要給后續(xù)的 WebRequest 對(duì)象添加 cc,就可以保持已驗(yàn)證用戶的狀態(tài)

使用 HttpClient 實(shí)現(xiàn)的方法:

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 編寫(xiě)HTTP服務(wù)器

我們可以使用 HttpListener 類編寫(xiě)自定義 HTTP 服務(wù)器钱雷,下面是一個(gè)監(jiān)聽(tīng)端口 51111 的簡(jiǎn)單服務(wù)器,它會(huì)等待一個(gè)客戶端請(qǐng)求吹零,然后返回一行回復(fù)罩抗。

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 內(nèi)部并步使用 .net Socket 對(duì)象,相反灿椅,它調(diào)用 Windows HTTP Server API套蒂。支持計(jì)算機(jī)中多個(gè)應(yīng)用程序監(jiān)聽(tīng)相同的IP地址和端口,前提是每一個(gè)應(yīng)用程序都注冊(cè)不同的地址前綴茫蛹。

調(diào)用 GetContext 時(shí)操刀,HttpListener 會(huì)等待下一個(gè)客戶端請(qǐng)求,這個(gè)方法會(huì)返回一個(gè)對(duì)象婴洼,其中包含 Request 和 Response 屬性骨坑。每一個(gè)屬性都與 WebRequest 和 WebResponse 對(duì)象類似,但是它們屬于服務(wù)器柬采。例如欢唾,我們可以讀寫(xiě)請(qǐng)求和響應(yīng)對(duì)象的頭信息和 Cookie,使用的方法與客戶端非常類似粉捻。

我們可以基于預(yù)期客戶端類型選擇如何支持 HTTP 協(xié)議特性匈辱。至少,每一個(gè)請(qǐng)求都需要設(shè)置內(nèi)容長(zhǎng)度和狀態(tài)碼杀迹。

下面這個(gè)簡(jiǎn)單網(wǎng)頁(yè)服務(wù)器亡脸,最多支持50并發(fā)請(qǐng)求:

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); }
  }
}

下面是啟動(dòng)程序的 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(); }
}

可以使用任何一個(gè) Web 瀏覽器作為客戶端進(jìn)行測(cè)試押搪,這里的URI是 http://localhost/ 加上網(wǎng)站名稱。

警告: 如果其他軟件占用同一個(gè)端口浅碾,那么 HttpListener 不會(huì)啟動(dòng)(除非這個(gè)軟件也使用Windows HTTP Server API)

使用異步函數(shù)可以實(shí)現(xiàn)服務(wù)器的可擴(kuò)展性和提高運(yùn)行效率大州。然而,從UI線程開(kāi)始都可能會(huì)影響可擴(kuò)展性垂谢,因?yàn)槊恳粋€(gè)請(qǐng)求中厦画,執(zhí)行過(guò)程都會(huì)在每一次等待之后返回UI線程。這種等待毫無(wú)意義滥朱,因?yàn)闆](méi)有共享的狀態(tài)根暑。所以UI中,最好去掉UI線程徙邻,或者這樣:
Task.Run(start);
或者在調(diào)用 GetContextAsync 之后調(diào)用 ConfigureAwait(false)排嫌。

注意,即使這些方法是異步的缰犁,但我們?nèi)匀辉?Task.Run 中調(diào)用 ProcessRequestAsync淳地。這樣就尅允許調(diào)用者馬上能夠處理另一個(gè)請(qǐng)求,而不需要同步等待方法執(zhí)行結(jié)束(一直到第一個(gè) await)帅容。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末颇象,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子并徘,更是在濱河造成了極大的恐慌遣钳,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,686評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件麦乞,死亡現(xiàn)場(chǎng)離奇詭異蕴茴,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)路幸,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,668評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門荐开,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人简肴,你說(shuō)我怎么就攤上這事晃听。” “怎么了砰识?”我有些...
    開(kāi)封第一講書(shū)人閱讀 158,160評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵能扒,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我辫狼,道長(zhǎng)初斑,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,736評(píng)論 1 284
  • 正文 為了忘掉前任膨处,我火速辦了婚禮见秤,結(jié)果婚禮上砂竖,老公的妹妹穿的比我還像新娘。我一直安慰自己鹃答,他們只是感情好乎澄,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,847評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著测摔,像睡著了一般置济。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上锋八,一...
    開(kāi)封第一講書(shū)人閱讀 50,043評(píng)論 1 291
  • 那天浙于,我揣著相機(jī)與錄音,去河邊找鬼挟纱。 笑死羞酗,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的樊销。 我是一名探鬼主播整慎,決...
    沈念sama閱讀 39,129評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼脏款,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼围苫!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起撤师,我...
    開(kāi)封第一講書(shū)人閱讀 37,872評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤剂府,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后剃盾,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體腺占,經(jīng)...
    沈念sama閱讀 44,318評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,645評(píng)論 2 327
  • 正文 我和宋清朗相戀三年痒谴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了衰伯。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,777評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡积蔚,死狀恐怖意鲸,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情尽爆,我是刑警寧澤怎顾,帶...
    沈念sama閱讀 34,470評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站漱贱,受9級(jí)特大地震影響槐雾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜幅狮,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,126評(píng)論 3 317
  • 文/蒙蒙 一募强、第九天 我趴在偏房一處隱蔽的房頂上張望株灸。 院中可真熱鬧,春花似錦擎值、人聲如沸蚂且。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,861評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)杏死。三九已至,卻和暖如春捆交,著一層夾襖步出監(jiān)牢的瞬間淑翼,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,095評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工品追, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留玄括,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,589評(píng)論 2 362
  • 正文 我出身青樓肉瓦,卻偏偏與公主長(zhǎng)得像遭京,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子泞莉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,687評(píng)論 2 351

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理哪雕,服務(wù)發(fā)現(xiàn),斷路器鲫趁,智...
    卡卡羅2017閱讀 134,637評(píng)論 18 139
  • 國(guó)家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報(bào)批稿:20170802 前言: 排版 ...
    庭說(shuō)閱讀 10,934評(píng)論 6 13
  • iOS網(wǎng)絡(luò)編程讀書(shū)筆記 Facade Tester客戶端門面模式的實(shí)例(被動(dòng)版本化) 被動(dòng)版本化斯嚎,所以硬編碼URL...
    melouverrr閱讀 1,602評(píng)論 3 7
  • AFHTTPRequestOperationManager 網(wǎng)絡(luò)傳輸協(xié)議UDP、TCP挨厚、Http堡僻、Socket、X...
    Carden閱讀 4,333評(píng)論 0 12
  • 《悉達(dá)多》讀書(shū)筆記 在等女兒上舞蹈課時(shí)候疫剃,重新讀了一遍德國(guó)作家黑塞的《悉達(dá)多》钉疫,之前看的一遍基本已經(jīng)忘得差不多,第...
    九林蜀黍閱讀 617評(píng)論 0 3