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

如何在.NET Core中测量HTTP请求各阶段耗时?

如何在.NET Core HttpClient中采集类似curl的HTTP请求细分耗时

.NET Core 中可以通过三种主流方案采集和curl维度对齐的请求耗时数据,不需要依赖第三方组件,完全基于框架原生能力实现:

方案1:基于SocketsHttpHandler回调采集(最接近curl的底层耗时精度)

.NET Core 2.1+ 版本HttpClient默认底层实现就是SocketsHttpHandler,它暴露了完整的请求生命周期回调,可以直接对应到curl的各个耗时维度:

  • DNS解析(lookup)耗时:ResolveAddressAsyncStart 回调触发到 ResolveAddressAsyncEnd 回调触发的时间差
  • TCP连接(connect)耗时:ConnectAsyncStart 回调触发到 ConnectAsyncEnd 回调触发的时间差
  • TLS握手(appconnect)耗时:EstablishSslConnectionAsyncStart 回调触发到 EstablishSslConnectionAsyncEnd 回调触发的时间差
  • 预传输(pretransfer)耗时:连接建立完成到请求头全部发送完成的时间差
  • 首包响应(starttransfer)耗时:请求发送完成到收到第一个响应字节的时间差
  • 总响应耗时:请求发起到底层响应流读取完成的总时长

参考实现代码:

var customHandler = new SocketsHttpHandler
{
    ConnectCallback = async (context, cancellationToken) =>
    {
        // 采集DNS解析耗时
        var dnsStartTs = Stopwatch.GetTimestamp();
        var ipAddresses = await Dns.GetHostAddressesAsync(context.DnsEndPoint.Host, cancellationToken);
        var dnsCost = Stopwatch.GetElapsedTime(dnsStartTs);
        // 此处写入你自己的日志/监控存储

        // 采集TCP连接耗时
        var connectStartTs = Stopwatch.GetTimestamp();
        var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        await socket.ConnectAsync(ipAddresses, context.DnsEndPoint.Port, cancellationToken);
        var connectCost = Stopwatch.GetElapsedTime(connectStartTs);
        // 此处写入你自己的日志/监控存储

        // 采集HTTPS场景的TLS握手耗时
        if (context.InitialRequestMessage.RequestUri.Scheme == Uri.UriSchemeHttps)
        {
            var sslStartTs = Stopwatch.GetTimestamp();
            var sslStream = new SslStream(new NetworkStream(socket, ownsSocket: true));
            await sslStream.AuthenticateAsClientAsync(new SslClientAuthenticationOptions
            {
                TargetHost = context.DnsEndPoint.Host
            }, cancellationToken);
            var sslCost = Stopwatch.GetElapsedTime(sslStartTs);
            // 此处写入你自己的日志/监控存储
            return sslStream;
        }
        return new NetworkStream(socket, ownsSocket: true);
    }
};

// 用自定义Handler初始化HttpClient即可生效
var httpClient = new HttpClient(customHandler);

方案2:自定义DelegatingHandler上层埋点(适合业务侧快速接入)

如果不需要底层网络维度的细粒度耗时,只需要拆分请求发送、响应接收的核心时长,可以直接基于HttpClient的管道机制实现无侵入埋点:

public class RequestTimingHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var totalStartTs = Stopwatch.GetTimestamp();
        // 采集首包响应耗时
        var firstByteStartTs = Stopwatch.GetTimestamp();
        var response = await base.SendAsync(request, cancellationToken);
        var firstByteCost = Stopwatch.GetElapsedTime(firstByteStartTs);
        // 此处写入你自己的日志/监控存储

        // 采集响应内容读取耗时
        var contentReadStartTs = Stopwatch.GetTimestamp();
        _ = await response.Content.ReadAsByteArrayAsync(cancellationToken);
        var contentReadCost = Stopwatch.GetElapsedTime(contentReadStartTs);
        // 采集总耗时
        var totalCost = Stopwatch.GetElapsedTime(totalStartTs);
        // 此处写入你自己的日志/监控存储

        return response;
    }
}

// 依赖注入场景下直接注册到HttpClient管道即可
builder.Services.AddHttpClient("YourServiceClient")
    .AddHttpMessageHandler<RequestTimingHandler>();

方案3:监听.NET内置DiagnosticSource事件(全局无侵入采集)

如果需要对项目中所有HttpClient请求做统一监控,不需要修改任何业务代码,可以直接监听框架默认抛出的诊断事件:

  • System.Net.Http.HttpRequest.Start:请求开始触发
  • System.Net.Http.HttpRequest.Stop:请求结束触发
  • System.Net.Http.HttpConnection.Connect.Start:新建连接开始触发
  • System.Net.Http.HttpConnection.Connect.Stop:新建连接结束触发
  • System.Net.Http.HttpConnection.WaitForAvailableConnection.Start:等待连接池空闲连接开始触发

所有事件都自带高精度时间戳,直接计算两个关联事件的时间差即可拿到对应维度的耗时数据。

注意:如果HttpClient开启了连接池复用,TCP连接、TLS握手耗时只会在新建连接的时候产生,复用连接的请求这两个维度耗时为0,和curl的默认行为完全一致。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.27 17:15:05