.NET 5網路操作的改進

  • 2021 年 1 月 18 日
  • 筆記

隨著.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("//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("//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("//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("//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("//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("//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, "//localhost:5001/h2c")
            //   {
            //     Version = HttpVersion.Version20,
            //     VersionPolicy = HttpVersionPolicy.RequestVersionExact
            //   }
            using var response = await _client.GetAsync("//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("//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("//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("//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("//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別名下找到我們。

歡迎關注我的公眾號,如果你有喜歡的外文技術文章,可以通過公眾號留言推薦給我。

原文鏈接://devblogs.microsoft.com/dotnet/net-5-new-networking-improvements/