# 网络请求各个指标的度量

![](https://img.hacpai.com/bing/20190627.jpg?imageView2/1/w/960/h/540/interlace/1/q/100)

> 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>

## 内容

* 效果
* 一共有哪些指标？
* 如何得到这些指标？
* 总结

## 一，效果

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

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

![httpmetrics.png](https://img.hacpai.com/file/2019/12/httpmetrics-6315f371.png)

## 二，一共有哪些指标？

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

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

```objectivec
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](https://img.hacpai.com/file/2019/12/WechatIMG1009-e5b4e40f.png)

## 三，如何得到这些指标？

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

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

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

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

```objectivec
- (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

            }
        }];
    }
}
```

> 1. 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>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://yongpenglovemimi123.gitbook.io/henry/ios/wang-luo-qing-qiu-ge-ge-zhi-biao-de-du-liang.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
