第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
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 皆可)。实例化方式有两种:
- 使用构造器,传入 字节数组 ;
- 使用静态方法
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 是一个具有特殊格式的字符串,它描述了一个 Internet 或 LAN 资源。
URI 可分为三个组成部分: 协议(scheme) 、 权限(authority) 、 路径(path) 。Uri 类便采用了该划分方式:
Info
关于 LAN 资源,见 LAN 资源和 UNC 路径。
16.3.1 Uri
类型
Uri
的构造器接受如下 3 种字符串:
-
URI 字符串
形如
http://www.ebay.com
或file://janespc/sharedpics/dolphin.jpg
-
硬盘中文件 的绝对路径
形如 c:\myfiles\data.xlsx
3. LAN 中文件的 UNC 路径
形如 \\janespc\sharedpics\dolphin.jpg
文件 和 UNC 路径会自动转换为 URI:
- 添加
file:
协议; - 将
\
转化为/
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
方法
用于获取地址类型是 DNS 、 IPv4 还是 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 客户端类型
提纲
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
使用方式如下:
- 实例化
WebClient
对象; - 设置
Proxy
属性值; - 若需进行验证,设置
Credentials
属性值; - 使用相应的 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:
”协议的通用基类。它们封装了这些协议共同的“请求/响应”模型。这两个类传输数据时仅支持流。
它们的使用步骤如下:
-
调用
WebRequest.Creat
方法并传入 URI,以获取WebRequest
实例; -
设置
Proxy
属性; -
若需要上传数据,则调用
WebRequest
实例的 GetRequestStream
方法获取流,并向流中写入数据; -
若需要获取响应/下载数据,则:
- 调用
WebRequest
实例的 GetResponse
方法,获得WebResponse
实例; - 调用
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)
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 请求的中间服务器。
16.4.4.1 WebClient
与 WebRequest
使用代理
我们可以使用 WebProxy
对象,令 WebClient
或者 WebRequest
对象通过代理服务器转发请求,使用步骤如下:
- 实例化
WebProxy
对象,并设置 Credentials
属性(可选); - 实例化
WebClient
/WebRequest
对象,用步骤 1 的实例设置 Proxy
属性; - 进行剩余的常规操作。
示例代码如下:
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 身份验证
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 用户:
-
Credentials
设置为 null,并将UserDefaultCredentials
设置为 true; -
Credentials
属性设置为CredentialCache.DefaultNetworkCredentials
:
Tips
使用表单进行身份验证时无须设置
Credentials
属性。详见 16.5.5 表单验证。
Info
NetworkCredential
的“域名称”,即Domain
参数,可通过构造函数传入。
16.4.5.3 身份验证的流程
身份验证最终是由 WebRequest
的子类完成,它会自动协商兼容协议(NTLM、Kerberos、Basic 和 Digest)。其验证步骤如下:
-
客户端向服务器请求内容;
-
服务器返回 401,表示需要验证身份;
返回的的内容包含服务器支持的身份验证协议。
-
客户端再次请求内容,头信息携带身份信息;
根据服务器支持的身份验证协议,自动将验证信息添加至头信息。
-
服务器验证通过,返回内容。
// 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 后的字符串,用于向服务器发送简单的数据。语法如下:
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
需要手动设置各种参数,步骤如下:
- 设置
ContentType
属性为 “application/x-www-form-urlencoded”; - 构建表单字符串,采用“application/x-www-form-urlencoded”编码格式,并使用
Encoding.UTF8.GetBytes
将字符串转换为byte
数组; - 设置
ContentLength
属性为byte
数组的长度; - 调用
GetRequestStream
方法,将byte
数组写入流中; - 调用
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");
16.5.4 cookie
16.5.4.1 WebRequest
使用 cookie
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);}
}
16.5.4.2 HttpClient
使用 cookie
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
创建服务器步骤如下:
-
创建
HttpListener
实例; -
实例的
Prefixes
属性添加要监听的地址、端口及前缀; -
调用实例的
Start
方法,开始监听; -
调用实例的
GetContextAsync
方法,等待客户端连接,获取客户端请求( HttpListenerContext
类型);通过异步等待,可以增强服务器的伸缩性。
-
设置状态码、内容长度等数据,并将信息通过流传出。
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 很简单:
- 设置身份验证(
Credentials
属性); - 设置 FTP 地址(
BaseAddress
属性); - 调用
Upload*
方法上传; - 调用
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 客户端的使用步骤如下:
- 创建
TcpClient
实例; - 调用实例的
Connect(Async)
方法,传入地址和端口,连接服务器; - 调用实例的
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 服务器使用步骤如下:
- 创建
TcpListener
实例,传入要监听的地址和端口; - 调用实例的
Start
方法,启用服务器,此时客户端已可以进行连接; - 调用实例的
AcceptTcpClient(Async)
方法,其返回值是 TcpClient
类型,用于后续与客户端通讯; -
调用实例的
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# 用不到,暂时不看