c# 10 教程:16 网络

c# 10 教程:16 网络.NET 在 System.Net.* 命名空间中提供了各种类,用于通过标准网络协议(如 HTTP 和 TCP/IP)进行通信。

大家好,欢迎来到IT知识分享网。

.NET 在 System.Net.* 命名空间中提供了各种类,用于通过标准网络协议(如 HTTP 和 TCP/IP)进行通信。以下是关键组件的摘要:

  • 用于使用 HTTP Web API 和 RESTful 服务的 HttpClient
  • 用于编写 HTTP 服务器的 HttpListener
  • 用于通过SMTP构建和发送邮件消息的smtp客户端
  • 用于在域名和地址之间进行转换的 DNS
  • TcpClient 、UdpClient 、TcpListener 和 Socket 类,用于直接访问传输层和网络层

本章中的 .NET 类型位于 System.Net.* 和 System.IO 命名空间中。

注意

.NET 还提供对 FTP 的客户端支持,但只能通过已从 .NET 6 标记为过时的类。如果需要使用 FTP,最好的选择是使用 NuGet 库,例如 FluentFTP。

网络架构

说明了 .NET 网络类型及其所在的通信层。大多数类型驻留在层或中。传输层定义了发送和接收字节(TCP 和 UDP)的基本协议;应用层定义了为特定应用设计的更高级别的协议,例如检索网页 (HTTP)、发送邮件 (SMTP) 以及在域名和 IP 地址 (DNS) 之间进行转换。

c# 10 教程:16 网络

网络架构

在应用程序层编程通常是最方便的;但是,您可能希望直接在传输层工作的原因有几个。一种是是否需要 .NET 中未提供的应用程序协议(如 POP3)来检索邮件。另一个是如果您想为特殊应用程序(如点对点客户端)发明自定义协议。

在应用程序协议中,HTTP在对通用通信的适用性方面是特殊的。它的基本操作模式(“给我包含此 URL 的网页”)很好地适应了“让我了解使用这些参数调用此终结点的结果”。(除了“get”动词之外,还有“put”,“post”和“delete”,允许基于REST的服务。

HTTP 还具有一组丰富的功能,这些功能在多层业务应用程序和面向服务的体系结构中非常有用,例如用于身份验证和加密、消息分块、可扩展标头和 Cookie 的协议,以及让许多服务器应用程序共享单个端口和 IP 地址的能力。由于这些原因,HTTP 在 .NET 中得到了很好的支持,既可以直接支持(如本章所述),也可以通过 Web API 和 ASP.NET Core 等技术在更高级别得到支持。

正如前面的讨论所表明的那样,网络是一个充斥着首字母缩略词的领域。我们在 中列出了最常见的。

网络缩略语

缩写

扩张

笔记

域名解析

域名服务

在域名(例如 )和 IP 地址(例如 199.54.213.2)之间进行转换

邮票

文件传输协议

用于发送和接收文件的基于互联网的协议

HTTP

超文本传输协议

检索网页并运行 Web 服务

二世

互联网信息服务

Microsoft的网络服务器软件

知识产权

网际协议

低于 TCP 和 UDP 的网络层协议

局域网

局域网

大多数局域网使用基于互联网的协议,如TCP/IP

流行

邮局协议

检索互联网邮件

休息

再现状态转移

一种流行的 Web 服务体系结构,在响应中使用机器可遵循的链接,并且可以在基本 HTTP 上运行

短信通信

简单邮件传输协议

发送互联网邮件

技术合作计划(TCP

传输和控制协议

传输层互联网协议,大多数更高层服务都在其上构建

UDP

通用数据报协议

用于低开销服务(如 VoIP)的传输层互联网协议

北卡罗来纳大学

通用命名约定

\\计算机\共享名\文件名

乌里

统一资源标识符

无处不在的资源命名系统(例如, 或mailto:)

网址

统一资源定位器

技术含义(从使用中淡出):URI的子集;通俗含义:URI 的同义词

地址和端口

要使通信正常工作,计算机或设备需要一个地址。互联网使用两种寻址系统:

IPv4

目前占主导地位的寻址系统;IPv4 地址的宽度为 32 位。当字符串格式时,IPv4 地址被写入为四个点分隔的小数(例如,101.102.103.104)。地址在世界上可以是唯一的,也可以在特定中是唯一的(例如在公司网络上)。

IPv6

较新的 128 位寻址系统。地址采用十六进制格式的字符串格式,带有冒号分隔符(例如,[3EA0:FFFF:198A:E4A3:4FF2:54fA:41BC:8D31])。.NET 要求在地址两边添加方括号。

System.Net 命名空间中的 IPAddress 类表示任一协议中的地址。它有一个接受字节数组的构造函数和一个接受正确格式字符串的静态 Parse 方法:

IPAddress a1 = new IPAddress (new byte[] { 101, 102, 103, 104 });
IPAddress a2 = IPAddress.Parse ("101.102.103.104");
Console.WriteLine (a1.Equals (a2));                     // True
Console.WriteLine (a1.AddressFamily);                   // InterNetwork

IPAddress a3 = IPAddress.Parse
  ("[3EA0:FFFF:198A:E4A3:4FF2:54fA:41BC:8D31]");
Console.WriteLine (a3.AddressFamily);   // InterNetworkV6

TCP 和 UDP 协议将每个 IP 地址分成 65,535 个端口,允许单个地址上的计算机运行多个应用程序,每个应用程序都在自己的端口上。许多应用程序具有标准的默认端口分配;例如,HTTP 使用端口 80;SMTP 使用端口 25。

注意

从 49152 到 65535 的 TCP 和 UDP 端口是正式未分配的,因此它们非常适合测试和小规模部署。

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

注意

防火墙阻止端口。在许多企业环境中,只有少数端口处于打开状态,通常是端口 80(用于未加密的 HTTP)和端口 443(用于安全 HTTP)。

目录

URI 是一个特殊格式的字符串,用于描述互联网或 LAN 上的资源,例如网页、文件或电子邮件地址。示例包括 、ftp://myisp/doc.txt 和 mailto:。确切的格式由 (IETF) 定义。

URI 可以分解为一系列元素,通常是、和。System 命名空间中的 Uri 类仅执行此划分,为每个元素公开一个属性,如图 所示。

c# 10 教程:16 网络

注意

当您需要验证 URI 字符串的格式或将 URI 拆分为其组成部分时,Uri 类非常有用。否则,可以将 URI 简单地视为字符串 – 大多数网络方法都会重载以接受 Uri 对象或字符串。

可以通过将以下任何字符串传递到其构造函数中来构造 Uri 对象:

  • URI 字符串,例如 或
  • 硬盘上文件的绝对路径,例如 或者在 Unix 上为 /
  • 局域网上文件的 UNC 路径,例如 \\

文件和 UNC 路径会自动转换为 URI:添加“file:”协议,并将反斜杠转换为正斜杠。Uri 构造函数还会在创建 Uri 之前对字符串执行一些基本的清理,包括将方案和主机名转换为小写以及删除默认和空白端口号。如果提供不带方案的 URI 字符串(如“”),则会引发 UriFormatException。

Uri 具有 IsLoopback 属性,该属性指示 Uri 是否引用本地主机(IP 地址 127.0.0.1)和一个 IsFile 属性,该属性指示 Uri 引用本地路径还是 UNC (IsUnc) 路径(对于挂载在 文件系统中的 共享,IsUnc 报告 false)。如果 IsFile 返回 true ,则 LocalPath 属性返回对本地操作系统友好的 AbsolutePath 版本(根据操作系统使用斜杠或反斜杠),您可以在该版本上调用 File.Open 。

Uri 的实例具有只读属性。要修改现有的 Uri,请实例化 UriBuilder 对象 – 该对象具有可写属性,可以通过其 Uri 属性转换回来。

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

相对 Uri,例如本例中的 ,如果您调用除 IsAbsoluteUri 和 ToString() 之外的几乎任何属性或方法,则会引发异常。你可以直接实例化一个相对的 Uri,如下所示:

Uri u = new Uri ("page.html", UriKind.Relative);

警告

尾部斜杠在 URI 中很重要,如果存在路径组件,则服务器如何处理请求会有所不同。

例如,在传统的Web服务器中,给定URI ,您可以期望HTTP Web服务器在站点Web文件夹中的子目录中查找并返回默认文档(通常是)。

如果没有尾部斜杠,Web 服务器将直接在站点的根文件夹中查找一个名为 的文件(不带扩展名),这通常不是您想要的。如果不存在此类文件,大多数 Web 服务器将假定用户键入错误,并将返回 301 错误,建议客户端使用尾部斜杠重试。默认情况下,.NET HTTP 客户端将以与 Web 浏览器相同的方式透明地响应 301,方法是使用建议的 URI 重试。这意味着,如果在应该包含尾部斜杠时省略了尾部斜杠,您的请求仍然有效,但会遭受不必要的额外往返。

Uri 类还提供了静态辅助方法,例如 EscapeUriString() ,它通过将 ASCII 值大于 127 的所有字符转换为十六进制表示形式,将字符串转换为有效的 URL。CheckHostName() 和 CheckSchemeName() 方法接受字符串并检查它对于给定属性在语法上是否有效(尽管它们不尝试确定主机或 URI 是否存在)。

HttpClient

HttpClient 类公开了一个用于 HTTP 客户端操作的现代 API,取代了旧的 WebClient 和 WebRequest / WebResponse 类型(这些类型已被标记为过时)。

HttpClient 是为了响应基于 HTTP 的 Web API 和 REST 服务的增长而编写的,并且在处理比简单地获取网页更复杂的协议时提供了良好的体验。特别:

  • 单个 HttpClient 实例可以处理并发请求,同时与自定义标头、Cookie 和身份验证方案等功能很好地配合使用。
  • HttpClient 允许您编写和插入自定义消息处理程序。这允许在单元测试中进行模拟,并创建自定义管道(用于日志记录、压缩、加密等)。
  • HttpClient 具有用于标头和内容的丰富且可扩展的类型系统。

注意

HttpClient 不支持进度报告。有关解决方案,请参阅 ,或通过 LINQPad 的交互式示例库。

使用 HttpClient 的最简单方法是实例化它,然后调用其 Get* 方法之一,传入一个 URI:

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

(还有GetByteArrayAsync和GetStreamAsync。HttpClient 中的所有 I/O 绑定方法都是异步的。

与其WebRequest / WebResponse的前身不同,要获得最佳性能 HttpClient ,重用相同的实例(否则DNS解析之类的事情可能会不必要地重复,并且套接字保持打开的时间超过必要的时间)。HttpClient 允许并发操作,因此以下内容是合法的,并且可以一次下载两个网页:

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

HttpClient 具有超时属性和 BaseAddress 属性,该属性为每个请求添加前缀 URI。HttpClient有点像一个薄壳:您可能希望在此处找到的大多数其他属性都是在另一个名为HttpClientHandler的类中定义的。要访问这个类,你实例化它,然后将实例传递到 HttpClient 的构造函数中:

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

在此示例中,我们告诉处理程序禁用代理支持,这有时可以通过避免自动代理检测的成本来提高性能。还有一些属性可以控制 Cookie、自动重定向、身份验证等(我们将在以下各节以及“使用 HTTP”中介绍这些属性)。

获取异步和响应消息

GetStringAsync 、GetByteArrayAsync 和 GetStreamAsync 方法是调用更通用的 GetAsync 方法的便捷快捷方式,该方法返回:

var client = new HttpClient();
// The GetAsync method also accepts a CancellationToken.
HttpResponseMessage response = await client.GetAsync ("http://...");
response.EnsureSuccessStatusCode();
string html = await response.Content.ReadAsStringAsync();

HttpResponseMessage 公开了用于访问标头(请参阅和 HTTP 状态代码的属性。不成功的状态代码(如 404(未找到))不会导致引发异常,除非您显式调用 确保成功状态代码 。但是,通信或 DNS 错误确实会引发异常。

HttpContent 有一个用于写入另一个流的 CopyToAsync 方法,该方法在将输出写入文件时很有用:

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

GetAsync 是对应于 HTTP 的四个动词的四种方法之一(其他方法是 PostAsync、PutAsync 和 DeleteAsync)。稍后我们将在“上传表单数据”中演示 PostAsync。

发送异步和请求消息

GetAsync 、PostAsync、PutAsync 和 DeleteAsync 都是调用 SendAsync 的快捷方式,SendAsync 是其他所有内容都馈送到的单一低级方法。要使用它,您首先构造一个 HttpRequestMessage :

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

实例化 HttpRequestMessage 对象意味着您可以自定义请求的属性,例如标头(请参阅和内容本身,从而允许您上传数据。

上传数据和 httpContent

实例化 HttpRequestMessage 对象后,可以通过分配其 Content 属性来上载内容。此属性的类型是一个名为 HttpContent 的抽象类。.NET 包含以下用于不同类型内容的具体子类(您也可以编写自己的子类):

  • 字节数组内容
  • 字符串内容
  • FormUrlEncodedContent(请参阅)
  • 流内容

例如:

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

HttpMessageHandler

我们之前说过,大多数用于自定义请求的属性不是在 HttpClient 中定义的,而是在 HttpClientHandler 中定义的。后者实际上是抽象 HttpMessageHandler 类的子类,定义如下:

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

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

SendAsync方法是从HttpClient的SendAsync方法调用的。

HttpMessageHandler非常简单,可以轻松进行子类化,并为HttpClient提供了一个扩展点。

单元测试和模拟

我们可以对 HttpMessageHandler 进行子类化,创建一个处理程序来帮助进行单元测试:

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

它的构造函数接受一个函数,该函数告诉模拟者如何从请求生成响应。这是最通用的方法,因为同一个处理程序可以测试多个请求。

SendAsync 是同步的,凭借 Task.FromResult 。我们本可以通过让我们的响应生成器返回一个 Task<HttpResponseMessage> 来保持异步性,但这是没有意义的,因为我们可以预期模拟函数运行时间很短。以下是使用我们的模拟处理程序的方法:

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

(Assert.AreEqual是您希望在单元测试框架(如NUnit)中找到的方法。

使用委派处理程序链接处理程序

您可以通过子类化 De委派处理程序 来创建调用另一个消息处理程序(生成处理程序链)。您可以使用它来实现自定义身份验证、压缩和加密协议。下面演示了一个简单的日志记录处理程序:

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

请注意,我们在覆盖 SendAsync 时保持了异步。在重写任务返回方法时引入异步修饰符是完全合法的,在这种情况下是可取的。

比写入控制台更好的解决方案是让构造函数接受某种日志记录对象。更好的办法是接受几个 Action<T> 委托,告诉它如何记录请求和响应对象。

代理

是可以路由 HTTP 请求的中介。组织有时会将代理服务器设置为员工访问互联网的唯一方式,主要是因为它简化了安全性。代理有自己的地址,可以要求身份验证,以便只有 LAN 上的选定用户才能访问互联网。

要将代理与 HttpClient 一起使用,首先创建一个 HttpClientHandler 并分配其 Proxy 属性,然后将其馈送到 HttpClient 的构造函数中:

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

var handler = new HttpClientHandler { Proxy = p };
var client = new HttpClient (handler);
...

HttpClientHandler 还有一个 UseProxy 属性,您可以将其分配给 false,而不是清空 Proxy 属性以阻止自动检测。

如果在构造 NetworkCredential 时提供域,则使用基于 Windows 的身份验证协议。若要使用当前经过身份验证的 Windows 用户,请将静态 CredentialCache.DefaultNetworkCredentials 值分配给代理的 Credentials 属性。

作为重复设置代理的替代方法,您可以按如下方式设置全局默认值:

HttpClient.DefaultWebProxy = myWebProxy;

认证

您可以向 HttpClient 提供用户名和密码,如下所示:

string username = "myuser";
string password = "mypassword";

var handler = new HttpClientHandler();
handler.Credentials = new NetworkCredential (username, password);
var client = new HttpClient (handler);
...

这适用于基于对话框的身份验证协议(如基本和摘要),并且可通过 AuthenticationManager 类进行扩展。它还支持 Windows NTLM 和 Kerberos(如果在构造 NetworkCredential 对象时包含域名)。如果要使用当前经过身份验证的 Windows 用户,可以将“凭据”属性保留为空,而是将“使用默认凭据”设置为 true 。

当您提供凭据时,HttpClient 会自动协商兼容的协议。在某些情况下,可以选择:例如,如果检查来自Microsoft Exchange 服务器 Web 邮件页面的初始响应,则它可能包含以下标头:

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 代码表示需要授权;“WWW 身份验证”标头指示理解的身份验证协议。但是,如果使用正确的用户名和密码配置 HttpClientHandler,则此消息将对你隐藏,因为运行时通过选择兼容的身份验证协议,然后使用额外的标头重新提交原始请求来自动响应。下面是一个示例:

Authorization: Negotiate TlRMTVNTUAAABAAAt5II2gjACDArAAACAwACACgAAAAQ
ATmKAAAAD0lVDRdPUksHUq9VUA==

此机制提供透明度,但会为每个请求生成额外的往返行程。通过将 HttpClientHandler 上的 PreAuthenticate 属性设置为 true 来避免对同一 URI 的后续请求进行额外的往返。

凭据缓存

您可以使用凭据缓存对象强制使用特定的身份验证协议。凭据缓存包含一个或多个 NetworkCredential 对象,每个对象都以特定协议和 URI 前缀为密钥。例如,您可能希望在登录 Exchange Server 时避免使用基本协议,因为它以纯文本形式传输密码:

CredentialCache cache = new CredentialCache();
Uri prefix = new Uri ("http://exchange.somedomain.com");
cache.Add (prefix, "Digest",  new NetworkCredential ("joe", "passwd"));
cache.Add (prefix, "Negotiate", new NetworkCredential ("joe", "passwd"));

var handler = new HttpClientHandler();
handler.Credentials = cache;
...

身份验证协议指定为字符串。有效值包括:

Basic, Digest, NTLM, Kerberos, Negotiate

在这种特殊情况下,它将选择协商,因为服务器在其身份验证标头中未指示它支持 Digest。协商是一种 Windows 协议,目前归结为 Kerberos 或 NTLM,具体取决于服务器的功能,但在部署未来安全标准时可确保应用程序的向前兼容性。

静态 CredentialCache.DefaultNetworkCredentials 属性允许您将当前经过身份验证的 Windows 用户添加到凭据缓存中,而无需指定密码:

cache.Add (prefix, "Negotiate", CredentialCache.DefaultNetworkCredentials);

通过标头进行身份验证

另一种身份验证方法是直接设置身份验证标头:

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

此策略也适用于自定义身份验证系统,如 OAuth。

HttpClient 允许您向请求添加自定义 HTTP 标头,以及在响应中枚举标头。标头只是包含元数据(如消息内容类型或服务器软件)的键/值对。HttpClient 公开具有标准 HTTP 标头属性的强类型集合。属性适用于应用于每个请求的标头:

var client = new HttpClient (handler);

client.DefaultRequestHeaders.UserAgent.Add (
  new ProductInfoHeaderValue ("VisualStudio", "2022"));

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

但是,类上的 Headers 属性用于特定于请求的标头。

查询字符串

查询字符串只是附加到带有问号的 URI 的字符串,用于将简单数据发送到服务器。可以使用以下语法在查询字符串中指定多个键/值对:

?key1=value1&key2=value2&key3=value3...

下面是一个带有查询字符串的 URI:

string requestURI = "http://www.google.com/search?q=HttpClient&hl=fr";

如果您的查询可能包含符号或空格,则可以使用 Uri 的 EscapeDataString 方法创建一个合法的 URI:

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

此生成的 URI 为:

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

(EscapeDataString 与 EscapeUriString 类似,不同之处在于它还对 & 和 = 等字符进行编码,否则会弄乱查询字符串。

上传表单数据

若要上载 HTML 表单数据,请创建并填充 FormUrlEncodedContent 对象。然后,可以将其传递到 PostAsync 方法中,也可以将其分配给请求的 Content 属性:

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

饼干

Cookie 是 HTTP 服务器在响应标头中发送到客户端的名称/值字符串对。Web 浏览器客户端通常会记住 Cookie,并在每次后续请求(到同一地址)中将它们重播到服务器,直到它们到期。Cookie 允许服务器知道它是在与一分钟前还是昨天的同一客户端通信,而无需在 URI 中提供混乱的查询字符串。

默认情况下,HttpClient 会忽略从服务器接收的任何 Cookie。要接受 cookie,请创建一个 CookieContainer 对象并为其分配一个 HttpClientHandler:

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

要在将来的请求中重播收到的 Cookie,只需再次使用相同的 CookieContainer 对象即可。或者,您可以从新的 CookieContainer 开始,然后手动添加 cookie,如下所示:

Cookie c = new Cookie ("PREF",
                       "ID=6b10df1da493a9c4:TM=1179...",
                       "/",
                       ".google.com");
freshCookieContainer.Add (c);

第三个和第四个参数指示发起方的路径和域。客户端上的 CookieContainer 可以容纳来自许多不同位置的 Cookie;HttpClient 仅发送路径和域与服务器路径和域匹配的 cookie。

编写 HTTP 服务器

注意

如果需要在 .NET 6 中编写 HTTP 服务器,另一种更高级别的方法是使用最小 API ASP.NET。以下是入门所需的全部内容:

var app = WebApplication.CreateBuilder().Build();
app.MapGet ("/", () => "Hello, world!");
app.Run();

您可以使用 HttpListener 类编写自己的 .NET HTTP 服务器。下面是一个简单的服务器,它侦听端口 51111,等待单个客户端请求,然后返回一行回复:

using var server = new SimpleHttpServer();

// Make a client request:
Console.WriteLine (await new HttpClient().GetStringAsync
  ("http://localhost:51111/MyApp/Request.txt"));

class SimpleHttpServer : IDisposable
{
  readonly HttpListener listener = new HttpListener();
  
  public SimpleHttpServer() => ListenAsync();  
  async void ListenAsync()
  {
    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);
  }

  public void Dispose() => listener.Close();
}

OUTPUT: You asked for: /MyApp/Request.txt

在Windows上,HttpListener在内部不使用.NET Socket对象;相反,它调用Windows HTTP Server API。这允许计算机上的许多应用程序侦听相同的 IP 地址和端口,只要每个应用程序注册不同的地址前缀即可。在我们的示例中,我们注册了前缀 http://localhost/myapp,因此另一个应用程序可以自由侦听另一个前缀(如 http://localhost/anotherapp)上的同一 IP 和端口。这是有价值的,因为在公司防火墙上打开新端口在政治上可能很困难。

当您调用 GetContext 时,HttpListener 会等待下一个客户端请求,返回具有请求和响应属性的对象。每个都类似于客户端请求或响应,但从服务器的角度来看。例如,您可以读取和写入标头和 Cookie 到请求和响应对象,就像在客户端一样。

您可以根据预期的客户端受众选择完全支持 HTTP 协议功能的程度。至少应设置每个请求的内容长度和状态代码。

这是一个非常简单的网页服务器,

using System;
using System.IO;
using System.Net;
using System.Text;
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); }
  }
}

以下代码启动了操作:

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

您可以使用任何 Web 浏览器在客户端对此进行测试;在这种情况下,URI 将 http://localhost:51111/ 加上网页的名称。

警告

如果其他软件竞争同一端口,HttpListener 将不会启动(除非该软件也使用 Windows HTTP Server API)。可能侦听默认端口 80 的应用程序示例包括 Web 服务器或对等程序(如 Skype)。

我们对异步函数的使用使该服务器具有可扩展性和效率。但是,从用户界面 (UI) 线程开始会阻碍可伸缩性,因为对于每个,执行会在每次等待后反弹回 UI 线程。鉴于我们没有共享状态,产生这样的开销特别没有意义,因此在 UI 场景中,我们会像这样离开 UI 线程。

Task.Run (Start);

或者在调用 GetContextAsync 后调用 ConfigureAwait(false)。

请注意,我们使用 Task.Run 来调用 ProcessRequestAsync,即使该方法已经是异步的。这允许调用方处理另一个请求,而不必首先等待方法的同步阶段(直到第一个等待)。

使用域名解析

静态 Dns 类封装 DNS,该 DNS 在原始 IP 地址(如 66.135.192.87)和人类友好域名(如 )之间进行转换。

方法从域名转换为 IP 地址(或多个地址):

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

GetHostEntry 方法则相反,从地址转换为域名:

IPHostEntry entry = Dns.GetHostEntry ("205.210.42.167");
Console.WriteLine (entry.HostName);                    // albahari.com

GetHostEntry 还接受 IPAddress 对象,因此您可以将 IP 地址指定为字节数组:

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

当您使用 WebRequest 或 TcpClient 等类时,域名会自动解析为 IP 地址。但是,如果您计划在应用程序的生命周期内向同一地址发出许多网络请求,则有时可以通过首先使用 Dns 将域名显式转换为 IP 地址,然后从该点开始直接与 IP 地址通信来提高性能。这避免了重复往返解析相同的域名,并且在传输层(通过 TcpClient 、UdpClient 或 Socket )处理时可能会有所帮助。

DNS 类还提供可等待的基于任务的异步方法:

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

使用 SMTP 客户端发送邮件

命名空间中的 SmtpClient 类允许您通过无处不在的简单邮件传输协议 (SMTP) 发送邮件。要发送简单的文本消息,请实例化 SmtpClient ,将其 Host 属性设置为 SMTP 服务器地址,然后调用 发送 :

SmtpClient client = new SmtpClient();
client.Host = "mail.myserver.com";
client.Send ("from@adomain.com", "to@adomain.com", "subject", "body");

构造 MailMessage 对象会公开更多选项,包括添加附件的功能:

SmtpClient client = new SmtpClient();
client.Host = "mail.myisp.net";
MailMessage mm = new MailMessage();

mm.Sender = new MailAddress ("kay@domain.com", "Kay");
mm.From   = new MailAddress ("kay@domain.com", "Kay");
mm.To.Add  (new MailAddress ("bob@domain.com", "Bob"));
mm.CC.Add  (new MailAddress ("dan@domain.com", "Dan"));
mm.Subject = "Hello!";
mm.Body = "Hi there. Here's the photo!";
mm.IsBodyHtml = false;
mm.Priority = MailPriority.High;

Attachment a = new Attachment ("photo.jpg",
                               System.Net.Mime.MediaTypeNames.Image.Jpeg);
mm.Attachments.Add (a);
client.Send (mm);

为了阻止垃圾邮件发送者,互联网上的大多数SMTP服务器仅接受来自经过身份验证的连接的连接,并要求通过SSL进行通信。

var client = new SmtpClient ("smtp.myisp.com", 587)
{
  Credentials = new NetworkCredential ("me@myisp.com", "MySecurePass"),
  EnableSsl = true
};
client.Send ("me@myisp.com", "someone@somewhere.com", "Subject", "Body");
Console.WriteLine ("Sent");

通过更改 DeliveryMethod 属性,可以指示 SmtpClient 改用 IIS 发送邮件,或者只是将每封邮件写入指定目录中的 文件。这在开发过程中可能很有用。

SmtpClient client = new SmtpClient();
client.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
client.PickupDirectoryLocation = @"c:\mail";

使用TCP

TCP 和 UDP 构成了传输层协议,大多数互联网和 LAN 服务都在其上构建。HTTP(版本 2 及更低版本)、FTP 和 SMTP 使用 TCP;DNS 和 HTTP 版本 3 使用 UDP。TCP 是面向连接的,包括可靠性机制;UDP 是无连接的,开销较低,并支持广播。使用UDP,IP语音(VoIP)也是如此。

与较高层相比,传输层提供了更大的灵活性,并可能提高性能,但它要求您自己处理身份验证和加密等任务。

使用 .NET 中的 TCP,您可以选择更易于使用的 TcpClient 和 TcpListener 外观类,也可以选择功能丰富的 Socket 类。 (实际上,您可以混合搭配,因为 TcpClient 通过 Client 属性公开基础套接字对象。Socket 类公开了更多的配置选项,并允许直接访问网络层 (IP) 和非基于 Internet 的协议,例如 Novell 的 SPX/IPX。

与其他协议一样,TCP 区分客户端和服务器:客户端发起请求,而服务器等待请求。下面是同步 TCP 客户端请求的基本结构:

using (TcpClient client = new TcpClient())
{
  client.Connect ("address", port);
  using (NetworkStream n = client.GetStream())
  {
    // Read and write to the network stream...
  }
}

TcpClient 的连接方法会阻塞,直到建立连接(ConnectAsync 是异步等价物)。然后,NetworkStream提供了一种双向通信方式,用于从服务器发送和接收字节的数据。

一个简单的TCP服务器如下所示:

TcpListener listener = new TcpListener (<ip address>, port);
listener.Start();

while (keepProcessingRequests)
  using (TcpClient c = listener.AcceptTcpClient())
  using (NetworkStream n = c.GetStream())
  {
    // Read and write to the network stream...
  }

listener.Stop();

TcpListener 需要侦听的本地 IP 地址(例如,具有两个网卡的计算机可以有两个地址)。您可以使用 IPAddress.Any 指示它侦听所有(或唯一)本地 IP 地址。AcceptTcpClient 阻塞,直到收到客户端请求(同样,还有一个异步版本),此时我们调用 GetStream ,就像在客户端一样。

在传输层工作时,您需要决定谁何时通话以及通话多长时间的协议,就像使用对讲机一样。如果双方同时交谈或倾听,沟通就会中断!

让我们发明一个协议,在这个协议中,客户端首先说“你好”,然后服务器通过说“你好马上回来!代码如下:

using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;

new Thread (Server).Start();       // Run server method concurrently.
Thread.Sleep (500);                // Give server time to start.
Client();

void Client()
{
  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());
  }
}

void Server()     // Handles a single client request, then exits.
{
  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();                      // Must call Flush because we're not
  }                                 // disposing the writer.
  listener.Stop();
}

// OUTPUT: Hello right back!

在此示例中,我们使用 localhost 环回在同一台计算机上运行客户端和服务器。我们任意选择了未分配范围内的端口(高于 49152),并使用 BinaryWriter 和 BinaryReader 对文本消息进行编码。我们避免关闭或处置读取器和编写器,以便在我们的对话完成之前保持底层 NetworkStream 打开。

BinaryReader 和 BinaryWriter 似乎是读取和写入字符串的奇怪选择。但是,它们比StreamReader和StreamWriter有一个主要优势:它们在字符串前面加上一个指示长度的整数,因此BinaryReader总是确切地知道要读取多少字节。如果你调用StreamReader.ReadToEnd,你可能会无限期地阻止,因为NetworkStream没有终点!只要连接处于打开状态,网络流就永远无法确定客户端不会发送更多数据。

注意

StreamReader实际上完全超出了NetworkStream的界限,即使你只打算调用ReadLine。这是因为 StreamReader 具有预读缓冲区,这可能导致它读取的字节数超过当前可用的字节数,从而无限期阻塞(或直到套接字超时)。其他流(如 FileStream)不会遭受与 StreamReader 的这种不兼容,因为它们有一个明确的 — 此时 Read 立即返回值 0 。

与 TCP 并发

TcpClient 和 TcpListener 提供基于任务的异步方法,以实现可扩展的并发性。使用这些只是将阻止方法调用替换为其 *Async 版本并等待返回的任务的问题。

在下面的示例中,我们编写了一个异步 TCP 服务器,该服务器接受长度为 5,000 字节的请求,反转字节,然后将其发送回客户端:

async void RunServerAsync ()
{
  var listener = new TcpListener (IPAddress.Any, 51111);
  listener.Start ();
  try
  {
    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 sequence
      await n.WriteAsync (data, 0, data.Length);
    }
  }
  catch (Exception ex) { Console.WriteLine (ex.Message); }
}

这样的程序是可扩展的,因为它不会在请求期间阻塞线程。因此,如果 1,000 个客户端通过慢速网络连接同时连接(例如,每个请求从开始到结束需要几秒钟),则该程序在这段时间内不需要 1,000 个线程(与同步解决方案不同)。相反,它仅在 await 表达式之前和之后执行代码所需的短时间内租用线程。

使用 TCP 接收 POP3 邮件

.NET 不提供对 POP3 的应用程序层支持,因此您需要在 TCP 层写入才能从 POP3 服务器接收邮件。幸运的是,这是一个简单的协议;POP3 对话是这样的:

客户

邮件服务器

笔记

客户端连接…

+好的 你好。

欢迎辞

用户乔

+确定 需要密码。

通票密码

+确定已登录。

列表

+OK 1 1876 2 5412 3 845 .

列出服务器上每封邮件的 ID 和文件大小

RETR 1

+OK 1876 八位字节 .

检索具有指定 ID 的邮件

德勒 1

+确定已删除。

从服务器中删除邮件

退出

+好的再见。

每个命令和响应都由换行符 (CR + LF) 终止,但多行 LIST 和 RETR 命令除外,它们由单独行上的单个点终止。因为我们不能将 StreamReader 与 网络流 ,我们可以从编写一个辅助方法开始,以非缓冲方式读取一行文本:

string ReadLine (Stream s)
{
  List<byte> lineBuffer = new List<byte>();
  while (true)
  {
    int b = s.ReadByte();
    if (b == 10 || b < 0) break;
    if (b != 13) lineBuffer.Add ((byte)b);
  }
  return Encoding.UTF8.GetString (lineBuffer.ToArray());
}

我们还需要一个帮助程序方法来发送命令。因为我们总是期望收到以 +OK 开头的响应,所以我们可以同时读取和验证响应:

void SendCommand (Stream stream, string line)
{
  byte[] data = Encoding.UTF8.GetBytes (line + "\r\n");
  stream.Write (data, 0, data.Length);
  string response = ReadLine (stream);
  if (!response.StartsWith ("+OK"))
    throw new Exception ("POP Error: " + response);
}

编写这些方法后,检索邮件的工作很容易。我们在端口 110(默认 POP3 端口)上建立 TCP 连接,然后开始与服务器通信。在此示例中,我们将每封邮件写入扩展名为 的随机命名文件,然后再从服务器中删除邮件:

using (TcpClient client = new TcpClient ("mail.isp.com", 110))
using (NetworkStream n = client.GetStream())
{
  ReadLine (n);                             // Read the welcome message.
  SendCommand (n, "USER username");
  SendCommand (n, "PASS password");
  SendCommand (n, "LIST");                  // Retrieve message IDs
  List<int> messageIDs = new List<int>();
  while (true)
  {
    string line = ReadLine (n);             // e.g.,  "1 1876"
    if (line == ".") break;
    messageIDs.Add (int.Parse (line.Split (' ')[0] ));   // Message ID
  }

  foreach (int id in messageIDs)         // Retrieve each message.
  {
    SendCommand (n, "RETR " + id);
    string randomFile = Guid.NewGuid().ToString() + ".eml";
    using (StreamWriter writer = File.CreateText (randomFile))
      while (true)
      {
        string line = ReadLine (n);      // Read next line of message.
        if (line == ".") break;          // Single dot = end of message.
        if (line == "..") line = ".";    // "Escape out" double dot.
        writer.WriteLine (line);         // Write to output file.
      }
    SendCommand (n, "DELE " + id);       // Delete message off server.
  }
  SendCommand (n, "QUIT");
}

注意

可以在 NuGet 上找到开源 POP3 库,这些库为协议方面提供支持,例如身份验证 TLS/SSL 连接、MIME 分析等。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/51166.html

(0)
上一篇 2024-04-26 16:15
下一篇 2024-04-27 14:15

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

关注微信