iOS-利用NSURLProtocol进行网络监控

参考文章:http://www.cocoachina.com/articles/19683

啥也不说,先上代码

/// 处理POST请求相关POST  用HTTPBodyStream来处理HTTPBody
/// @param request 处理后的request
- (NSMutableURLRequest *)handlePostRequestBodyWithRequest:(NSURLRequest *)request {
    NSMutableURLRequest * mutableRrequest = [request mutableCopy];
    if ([request.HTTPMethod isEqualToString:@"POST"]) {
        if (!request.HTTPBody) {
            uint8_t d[1024] = {0};
            NSInputStream *stream = request.HTTPBodyStream;
            NSMutableData *data = [[NSMutableData alloc] init];
            [stream open];
            while ([stream hasBytesAvailable]) {
                NSInteger len = [stream read:d maxLength:1024];
                if (len > 0 && stream.streamError == nil) {
                    [data appendBytes:(void *)d length:len];
                }
            }
            mutableRrequest.HTTPBody = [data copy];
            [stream close];
        }
    }
    return mutableRrequest;
}

/// 重定向url,重新生成一个request
/// @param request 重定向后的request
+(NSMutableURLRequest*)redirectHostInRequset:(NSMutableURLRequest*)request
{
    if ([request.URL host].length == 0) {
        return request;
    }
    NSString *originUrlString = [request.URL absoluteString];
    NSString *originHostString = [request.URL host];
    NSRange hostRange = [originUrlString rangeOfString:originHostString];
    if (hostRange.location == NSNotFound) {
        return request;
    }
    // 替换host
    if ([originHostString containsString:@"baoyinxiaofei"]) {
        NSString *ip = @"app.baoyinxiaofei.com";
        NSString *urlString = [originUrlString stringByReplacingCharactersInRange:hostRange withString:ip];
        NSURL *url = [NSURL URLWithString:urlString];
        request.URL = url;
    }

    NSMutableURLRequest *mutableRequest = [[self new] handlePostRequestBodyWithRequest:request];
    return mutableRequest;
}

//所有注册此Protocol的请求都会经过这个方法的判断,询问是否对该请求进行处理
+ (BOOL)canInitWithRequest:(NSURLRequest *)request{

    if (![request.URL.scheme isEqualToString:@"http"] &&
        ![request.URL.scheme isEqualToString:@"https"]) {
        return NO;
    }

    //看看是否已经处理过了,防止无限循环 根据业务来截取
    if ([NSURLProtocol propertyForKey: URLProtocolHandledKey inRequest:request]) {
        return NO;
    }
    return YES;
}
//可选方法,对需要拦截的请求进行自定的处理
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request{

    NSMutableURLRequest *mutableReqeust = [request mutableCopy];
    [NSURLProtocol setProperty:@YES
                        forKey:URLProtocolHandledKey
                     inRequest:mutableReqeust];
    mutableReqeust = [self redirectHostInRequset:mutableReqeust];

    return [mutableReqeust copy];

}
/**
    开始请求在这里需要我们手动的把请求发出去,可以使用原生的NSURLSessionDataTask,也可以使用的第三方网络库
    同时设置"NSURLSessionDataDelegate"协议,接收Server端的响应
*/
- (void)startLoading {

    NSMutableURLRequest *mutableRequest = [self handlePostRequestBodyWithRequest:self.request];
    NSDictionary *dic = nil;
    NSError *error = nil;
    if (mutableRequest.HTTPBody) {
        dic = [NSJSONSerialization JSONObjectWithData:mutableRequest.HTTPBody options:NSJSONReadingFragmentsAllowed error:&error];
    }

    self.startDate                                        = [NSDate date];
    self.data                                             = [NSMutableData data];
    NSURLSessionConfiguration *configuration              = [NSURLSessionConfiguration defaultSessionConfiguration];
    //此处不能用主线程,不然会导致卡顿,并且如果多个请求会导致请求延迟
    self.sessionDelegateQueue                             = [[NSOperationQueue alloc] init];
    self.sessionDelegateQueue.maxConcurrentOperationCount = 1;
    self.sessionDelegateQueue.name                        = @"com.hujiang.wedjat.session.queue";
    NSURLSession *session                                 = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:self.sessionDelegateQueue];
//此处request的HTTPBody为空,但是HTTPBodyStream有值,可以猜测当HTTPBody为空时候,会去找HTTPBodyStream里面的值,并且HTTPBodyStream的流本身也是HTTPBody转换过来的
    self.dataTask                                         = [session dataTaskWithRequest:self.request];
    [self.dataTask resume];

    self.startDateString                             = [[NSDate date] timeIntervalSince1970];

    NSLog(@"%s ---  %@ --- %f -- %@-- %@ -- %@ -- %f",__func__,mutableRequest.URL.absoluteString,mutableRequest.timeoutInterval,mutableRequest.HTTPBody,dic,error,self.startDateString);

}

- (void)stopLoading {

    [self.dataTask cancel];
    self.dataTask                    = nil;
    NSString *endDateString          = [BYDateTool stringToDate:[NSDate date] andFormatStr:@"yyyy-MM-dd HH:mm:ss"];
    NSString *mimeType               = self.response.MIMEType;
}

#pragma mark - NSURLSessionTaskDelegate

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    if (!error) {
        [self.client URLProtocolDidFinishLoading:self];
    } else if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorCancelled) {
    } else {
        [self.client URLProtocol:self didFailWithError:error];
    }
    self.dataTask = nil;
}

#pragma mark - NSURLSessionDataDelegate

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    [self.client URLProtocol:self didLoadData:data];
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageAllowed];
    completionHandler(NSURLSessionResponseAllow);
    self.response = response;
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler {
    if (response != nil){
        self.response = response;
        [[self client] URLProtocol:self wasRedirectedToRequest:request redirectResponse:response];
    }
}

/*
如果实现这个代理方法,就可以通过该回调的 NSURLSessionTaskMetrics 类型参数获取到采集的网络指标,实现对网络请求中 DNS 查询/TCP 建立连接/TLS 握手/请求响应等各环节时间的统计。

 */
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)){

    NSTimeInterval startTime = [metrics.taskInterval.startDate timeIntervalSince1970];
    NSTimeInterval endTime = [metrics.taskInterval.endDate timeIntervalSince1970];
    NSTimeInterval duration = metrics.taskInterval.duration;;

    NSLog(@"%s :  %f  ---   %f   ----   %f   %f",__func__,startTime,endTime,duration,startTime - endTime);

    NSArray *arr = metrics.transactionMetrics;
    for (int i = 0; i < arr.count; i++) {

        NSURLSessionTaskTransactionMetrics *metrics = arr[i];
        NSLog(@"\n %@---\n网络协议  %@   \n---是否使用代理进行网络连接:  %d \n---网络加载类型: %ld --- %f ",
              metrics.request.URL.absoluteURL,
              metrics.networkProtocolName,
              metrics.isProxyConnection,
              (long)metrics.resourceFetchType,
              [metrics.fetchStartDate timeIntervalSince1970]);
    }
}

这幅图示意了一次 HTTP 请求在各环节分别做了哪些工作


iOS-利用NSURLProtocol进行网络监控_第1张图片
image.png
NSURLSessionTaskMetrics对象立马封装了 session task 的指标,每个 NSURLSessionTaskMetrics 对象有 taskInterval 和 redirectCount 属性,还有在执行任务时产生的每个请求/响应事务中收集的指标。
 然后我们看下立马的属性:
 transactionMetrics:数组里面对象是NSURLSessionTaskTransactionMetrics,包含了在执行任务时产生的每个请求/响应事务中收集的指标。
 taskInterval:任务从创建到完成花费的总时间,任务的创建时间是任务被实例化时的时间;任务完成时间是任务的内部状态将要变为完成的时间
 redirectCount:记录了被重定向的次数
 ###############################
 NSURLSessionTaskTransactionMetrics对象封装了任务执行时收集的性能指标,包括了 request 和 response 属性,对应 HTTP 的请求和响应,还包括了从 fetchStartDate 开始,到 responseEndDate 结束之间的指标,当然还有 networkProtocolName 和 resourceFetchType 属
 再看一下里面的属性:
 request:表示了网络请求对象
 response:表示了网络响应对象,如果网络出错或没有响应时,response 为 nil
 networkProtocolName:获取资源时使用的网络协议,由 ALPN 协商后标识的协议,比如 h2, http/1.1, spdy/3.1
 isProxyConnection:是否使用代理进行网络连接
 isReusedConnection:是否复用已有连接
 resourceFetchType:NSURLSessionTaskMetricsResourceFetchType 枚举类型,标识资源是通过网络加载,服务器推送还是本地缓存获取的
 对于下面所有 NSDate 类型指标,如果任务没有完成,所有相应的 EndDate 指标都将为 nil。例如,如果 DNS 解析超时、失败或者客户端在解析成功之前取消,domainLookupStartDate 会有对应的数据,然而 domainLookupEndDate 以及在它之后的所有指标都为 nil
 如果是复用已有的连接或者从本地缓存中获取资源,下面的指标都会被赋值为 nil:
 domainLookupStartDate
 domainLookupEndDate
 connectStartDate
 connectEndDate
 secureConnectionStartDate
 secureConnectionEndDate

 fetchStartDate:客户端开始请求的时间,无论资源是从服务器还是本地缓存中获取
 domainLookupStartDate:DNS 解析开始时间,Domain -> IP 地址
 domainLookupEndDate:DNS 解析完成时间,客户端已经获取到域名对应的 IP 地址
 connectStartDate:客户端与服务器开始建立 TCP 连接的时间
 secureConnectionStartDate:HTTPS 的 TLS 握手开始时间
 secureConnectionEndDate:HTTPS 的 TLS 握手结束时间
 connectEndDate:客户端与服务器建立 TCP 连接完成时间,包括 TLS 握手时间
 requestStartDate:开始传输 HTTP 请求的 header 第一个字节的时间
 requestEndDate:HTTP 请求最后一个字节传输完成的时间
 responseStartDate:客户端从服务器接收到响应的第一个字节的时间
 responseEndDate:客户端从服务器接收到最后一个字节的时间

你可能感兴趣的:(iOS-利用NSURLProtocol进行网络监控)