《c#10 in a nutshell》--- 读书随记(11)

Chapter 16. Networking

内容来自书籍《C# 10 in a Nutshell》
Author:Joseph Albahari
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的

System.Net.*命名空间中,.Net 提供了标准的网络协议,例如HTTP和TCP/IP

  • HttpClient 可以用来消费HTTP Web APIs 和RESTful 服务
  • HttpListener 可以创建HTTP服务端
  • SmtpClient 可以通过SMTP发送有邮件信息
  • Dns 可以在域名和网络地址之间转换
  • TcpClient, UdpClient, TcpListener 和 Socket 可以直接访问网络传输层

Network Architecture

在应用程序层进行编程通常是最方便的; 然而,有几个原因可以解释为什么您可能希望直接在传输层进行编程。一种是如果您需要 .Net 中没有提供的应用程序协议,例如用于检索邮件的 POP3。另一个问题是,如果您想为一个特殊的应用程序(如对等客户端)发明一个自定义协议。

HTTP 还具有一系列丰富的特性,这些特性在多层业务应用程序和面向服务的体系结构中非常有用,例如用于认证和加密的协议、消息块、可扩展的报头和 cookie,以及让许多服务器应用程序共享单一端口和 IP 地址的能力。由于这些原因,HTTP 在 .Net 中得到了很好的支持

Addresses and Ports

为了通信工作,计算机或设备需要一个地址。internet 使用两个寻址系统

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

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

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

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

在 .NET 中,IP 地址和端口组合由 IPEndPoint 类表示:

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

URIs

URI 是一个特殊格式的字符串,用于描述 Internet 或 LAN 上的资源,如网页、文件或电子邮件地址

URI 可以分解为一系列元素ーー通常是方案、权限和路径。System 命名空间中的 Uri 类只执行这种划分,为每个元素公开一个属性

如果您提供了一个没有该方案的 URI 字符串,比如 www.test. com,则会抛出一个 UriFormatException。

IsLoopback属性,表明这个URI是否指向本机;IsFile属性,表明这个URI是否一个文件,LocalPath会返回一个本地OS友好的绝对路径,方便使用File.Open打开这个文件

URI实例是只读的,如果需要修改,需要实例化一个UriBuilder对象,它有可写属性,还可以通过Uri属性转换到URI实例

HttpClient

HttpClient 类为 HTTP 客户端操作公开了一个现代 API。

HttpClient 是为了响应基于 HTTP 的 Web API 和 REST 服务的增长而编写的,它在处理比仅仅获取网页更复杂的协议时提供了很好的体验,特别是

  • 单个 HttpClient 实例可以处理并发请求,并且可以很好地使用自定义头、 cookie 和身份验证模式等特性。
  • HttpClient 允许您编写和插入自定义消息处理程序。这样就可以在单元测试中进行模拟,并创建自定义管道
  • HttpClient 有一个丰富的、可扩展的头和内容类型系统。

最简单的用法:string html = await new HttpClient().GetStringAsync ("http://linqpad.net");

与之前的 WebRequest/WebResponse 不同,为了在 HttpClient 中获得最佳性能,您必须重用相同的实例(否则,DNS 解析之类的事情可能会不必要地重复,没必要保持Sokcet一直打开)。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 的 Timeout 属性和 BaseAddress 属性。HttpClient 在某种程度上是一个瘦 shell: 您可能希望在这里找到的大多数其他属性都是在另一个名为 HttpClientHandler 的类中定义的

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

在本例中,我们告诉处理程序禁用代理支持,这有时可以通过避免自动代理检测的代价来提高性能。还有控制 cookie、自动重定向、身份验证等属性

GetAsync and Response Messages

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

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();
// 或者可以直接将body的内容写入文件
using (var fileStream = File.Create ("linqpad.html"))
    await response.Content.CopyToAsync (fileStream);

HttpResponseMessage 公开用于访问 headers 和 HTTP StatusCode 的属性。不成功的状态代码(如404(未找到))不会引发异常,除非显式调用 EnsureSuccess StatusCode。

SendAsync and Request Messages

GetAsync、 PostAsync、 PutAsync 和 DeleteAsync 都是调用 SendAsync 的快捷方式,SendAsync 是一个底层方法,其他所有内容都可以通过它进行

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

Uploading Data and HttpContent

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

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

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

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

HttpMessageHandler 非常简单,可以轻松地进行子类化,并提供了到 HttpClient 的扩展点

Unit testing and mocking

我们可以子类化 HttpMessageHandler 来创建一个mocking处理程序来协助单元测试:

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

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

SendAsync 方法是同步的,因为内部其实没有作请求而是直接返回结果。我们可以通过让响应生成器返回 Task < HttpResponseMessage > 来维护异步,但是这是没有意义的,因为我们可以预期模仿函数会很快运行,现在看看这个mock如何使用

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

Chaining handlers with DelegatingHandler

您可以创建一个消息处理程序,通过子类 DelegatingHandler 该处理程序调用另一个消息处理程序(导致处理程序链)。您可以使用它来实现自定义身份验证、压缩和加密协议。

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

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

注意,我们在重写 SendAsync 时维护了异步。在这种情况下,在重写任务返回方法时引入异步修饰符是完全合法和可取的。
比写入控制台更好的解决方案是让构造函数接受某种类型的日志记录对象。更好的做法是接受一些 Action < T > 委托,它们告诉它如何记录请求和响应对象

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

Proxies

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

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

也可以设置全局代理 HttpClient.DefaultWebProxy = myWebProxy;

Authentication

handler.Credentials = new NetworkCredential (username, password); 身份证件

这可以与基于对话框的身份验证协议(如 Basic 和 Digest)一起使用,并且可以通过 AuthenticationManager 类进行扩展。它还支持 WindowsNTLM 和 Kerberos (如果在构造 NetworkCredential 对象时包含域名)。如果希望使用当前经过身份验证的 Windows 用户,则可以保持“凭据”属性为空,而将“ UseDefaultCredenals”设置为 true。

当您提供凭据时,HttpClient 会自动协商一个兼容的协议。在某些情况下,可以有一个选择: 如果您检查来自 MicrosoftExchange 服务器 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

WWW-Authenticate头部信息,表明了可以解析的认证协议。但是,如果您使用正确的用户名和密码配置 HttpClientHandler,则此消息将对您隐藏,因为运行时将通过选择兼容的身份验证协议自动响应,然后重新提交带有额外标头的原始请求。

这种机制提供了透明性,但是对于每个请求都会产生额外的往返过程。通过将 HttpClientHandler 上的 PreAuthenticate 属性设置为 true,可以避免对同一 URI 的后续请求进行额外的往返。

CredentialCache

可以强制使用 CreentialCache 对象执行特定的身份验证协议。凭据缓存包含一个或多个 NetworkCredential 对象,每个对象都键入了特定的协议和 URI 前缀。例如,在登录到 ExchangeServer 时,可能希望避免使用基本协议,因为该协议以纯文本形式传输密码

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;

协议指定是string: Basic, Digest, NTLM, Kerberos, Negotiate

Authenticating via headers

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

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

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

Headers

全局设置头

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

Query Strings

查询字符串只是附加到 URI 的带问号的字符串,用于向服务器发送简单数据。可以使用以下语法在查询字符串中指定多个键/值对?key1=value1&key2=value2&key3=value3...

如果字符串包含了其他符号或者空格,可以使用工具转换一下:

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

Uploading Form Data

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

Cookies

Cookie 是 HTTP 服务器在响应头中发送给客户端的名称/值字符串对。Web 浏览器客户端通常会记住 Cookie,并在后续的每个请求(到相同的地址)中向服务器重放它们,直到它们过期为止。Cookie 允许服务器知道它是在与一分钟前(或昨天)的同一个客户机通话,而不需要在 URI 中添加混乱的查询字符串。

默认情况下,HttpClient 拒绝接收任何cookies,如果需要,那就要创建一个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);

Writing an HTTP Server

using System.Net;
using System.Text;

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/");
        listener.Start();
        // Listen on
        // 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();
}

在 Windows 上,HttpListener 不在内部使用 .NET Socket 对象; 而是调用 WindowsHTTPServerAPI。这使得计算机上的许多应用程序可以监听相同的 IP 地址和端口ーー只要每个应用程序注册不同的地址前缀。在我们的示例中,我们注册了前缀 http://localhost/myapp ,因此另一个应用程序可以自由地监听同一个 IP,并在另一个前缀(如 http://localhost/anotherapp )上端口。这是有价值的,因为在企业防火墙上开放新的端口可能在政治上是艰巨的。

HttpListener 在调用 GetContext 时等待下一个客户端请求,返回具有 Request 和 Response 属性的对象。每个都类似于客户机请求或响应,但是从服务器的角度来看。例如,您可以向请求和响应对象读写头和 cookie,就像您在客户端所做的那样。

Using DNS

暂时跳过

Sending Mail with SmtpClient

暂时跳过

Using TCP

TCP 和 UDP 构成了传输层协议,大多数互联网和局域网业务都是在这些协议的基础上构建的。HTTP (版本2及以下)、 FTP 和 SMTP 使用 TCP; DNS 和 HTTP 版本3使用 UDP。TCP 是面向连接的,包括可靠性机制; UDP 是无连接的,具有较低的开销,并支持广播

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

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

synchronous TCP client:

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

Connect方法会一直阻塞到建立连接,然后,NetworkStream 提供了双向通信的方式,用于从服务器传输和接收字节数据

一个简单的TCP Sever :

TcpListener listener = new TcpListener(10086);
listener.Start();
while (keepProcessingRequests)
    using (TcpClient c = listener.AcceptTcpClient())
    using (NetworkStream n = c.GetStream())
    {
        // Read and write to the network stream...

    }
    
listener.Stop();

AcceptTcpClient 会阻塞直到有客户端连接

Concurrency with TCP

TcpClient 和 TcpListener 为可伸缩并发提供了基于任务的异步方法。使用这些方法只是用它们的 * Async 版本替换阻塞方法调用并等待返回的任务的问题。

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 表达式之前和之后执行代码所需的一小段时间内租用线程。

Receiving POP3 Mail with TCP

暂时跳过

Chapter 17. Assemblies

暂时跳过

Chapter 18. Reflection and Metadata

暂时跳过

Chapter 19. Dynamic Programming

暂时跳过

Chapter 20. Cryptography

暂时跳过

posted @ 2022-07-02 22:05  huang1993  阅读(117)  评论(0)    收藏  举报