第16章 网络

第16章 网络

纲要

.NET Framework 在 System.Net.*命名空间中包含了支持各种网络标准的类,支持的标准包括 HTTP、TCP/IP 以及 FTP 等。以下列出了其中的主要组件:

  • Webclient​ 类

    支持通过 HTTP 或者 FTP 执行简单的下载/上传操作。

  • WebRequest​ 和 WebResponse​ 类

    可以从底层控制客户端 HTTP 或 FTP 操作。

  • HttpClient​ 类

    消费 HTTP Web API 和 RESTful 服务。

  • HttpListener​ 类

    用于编写 HTTP 服务器。

  • SmtpClient​ 类

    构造并通过 SMTP 协议发送邮件。

  • Dns​ 类

    用于进行域名和地址之间的转换。

  • Tcpclient​、Udpclient​、TcpListener​ 和 Socket ​类

    用于直接访问传输层和网络层。

16.1 .NET 网络架构

.NET 网络架构如下图所示,传输层和应用层作用如下:

  • 传输层:定义了发送和接收 字节 的基础协议(TCP 和 UDP)
  • 应用层:定义了为特定应用程序设计的上层协议。例如下载网络页面(HTTP)、传输文件(FTP)、发送邮件(SMTP)以及在域名和 IP 地址间进行转换(DNS)。

C7.0 核心技术指南 第7版.pdf - p698 - C7.0 核心技术指南 第 7 版-P698-20240517172012-xmua9g0

image

16.1.1 网络术语缩写

网络的缩略术语极多,下表列出了常用的术语:

缩写 全称 说明
DNS Domain Name Service
(域名服务)
在域名(例如 ebay.com)和 IP 地址(例如 199.54.213.2)之间进行转换
FTP File Transfer Protocol
(文件传输协议)
基于 Internet 的文件发送和接收的协议
HTTP Hypertext Transfer Protocol
(超文本传输协议)
用于获得网页或运行 Web 服务
IIS Internet Information Services
(Internet 信息服务)
Microsoft 的 Web 服务器软件
IP Internet Protocol
(Internet 协议)
TCP 与 UDP 之下的网络层协议
LAN Local Area Network
(局域网)
大多数 LAN 使用了 TCP/IP 等基于 Internet 的协议
POP Post Office Protocol
(邮局协议)
用于接收 Internet 邮件
REST REpresentational StateTransfer
基于基本的 HTTP 协议,并在响应中包含机器可追踪链接的做法。广泛应用于 WebService
SMTP Simple Mail TransferProtocol
(简单邮件传输协议)
用于发送 Internet 邮件
TCP Transmission and ControlProtocol
(传输和控制协议)
传输层 Internet 协议。很多更高层的服务都是基于该协议构建的。
UDP Universal Datagram Protocol
(通用数据报协议)
传输层 Internet 协议。多用于低开销的服务(例如 VoIP)
UNC Universal Naming Convention
(通用名称转换)
\\computer\\sharename\\filename
URI Uniform Resource Identifier
(统一资源标识符)
广泛使用的资源命名系统(例如 http://www.amazon.com 或者 mailto:joe@bloggs.org)
URL Uniform Resource Locator
(统一资源定位符)
其技术含义为(已逐渐停止使用):URI 的子集;而应用上的含义为:URI 的简称

16.2 地址与端口

Internet 使用了两套地址系统:

  • IPv4: 32 位宽

  • IPv6: 128 位宽

    用字符串表示时,为 号分隔的 16 进制数。.NET 要求 IPv6 地址需用 括号包裹:

    [3EA0:FFFF:198A:E4A3:4FF2:54fA:41BC:8D31]
    

16.2.1 IPAddress ​ 类

.NET 使用 IPAddress ​ 类表示 IP 地址(v4、v6 皆可)。实例化方式有两种:

  1. 使用构造器,传入 字节数组
  2. 使用静态方法 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);                   // InterNetworkIPAddress a3 = new IPAddress(new byte[] { 62, 160, 255, 255, 25, 138, 228, 163, 79, 242, 84, 250, 65, 188, 141, 49 });
IPAddress a4 = IPAddress.Parse("[3EA0:FFFF:198A:E4A3:4FF2:54fA:41BC:8D31]");
Console.WriteLine(a3.Equals(a4));       // True
Console.WriteLine (a3.AddressFamily);   // InterNetworkV6

16.2.2 端口

TCP 和 UDP 协议将每一个 IP 地址划分为 65535 个端口。其中 49512~65534 是未分配端口,常用于测试及小规模部署。

IP 地址和端口组合在.NET 中使用 IPEndPoint ​类表示:

IPAddress a = IPAddress.Parse ("101.102.103.104");
IPEndPoint ep = new IPEndPoint (a, 222);      // Port 222
Console.WriteLine (ep.ToString());            // 101.102.103.104:222

Info

许多应用程序都分配有标准端口,例如,HTTP 默认使用 80 端口,SMTP 使用 25 端口,HTTPS 使用 443 端口。

16.3 URI

URI 是一个具有特殊格式的字符串,它描述了一个 InternetLAN 资源。

URI 可分为三个组成部分: 协议(scheme)权限(authority)路径(path) 。Uri 类便采用了该划分方式:

image

Info

关于 LAN 资源,见 LAN 资源和 UNC 路径

16.3.1 Uri​ 类型

Uri​ 的构造器接受如下 3 种字符串:

  1. URI 字符串

    形如 http://www.ebay.com​ 或 file://janespc/sharedpics/dolphin.jpg

  2. 硬盘中文件 的绝对路径

形如 c:\myfiles\data.xlsx
3. LAN 中文件的 UNC 路径

形如 \\janespc\sharedpics\dolphin.jpg

文件UNC 路径会自动转换为 URI:

  1. 添加 file: ​ 协议;
  2. \​ 转化为 /

16.3.2 Uri​ 类型中与路径相关的属性

IsLoopback ​ 属性

表示 Uri 是否引用本地主机(IP 地址为 127.0.0.1)。

IsFile ​、 LocalPath ​、 AbsolutePath ​ 属性

IsFile ​ 属性表示 Uri 是否引用了本地或 UNC 路径。该属性如果为 true,则:

  • LocalPath ​ 属性:返回用反斜杠 \​ 分隔的绝对路径。

    File.Open​ 方法可以调用该路径。

  • AbsolutePath ​ 属性:返回用正斜杠 / 分隔的绝对路径

Tips

Uri​ 实例拥有一些只读属性,若希望先创建,再修改 Uri 内容,需要借助 UriBuilder ​ 类型,我们甚至可以令文件的协议是 http。

16.3.3 Uri​ 类型中与地址相关的属性和方法

Host ​ 属性

表示 Host。

Port ​ 属性

表示端口,对于一些协议,Uri 知道其默认端口(如 HTTP 默认端口 80)。

IsBaseOf ​ 方法

判断两个 Uri 是否为父子关系。

MakeRelativeUri ​ 方法

获取相对 Uri。

IsAbsoluteUri ​ 属性

判断当前 Uri 是否为相对地址。

上述方法/属性用例如下:

Uri info = new Uri ("http://www.domain.com:80/info/");
Uri page = new Uri ("http://www.domain.com/info/page.html");Console.WriteLine (info.Host);     // www.domain.com
Console.WriteLine (info.Port);     // 80
Console.WriteLine (page.Port);     // 80  (Uri knows the default HTTP port)Console.WriteLine (info.IsBaseOf (page));         // True
Uri relative = info.MakeRelativeUri (page);
Console.WriteLine (relative.IsAbsoluteUri);       // False
Console.WriteLine (relative.ToString());          // page.html

16.3.4 Uri​ 的静态方法

EscapeUriString​ 方法

该方法将 ASCII 值大于 127 的字符转换为 十六进制 ,用于将字符串转换为有效的 URL。

CheckHostName​ 方法

用于获取地址类型是 DNSIPv4 还是 IPv6

var info = new Uri("http://bing.com");
// 输出 DNS
Uri.CheckHostName(info.Host).Dump();

CheckSchemeName​ 方法

用于判断传入的 协议 字符串是否合法。

var info = new Uri("http://bing.com");
// 输出 true
Uri.CheckSchemeName(info.Scheme).Dump();

16.3.5 URI 中的斜杠

URI 中的斜杠非常重要。服务器会根据它确定 URI 是否包含路径信息。

http://www.albahari.com/nutshell/​ 为例,HTTP 服务器会在网站的 Web 文件夹中查找名为 nutshell 的子文件夹,并返回该文件夹中的默认文档(通常为 index.html )。

若该 URI 结尾处没有斜杠,则 Web 服务器则会试图在网站的根目录下寻找名为 nutshell (没有扩展名)的文件,如果该文件不存在,大多数 Web 服务器会认定为用户输入错误,返回 301 永久重定向错误,表示客户端应当尝试在结尾加上斜杠。

默认情况下,.NET 的 HTTP 客户端和 Web 浏览器采用相同的行为来处理 301 错误:使用推荐的 URI 重试一次。这意味着,如果忽略末尾本该添加的斜杠,请求仍然是有效的,只是会额外产生一个不必要的回程。

16.4 客户端类型

提纲

mindmap 客户端WebRequest<br>WebResponseHttpWebRequestFtpWebRequestFileWebRequest常用属性TimeoutGET-><br>GetResponseStreamPOST-><br>GetRequestStreamWebClientGET->Download 方法POST->Upload 方法HttpClientHttpClientHandlerHttpMessageHandlerGET->Get* 方法POST->SendAsync 方法HttpContentHttpResponseMessageHttpRequestMessage
WebRequest​ 和 WebResponse

WebRequest​ 和 WebResponse​ 是管理 HTTP​ 和 FTP​ 客户端活动,以及“file:​”协议的通用基类。它们封装了这些协议共同的“请求/响应”模型。这两个类传输数据时仅支持流。

WebClient

WebClient​ 是一个易于使用的门面(facade)类,它负责调用 WebRequest ​ 和 WebResponse ​ 类的功能,用于简化调用者工作。WebClient​ 支持字符串、字节数组、文件或者流。WebClient​ 也不是万能的,例如它不支持 cookie。

HttpClient

HttpClient​ 是另一个基于 WebRequest​ 和 WebResponse​ 的类(更准确说是基于 HttpWebRequest ​ 和 HttpWebResponse ​),它是 .NET Framework 4.5 新引入的类型。HttpClient​ 主要为基于 HTTP 的 WebAPI、基于 REST 的服务以及自定义的认证协议增加了很多功能性支持。

16.4.1 WebClient​ 类

WebClient​ 是一个易于使用的门面(facade)类,它负责调用 WebRequest ​ 和 WebResponse ​ 类的功能,用于简化调用者工作。WebClient​ 支持字符串、字节数组、文件或者流。WebClient​ 也不是万能的,例如它不支持 cookie。

WebClient​ 使用方式如下:

  1. 实例化 WebClient​ 对象;
  2. 设置 Proxy ​ 属性值;
  3. 若需进行验证,设置 Credentials ​ 属性值;
  4. 使用相应的 URI 调用 DownloadXXX​ 或 UploadXXX​ 方法。

Tips

WebClient​ ​被动实现了 IDisposable​,(它继承了 Component​,因此它能够显示在 VisualStudio 的设计器组件托盘中)。然而,它的 Dispose​ ​方法在运行时并没有执行太多实际的操作,因此不需要销毁 WebClient​ ​的实例。

16.4.1.1 WebClient​ 的 Download​ 方法

WebClient​ 的下载方法对应着 GET 请求:

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

WebClient​ 的 BaseAddress ​ 属性若进行了设置,上述方法的地址可以传递 对路径。

以如下代码为例,它将网页内容下载至本地:

WebClient wc = new WebClient { Proxy = null };
wc.DownloadFile ("http://www.albahari.com/nutshell/code.aspx", "code.htm");
WebClient wc = new WebClient { Proxy = null };
wc.BaseAddress = "http://www.albahari.com/";
wc.DownloadFile ("nutshell/code.aspx", "code5.htm");

16.4.1.2 WebClient​ 的 Upload​ 方法

上传方法与下载方法类似,对应着 POST、PUT、DELETE 请求:

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

UploadValues​ 方法的 method​ 参数为 “ POST ”时,可以提交 HTTP 表单值。

WebClient​ 的 BaseAddress ​ 属性若进行了设置,上述方法的地址可以传递 对路径。

16.4.1.3 WebClient​ 的异步方法

WebClient​ 于 .NET Framework 4.5 提供了异步版本:

  • 异步方法:以 TaskAsync ​ 为后缀的方法。

  • 取消操作:WebClient​ 的 CancelAsync ​ 方法。

  • 进度报告:通过订阅 DownloadProgressChanged ​、 UploadProgressChanged ​ 事件实现。

    进度报告事件会捕获并提交至同步上下文,因此无需调用 *Invoke​ 方法。

以如下代码为例,该代码尝试下载 Web 页面并进行进度报告。若下载时间大于 5 秒,取消下载:

var wc = new WebClient();
wc.DownloadProgressChanged += (sender, e) => Console.WriteLine(e.ProgressPercentage + "% complete");
Task.Delay(TimeSpan.FromSeconds(5)).ContinueWith(ant => wc.CancelAsync());
await wc.DownloadFileTaskAsync("http://oreilly.com", "webpage.htm");

Notice

当请求取消时会抛出一个 WebException​ 异常,且其 Status​ 属性的值为 WebExceptionStatus.RequestCanceled​(由于历史原因,这里不会抛出 OperationCanceledException​)。

Don't

如果使用了取消或进度报告功能,要避免使用同一个 WebClient​ 对象依次执行多个操作,因为这样会造成竞争条件。

Tips

旧版异步使用 EAP,已经占用了 Async 后缀。

16.4.2 WebRequest​ 和 WebResponse

WebRequest​ 和 WebResponse​ 是管理 HTTP​ 和 FTP​ 客户端活动,以及“file:​”协议的通用基类。它们封装了这些协议共同的“请求/响应”模型。这两个类传输数据时仅支持流。

它们的使用步骤如下:

  1. 调用 WebRequest.Creat ​ 方法并传入 URI,以获取 WebRequest​ 实例;

  2. 设置 Proxy ​ 属性;

  3. 若需要上传数据,则调用 WebRequest​ 实例的 GetRequestStream ​ 方法获取流,并向流中写入数据;

  4. 若需要获取响应/下载数据,则:

    1. 调用 WebRequest​ 实例的 GetResponse ​ 方法,获得 WebResponse​ 实例;
    2. 调用 WebResponse​ 实例的 GetResponseStream ​ 方法获取流,并从流中读取数据。

以如下代码为例,它们实现了与该代码相同的功能:

WebRequest req = WebRequest.Create("http://www.albahari.com/nutshell/code.html");
req.Proxy = null;
using (WebResponse res = req.GetResponse())
using (Stream rs = res.GetResponseStream())
using (FileStream fs = File.Create ("code_sync.html")) {// 同步写法rs.CopyTo (fs);// 异步写法await rs.CopyToAsync (fs);
}

Don't

一个 WebRequest​ 对象 不可 用于多个请求,每一个实例仅可用于 个作业。

Notice

Web 响应对象包含一个 ContentLength​ 属性,它表示(由服务器报告的)响应流的长度。这个值位于响应头部,但有可能不存在也可能不正确。特别是当 HTTP 服务器选择使用分块模式传输大响应时,其 ContentLength ​属性值为-1。动态生成的网页往往也有相同的结果。

16.4.2.1 各类 *WebRequest

WebRequest.Creat​ 静态方法会根据 URI 的前缀(即协议)创建 WebRequest​ 类型的子类实例,对应关系如下:

前缀 Web 请求类型
http:https: HttpWebRequest
ftp FtpWebRequest
file: FileWebRequest

将 Web 请求对象转换为具体的类型(HttpWebRequest​ ​或者 FtpWebRequest​)就可以访问特定协议的特性了。

Tips

通过 WebRequest.RegisterPrefix ​ 静态方法可以自定义前缀及其对应的 *WebRequest​ 类型。

Info

“file:”协议会将请求直接发送给 FileStream​ 对象,其目的是统一 URI 读取协议。不论该 URI 是网页、FTP 站点还是文件路径。

16.4.2.2 WebRequest​ 的 Timeout

WebRequest​ 类提供了 Timeout​ 属性,单位为 毫秒 。如果发生超时,将抛出 WebException ​,并将其 Status ​ 属性设为 WebExceptionStatus.Timeout​。HTTP 协议默认超时时间为 100 秒 ,FTP 协议默认超时时间 不设置

提纲(16.4.3)

classDiagram direction TB class HttpClient{TimeoutBaseAddressGetStringAsync()GetByteArrayAsync()GetStreamAsync()GetAsync()SendAsync(HttpRequestMessage) }class HttpClientHandler{UserProxySendAsync() }class HttpRequestMessage{HttpContent Content }class HttpResponseMessage{StatusCodeHttpContent ContentEnsureSuccessStatusCode() }HttpClient o-- HttpClientHandler HttpClientHandler ..> HttpRequestMessage HttpClient *-- HttpResponseMessageclass HttpContent{CopyToAsync() }HttpContent <-- ByteArrayContent HttpContent <-- StringContent HttpContent <-- FromUrlEncodedContent HttpContent <-- StreamContent

16.4.3 HttpClient​ 类

HttpClient​ 是另一个基于 WebRequest​ 和 WebResponse​ 的类(更准确说是基于 HttpWebRequest ​ 和 HttpWebResponse ​),它是 .NET Framework 4.5 新引入的类型。HttpClient​ 主要为基于 HTTP 的 WebAPI、基于 REST 的服务以及自定义的认证协议增加了很多功能性支持。

它有如下特点:

  • 一个 HttpClient ​实例就可以支持并发请求。

  • 支持插件式的自定义消息处理器。

    这可用于创建单元测试替身,以及创建(日志记录、压缩、加密等)自定义管道。而对于 WebClient ​来说则很难进行单元测试代码的编写。

  • 有丰富且易于扩展的请求头部与内容类型系统。

    可以更便捷的自定义头部信息、cookie 信息,以及身份验证信息。

Tips

HttpClient​ 相较 WebClient​ 的缺点有:

  • 不支持进度报告;
  • 不支持 FTP、file:// 及自定义 URI;
  • 不兼容旧版 .NET Framework。

16.4.3.1 HttpClient​ 使用步骤

使用 HttpClient​ ​的最简单方式是创建一个实例,然后使用一个 URI 调用其 Get* ​ ​方法。HttpClient支持 并发操作,因此可以同时下载多个网页:

var client = new HttpClient();
var task1 = client.GetStringAsync ("http://www.linqpad.net");
var task2 = client.GetStringAsync ("http://www.albahari.com");(await task1).Length.Dump ("First page length");
(await task2).Length.Dump ("Second page length");

HttpClient ​的所有 I/O 相关方法都是异步的(且没有同步版本)。

Notice

WebClient​ ​不同,若想获得 HttpClient​ ​的最佳性能, 必须 重用相同的实例(否则,诸如 DNS 解析等操作会不必要地重复执行)。

16.4.3.2 HttpClient​ 的属性

HttpClient​ 的直接属性有 Timeout​、BaseAddress​,更多的属性定义在 HttpClientHandler ​ 类中。该类使用方式如下:

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

上述代码禁用了代理支持。此外,还有专门控制 cookie、自动重定向、身份验证等功能的属性。详见 16.5 使用 HTTP

Notice

HttpClientHandler​ 定义的不是“头信息”,而是一系列 HttpClient​ 通讯时的配置。当然,头信息也是配置的一部分,我们可以通过 DefaultRequestHeaders ​ 属性进行设置,见 16.5.1.3 HttpClient 的头部信息


16.4.3.3 GetAsync​ 方法与响应消息(HttpResponseMessage​)

HttpClient​ 的 Get​ 方法有 4 个:

  • GetStringAsync
  • GetByteArrayAsync
  • GetStreamAsync
  • GetAsync

其中 GetAsync​ 方法使用最为繁琐,它会返回一个 HttpResponseMessage​ 实例,它包含了更为全面的响应信息,我们可以通过它获得识别的状态码(StatusCode​ 属性)、头部信息。

GetAsync​ 使用示例如下:

var client = new HttpClient();
HttpResponseMessage response = await client.GetAsync ("http://www.linqpad.net");
response.EnsureSuccessStatusCode();
string html = await response.Content.ReadAsStringAsync();

Notice

GetAsync​ 获取失败并不会抛出异常,可以通过 HttpResponseMessage​ 的 StatusCode​ 属性获取错误码,或调用 EnsureSuccessStatusCode​ 检查响应状态。

其他 Get​ 方法,获取失败会照常抛出异常。

16.4.3.4 下载数据和 HttpContent

GetAsync​ 获取的数据存放在 HttpResponseMessage​ 实例的 Content ​ 属性中,该属性为 HttpContent​ 类型。

HttpContent​ 类型包含诸多方法输出内容。其 CopyToAsync ​ 方法可以将内容数据写入另一个流中,例如:

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

16.4.3.5 SendAsync ​ 方法与请求消息(HttpRequestMessage​)

HttpClient​ 的各种 Get*​ 方法,底层均通过 SendAsync ​ 实现,该方法也是上传方法。它借助 HttpRequestMessage​ 类型请求数据。使用示例如下:

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

实例化 HttpRequestMessage​ 时可以自定义请求的属性,例如上传的数据内容(通过 Content​ 属性)和头信息。

Notice

我们在 16.4.3.3 GetAsync 方法与响应消息(HttpResponseMessage)提到的诸多 Get*​ 方法,是无法自定义头信息的。

16.4.3.6 上传数据和 HttpContent

实例化 HttpRequestMessage​ 对象后,可以设置其 Content​ 属性指定上传内容。该属性类型是 HttpContent ​ 抽象类,其子类有:

  • ByteArrayContent
  • StringContent
  • FromUrlEncodedContent​(见 16.5.3.3 HttpClient 的 POST
  • 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());

16.4.3.7 HttpMessageHandler​ 类

我们在 16.4.3.2 HttpClient 的属性提到,HttpClient​ 大部分属性定义在 HttpClientHandler​ 中,它是 HttpMessageHandler ​ 的子类。HttpMessageHandler​ 的定义如下:

public abstract class HttpMessageHandler : IDisposable
{protected internal abstract Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken);protected virtual void Dispose(bool disposing);public void Dispose();
}

HttpClient​ 的 SendAsync​ 方法会调用 HttpMessageHandler​ 的 SendAsync ​ 方法。更进一步说,HttpClient​ 的 SendAsync​ 实际是通过 HttpMessageHandler​ 的 SendAsync ​ 实现的。因此,我们可以通过 派生 HttpMessageHandler​,扩展 HttpClient​ 的功能。

16.4.3.8 单元测试和桩

HttpClient​ 的 SendAsync​ 方法会调用 HttpMessageHandler​ 的 SendAsync ​ 方法。更进一步说,HttpClient​ 的 SendAsync​ 实际是通过 HttpMessageHandler​ 的 SendAsync ​ 实现的。因此,我们可以通过 派生 HttpMessageHandler​,扩展 HttpClient​ 的功能。

借助这一点,我们可以自定义桩替身类(moking handler),用于单元测试:

class MockHandler : HttpMessageHandler {Func<HttpRequestMessage, HttpResponseMessage> _responseGenerator;public MockHandler (Func<HttpRequestMessage, HttpResponseMessage> responseGenerator) {_responseGenerator = responseGenerator;}protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) {cancellationToken.ThrowIfCancellationRequested();var response = _responseGenerator (request);response.RequestMessage = request;return Task.FromResult (response);}
}

Suggest

上述代码通过 Task.FromResult​ 返回了已完成的 Task​,其实并没有触发异步。一般来说,桩替身的运行时间非常短暂,因此维持异步性没什么必要。

使用方式如下:

var mocker = new MockHandler (request =>new HttpResponseMessage (HttpStatusCode.OK){Content = new StringContent ("You asked for " + request.RequestUri)});var client = new HttpClient (mocker);
var response = await client.GetAsync ("http://www.linqpad.net");
string result = await response.Content.ReadAsStringAsync();Assert.AreEqual ("You asked for http://www.linqpad.net/", result);

16.4.3.9 使用 DelegateHandler​ 串联请求处理器

DelegateHandler​ 是 HttpMessageHandler​ 的抽象派生类,其构造器接受一个 HttpMessageHandler ​ 实例。因此 DelegateHandler​ 可以像单向链表一样,一个套一个,形成“处理器链”。这种方式可用于实现自定义身份验证、压缩以及加密协议。

以下代码演示了在调用内嵌 HttpMessageHandler​ 的 SendAsync​ 方法前、后添加了“日志”记录:

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

16.4.4 代理

代理服务器是一个负责转发 HTTP 和 FTP 请求的中间服务器。

mindmap 代理NetworkCredentialWebProxy 属性WebRequestWebClientHttpClientHandler 因子<br>+<br>Proxy 属性<br>+<br>UserProxy 属性HttpClient全局代理WebRequest.DefaultWebProxyHttpClient.DefaultProxy

16.4.4.1 WebClient​ 与 WebRequest​ 使用代理

我们可以使用 WebProxy ​对象,令 WebClient ​或者 WebRequest ​对象通过代理服务器转发请求,使用步骤如下:

  1. 实例化 WebProxy ​ 对象,并设置 Credentials ​ 属性(可选);
  2. 实例化 WebClient​/WebRequest​ 对象,用步骤 1 的实例设置 Proxy ​ 属性;
  3. 进行剩余的常规操作。

示例代码如下:

WebProxy p = new WebProxy("192.178.10.49", 808);
p.Credentials = new NetworkCredential("username", "password");
// 或者
p.Credentials = new NetworkCredential("username", "password", "domain");
// WebClient
WebClient wc = new WebClient();
wc.Proxy = p;
// WebRequest
WebRequest req = WebRequest.Create("...");
req.Proxy = p;

Warn

如果不使用代理,务必将 WebClient​ 和 WebRequest​ 的 Proxy​ 属性设置为 null ,否则 Framework 可能会尝试自动检查代理设置,可能造成多达 30 秒的延迟!

16.4.4.2 HttpClient​ 使用代理

从前文已知,HttpClient​ 访问网络,实际是借助 HttpClientHandler ​ 实现的,设置代理也是它负责:

WebProxy p = new WebProxy("192.178.10.49", 808);
p.Credentials = new NetworkCredential("username", "password");
// 或者
p.Credentials = new NetworkCredential("username", "password", "domain");
var handler = new HttpClientHandler { Proxy = p };
var client = new HttpClient(handler);

Warn

如果不使用代理,务必将 WebClient​ 和 WebRequest​ 的 Proxy​ 属性设置为 null ,否则 Framework 可能会尝试自动检查代理设置,可能造成多达 30 秒的延迟!

HttpClientHandler​ 需设置 UseProxy ​ 属性为 false。

16.4.4.4 全局代理

若需重复设置 Proxy​ 的值,可以直接设置全局默认值:

WebRequest.DefaultWebProxy = p;WebClient wc = new WebClient();
wc.Proxy.Dump();  // 此处会显示设置的全局代理信息
HttpClient.DefaultProxy = p;
var handler = new HttpClientHandler();
var client = new HttpClient(handler);
handler.Proxy.Dump();    // 虽然设置了全局代理,此处 Proxy 值仍会为 null

这个设定将影响整个应用程序域的生命周期(除非有其他的代码修改了这个值)。

16.4.5 身份验证

mindmap 身份验证ICredential 接口NetworkCredentialCredentialCacheCredentials 属性WebClientWebRequestHttpClient<br>HttpClientHandlerDefaultRequestHeaders.<br>Authorization 属性HttpClientWebRequest.<br>PreAuthenticateWebRequestHttpClientWindows 凭据UserDefaultCredentialsCredentialCache.<br>DefaultNetworkCredentials

16.4.5.1 NetworkCredential

NetworkCredential​ 用于身份验证,若 HTTP 或 FTP 站点需要用户名和密码,可以创建该类型实例,并赋值给 WebClient​/WebRequest​/HttpClientHandler​ 的 Credentials ​ 属性:

WebClient wc = new WebClient{Proxy = null};
wc.BaseAddress="ftp://ftp.albahari.com";string username = "nutshell";
string password = "oreilly";
wc.Credentials = new NetworkCredential(username, password);
wc.DownloadFile("guestbook.txt", "guestbook.txt");
...

此外,代理也可以进行身份验证:

WebProxy p = new WebProxy("192.178.10.49", 808);
p.Credentials = new NetworkCredential(username, password);

16.4.5.2 使用 Windows 凭据进行身份验证

NetworkCredential​ 设置了“ 域名称 ”,将会使用基于 Windows 的身份验证协议。WebClient​/WebRequest​/HttpClientHandler​ 还可以用如下两种方式使用当前验证了的 Windows 用户:

  1. Credentials​ 设置为 null,并将 UserDefaultCredentials​ 设置为 true;
  2. Credentials​ 属性设置为 CredentialCache.DefaultNetworkCredentials​:

Tips

使用表单进行身份验证时无须设置 Credentials​ 属性。详见 16.5.5 表单验证

Info

NetworkCredential​ 的“域名称”,即 Domain​ 参数,可通过构造函数传入。

16.4.5.3 身份验证的流程

身份验证最终是由 WebRequest​ 的子类完成,它会自动协商兼容协议(NTLM、Kerberos、Basic 和 Digest)。其验证步骤如下:

  1. 客户端向服务器请求内容;

  2. 服务器返回 401,表示需要验证身份;

    返回的的内容包含服务器支持的身份验证协议。

  3. 客户端再次请求内容,头信息携带身份信息;

    根据服务器支持的身份验证协议,自动将验证信息添加至头信息。

  4. 服务器验证通过,返回内容。

// 401,表示需要进行授权
HTTP/1.1 401 Unauthorized
Content-Length: 83
Content-Type: text/html
Server :Microsoft-IIS/6.0
// 支持的协议种类
www-Authenticate: Negotiate
www-Authenticate: NTLM
www-Authenticate: Basic realm="exchange.somedomain.com"
X-Powered-By: ASP.NET
Date: Sat, 05 Aug 2006 12:37:23 GMT

这种机制具有“透明性”,无需开发者介入。不过这也会产生额外的“回程开销”(即每次都经历“401→ 添加验证信息并二次请求”)。此时可以将 (Http)WebRequest.PreAuthenticate​ 属性设置为 true 避免此问题。

Tips

PreAuthenticate​ 虽然在 WebRequest​ 上定义,但它只适用于 HttpWebRequest ​ 和 HttpClientHandler ​。WebClient​ 不支持该属性,除非每次都手动为 Credentials​ 属性添加身份信息,否则每次都有回程开销。

Eureka

实际上,相较 HttpClient​, WebClient​ 是更高级的抽象类,因此很多功能不支持。新版 .NET 已弃用 WebClient​。

16.4.5.4 CredentialCache ​ 类

在上一节我们提到:

根据服务器支持的身份验证协议,自动将验证信息添加至头信息。

有时我们想限制验证方式(例如考虑到 Basic 协议不够安全),便需要借助 CredentialCache ​ 类。它的使用方式如下:

CredentialCache cache = new CredentialCache();
var prefix = new Uri("http://exchange.somedomain.com");
cache.Add(prefix, "Digest", new NetworkCredential("joe", "passwd"));
// 使用当前验证的 Windows 用户凭据
cache.Add(prefix, "Negotiate", CredentialCache.DefaultNetworkCredentials);var wc = new WebClient();
wc.Credentials = cache;
...

身份验证协议用字符串表示,有效的协议字符串包括:NTLM、Kerberos、Basic 和 Digest 以及 Negotiate(该协议表示 NTLM 和 Kerberos 都支持)。

Eureka

WebClient​、WebRequest​、HttpClientHandler​ 的 Credentials​ 属性均为 ICredentials​ 接口类型。 CredentialCache ​ 和 NetworkCredential​ 均实现了该接口。

16.4.5.5 使用 HttpClient​ 进行头部信息身份验证

前文所述的身份验证方式 HttpClient​(借助 HttpClientHandler​)均可使用。我们也可以直接用 HttpClient​ 的 DefaultRequestHeaders ​ 属性设置头信息,以添加验证信息:

var client = new HttpClient();
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes("username:password")));

Tips

也可以用前文提到的方式添加身份信息。如下代码产生的头信息与上述代码一致:

var cache = new CredentialCache();
// 我们假设都访问的是 bing.com
var prefix = new Uri("http://bing.com");
cache.Add(prefix, "Basic", new NetworkCredential("username", "password"));
var handler = new HttpClientHandler{Credentials = cache,PreAuthenticate = true
};
var client = new HttpClient(handler);

还可以这样添加身份信息,它将根据返回内容自动匹配验证协议:

var handler = new HttpClientHandler{Credentials = new NetworkCredential("username", "password"),PreAuthenticate = true
};
var client = new HttpClient(handler);

16.4.6 异常处理

在发生网络错误或协议错误时,WebRequest​、WebReponse​、WebClient​ 及它们包装的流会抛出 WebException ​ 异常;HttpClient​ 则抛出 HttpRequestException ​ 异常(WebException​ 的子类)。

WebException​ 的 Status ​ 属性为 WebExceptionStatus​ 枚举,常见的成员有:

  • NameResolutionFailure​:无效域名

  • ConnectFailure​:网络中断

  • Timeout​:请求时间超过 WebRequest.Timeout​ 限定

  • ProtocolError

    HTTP 和 FTP 协议特有的错误,表示诸如“页面不存在”、“已永久移除”、“未登录”等。

WebException​ 的 Response​ 属性包含了服务器返回的响应,我们可以在 catch 块中进行更为详细的异常排查。

16.4.6.1 HttpStatusCode​ 枚举、FtpStatusCode​ 枚举、WebExceptionStatus​ 枚举

之前我们提到:

GetAsync​ 获取失败并不会抛出异常,可以通过 HttpResponseMessage​ 的 StatusCode​ 属性获取错误码,或调用 EnsureSuccessStatusCode​ 检查响应状态。

Http 响应状态(HttpWebResponse.Status​)对应的类型为 HttpStatusCode ​ 枚举,Ftp 响应状态(FtpWebResponse.Status​)对应的枚举类型为 FtpStatusCode ​,WebException.Status​ 类型则为 WebExceptionStatus ​ 枚举,三者的枚举值大不相同:

public enum HttpStatusCode
{Continue = 100,SwitchingProtocols = 101,Processing = 102,EarlyHints = 103,OK = 200,Created = 201,Accepted = 202,NonAuthoritativeInformation = 203,...
}
public enum FtpStatusCode
{Undefined = 0,RestartMarker = 110,ServiceTemporarilyNotAvailable = 120,DataAlreadyOpen = 125,OpeningData = 150,CommandOK = 200,CommandExtraneous = 202,DirectoryStatus = 212,...
}
public enum WebExceptionStatus
{Success,NameResolutionFailure,ConnectFailure,ReceiveFailure,SendFailure,PipelineFailure,RequestCanceled,ProtocolError,...
}

WebException​ 不包含状态代码,但它的 Response​ 属性持有着服务器返回的响应。因此可以将其强制转换为 HttpWebResponse ​ 或 FtpWebResponse ​,再获取状态代码( Status ​/ StatusDescription ​ 属性)。例如:

var wc = new WebClient { Proxy = null };
try {	      var content = wc.DownloadString("http://www.albahari.com/notthere");
}
catch (WebException ex) {if(ex.Status == WebExceptionStatus.ProtocolError) {var response = (HttpWebResponse)ex.Response;Console.WriteLine(response.StatusDescription);}
}

16.5 使用 HTTP

16.5.1 头部信息

头部信息,由一些包含元数据的键值对组成。WebClient​、WebRequest​ 和 HttpClient​ 都可以添加自定义头部信息。

16.5.1.2 WebRequest​ 的头部信息

WebRequest​ 的头部信息存放在 Headers ​ 属性中,添加时需要调用 Add​ 方法。此外,一些标准 HTTP 头部信息(如 UserAgent​、ContentType​、Accept​ 等)有专门的属性:

WebRequest request = WebRequest.Create("http://example.com/api/resource");// 设置请求方法(GET、POST 等)
request.Method = "GET";request.Headers.Add("Custom-Header", "HeaderValue");
request.ContentType = "application/json";
request.Accept = "application/json";
request.UserAgent = "MyCustomUserAgent";

16.5.1.1 WebClient​ 的头部信息

WebClient​ 师承 WebRequest​,其头部信息和 WebRequest​ 相似,存放在 Headers ​ 属性中,添加时需调用 Add​ 方法;响应的头部信息则存放在 ResponseHeaders ​ 中:

WebClient wc = new WebClient { Proxy = null };
wc.Headers.Add ("CustomHeader", "JustPlaying/1.0");
wc.DownloadString ("http://www.oreilly.com");foreach (string name in wc.ResponseHeaders.Keys)Console.WriteLine (name + "=" + wc.ResponseHeaders [name]);

Tips

WebClient​ 作为高级抽象类型,为简化操作,没有添加标准头部信息。

Eureka

WebClient​ 请求后,其 Headers​ 属性会包含本次请求的头部信息(ResponseHeaders​),这也意味着 WebClient​ 本身包含了本次响应的内容。

这也正是“要避免使用同一个 WebClient​ 对象依次执行多个操作”的原因。

16.5.1.3 HttpClient​ 的头部信息

HttpClient​ 的头部信息存放在 DefaultRequestHeaders ​ 属性中。该属性内含与标准 HTTP 头部信息对应的属性,添加时需要调用 Add​ 方法:

var client = new HttpClient();
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("VisualStudio", "2015"));
client.DefaultRequestHeaders.Add("CustomHeader", "VisualStudio/2015");

而这些头部信息,最终会赋值给每一个请求对象( HttpRequestMessage ​ 实例)的 Headers​ 属性,通过 SendAsync​ 方法发出;而响应的头部信息,则放在 HttpResponseMessage ​ 实例的 Headers​ 属性中。

16.5.2 查询字符串(GET)

查询字符串是一个以 号开始的、附加在 URI 后的字符串,用于向服务器发送简单的数据。语法如下:

\[?key1=value1\&key2=value2\&key3=value3... \]

16.5.2.1 WebClient​ 的查询字符串

WebClient​ 使用查询字符串最为简单,通过 QueryString ​ 属性的 Add​ 方法便可以添加键值对:

WebClient wc = new WebClient { Proxy = null };
wc.QueryString.Add ("q", "WebClient");     // Search for "WebClient"
wc.QueryString.Add ("hl", "fr");           // Display page in French
wc.DownloadFile ("http://www.bing.com/search", "results.html");

Warn

通过 WebClient​ 类的 QueryString​ 属性添加查询参数时,它本身不会自动进行 URL 编码。这意味着添加包含特殊字符(如括号、分号、空格等)的值时,这些字符不会被自动转义。这可能导致生成的 URL 不符合 URI 规范。

16.5.2.2 WebRequest​ 和 HttpClient​ 的查询字符串

WebRequest​ 和 HttpClient​ 需要手动构建查询字符串:

var uri = "http://www.bing.com/search?q=WebClient&h1=fr";

如果查询中包含符号或空格,需要使用 Uri​ 的 EscapeDataString ​ 方法进行处理:

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

Tips

EscapeDataString​ 和 EscapeUriString​ 类似,但是 前者 会对特殊字符进行转义(例如 & 或 =),否则这些字符会破坏查询字符串。

16.5.2.3 HttpUtility​ 与查询字符串

WebRequest​ 和 HttpClient​ 手动构建查询字符串显然很麻烦,.NET 提供了 HttpUtility​ 辅助类,通过 ParseQueryString ​ 方法可以快速构建查询语句:

Uri uri = new Uri("https://bing.com");
// 该 uri 的 Query 并不包含查询字符串,因此 uri.Query 为空。
var query = HttpUtility.ParseQueryString(uri.Query);
query["name"] = "Tom&Jerry";
query["age"] = "18";
// name=Tom%26Jerry&age=18
query.ToString().Dump();

Eureka

HttpUtility.ParseQueryString​ 的返回值是私有类型 HttpQSCollection​,为 NameValueCollection ​ 的派生类。HttpQSCollection​ 覆写了 ToString​ 方法,其内部会对特殊字符进行转义(例如 & 和 =)。

16.5.3 上传表单数据(POST)

Notice

以下示例均传输的是表单数据,而非 json 字符串。

16.5.3.1 WebClient​ 的 POST

WebClient​ 的 UploadValues​ 方法接收 NameValueCollection ​ 为参数,该参数将转换为 HTML 表单数据格式(采用 application/x-www-form-urlencoded 编码格式),

WebClient wc = new WebClient { 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);

Tips

application/x-www-form-urlencoded”编码格式如下:

\[name1=value1\&name2=value2\&name3=value3... \]

Notice

WebClient​ 原生仅支持“application/x-www-form-urlencoded”格式的数据,若想传输 json 字符串,需手动设置 Headers​、使用 UploadString​ 方法。

16.5.3.2 WebRequest​ 的 POST

WebRequest​ 需要手动设置各种参数,步骤如下:

  1. 设置 ContentType ​ 属性为 “application/x-www-form-urlencoded”;
  2. 构建表单字符串,采用“application/x-www-form-urlencoded”编码格式,并使用 Encoding.UTF8.GetBytes ​ 将字符串转换为 byte​ 数组;
  3. 设置 ContentLength ​ 属性为 byte​ 数组的长度;
  4. 调用 GetRequestStream ​ 方法,将 byte​ 数组写入流中;
  5. 调用 GetResponse ​ 读取服务器响应。
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());

Tips

ContentType​ 改为 “application/json”、reqString​ 改为 json 字符串,便可发送 json 数据。

16.5.3.3 HttpClient​ 的 POST

HttpClient​ 需要创建 FromUrlEncodedContent ​ 对象完成 POST 请求。它采用“application/x-www-form-urlencoded”编码格式编码键值对。它的构造函数需要 IEnumerable<KeyValuePair<string, string>>​ 类型的参数,用法如下:

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

Eureka

WebClient​ 于 .NET Framework 2.0 引入,当时考虑到开发者的习惯、NameValueCollection​ 已被大量使用,并未选择 IEnumerable<KeyValuePair<string, string>>​ 作为 UploadValues​ 的表单参数类型。

Tips

FormUrlEncodedContent​ 对应的是“application/x-www-form-urlencoded”编码格式;若想传输 json 字符串,使用 StringContent ​ 即可:

var content = new StringContent(jsonString, Encoding.UTF8, "application/json"); 

cookie 是 HTTP 协议的内容,因此 WebRequest​ 需转换为 HttpWebRequest ​ 再处理 cookie。

HttpWebRequest​ 默认会忽略从服务器接收的 cookie。HttpWebRequest​ 实例的 CookieContainer ​ 属性需赋值为 CookieContainer ​ 实例,才会记录 cookie:

var cc = new CookieContainer();var request = (HttpWebRequest)WebRequest.Create ("http://www.bing.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);}
}

HttpClient​ 使用 cookie 与 HttpWebRequest​ 相似,通过为 HttpClientHandler ​ 的 CookieContainer ​ 属性设置 CookieContainer ​ 实例启用 cookie:

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

Eureka

HttpWebRequest​、HttpClient​ 在与服务器通信后,会将 cookie 信息记录在 CookieContainer​ 实例中。因此所有请求使用同一个 CookieContainer​ 实例即可应用相同的 cookie。

此外,CookieContainer​ 支持序列化,因此可以将 cookie 信息保存,以便下次使用。

16.5.5 表单验证

实现表单验证的网站一般包含如下 HTML 代码,用于验证身份:

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

我们输入用户名、密码,点击“Log In”按钮,向服务器发送的数据与如下代码发送的数据相同​:

// 使用 HttpWebRequest
string loginUri = "http://www.somesite.com/login";
string username = "usernane";
string password = "password";
string reqString = $"username={username}&password={password}";
var requestData = Encoding.UTF8.GetBytes(reqString);
var 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(var s = request.GetRequestStream())s.Write(requestData, 0, requestData.Length);
// 使用 HttpClient
string loginUri = "http://www.somesite.com/login";
string username = "username";
string password = "password";var 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();

借此,我们可以通过发送表单数据模拟登录。

Eureka

上述代码实际上是对 POST 和表单数据的应用。

16.5.6 SSL

在指定“https:”前缀时,Webclient​、HttpClient​ 和 WebRequest​ 都会自动使用 SSL。

如果服务端证书由于某种原因失效(例如,它是一个测试证书),那么在通信时就会抛出一个异常。此类问题可以在静态类 ServicePointManager​ 上附加一个自定义证书验证器解决:

ServicePointManager.ServerCertificateValidationCallback = CertChecker;static bool CertChecker(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)=> true;

ServerCertificateValidationCallback​ 是一个委托,该委托返回 true 则证书是可以接受的。

16.6 编写 HTTP 服务器

16.6.1 HttpListener​ 的使用

使用 HttpListener​ 创建服务器步骤如下:

  1. 创建 HttpListener​ 实例;

  2. 实例的 Prefixes ​ 属性添加要监听的地址、端口及前缀;

  3. 调用实例的 Start ​ 方法,开始监听;

  4. 调用实例的 GetContextAsync ​ 方法,等待客户端连接,获取客户端请求( HttpListenerContext ​ 类型);

    通过异步等待,可以增强服务器的伸缩性。

  5. 设置状态码、内容长度等数据,并将信息通过流传出。

async void ListenAsync()
{listener.Prefixes.Add ("http://localhost:51111/MyApp/");  // Listen onlistener.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);
}

16.6.2 Windows HTTP Server API

HttpListener​ 内部调用的是 Windows HTTP Server API,该 API 允许多个应用程序监听相同的 IP 地址和端口,同样的,HttpListener​ 需要设置不同的地址前缀。

如果其他软件通过 Socket​、TcpListener​ 占用了该端口,那么 HttpListener​ 将不会启动。

16.6.3 HttpListenerContext

HttpListenerContext​ 包含 Request ​(HttpListenerRequest​ 类型)和 Response ​(HttpListenerResponse​ 类型)属性。它们同 WebRequest​、WebResponse​ 相似,我们可以像操作客户端那样,读写 Request​ 和 Response​ 的头部信息和 cookie​。

我们可以基于对客户端的估计来选择支持的 HTTP 协议特性。至少,每一个请求都需要设置 内容长度Response.ContentLength64 ​)和 状态码Response.StatusCode ​)。

16.6.4 UI 线程与服务器的伸缩性

以如下代码(一个简单的服务器例子)为例:

// Listen on port 51111, serving files in d:\webroot:
var server = new WebServer ("http://localhost:51111/", Path.Combine (GetTempDirectory(), "webroot"));
try
{server.Start();// If running in LINQPad, stop the query manually:Console.WriteLine ("Server running... press Enter to stop");Console.ReadLine();
}
finally { server.Stop(); }string GetTempDirectory() =>RuntimeInformation.IsOSPlatform (OSPlatform.Windows) ? @"C:\Temp" : "/tmp";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); }}
}

我们使用异步函数提高服务器的可伸缩性和运行效率。然而 UI 线程中,每一个 await 都会 返回 UI 线程 。我们并没有共享的状态,因此这种开销是毫无意义的。

在具有 UI 的情形下,最好不要和 UI 线程产生联系。可以使用:

Task.Run(Start);
public async void Start() { ... };

也可以在 GetContextAsync​ 后调用 ConfigureAwait​:

var context = await _listener.GetContextAsync().ConfigureAwait(false);

Eureka

上述代码的区别在于,通过 Task.Run​ 方法可以提前进入异步,无需到第一个 await 才进入异步

16.7 使用 FTP

16.7.1 WebClient​ 使用 FTP

WebClient​ 作为高级抽象类,通过它操作 FTP 很简单:

  1. 设置身份验证( Credentials ​ 属性);
  2. 设置 FTP 地址( BaseAddress ​ 属性);
  3. 调用 Upload* ​ 方法上传;
  4. 调用 Download* ​ 方法下载。
var wc = new WebClient { Proxy = null };
wc.Credentials = new NetworkCredential("nutshell", "oreilly");
wc.BaseAddress = "ftp://ftp.albahari.com";
wc.UploadString("tmpfile.txt", "helllo!");
Console.WriteLine(wc.DownloadString("tmpfile.txt"));

16.7.2 WebRequest​ 使用 FTP

类似于 HTTP 的 GET、POST,FTP 也有一系列命令,它们以字符串常量的形式定义在 WebRequestMethods.Ftp ​ 中:

public static class Ftp
{public const string DownloadFile = "RETR";public const string ListDirectory = "NLST";public const string UploadFile = "STOR";public const string DeleteFile = "DELE";public const string AppendFile = "APPE";public const string GetFileSize = "SIZE";public const string UploadFileWithUniqueName = "STOU";public const string MakeDirectory = "MKD";public const string RemoveDirectory = "RMD";public const string ListDirectoryDetails = "LIST";public const string GetDateTimestamp = "MDTM";public const string PrintWorkingDirectory = "PWD";public const string Rename = "RENAME";
}

我们需要通过 WebRequest​ 的 Method​ 属性设置命令。其中部分命令通过 响应流 返回请求内容,部分通过 Response ​ 的属性返回,还有部分命令只执行,不请求内容。

16.7.2.1 通过流返回结果

此处以 ListDirectory​ 命令为例,我们需要通过流获知文件目录:

var req = (FtpWebRequest)WebRequest.Create("ftp://ftp.albahari.com");
req.Proxy = null;
req.Credentials = new NetworkCredential("nutshell", "oreilly");
req.Method = WebRequestMethods.Ftp.ListDirectory;
using(var resp = req.GetResponse())
using(var reader = new StreamReader(resp.GetResponseStream()))reader.ReadToEnd().Dump();

Eureka

这里使用 StreamReader​ 读取流中的内容,但在 16.10.3 StreamReader 与 NetworkStream 提到这可能会无限阻塞。

实际上,WebRequest.GetResponse().GetResponseStream()​ 方法获取的是 ContentLengthReadStream​ 流,它的底层是 HttpBaseStream​ 而非 NetworkStream​。

16.7.2.2 通过 (Ftp)WebResponse​ 属性获取结果

GetFileSize​、GetDateTimestamp​ 命令为例,GetFileSize​ 通过 ContentLength ​ 属性获知文件大小;GetDateTimestamp​ 通过 LastModified ​ 属性获知最后修改日期:

req.Method = WebRequestMethods.Ftp.GetFileSize;
using(var resp = req.GetResponse())resp.ContentLength.Dump();
req.Method = WebRequestMethods.Ftp.GetDateTimestamp;
using (var resp = (FtpWebResponse)req.GetResponse())resp.LastModified.Dump();

16.7.2.3 仅执行,没有返回内容的命令

Rename​ 命令为例,该命令通过 地址 设置需要重命名的文件,通过 RenameTo ​ 属性设置新名称,执行后也没有返回内容。如下代码将 “** tmpfile.txt ” 重命名为 “ deleteme.txt **”:

var req = (FtpWebRequest)WebRequest.Create("ftp://ftp.albahari.com/tmpfile.txt");
// 其他设置
req.Method = WebRequestMethods.Ftp.Rename;
req.RenameTo = "deleteme.txt";
req.GetResponse().Close();

DeleteFile​ 命令为例,该命令通过 地址 设置要删除的文件。如下代码删除 “** deleteme.txt **” 文件:

var req = (FtpWebRequest)WebRequest.Create("ftp://ftp.albahari.com/deleteme.txt");
// 其他设置
req.Method = WebRequestMethods.Ftp.DeleteFile;
req.GetResponse().Close();

16.8 使用 DNS

静态 Dns 类封装了域名服务,用于 IP 地址和域名间相互转换。

16.8.1 域名转换为 IP 地址

Dns​ 的 GetHostAddress(Async) ​ 方法用于将域名转换为 IP 地址(或一系列地址):

foreach (IPAddress a in Dns.GetHostAddresses ("albahari.com"))Console.WriteLine (a.ToString());     // 205.210.42.167
foreach (IPAddress a in await Dns.GetHostAddressesAsync ("albahari.com"))Console.WriteLine (a.ToString());

Suggest

当我们使用 WebRequest​ 或 TcpClient​ 时域名会自动解析为 IP 地址。若应用程序会反复向同一个地址发送网络请求,为了提高性能可以显式使用 Dns​ 将域名转换为 IP 地址,而后直接对 IP 地址进行通信。这样可以避免重复解析同一个域名,尤其有益于(通过 TcpClient​、UdpClient​ 或 Socket​)处理传输层通信。

16.8.2 IP 地址转换为域名

Dns​ 的 GetHostEntry ​ 方法执行反向操作,以 地址字符串IPAddress ​ 实例作为参数:

IPHostEntry entry = Dns.GetHostEntry ("205.210.42.167");
Console.WriteLine (entry.HostName);     // albahari.com
IPAddress address = new IPAddress (new byte[] { 205, 210, 42, 167 });
IPHostEntry entry2 = Dns.GetHostEntry (address);
Console.WriteLine (entry2.HostName);   

16.9 通过 SmtpClient​ 类发送邮件

​#delay#​ 用不到,暂时不看

16.10 使用 TCP

.NET 中,TcpClient​ 和 TcpListener​ 是门面类, Socket ​ 为底层类,功能更为丰富。我们还可以通过 TcpClient.Client​ 属性、TcpListener.Server​ 属性获取它们背后的 Socket ​ 实例,进行更为丰富的操作。

Socket​ 功能丰富,体现在:

  • 有更多的配置选项;

  • 可以直接进行网络层(IP)访问;

  • 支持一些非 Internet 协议

    例如 Novell 的 SPX/IPX 协议

16.10.1 TCP 客户端

TCP 客户端的使用步骤如下:

  1. 创建 TcpClient ​ 实例;
  2. 调用实例的 Connect(Async) ​ 方法,传入地址和端口,连接服务器;
  3. 调用实例的 GeStream ​ 方法,获取 NetworkStream ​ 流,进行通讯。
using (TcpClient client = new TcpClient ("localhost", 51111))
using (NetworkStream n = client.GetStream())
{BinaryWriter w = new BinaryWriter (n);w.Write ("Hello");w.Flush();Console.WriteLine (new BinaryReader (n).ReadString());
}

16.10.2 TCP 服务器

TCP 服务器使用步骤如下:

  1. 创建 TcpListener ​ 实例,传入要监听的地址和端口;
  2. 调用实例的 Start ​ 方法,启用服务器,此时客户端已可以进行连接;
  3. 调用实例的 AcceptTcpClient(Async) ​ 方法,其返回值是 TcpClient ​ 类型,用于后续与客户端通讯;
  4. 调用实例的 GeStream ​ 方法,获取 NetworkStream ​ 流,进行通讯。

TcpListener listener = new TcpListener (IPAddress.Any, 51111);
listener.Start();
using (TcpClient c = listener.AcceptTcpClient())
using (NetworkStream n = c.GetStream())
{string msg = new BinaryReader (n).ReadString();BinaryWriter w = new BinaryWriter (n);w.Write (msg + " right back!");w.Flush();                    // 从此未释放 Writer,
}                                 // 因此必须调用 Flush 方法
listener.Stop();

16.10.3 StreamReader​ 与 NetworkStream

对于 NetWorkStream​,切勿通过 StreamReader​ 读取数据。网络流(NetworkStream​)并不包含 结尾(EOF) ,因此 StreamReader​ 的 ReadToEnd​ 方法可能会无限期阻塞。只要连接是打开状态,网络流就无法确定客户端何时停止发送数据。

StreamReader​ 使用内部缓冲区从基础流中读取数据,即使调用 ReadLine​ 方法,也会预先读取多于一行的数据以提高效率。对于网络流来说,如果流中没有更多数据且没有遇到行结束符,ReadLine​ 方法会阻塞等待数据到达,直到从流中读取到完整的一行或者连接超时。这可能会导致在网络连接缓慢或不稳定时出现长时间阻塞的情况。

16.10.4 并发 TCP

如下是并发使用 TCP 的例子,后续可以进行参考:

RunServerAsync();using (TcpClient client = new TcpClient ("localhost", 51111))
using (NetworkStream n = client.GetStream())
{BinaryWriter w = new BinaryWriter (n);w.Write (Enumerable.Range (0, 5000).Select (x => (byte) x).ToArray());w.Flush();Console.WriteLine (new BinaryReader (n).ReadBytes (5000));
}
async void RunServerAsync ()
{var listener = new TcpListener (IPAddress.Any, 51111);listener.Start ();try{// Accept 内进行了 await,因此 while 循环不会连续执行while (true)Accept (await listener.AcceptTcpClientAsync());}finally { listener.Stop(); }
}
async Task Accept (TcpClient client)
{await Task.Yield ();try{using (client)using (NetworkStream n = client.GetStream ()){byte[] data = new byte [5000];int bytesRead = 0; int chunkSize = 1;while (bytesRead < data.Length && chunkSize > 0)bytesRead += chunkSize =await n.ReadAsync (data, bytesRead, data.Length - bytesRead);Array.Reverse (data);   // Reverse the byte sequenceawait n.WriteAsync (data, 0, data.Length);}}catch (Exception ex) { Console.WriteLine (ex.Message); }
}

上述程序在请求过程中不会阻塞线程,因此是可伸缩的(即可根据需求连接任意数量的客户端)。

16.11 使用 TCP 接收 POP3 邮件

​#delay#​ 用不到,暂时不看

16.12 在 Windows Runtime 中使用 TCP

WinRT 主要为 UWP 服务,#delay#​ 用不到,暂时不看

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/862364.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

第13章 诊断

第13章 诊断 13.1 条件编译 预编译的指令见 4.16 预处理指令,我们这里的条件编译用到的指令有:​#if​​、#else​​、#endif​​、#elif​​ 条件编译指令可以进行 与 ​&&​ ​、 或 ​||​ ​、 非 ​!​ 运算。预定义指令可以通过三种方式定义:在文件中通…

第14章 并发与异步

第14章 并发与异步 14.2 线程 进 程提供了程序执行的独立环境, 进 程持有 线 程,且至少持有一个 线 程。这些 线 程共享 进 程提供的执行环境。 14.2.1 创建线程 创建线程的步骤为:实例化 ​Thread​ ​ 对象,通过构造函数传入 ​ThreadStart​ ​ 委托。 调用 ​Thread…

Sqlserver With as 实现循环递归

一、脚本示例declare @Separator varchar(10), @str varchar(100) declare @l int, @i int select @Separator=,,@str=111,22,777,99,666 select @i = len(@Separator), @l = len(@str); with cte7 as ( select 0 a, 1 b union all select b, charindex(@Separator, @str, b)+@…

JAVA 分布式锁

分布式锁 JVM 自带的 synchronized 及 ReentrantLock 锁都是单进程内的,不能跨进程,如下,同时来个两个请求被分配到不同的tomcat,这种锁将失效:REDIS 实现分布式锁 可以借助 REDIS 的setnx 命令实现: https://blog.csdn.net/T_Y_F_/article/details/144238022 注:redis …

java8--类Scanner--文件内容输入--windows路径分隔符转义

try { Scanner in = new Scanner(Paths.get("C:\Users\Administrator\IdeaProjects\untitled2\src\test\myfile.txt"),"UTF-8"); } catch (IOException ioException) { ioException.printStackTrace(); }ps: 1.打印当前工…

[Windows] 启动 Windows Update 服务失败,报:Windows 无法启动 Windows Update 服务(位于 本地计算机 上) 错误 126:找不到指定的模块

1 问题描述现象1:Windows 10 家庭版-服务(services.msc)-启动 Windows Update 服务失败,报:"Windows 无法启动 Windows Update 服务(位于 本地计算机 上) 错误 126:找不到指定的模块"注: C:\Windows\System32\wuaueng.dll 文件存在注:注册表regedit:计算机\HKEY_L…

共享ubuntu系统宿主机的部分文件到win虚拟机--通过ISO文件挂载

安装genisoimage sudo apt-get update sudo apt-get install genisoimage将需要共享的文件放入指定文件夹 cp /path/to/your/file ~/iso_work/使用genisoimage生成新镜像 genisoimage -o /path/to/new.iso -J -R -V "NEW_ISO_LABEL" ~/iso_work/其中new.iso就是新镜像…

Luogu P9646 SNCPC2019 Paper-cutting 题解 [ 紫 ] [ manacher ] [ 贪心 ] [ 哈希 ] [ BFS ]

manacher 与贪心的好题。Paper-cutting:思维很好,但代码很构式的 manacher 题。 蒟蒻 2025 年切的第一道题,是个紫,并且基本独立想出的,特此纪念。 判断能否折叠 我们先考虑一部分能折叠需要满足什么条件。显然,这一部分需要是一个长度为偶数的回文串。 那么横向和纵向会…

深度学习基础理论————分布式训练(模型并行/数据并行/流水线并行/张量并行)

主要介绍Pytorch分布式训练代码以及原理以及一些简易的Demo代码 模型并行 是指将一个模型的不同部分(如层或子模块)分配到不同的设备上运行。它通常用于非常大的模型,这些模型无法完整地放入单个设备的内存中。在模型并行中,数据会顺序通过各个层,即一层处理完所有数据之后…

overleaf-Latex教程

1.领取免费服务器,推荐免费服务器(SanFengYun)见下图。2.安装宝塔面板,配置内网为127.0.0.1,访问外网地址。 3.可以在宝塔面板一键部署网站,输入自己的域名即可。 4.关键:安装docker,安装yum,设置github可以访问。 5.更换docker镜像,自带镜像无法访问 6.按照overleaf…

Sola的2024年度总结

前言 2024 这一年对我来说确实意义非凡,很想写点东西来记录一下这一年我的经历,算是第一次写年度总结了。 简短的记录一下我这一年。 现在?未来? 回忆起大一下最后一节体育课,体育老师让每个人想一个词来描述这个上半年,我给出的答案是 : 迷茫 。 现在来看,这个答案贯穿…

洛谷 P11487 「Cfz Round 5」Gnirts 10——题解

洛谷P11487「Cfz Round 5」Gnirts 10传送锚点摸鱼环节 「Cfz Round 5」Gnirts 10 题目背景 English statement. You must submit your code at the Chinese version of the statement.In Memory of \(\text{F}\rule{66.8px}{6.8px}\). 题目描述 题面还是简单一点好。给定 \(n, …