乐趣区

关于c#:NET-5网络操作的改进

随着.net 5 在 11 月的公布,当初是议论网络栈中许多改良的好时机。这包含对 HTTP、套接字、与网络相干的安全性和其余网络通信的改良。在这篇文章中,我将重点介绍一些版本中更有影响力和更乏味的变动。

HTTP

更好的错误处理 

自从.net 3.1 公布以来,HTTP 畛域进行了许多改良和修复。当应用 HttpClien 时,最受关注的是增加如何辨别超时和勾销。最后,不得不应用自定义的 CancellationToken 辨别超时和勾销:

class Program{private static readonly HttpClient _client = new HttpClient()
    {Timeout = TimeSpan.FromSeconds(10)
    };

    static async Task Main()
    {var cts = new CancellationTokenSource();
        try
        {
            // Pass in the token.
            using var response = await _client.GetAsync("http://localhost:5001/sleepFor?seconds=100", cts.Token);
        }
        // If the token has been canceled, it is not a timeout.
        catch (TaskCanceledException ex) when (cts.IsCancellationRequested)
        {
            // Handle cancellation.
            Console.WriteLine("Canceled:" + ex.Message);   
        }
        catch (TaskCanceledException ex)
        {
            // Handle timeout.
            Console.WriteLine("Timed out:"+ ex.Message);
        }
    }
}

这样做,客户端依然抛出 TaskCanceledException(为了兼容),但外部异样是超时时的 TimeoutException:

class Program{private static readonly HttpClient _client = new HttpClient()
    {Timeout = TimeSpan.FromSeconds(10)
    };

    static async Task Main()
    {
        try
        {using var response = await _client.GetAsync("http://localhost:5001/sleepFor?seconds=100");
        }
        // Filter by InnerException.
        catch (TaskCanceledException ex) when (ex.InnerException is TimeoutException)
        {
            // Handle timeout.
            Console.WriteLine("Timed out:"+ ex.Message);
        }
        catch (TaskCanceledException ex)
        {
            // Handle cancellation.
            Console.WriteLine("Canceled:" + ex.Message);   
        }
    }
}

另一个改良是将 HttpStatusCode 增加到 HttpRequestException 中。当响应上调用 EnsureSuccessStatusCode 时,新的 StatusCode 属性能够设置为空。而后,它能够在异样过滤器中应用:

class Program{private static readonly HttpClient _client = new HttpClient();

    static async Task Main()
    {
        try
        {using var response = await _client.GetAsync("https://localhost:5001/doesNotExists");
            // The following line will throw HttpRequestException with StatusCode set if it wasn't 2xx.
            response.EnsureSuccessStatusCode();}
        catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
        {
            // Handle 404
            Console.WriteLine("Not found:" + ex.Message);
        }
    }
}

因为 HttpClient 中办法:GetStringAsync, GetByteArrayAsync 和 GetStreamAsync 不返回 HttpResponseMessage,它们本人调用 EnsureSuccessStatusCode。这些调用的异样过滤如下所示:

class Program{private static readonly HttpClient _client = new HttpClient();

    static async Task Main()
    {
        try
        {
            // The helper method will throw HttpRequestException with StatusCode set if it wasn't 2xx.
            using var stream = await _client.GetStreamAsync("https://localhost:5001/doesNotExists");
        }
        catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
        {
            // Handle 404
            Console.WriteLine("Not found:" + ex.Message);
        }
    }
}

因为新的构造函数是 public 的,所以能够手动创立带有状态码的 HttpRequestException:

class Program{private static readonly HttpClient _client = new HttpClient();

    static async Task Main()
    {
        try
        {using var response = await _client.GetAsync("https://localhost:5001/doesNotExists");
            // Throw for anything higher than 400.
            if (response.StatusCode >= HttpStatusCode.BadRequest)
            {throw new HttpRequestException("Something went wrong", inner: null, response.StatusCode);
            }
        }
        catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.NotFound)
        {
            // Handle 404
            Console.WriteLine("Not found:" + ex.Message);
        }
    }
}

统一的跨平台实现

最后,.NET Core 中的 HTTP 栈依赖于平台相干的处理程序:

  1. WinHttpHandler 基于 WinHTTP,实用于 Windows。
  2. CurlHandler 基于 libcurl,实用于 Linux 和 Mac。

因为两个库之间的差别,简直不可能实现跨平台的一致性。因而,在.net Core 2.1 中,咱们引入了一个名为 SocketsHttpHandler 的托管 HTTP 实现。咱们将大部分工作转移到 SocketsHttpHandler,随着咱们对它的可靠性越来越有信念,咱们决定齐全从 System.Net.Http.dll 中删除特定于平台的处理程序。在.net 5 中,不再可能应用切换回 System.Net.Http。然而,WinHttpHandler 依然作为一个独立的 NuGet 包可用。任何应用它的代码都须要更改为援用 System.Net.Http.WinHttpHandler 的 NuGet 包:

 dotnet add package System.Net.Http.WinHttpHandler

并显式地向 HttpClient 构造函数传递 WinHttpHandler 实例:

class Program{private static readonly HttpClient _client = new HttpClient(new WinHttpHandler());

    static async Task Main()
    {using var response = await _client.GetAsync("http://localhost:5001/");
    }
}

SocketsHttpHandler 扩大点

HttpClient 是一个高级 API,使用方便,但在某些状况下不足灵活性。在更高级的场景中,须要更精密的管制。咱们试图弥合这些差距,并在 SocketsHttpHandler 中引入了两个扩大点——ConnectCallback 和 PlaintextStreamFilter。

ConnectCallback 容许自定义创立新连贯。每次关上一个新的 TCP 连贯时都会调用它。回调可用于建设过程内传输、管制 DNS 解析、管制根底套接字的通用或特定于平台的选项,或者仅用于在新连贯关上时告诉。回调有以下注意事项:

  1. 传递给它的是确定近程端点的 DnsEndPoint 和发动创立连贯的 HttpRequestMessage。
  2. 因为 SocketsHttpHandler 提供了连接池,所创立的连贯能够用于解决多个后续申请,而不仅仅是初始申请。
  3. 将返回一个新的流。
  4. 回调不应该尝试建设 TLS 会话。这是随后由 SocketsHttpHandler 解决的。

当不提供回调时的默认实现等价于以下最小的、基于套接字的回调:

private static async ValueTask<Stream> DefaultConnectAsync(SocketsHttpConnectionContext context, CancellationToken cancellationToken){
    // The following socket constructor will create a dual-mode socket on systems where IPV6 is available.
    Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
    // Turn off Nagle's algorithm since it degrades performance in most HttpClient scenarios.
    socket.NoDelay = true;

    try
    {await socket.ConnectAsync(context.DnsEndPoint, cancellationToken).ConfigureAwait(false);
        // The stream should take the ownership of the underlying socket,
        // closing it when it's disposed.
        return new NetworkStream(socket, ownsSocket: true);
    }
    catch
    {socket.Dispose();
        throw;
    }
}

另一个扩大点,PlaintextStreamFilter,容许在新关上的连贯上插入一个自定义层。在连贯齐全建设之后(包含用于平安连贯的 TLS 握手),但在发送任何 HTTP 申请之前调用此回调。因而,能够应用它来监听通过平安连贯发送的纯文本数据。这个回调的个别准则是:

  1. 它被传递一个流、协商的 HTTP 版本 (可能与申请版本不同,参见 ALPN) 和初始化的 HTTP 申请。随后的申请也将应用雷同的流。
  2. 流作为返回值。它能够是在没有任何更改的状况下传入的,也能够是封装它的自定义流。

如何实现自定义流能够在文档中找到。自定义流最终应该将读写工作委托给所提供的流,但它能够拦挡替换的数据。

一个十分小的没有自定义流的 PlaintextStreamFilter 示例如下:

socketsHandler.PlaintextStreamFilter = (context, token) =>{Console.WriteLine($"Request {context.InitialRequestMessage} --> negotiated version {context.NegotiatedHttpVersion}");
    return ValueTask.FromResult(context.PlaintextStream);
};

创立新扩大点连贯的时间轴为:

  1. 调用 ConnectCallback 来关上 TCP 连贯。
  2. 如果须要,SocketsHttpHandler 外部建设 TLS。
  3. 应用上一步中的流调用 PlaintextStreamFilter。

如果没有注册回调函数,这里就不会调用任何货色。这两个回调函数都是为了对 SocketsHttpHandler 中的连贯进行高级管制。应该十分小心地执行和测试它们,因为它们可能会无心中导致性能和稳定性问题。

HttpClient.Send 的同步 API

尽管咱们倡议应用异步网络 API 以取得更好的性能和可伸缩性,但咱们也意识到,在某些状况下,应用同步 API 是必要的,并且会同步阻塞期待 HttpClient。SendAsync 常常有可伸缩性问题,因为须要多个线程来实现一个操作。这种办法的其余缺点,包含臭名远扬的 UI 线程死锁。

为了启用同步场景并防止这些问题,咱们增加了一个同步版本的 HttpClient.Send,然而实现有一些注意事项:

  1. 仅反对 HTTP/1.1 协定。HTTP/ 2 在共享连贯上应用多路复用申请,因而穿插申请可能会被同步操作阻塞或阻塞。
  2. 它不能与后面提到的 ConnectCallback 一起应用。如果应用了默认 SocketsHttpHandler 以外的处理程序,它必须实现 HttpMessageHandler.Send。否则,同步 HttpMessageHandler.Send 的默认实现将抛出。
  3. 相似的限度也实用于自定义 HttpContent 实现,它必须重写 HttpContent.SerializeToStream,以便可能在同步调用中用作申请内容。

咱们强烈建议尽可能持续应用异步 api。

HTTP / 2

版本抉择

这个个性是从反对明文 HTTP/2 (h2c)的申请演变而来的。明文通信不仅实用于本地调试或测试环境,还可能存在防火墙或反向代理后的基于 HTTP/ 2 的服务,这些服务不应用 TLS。例如,gRPC 服务应用 HTTP/ 2 作为传输协定,有些服务抉择放弃加密。

直到.net 5,一个不受反对的应用程序开关必须被关上能力启用明文 HTTP/ 2 通信,这可能会有问题,因为它不能启用每个申请管制。如果没有交换机,每个明文 HTTP/ 2 申请都会主动降级为 HTTP/1.1。这是因为 TLS 扩大 ALPN 被用于与服务器协商最终的 HTTP 版本。没有 TLS,因而没有 ALPN,客户端不能确定服务器将可能解决 HTTP/2。因而,客户端防止了危险,并抉择了广泛反对的 HTTP/1.1。然而,在后面提到的后端服务和 gRPC 的状况下,可能当时就晓得所有参与者都能够解决 h2c,因而主动降级是不可取的。

当咱们设计版本抉择时,咱们试图概括原来的问题,并使 API 正当地“证实将来”。因而,咱们决定让用户管制如何解决版本的降级和降级。咱们引入了 HttpVersionPolicy,这是一个新的 enum,示意是否承受降级、降级,或者只承受精确的版本。用于手动创立并由 HttpClient.SendAsync 的发送。策略能够通过 HttpRequestMessage.VersionPolicy 间接设置到申请。对于 GetAsync、PostAsync、DeleteAsync 等在外部创立申请的调用,HttpClient 实例属性 HttpClient.DefaultVersionPolicy 用于控制策略。

例如,要启用 h2c 场景,咱们能够这样做:

class Program{private static readonly HttpClient _client = new HttpClient()
    {
        // Allow only HTTP/2, no downgrades or upgrades.
        DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact,
        DefaultRequestVersion = HttpVersion.Version20
    };

    static async Task Main()
    {
        try
        {
            // Request clear-text http, no https.
            // The call will internally create a new request corresponding to:
            //   new HttpRequestMessage(HttpMethod.Get, "http://localhost:5001/h2c")
            //   {
            //     Version = HttpVersion.Version20,
            //     VersionPolicy = HttpVersionPolicy.RequestVersionExact
            //   }
            using var response = await _client.GetAsync("http://localhost:5001/h2c");
        }
        catch (HttpRequestException ex)
        {
            // Handle errors, including when h2c connection cannot be established.
            Console.WriteLine("Error:" + ex.Message);
        }
    }
}

与 HTTP/ 2 的多个连贯

HTTP/ 2 容许多个并发申请一个 TCP 连贯上的多路传输。依据 HTTP/ 2 标准,只应该向服务器关上一个 TCP 连贯。这个倡议对于浏览器十分无效,并且解决了 HTTP/ 1 关上每个源的多个连贯的问题。然而,这将最大并发申请数缩小到设置帧中的值,通常能够设置为 100。对于服务到服务的通信,其中一个客户机向大量服务器发送十分多的申请,并且 / 或能够放弃多个长期存在的申请,这一限度会显著影响吞吐量和性能。为了克服这个限度,咱们引入了向单个端点关上多个 HTTP/ 2 连贯的能力。

默认状况下,多个 HTTP/ 2 连贯是禁用的。要启用它们,将 SocketsHttpHandler.EnableMultipleHttp2Connections 设置为 true。

多个并发申请的示例如下:

class Program{private static readonly HttpClient _client = new HttpClient(new SocketsHttpHandler()
    {
        // Enable multiple HTTP/2 connections.
        EnableMultipleHttp2Connections = true,

        // Log each newly created connection and create the connection the same way as it would be without the callback.
        ConnectCallback = async (context, token) =>
        {
            Console.WriteLine($"New connection to {context.DnsEndPoint} with request:{Environment.NewLine}{context.InitialRequestMessage}");

            var socket = new Socket(SocketType.Stream, ProtocolType.Tcp) {NoDelay = true};
            await socket.ConnectAsync(context.DnsEndPoint, token).ConfigureAwait(false);
            return new NetworkStream(socket, ownsSocket: true);
        },
    })
    {
        // Allow only HTTP/2, no downgrades or upgrades.
        DefaultVersionPolicy = HttpVersionPolicy.RequestVersionExact,
        DefaultRequestVersion = HttpVersion.Version20
    };

    static async Task Main()
    {
        // Burst send 2000 requests in parallel.
        var tasks = new Task[2000];
        for (int i = 0; i < tasks.Length; ++i)
        {tasks[i] = _client.GetAsync("http://localhost:5001/");
        }
        await Task.WhenAll(tasks);
    }
}

控制台将显示来自 ConnectCallback 对于创立新连贯的多条音讯。如果将 EnableMultipleHttp2Connections 正文掉,控制台将只显示一条音讯。

可配置的 PING

HTTP/ 2 标准定义了 PING 帧,这是一种确保闲暇连贯放弃沉闷的机制。此个性对于长时间运行的闲暇连贯十分有用,否则这些闲暇连贯将被删除。这样的连贯能够在 gRPC 场景中找到,比方流和长时间的近程过程调用。到目前为止,咱们只回复 PING 申请,从不发送。

在.net 5 中,咱们曾经实现了发送 PING 帧的可配置距离、超时,以及是否总是或仅在流动申请时发送它们。默认值的配置是:

public class SocketsHttpHandler{
    ...

    // The client will send PING frames to the server if it hasn't receive any frame on the connection for this period of time.
    public TimeSpan KeepAlivePingDelay {get; set;} = Timeout.InfiniteTimeSpan;

    // The client will close the connection if it doesn't receive PING ACK frame within the timeout.
    public TimeSpan KeepAlivePingTimeout {get; set;} = TimeSpan.FromSeconds(20);

    // Whether the client will send PING frames only if there are any active streams on the connection or even if it's idle.
    public HttpKeepAlivePingPolicy KeepAlivePingPolicy {get; set;} = HttpKeepAlivePingPolicy.Always;

    ...}public enum HttpKeepAlivePingPolicy{
    // PING frames are sent only if there are active streams on the connection.
    WithActiveRequests,

    // PING frames are sent regardless if there are any active streams or not.
    Always
}

默认值 KeepAlivePingDelay (Timeout.InfiniteTimeSpan)意味着该个性通常是敞开的,PING 帧不会主动发送到服务器。客户端依然会回复收到的 PING 帧,这是不能敞开的。为了启用主动 PING, KeepAlivePingDelay 必须更改,例如 1 分钟:

class Program{private static readonly HttpClient _client = new HttpClient(new SocketsHttpHandler()
    {KeepAlivePingDelay = TimeSpan.FromSeconds(60)
    });
}

只有当与服务器没有被动通信时才发送 PING 帧。每一个来自服务器的传入帧都将重置提早,只有在 KeepAlivePingDelay 没有接管到帧之后,才会发送一个 PING 帧。而后,服务器被给予 KeepAlivePingTimeout 应答工夫距离。如果没有,则认为连贯失落并被拆除。该算法会定期检查提早和超时,但最多每秒钟查看一次。将 KeepAlivePingDelay 或 KeepAlivePingTimeout 设置为更小的值将导致异样。

例如,设置如下:

new SocketsHttpHandler(){KeepAlivePingDelay = TimeSpan.FromSeconds(15),
    KeepAlivePingTimeout = TimeSpan.FromSeconds(7.5)
};

将导致 1.875 秒距离,因为它是两个值的 1 /4,即 min(KeepAlivePingDelay, KeepAlivePingTimeout)/4。在这种状况下,超时可能产生在发送 PING 帧后的 7.5 到 9.5 秒之间。留神,查看距离的计算是一个实现细节,未来可能会更改。

HTTP / 3

HTTP/ 3 及其底层传输层 QUIC 正处于标准化的最初阶段。QUIC 是一种新的基于 udp 的传输,与基于 TCP 的连贯相比,它提供了一些益处:

-TLS 平安链接握手更快

- 在单个连贯上更牢靠的多路复用多个申请,打消了当数据包被抛弃时线路阻塞问题。

- 连贯迁徙使挪动客户端网络之间的转换更加晦涩,例如 Wi-Fi 到 LTE 再返回。

. net 5 引入了对 HTTP/ 3 的实验性反对——目前还不倡议在生产环境中应用该个性。在底层,咱们应用的是 MsQuic 库,它是一个开源的、跨平台的 QUIC 协定实现。如何应用 QUIC 启用 HTTP/ 3 的具体阐明能够在 System.Net.Experimental.MsQuic 中找到。

  1. 目前,它只在外部构建的 Windows 上可用,这有 QUIC 所需的通道反对。
  2. 须要援用蕴含 MsQuic 库的包,该库目前仅通过实验性可用。
  3. HttpClient QUIC 反对必须通过 AppContext 开关或 DOTNET_SYSTEM_NET_HTTP_SOCKETSHTTPHANDLER_HTTP3DRAFTSUPPORT 环境变量来启用,例如:

    AppContext.SetSwitch("System.Net.SocketsHttpHandler.Http3DraftSupport", true);
  4. 申请必须有如下设置的 Version 和 VersionPolicy 属性:

    new HttpRequestMessage{
        // Request HTTP/3 version.
        Version = new Version(3, 0),
        // Only HTTP/3 is allowed, no version downgrades should happen.
    VersionPolicy = HttpVersionPolicy.RequestVersionExact
    }

    这个设置通知 HttpClient 咱们曾经事后晓得了服务器反对 HTTP/ 3 并申请它。如果不反对 HTTP/3,则会抛出异样。另一种抉择是让服务器通过 Alt-Svc 报头公布 HTTP/3。而后客户端能够将其用于后续申请。对于这个场景,申请不应该要求 RequestVersionExact,因为它须要用较低的协定版本解决第一个申请。

更好的勾销反对

基于 Task 的异步办法当初是异步编程的首选模式。它们在须要进行大量 I / O 操作的网络中特地有价值。Task 模式使代码比原来的开始 / 完结“APM”模式更容易了解。基于 Task 的异步模式的一部分是应用 CancellationToken 来勾销和超时。咱们始终在致力增加勾销令牌,并将其正确地利用到各个中央。咱们依然有脱漏重载的破绽,但咱们曾经在.net 5 中填补了许多。

对于 socket,咱们在 SocketTaskExtensions 中增加了重载——咱们想在.net 6 中将它们放 Socket 类自身中。应用 CancellationToken 的新重载如下:

public static class SocketTaskExtensions{public static ValueTask ConnectAsync(this Socket socket, EndPoint remoteEP, CancellationToken cancellationToken);
    public static ValueTask ConnectAsync(this Socket socket, IPAddress address, int port, CancellationToken cancellationToken);
    public static ValueTask ConnectAsync(this Socket socket, IPAddress[] addresses, int port, CancellationToken cancellationToken);
    public static ValueTask ConnectAsync(this Socket socket, string host, int port, CancellationToken cancellationToken);
}

这些重载曾经在 HttpClient 和 TcpClient 中应用,导致了 TcpClient 中的新的重载:

public class TcpClient{public ValueTask ConnectAsync(IPAddress address, int port, CancellationToken cancellationToken);
    public ValueTask ConnectAsync(IPAddress[] addresses, int port, CancellationToken cancellationToken);
    public ValueTask ConnectAsync(string host, int port, CancellationToken cancellationToken);
}

在 HTTP 命名空间中,咱们增加了 HttpClient 和 HttpContent 的重载。’ HttpClient ‘ 被扩大为 ’ Get(ByteArray|Stream|String)Async ‘ 重载:

class HttpClient{public Task<byte[]> GetByteArrayAsync(string requestUri, CancellationToken cancellationToken);
    public Task<byte[]> GetByteArrayAsync(Uri requestUri, CancellationToken cancellationToken);

    public Task<Stream> GetStreamAsync(string requestUri, CancellationToken cancellationToken);
    public Task<Stream> GetStreamAsync(Uri requestUri, CancellationToken cancellationToken);

    public Task<string> GetStringAsync(string requestUri, CancellationToken cancellationToken);
    public Task<string> GetStringAsync(Uri requestUri, CancellationToken cancellationToken);
}

HttpContent 增加了序列化和读取的重载:

class HttpContent{public Task<byte[]> ReadAsByteArrayAsync(CancellationToken cancellationToken);
    public Task<Stream> ReadAsStreamAsync(CancellationToken cancellationToken);
    public Task<string> ReadAsStringAsync(CancellationToken cancellationToken);

    protected virtual Task<Stream> CreateContentReadStreamAsync(CancellationToken cancellationToken);

    protected virtual Task SerializeToStreamAsync(Stream stream, TransportContext context, CancellationToken cancellationToken);

    public Task CopyToAsync(Stream stream, CancellationToken cancellationToken);
    public Task CopyToAsync(Stream stream, TransportContext context, CancellationToken cancellationToken);
}

如果咱们当初设计 HttpContent,应用所有重载,咱们将使 CreateContentReadStreamAsync 和 SerializeToStreamAsync 变为 abstract 而不是 virtual。问题是咱们试图不通过扭转公共类的契约来毁坏现有的代码。在.net Core 3.1 下运行的内容应该持续在.net 5 下运行,没有任何扭转。增加 abstract 办法违反了这一承诺,所以咱们不得不求助于 virtual 办法。自定义 HttpContent 实现应该重写它们,只管它们只是 virtual。所有 HttpContent 实现,比方 byteraycontent、MultipartContent 和 StreamContent,都曾经这样做了。

网络 遥测

咱们曾经意识到,用户对于监督.net Core 应用程序的外部网络形容并不好。到目前为止,只能收集十分具体且不统一的日志音讯,侦听它们对性能有影响。对于.net 5,咱们设计并实现了一套新的遥测事件和计数器。这些事件和计数器是在思考继续监督的状况下创立的,因而它们不像外部日志那样占用大量资源。然而,它们并不是齐全没有代价的,监听会耗费一些(只管很少)CPU 周期。

咱们正在公开这些新的遥测事件和计数器,它们将供.net 用户应用。咱们打算在将来反对它们,对它们的任何更改都将被视为突破性的更改。

遥测事件和计数器都基于 EventSource。它们能够通过 EventListener 在过程内应用,也能够通过 EventPipe 通过 dotnet-trace 和 dotnet-counters 命令行工具在过程外应用。

自定义遥测事件

一种自定义遥测事件的办法是通过 EventListener 在过程中编程:

class Program{private static readonly HttpClient _client = new HttpClient();

    static async Task Main()
    {
        // Instantiate the listener which subscribes to the events. 
        using var listener = new HttpEventListener();

        // Send an HTTP request.
        using var response = await client.GetAsync("https://github.com/runtime");
    }}
internal sealed class HttpEventListener : EventListener{
    // Constant necessary for attaching ActivityId to the events.
    public const EventKeywords TasksFlowActivityIds = (EventKeywords)0x80;

    protected override void OnEventSourceCreated(EventSource eventSource)
    {
        // List of event source names provided by networking in .NET 5.
        if (eventSource.Name == "System.Net.Http" ||
            eventSource.Name == "System.Net.Sockets" ||
            eventSource.Name == "System.Net.Security" ||
            eventSource.Name == "System.Net.NameResolution")
        {EnableEvents(eventSource, EventLevel.LogAlways);
        }
        // Turn on ActivityId.
        else if (eventSource.Name == "System.Threading.Tasks.TplEventSource")
        {
            // Attach ActivityId to the events.
            EnableEvents(eventSource, EventLevel.LogAlways, TasksFlowActivityIds);
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {var sb = new StringBuilder().Append($"{eventData.TimeStamp:HH:mm:ss.fffffff}  {eventData.ActivityId}.{eventData.RelatedActivityId}  {eventData.EventSource.Name}.{eventData.EventName}(");
        for (int i = 0; i < eventData.Payload?.Count; i++)
        {sb.Append(eventData.PayloadNames?[i]).Append(":").Append(eventData.Payload[i]);
            if (i < eventData.Payload?.Count - 1)
            {sb.Append(",");
            }
        }

        sb.Append(")");
        Console.WriteLine(sb.ToString());
    }
}

这个小程序将产生如下的控制台日志:

19:16:42.5983845  00000011-0000-0000-0000-00005d729c59.00000000-0000-0000-0000-000000000000  System.Net.Http.RequestStart(scheme: https, host: github.com, port: 443, pathAndQuery: /runtime, versionMajor: 1, versionMinor: 1, versionPolicy: 0)
19:16:42.6247315  00001011-0000-0000-0000-00005d429c59.00000011-0000-0000-0000-00005d729c59  System.Net.NameResolution.ResolutionStart(hostNameOrAddress: github.com)
19:16:42.6797116  00001011-0000-0000-0000-00005d429c59.00000000-0000-0000-0000-000000000000  System.Net.NameResolution.ResolutionStop()
19:16:42.6806290  00002011-0000-0000-0000-00005d529c59.00000011-0000-0000-0000-00005d729c59  System.Net.Sockets.ConnectStart(address: InterNetworkV6:28:{1,187,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,140,82,121,3,0,0,0,0})
19:16:42.7115980  00002011-0000-0000-0000-00005d529c59.00000000-0000-0000-0000-000000000000  System.Net.Sockets.ConnectStop()
19:16:42.7157362  00003011-0000-0000-0000-00005d229c59.00000011-0000-0000-0000-00005d729c59  System.Net.Security.HandshakeStart(isServer: False, targetHost: github.com)
19:16:42.8606049  00003011-0000-0000-0000-00005d229c59.00000000-0000-0000-0000-000000000000  System.Net.Security.HandshakeStop(protocol: 12288)
19:16:42.8624541  00000011-0000-0000-0000-00005d729c59.00000000-0000-0000-0000-000000000000  System.Net.Http.ConnectionEstablished(versionMajor: 1, versionMinor: 1)
19:16:42.8651762  00004011-0000-0000-0000-00005d329c59.00000011-0000-0000-0000-00005d729c59  System.Net.Http.RequestHeadersStart()
19:16:42.8658442  00004011-0000-0000-0000-00005d329c59.00000000-0000-0000-0000-000000000000  System.Net.Http.RequestHeadersStop()
19:16:42.8979467  00005011-0000-0000-0000-00005d029c59.00000011-0000-0000-0000-00005d729c59  System.Net.Http.ResponseHeadersStart()
19:16:42.9037560  00005011-0000-0000-0000-00005d029c59.00000000-0000-0000-0000-000000000000  System.Net.Http.ResponseHeadersStop()
19:16:42.9090497  00006011-0000-0000-0000-00005d129c59.00000011-0000-0000-0000-00005d729c59  System.Net.Http.ResponseContentStart()
19:16:43.1092912  00006011-0000-0000-0000-00005d129c59.00000000-0000-0000-0000-000000000000  System.Net.Http.ResponseContentStop()
19:16:43.1093326  00000011-0000-0000-0000-00005d729c59.00000000-0000-0000-0000-000000000000  System.Net.Http.RequestStop()
19:16:43.1109221  00000000-0000-0000-0000-000000000000.00000000-0000-0000-0000-000000000000  System.Net.Http.ConnectionClosed(versionMajor: 1, versionMinor: 1)

命名为Start 和Stop 的事件应用雷同的 ActivityId 触发。这些事件具备非凡的意义并主动关联。它容许像 PerfView 这样的监督工具计算操作所耗费的工夫,或将其余事件链接到父事件。

另一种办法是通过 dotnet-trace 过程之外:

class Program{private static readonly HttpClient _client = new HttpClient();

    static async Task Main()
    {
        // No listener needed but print the process ID and wait for a key press to start the request.
        Console.WriteLine(Environment.ProcessId);
        Console.ReadKey();

        // Send an HTTP request.
        using var response = await client.GetAsync("https://github.com/runtime");
    }
}
# Name the source events from previous example as providers (--providers).# Set the output file name (-o).# Set the process ID (-p).
dotnet trace collect --providers System.Net.Http,System.Net.Sockets,System.Net.Security,System.Net.NameResolution -o networking.nettrace -p 1234

计数器

计数器能够通过 EventListener 在过程中以编程形式应用:

class Program{private static readonly HttpClient _client = new HttpClient();

    static async Task Main()
    {
        // Instantiate the listener which subscribes to the events. 
        using var listener = new HttpEventListener();

        // Send an HTTP request.
        using var response = await client.GetAsync("https://github.com/runtime");
    }}
internal sealed class HttpEventListener : EventListener{protected override void OnEventSourceCreated(EventSource eventSource)
    {
        // List of event source names provided by networking in .NET 5.
        if (eventSource.Name == "System.Net.Http" ||
            eventSource.Name == "System.Net.Sockets" ||
            eventSource.Name == "System.Net.Security" ||
            eventSource.Name == "System.Net.NameResolution")
        {EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.All, new Dictionary<string, string>()
            {
                // These additional arguments will turn on counters monitoring with a reporting interval set to a half of a second. 
                ["EventCounterIntervalSec"] = TimeSpan.FromSeconds(0.5).TotalSeconds.ToString()});
        }
    }

    protected override void OnEventWritten(EventWrittenEventArgs eventData)
    {
        // It's a counter, parse the data properly.
        if (eventData.EventId == -1)
        {var sb = new StringBuilder().Append($"{eventData.TimeStamp:HH:mm:ss.fffffff}  {eventData.EventSource.Name}");
            var counterPayload = (IDictionary<string, object>)(eventData.Payload[0]);
            bool appendSeparator = false;
            foreach (var counterData in counterPayload)
            {if (appendSeparator)
                {sb.Append(",");
                }
                sb.Append(counterData.Key).Append(":").Append(counterData.Value);
                appendSeparator = true;
            }
            Console.WriteLine(sb.ToString());
        }
    }
}

控制台日志是这样的: 

19:38:55.9452792  System.Net.Http  Name: requests-started, DisplayName: Requests Started, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 0.0004773, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9487031  System.Net.Http  Name: requests-started-rate, DisplayName: Requests Started Rate, DisplayRateTimeScale: 00:00:01, Increment: 1, IntervalSec: 0.0004773, Metadata: , Series: Interval=500, CounterType: Sum, DisplayUnits: 
19:38:55.9487610  System.Net.Http  Name: requests-failed, DisplayName: Requests Failed, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 0.0004773, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9487888  System.Net.Http  Name: requests-failed-rate, DisplayName: Requests Failed Rate, DisplayRateTimeScale: 00:00:01, Increment: 0, IntervalSec: 0.0004773, Metadata: , Series: Interval=500, CounterType: Sum, DisplayUnits: 
19:38:55.9488052  System.Net.Http  Name: current-requests, DisplayName: Current Requests, Mean: 1, StandardDeviation: 0, Count: 1, Min: 1, Max: 1, IntervalSec: 0.0004773, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9488201  System.Net.Http  Name: http11-connections-current-total, DisplayName: Current Http 1.1 Connections, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 0.0004773, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9488524  System.Net.Http  Name: http20-connections-current-total, DisplayName: Current Http 2.0 Connections, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 0.0004773, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9490235  System.Net.Http  Name: http11-requests-queue-duration, DisplayName: HTTP 1.1 Requests Queue Duration, Mean: 0, StandardDeviation: 0, Count: 0, Min: ∞, Max: -∞, IntervalSec: 0.0004773, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: ms
19:38:55.9494528  System.Net.Http  Name: http20-requests-queue-duration, DisplayName: HTTP 2.0 Requests Queue Duration, Mean: 0, StandardDeviation: 0, Count: 0, Min: ∞, Max: -∞, IntervalSec: 0.0004773, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: ms
19:38:55.9643081  System.Net.Sockets  Name: outgoing-connections-established, DisplayName: Outgoing Connections Established, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 7.64E-05, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9643725  System.Net.Sockets  Name: incoming-connections-established, DisplayName: Incoming Connections Established, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 7.64E-05, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9644278  System.Net.Sockets  Name: bytes-received, DisplayName: Bytes Received, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 7.64E-05, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9644685  System.Net.Sockets  Name: bytes-sent, DisplayName: Bytes Sent, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 7.64E-05, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9644858  System.Net.Sockets  Name: datagrams-received, DisplayName: Datagrams Received, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 7.64E-05, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9645243  System.Net.Sockets  Name: datagrams-sent, DisplayName: Datagrams Sent, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 7.64E-05, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:55.9662685  System.Net.NameResolution  Name: dns-lookups-requested, DisplayName: DNS Lookups Requested, Mean: 0, StandardDeviation: 0, Count: 1, Min: 0, Max: 0, IntervalSec: 1.6E-05, Series: Interval=500, CounterType: Mean, Metadata: , DisplayUnits: 
19:38:56.0093961  System.Net.Security  Name: tls-handshake-rate, DisplayName: TLS handshakes completed, DisplayRateTimeScale: 00:00:01, Increment: 0, IntervalSec: 1.99E-05, Metadata: , Series: Interval=500, CounterType: Sum, DisplayUnits: 

或过程内部的启动计数器:

# Name the source events from previous example as providers (--providers).# Set the process ID (-p).
dotnet counters monitor System.Net.Http System.Net.Sockets System.Net.Security System.Net.NameResolution -p 1234

这将启动计数器监督,用理论值笼罩终端窗口,看起来相似:

[System.Net.Http]
    Current Http 1.1 Connections                                   1    
    Current Http 2.0 Connections                                   0    
    Current Requests                                               1    
    HTTP 1.1 Requests Queue Duration (ms)                          0    
    HTTP 2.0 Requests Queue Duration (ms)                          0    
    Requests Failed                                                0    
    Requests Failed Rate (Count / 1 sec)                           0    
    Requests Started                                               8    
    Requests Started Rate (Count / 1 sec)                          1    [System.Net.Sockets]
    Bytes Received                                         1,220,260    
    Bytes Sent                                                 3,819    
    Datagrams Received                                             0    
    Datagrams Sent                                                 0    
    Incoming Connections Established                               0    
    Outgoing Connections Established                               1    [System.Net.NameResolution]
    Average DNS Lookup Duration (ms)                               0    
    DNS Lookups Requested                                          1    [System.Net.Security]
    All TLS Sessions Active                                        1    
    Current TLS handshakes                                         0    
    TLS 1.0 Handshake Duration (ms)                                0    
    TLS 1.0 Sessions Active                                        0    
    TLS 1.1 Handshake Duration (ms)                                0    
    TLS 1.1 Sessions Active                                        0    
    TLS 1.2 Handshake Duration (ms)                                0    
    TLS 1.2 Sessions Active                                        0    
    TLS 1.3 Handshake Duration (ms)                                0    
    TLS 1.3 Sessions Active                                        1    
    TLS Handshake Duration (ms)                                    0    
    TLS handshakes completed (Count / 1 sec)                       0    
    Total TLS handshakes completed                                 1    
    Total TLS handshakes failed                                    0 

平安

. net 中的平安层依赖于底层操作系统及其性能。

- 对于基于 Linux 的零碎,咱们应用 OpenSSL,它从 1.1.1 版本起就反对 TLS 1.3。

- 对于 Windows 10, TLS 1.3 是可用的版本 1903,但只用于测试目标,而不是生产。

此外,它是可选的,必须在注册表中启用。因而,TLS 1.3 在之前的.net Core 版本不能在 Windows 上工作。

这在外部预览版中有所扭转,其中 TLS 1.3 是默认开启的,能够通过新的 API 应用。咱们针对新的 API 调整了 Windows 上的 SslStream 实现,并在.net 5 的 Windows 外部预览版本中对其进行了测试。

咱们还谋求在.net 5 的 SSL 测试中取得 A 级。为了实现这一点,咱们必须对 Linux 上的 SslStream 引入一个破坏性的更改,咱们当初设置了一个被认为是弱小的默认明码套件的自以为是的列表:

TLS 1.3 cipher suites
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256

然而,这些能够被多种形式重写:

  1. 在代码中,通过手动设置 CipherSuitesPolicy 或间接在调用 SslStream.AuthenticateAs…办法。例如:

    var sslStream = new SslStream(networkStream);
    await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions(){CipherSuitesPolicy = new CipherSuitesPolicy(new[]
        {
            TlsCipherSuite.TLS_AES_256_GCM_SHA384,
            TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256,
            TlsCipherSuite.TLS_AES_128_GCM_SHA256,
            TlsCipherSuite.TLS_AES_128_CCM_8_SHA256,
            TlsCipherSuite.TLS_AES_128_CCM_SHA256
        }),
    });
  2. 或者通过 SocketsHttpHandler.SslOptions 间接为 HttpClient:

    class Program{private static readonly HttpClient _client = new HttpClient(new SocketsHttpHandler()
        {SslOptions = new SslClientAuthenticationOptions()
            {CipherSuitesPolicy = new CipherSuitesPolicy(new []
                {
                    TlsCipherSuite.TLS_AES_256_GCM_SHA384,
                    TlsCipherSuite.TLS_CHACHA20_POLY1305_SHA256,
                    TlsCipherSuite.TLS_AES_128_GCM_SHA256,
                    TlsCipherSuite.TLS_AES_128_CCM_8_SHA256,
                    TlsCipherSuite.TLS_AES_128_CCM_SHA256
                })
            }
        };
    ...}

最初指出

本文并不是咱们所做的所有更改的残缺列表。如果你发现任何谬误,请毫不犹豫地分割咱们,你能够在 dotnet/ncl 别名下找到咱们。

欢送关注我的公众号,如果你有喜爱的外文技术文章,能够通过公众号留言举荐给我。

退出移动版