You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在.NET 5中搭配HttpClientFactory使用HttpClient时如何记录连接池信息?

实现HttpClient请求全链路时序耗时采集方案

要获取类似Chrome的请求各阶段耗时(DNS解析、TCP连接、TLS握手等),可以通过以下两种方案实现,两种方案均兼容你现有.NET 5 + HttpClientFactory + NLog的技术栈,无需修改现有日志配置:


方案1:使用DiagnosticSource全局监听(无侵入,适用于所有HttpClient实例)

.NET 内置的HttpClient会通过DiagnosticSource发布全生命周期的事件,无需修改现有HttpClient注册逻辑,只需订阅对应事件即可采集各阶段耗时:

  1. 实现诊断观察者类
using System.Diagnostics;
using System.Net.Http;

public class HttpClientDiagnosticObserver : IObserver<DiagnosticListener>
{
    private readonly ILogger<HttpClientDiagnosticObserver> _logger;
    private readonly AsyncLocal<HttpRequestTiming> _timingContext = new();

    public HttpClientDiagnosticObserver(ILogger<HttpClientDiagnosticObserver> logger)
    {
        _logger = logger;
    }

    public void OnCompleted() { }
    public void OnError(Exception error) { }

    public void OnNext(DiagnosticListener listener)
    {
        if (listener.Name == "HttpHandlerDiagnosticListener")
        {
            listener.Subscribe(new HttpHandlerEventObserver(_logger, _timingContext));
        }
    }
}

public class HttpHandlerEventObserver : IObserver<KeyValuePair<string, object>>
{
    private readonly ILogger _logger;
    private readonly AsyncLocal<HttpRequestTiming> _timingContext;

    public HttpHandlerEventObserver(ILogger logger, AsyncLocal<HttpRequestTiming> timingContext)
    {
        _logger = logger;
        _timingContext = timingContext;
    }

    public void OnCompleted() { }
    public void OnError(Exception error) { }

    public void OnNext(KeyValuePair<string, object> value)
    {
        switch (value.Key)
        {
            case "System.Net.Http.HttpRequestOut.Start":
                _timingContext.Value = new HttpRequestTiming
                {
                    Request = (HttpRequestMessage)value.Value.GetType().GetProperty("Request")?.GetValue(value.Value),
                    StartTime = Stopwatch.GetTimestamp()
                };
                break;
            case "System.Net.Http.DnsResolutionStart":
                if (_timingContext.Value != null) _timingContext.Value.DnsStart = Stopwatch.GetTimestamp();
                break;
            case "System.Net.Http.DnsResolutionStop":
                if (_timingContext.Value != null)
                    _timingContext.Value.DnsDuration = Stopwatch.GetElapsedTime(_timingContext.Value.DnsStart).TotalMilliseconds;
                break;
            case "System.Net.Http.TcpConnectStart":
                if (_timingContext.Value != null) _timingContext.Value.TcpConnectStart = Stopwatch.GetTimestamp();
                break;
            case "System.Net.Http.TcpConnectStop":
                if (_timingContext.Value != null)
                    _timingContext.Value.TcpConnectDuration = Stopwatch.GetElapsedTime(_timingContext.Value.TcpConnectStart).TotalMilliseconds;
                break;
            case "System.Net.Http.TlsHandshakeStart":
                if (_timingContext.Value != null) _timingContext.Value.TlsHandshakeStart = Stopwatch.GetTimestamp();
                break;
            case "System.Net.Http.TlsHandshakeStop":
                if (_timingContext.Value != null)
                    _timingContext.Value.TlsHandshakeDuration = Stopwatch.GetElapsedTime(_timingContext.Value.TlsHandshakeStart).TotalMilliseconds;
                break;
            case "System.Net.Http.HttpRequestOut.Stop":
                if (_timingContext.Value != null)
                {
                    var totalDuration = Stopwatch.GetElapsedTime(_timingContext.Value.StartTime).TotalMilliseconds;
                    _logger.LogInformation("请求时序详情|请求地址:{RequestUri}|DNS解析耗时:{DnsDuration}ms|TCP连接耗时:{TcpConnectDuration}ms|TLS握手耗时:{TlsDuration}ms|总耗时:{TotalDuration}ms",
                        _timingContext.Value.Request.RequestUri,
                        _timingContext.Value.DnsDuration,
                        _timingContext.Value.TcpConnectDuration,
                        _timingContext.Value.TlsHandshakeDuration,
                        totalDuration);
                    _timingContext.Value = null;
                }
                break;
        }
    }
}

public class HttpRequestTiming
{
    public HttpRequestMessage Request { get; set; }
    public long StartTime { get; set; }
    public long DnsStart { get; set; }
    public double DnsDuration { get; set; }
    public long TcpConnectStart { get; set; }
    public double TcpConnectDuration { get; set; }
    public long TlsHandshakeStart { get; set; }
    public double TlsHandshakeDuration { get; set; }
}
  1. 服务注册与启动订阅
// Startup.cs ConfigureServices 中注册观察者
services.AddSingleton<HttpClientDiagnosticObserver>();

// Startup.cs Configure 中添加全局订阅
var observer = app.ApplicationServices.GetRequiredService<HttpClientDiagnosticObserver>();
DiagnosticListener.AllListeners.Subscribe(observer);
  1. 替换原有HttpClientHandler为SocketsHttpHandler(兼容所有原有配置,保证事件正常触发)
services.AddHttpClient("BasicUsage", c =>
{
    // c.Timeout = TimeSpan.FromMilliseconds(30);
}).ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    AllowAutoRedirect = true,
    AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
    UseCookies = true,
    Credentials = CredentialCache.DefaultCredentials,
    SslOptions = new SslClientAuthenticationOptions
    {
        EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11,
        RemoteCertificateValidationCallback = (message, cert, chain, errors) => true
    },
    MaxAutomaticRedirections = 5,
    CookieContainer = cookies,
});

方案2:自定义DelegatingHandler(可控性强,仅针对指定HttpClient生效)

如果只需要给特定名称的HttpClient增加时序统计,可以自定义委托Handler实现:

  1. 实现自定义时序Handler
public class TimingDelegatingHandler : DelegatingHandler
{
    private readonly ILogger<TimingDelegatingHandler> _logger;

    public TimingDelegatingHandler(ILogger<TimingDelegatingHandler> logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var timingInfo = new HttpRequestTiming();
        request.Properties["TimingInfo"] = timingInfo;
        var totalStopwatch = Stopwatch.StartNew();
        var response = await base.SendAsync(request, cancellationToken);
        totalStopwatch.Stop();
        
        _logger.LogInformation("请求时序详情|请求地址:{RequestUri}|DNS解析耗时:{DnsDuration}ms|TCP连接耗时:{TcpConnectDuration}ms|TLS握手耗时:{TlsDuration}ms|总耗时:{TotalDuration}ms",
            request.RequestUri,
            timingInfo.DnsDuration,
            timingInfo.TcpConnectDuration,
            timingInfo.TlsHandshakeDuration,
            totalStopwatch.Elapsed.TotalMilliseconds);
        return response;
    }
}
  1. 注册HttpClient时注入Handler并配置阶段回调,直接替换你原有HttpClient注册逻辑即可:
services.AddTransient<TimingDelegatingHandler>();
services.AddHttpClient("BasicUsage", c =>
{
    // c.Timeout = TimeSpan.FromMilliseconds(30);
})
.AddHttpMessageHandler<TimingDelegatingHandler>()
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
    AllowAutoRedirect = true,
    AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
    UseCookies = true,
    Credentials = CredentialCache.DefaultCredentials,
    MaxAutomaticRedirections = 5,
    CookieContainer = cookies,
    // 自定义连接回调统计DNS、TCP耗时
    ConnectCallback = async (context, token) =>
    {
        var timingInfo = (HttpRequestTiming)context.InitialRequestMessage.Properties["TimingInfo"];
        // DNS计时
        var dnsStart = Stopwatch.GetTimestamp();
        var addresses = await Dns.GetHostAddressesAsync(context.DnsEndPoint.Host, token);
        timingInfo.DnsDuration = Stopwatch.GetElapsedTime(dnsStart).TotalMilliseconds;
        // TCP连接计时
        var tcpStart = Stopwatch.GetTimestamp();
        var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        try
        {
            await socket.ConnectAsync(addresses, context.DnsEndPoint.Port, token);
            timingInfo.TcpConnectDuration = Stopwatch.GetElapsedTime(tcpStart).TotalMilliseconds;
            return new NetworkStream(socket, ownsSocket: true);
        }
        catch
        {
            socket.Dispose();
            throw;
        }
    },
    SslOptions = new SslClientAuthenticationOptions
    {
        EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11,
        RemoteCertificateValidationCallback = (message, cert, chain, errors) => true,
        // TLS握手计时
        OnAuthenticateAsClientAsync = async (stream, targetHost, clientCertificates, serverCertificate, enabledSslProtocols, cancellationToken) =>
        {
            var timingInfo = (HttpRequestTiming)((SocketsConnectionContext)stream.GetType().GetProperty("Context", BindingFlags.NonPublic | BindingFlags.Instance)?.GetValue(stream)).InitialRequestMessage.Properties["TimingInfo"];
            var tlsStart = Stopwatch.GetTimestamp();
            await stream.AuthenticateAsClientAsync(targetHost, clientCertificates, enabledSslProtocols, false, cancellationToken);
            timingInfo.TlsHandshakeDuration = Stopwatch.GetElapsedTime(tlsStart).TotalMilliseconds;
        }
    }
});

注意事项

  • 连接复用场景下,DNS、TCP、TLS阶段仅在新建连接时产生耗时,复用已有连接的请求这三项耗时会显示为0,符合实际网络逻辑
  • 两种方案采集的时序数据会直接写入你已配置的NLog链路,无需额外适配日志组件

内容的提问来源于stack exchange,提问作者vaishali

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.01 02:39:03