网络请求各个指标的度量

发布于 2024-06-23 14:48:06 字数 10077 浏览 34 评论 0

一,效果

比如我们向 https://github.com/ 发起一个 http 的请求,我们看下可以拿到哪些指标。

通过下面的这些指标,我们可以分析这些数据,得知我们的网络请求为什么比较慢,哪个阶段比较慢,再逐步优化。


httpmetrics.png

二,一共有哪些指标?

下面这个类,在 >= iOS10 的操作系统才可以运作,部分属性在 >= iOS13 才可以使用,我分别解释下他们的意思,但是 有些属性,我是真的不知道是干什么的,经过尝试,没有变化,这里我就不做解释。

如果觉得我是在这瞎 BB,可以去 navigate to NSURLSessionTaskTransactionMetrics define 自己查看,上面备注写的也很清楚了。

API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0))
@interface NSURLSessionTaskTransactionMetrics : NSObject

// URLRequest 请求的 Body 和 Header 都在这里面呢,包含请求的全部信息。
@property (copy, readonly) NSURLRequest *request;

// URLResponse 相应的信息都在这个对象里面
@property (nullable, copy, readonly) NSURLResponse *response;

// 请求任务开始前,可以理解为整个请求的最开始的时间点。
@property (nullable, copy, readonly) NSDate *fetchStartDate;

// 开始 DNS 解析的时间点
@property (nullable, copy, readonly) NSDate *domainLookupStartDate;

// 结束 DNS 解析的时间点
@property (nullable, copy, readonly) NSDate *domainLookupEndDate;

// 建立连接的起始点
@property (nullable, copy, readonly) NSDate *connectStartDate;

// 建立 SSL/TLS 对话密钥的起始时间
@property (nullable, copy, readonly) NSDate *secureConnectionStartDate;

// 建立 SSL/TLS 对话密钥的终点时间
@property (nullable, copy, readonly) NSDate *secureConnectionEndDate;

// 建立连接的终点
@property (nullable, copy, readonly) NSDate *connectEndDate;

// 建立好连接通道后,请求开始的时间点
@property (nullable, copy, readonly) NSDate *requestStartDate;

// 建立好连接通道后,请求结束的时间点
@property (nullable, copy, readonly) NSDate *requestEndDate;

// 开始得到响应的时间点
@property (nullable, copy, readonly) NSDate *responseStartDate;

// 接收完最后一字节的数据的响应结束时间点
@property (nullable, copy, readonly) NSDate *responseEndDate;

// 网络协议的名称
@property (nullable, copy, readonly) NSString *networkProtocolName;

// 是否使用了网络代理
@property (assign, readonly, getter=isProxyConnection) BOOL proxyConnection;

// 是否重用了连接
@property (assign, readonly, getter=isReusedConnection) BOOL reusedConnection;

// 资源获取的类型,有缓存,Network,不知道,等
@property (assign, readonly) NSURLSessionTaskMetricsResourceFetchType resourceFetchType;

// iOS13+ 发送的请求头占的字节
@property (readonly) int64_t countOfRequestHeaderBytesSent API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 发送的 Body 占的字节
@property (readonly) int64_t countOfRequestBodyBytesSent API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 发送的 Body 在 Encoding 之前占的字节
@property (readonly) int64_t countOfRequestBodyBytesBeforeEncoding API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 响应头占字节数
@property (readonly) int64_t countOfResponseHeaderBytesReceived API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 响应 Body 体占字节数
@property (readonly) int64_t countOfResponseBodyBytesReceived API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ Decoding 之后响应消息体占字节数
@property (readonly) int64_t countOfResponseBodyBytesAfterDecoding API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 本地 IP 地址
@property (nullable, copy, readonly) NSString *localAddress API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 本地端口
@property (nullable, copy, readonly) NSNumber *localPort API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 访问服务器机器的 IP 地址
@property (nullable, copy, readonly) NSString *remoteAddress API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 访问服务器机器的端口号
@property (nullable, copy, readonly) NSNumber *remotePort API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ TLS 协议的版本 
@property (nullable, copy, readonly) NSNumber *negotiatedTLSProtocolVersion API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ TLS 选择的加密协议
@property (nullable, copy, readonly) NSNumber *negotiatedTLSCipherSuite API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 不知道,我不论怎么切换网络环境一直都是 NO
@property (readonly, getter=isCellular) BOOL cellular API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 不知道,我不论怎么切换网络环境一直都是 NO
@property (readonly, getter=isExpensive) BOOL expensive API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 不知道,我不论怎么切换网络环境一直都是 NO
@property (readonly, getter=isConstrained) BOOL constrained API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

// iOS13+ 不知道,我不论怎么切换网络环境一直都是 NO
@property (readonly, getter=isMultipath) BOOL multipath API_AVAILABLE(macos(10.15), ios(13.0), watchos(6.0), tvos(13.0));

兄弟别慌,我刚开始看也懵逼,我给你看一张图,你就不懵逼了。在下方:

红字: 描述请求的整个过程。 白字: 对应上面的属性名字,各个阶段的位置。

是不是懵逼症状好一些了。


WechatIMG1009.png

三,如何得到这些指标?

这里还是要感谢苹果爸爸,虽然 iOS10 才给出来,但是现在已经 9102 年了,以支持最近 3 个版本的原则,我们的用户也应该都是 iOS10+ 的了,所以这个官方提供的功能,用的安全放心,还简单,不用去 hook 这,hook 那,然后得到的数据还不准,还让工程复杂化。

首先 调用 session 的构造器的时候,传入 delegate,这里比如就是 self。

NSURLSession *session = [NSURLSession  
 sessionWithConfiguration: config  
 delegate:self delegateQueue:nil];

其次让 self 遵守 NSURLSessionTaskDelegate 协议 实现如下代理方法

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics {

  if ([metrics.transactionMetrics count] > 0) {
    [metrics.transactionMetrics enumerateObjectsUsingBlock:^(NSURLSessionTaskTransactionMetrics *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
      if (obj.resourceFetchType == NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad) {

        NSLog(@"========================================================================");

        if (obj.domainLookupStartDate && obj.domainLookupEndDate) {
          int dnsLookupTime = ceil([obj.domainLookupEndDate timeIntervalSinceDate:obj.domainLookupStartDate] * 1000);
          NSLog(@"DNS 解析时长 单位 ms:%d",dnsLookupTime);
        }

        if (obj.connectStartDate && obj.connectEndDate) {
          int tcpTime = ceil([obj.connectEndDate timeIntervalSinceDate:obj.connectStartDate] * 1000);
          NSLog(@"TCP+SSL 整个连接建立的时间 单位 ms:%d",tcpTime);
        }

        if (obj.secureConnectionEndDate && obj.secureConnectionStartDate) {
           int sslTime = ceil([obj.secureConnectionEndDate timeIntervalSinceDate:obj.secureConnectionStartDate] * 1000);
          NSLog(@"SSL 四次握手时长 单位 ms:%d",sslTime);
        }

        if (obj.requestStartDate && obj.responseEndDate) {
          int transmissionTime = ceil([obj.responseEndDate timeIntervalSinceDate:obj.requestStartDate] * 1000);
          NSLog(@"传输时间 单位 ms:%d",transmissionTime);
        }

        if (obj.fetchStartDate && obj.responseEndDate) {
          int requestTime = ceil([obj.responseEndDate timeIntervalSinceDate:obj.fetchStartDate] * 1000);
          NSLog(@"完整请求时长 单位 ms:%d",requestTime);
        }

        if (obj.fetchStartDate) {
          UInt64 requestDate = [obj.fetchStartDate timeIntervalSince1970] * 1000;
          NSLog(@"开始发起请求的时间点:%llu",requestDate);
        }

        if (obj.responseEndDate) {
          UInt64 responseEndDate = [obj.responseEndDate timeIntervalSince1970] * 1000;
          NSLog(@"请求结束的时间点:%llu",responseEndDate);
        }

        NSLog(@"HTTP 请求类型:%@",obj.request.HTTPMethod);
        NSLog(@"协议名称:%@",obj.networkProtocolName);
        NSLog(@"请求 URL:%@",[obj.request.URL absoluteString]);
        NSLog(@"是否使用代理:%d",obj.isProxyConnection);
        NSLog(@"是否重用连接获取资源:%d",obj.reusedConnection);


        // iOS13 或者 iOS13 以上才可以使用
        NSLog(@"服务器 IP:%@",obj.remoteAddress);
        NSLog(@"服务器端口:%@",obj.remotePort);
        NSLog(@"请求的 Header 字节数:%lld bytes",obj.countOfRequestHeaderBytesSent);
        NSLog(@"请求的 Body 字节数:%lld bytes",obj.countOfRequestBodyBytesSent);
        NSLog(@"请求的 Encoding 之前的 Body 字节数:%lld bytes",obj.countOfRequestBodyBytesBeforeEncoding);
        NSLog(@"响应的 Header 字节数:%lld bytes",obj.countOfResponseHeaderBytesReceived);
        NSLog(@"响应的 Body 字节数:%lld bytes",obj.countOfResponseBodyBytesReceived);
        NSLog(@"响应的 Decoding 之后的 Body 字节数:%lld bytes",obj.countOfResponseBodyBytesAfterDecoding);


        // 网络类型
//        NSURLSessionTaskMetricsResourceFetchTypeUnknown
//        NSURLSessionTaskMetricsResourceFetchTypeNetworkLoad
//        NSURLSessionTaskMetricsResourceFetchTypeServerPush
//        NSURLSessionTaskMetricsResourceFetchTypeLocalCache

      }
    }];
  }
}

Run 一下,看看 delegate 方法都捕捉到了哪些指标参数 。

四,总结

但是我发现一个问题,requestStartDate 比 requestEndDate 晚, 例如打印如下: requestStartDate:1576467073.038733 requestEndDate:1576467071.590589 按照 WWDC 上面所描述的,应该 start 先于 end,但是实际测试结果并不是这样。

我也弄不清楚为什么,将这个问题放在了 stackoverflow 上面: https://stackoverflow.com/questions/59350261/ios-why-is-requestenddate-earlier-than-requeststartdate 如果有知道的小伙伴告诉我下哈!

补: 我自己回答了我的提问,在 OC 上面测试,我发现结果是正确的,但是在 swift 却是有问题的。

了解了一个完整的网路请求链路,以及每个链路的时间点,都是做什么,如何使用官方提供的 API 来进行收集数据,进行性能分析。

如果觉得自己写 Demo 测试验证麻烦,直接下载我上传的 Demo 没毒放心

Demo: https://github.com/zColdWater/iOSURLMetricsDemo/tree/master

2016 - 2019 WWDC 介绍这块的 PPT

https://github.com/zColdWater/Resources/blob/master/Images/711_nsurlsession_new_features_and_best_practices.pdf

https://github.com/zColdWater/Resources/blob/master/Images/713_advances_in_networking_part_2.pdf

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

0 文章
0 评论
22 人气
更多

推荐作者

新人笑

文章 0 评论 0

mb_vYjKhcd3

文章 0 评论 0

小高

文章 0 评论 0

来日方长

文章 0 评论 0

哄哄

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文