ios安卓

APP网络监控-技术分享

2021-07-12  本文已影响0人  coder_feng

1.背景

随着业务的发展,用户对于网络的依赖场景会越来越多,随之而来遇到的各种异常网络场景也越来越多。

1、为了保证网络的接口的持续健康、及时发现问题,为网络性能优化提供数据基础。

2、同时以报表的形式直观的去展现现有网络质量。

3、也为了更好的支撑后续业务的发展,就需要我们去搭建起相应的网络监控体系。

二、目标

提供一套完整的网络采集、监控和预警的可视化机制,用于线上接口可用性和健康度观察,并提供一系列排查问题的辅助信息。

通过仪表盘可视化呈现网络质量,包括但不限于以下能力:

三、总体方案

网络接口监控架构.png

不管是 iOS 还是 Android ,两者最终要做的目标方案如上图。这里从下到上分别阐述每个部分的功能:

网络基础库:针对平台特性,这里有 iOS 的 AFnetworking、Alamofire 和 Android 的 okhttp3、okhttp4 ,其实现原理应该都差不多,都是针对底层的网络api进行进一步的封装,提高接口的易用性。

拦截器:主要是针对各个基础网络库进行接口拦截。根据平台不同,iOS 主要使用 NSProtocol + Hook,Android 使用 Aspect 。

网络封装库:一般开发过程都会针对基础的网络库再做二次封装,加入一些策略、缓存、安全校验等管理,使其更加贴合业务和快速接入使用。

插件/功能模块:以插件化的形式提供额外的网络功能

统计模块:将从业务开始调用到回调给业务方的各个环节的耗时及状态值,变成统计数据汇报到APM。
网络诊断模块:对关键业务进行诊断,包括dns解析、ping、弱网检测等,输出诊断报告并上报到APM。
重试模块:根据策略进行重试,包括 ip 重试、https 降级重试、原 url 重试等。
httpdns模块:提供 httpdns 能力,解决域名劫持问题。
上传模块:提供上传能力,包括断点续传、分片上传以及包体大小、上传耗时等信息监控。
下载模块:提供下载能力,包括大文件下载、断点续传以及包体大小、下载耗时等信息监控。
mock 模块:提供 mock 能力,主要用于测试和后台接口还没有准备好的情况下使用。
对外接口层:这一层直接对接上层业务。

四、具体实现

1)请求方式

iOS 常用的第三方网络 AFNetworking、Alamofire 基本都是基于 NSURLConnection 或者是 NSURLSession 的封装,其中 NSURLConnection 是比较旧的使用方式了,而 NSURLSession 则是比较新的也是比较被推荐的使用方式。

2)底层原理

在使用 NSURLConnection 和 NSURLSession 进行网络请求的时候,实际上走的都是更底层的 URL Loading System,URL Loading System 使用标准协议 https 或者自定义协议访问标识资源,本身支持 http,https,文件,ftp 和数据协议。

可以通过继承 NSURLProtocol 实现一个自定义的 Protocol,然后调用 registerClass:方法注册到 URL Loading System 中去,这样 NSURLConnection、NSURLSession 或者是 NSURLDownload 在使用 NSURLRequest 初始化一个连接的时候,URL Loading System 就会

将按照注册时的相反顺序询问每个注册的类,询问到第一个 +canInitWithRequest: 方法返回 YES 的时候则使用该类去处理请求。

可以看到 OHHTTPStubs 开源库在注册子类的时候也是这样处理的

+ (void)setEnabled:(BOOL)enable forSessionConfiguration:(NSURLSessionConfiguration*)sessionConfig
{
    // Runtime check to make sure the API is available on this version
    if ([sessionConfig respondsToSelector:@selector(protocolClasses)]
        && [sessionConfig respondsToSelector:@selector(setProtocolClasses:)])
    {
        NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses];
        Class protoCls = HTTPStubsProtocol.class;
        if (enable && ![urlProtocolClasses containsObject:protoCls])
        {
            // 将自己的 NSURLProtocol 插入到 protocolClasses 的第一个,进行拦截
            [urlProtocolClasses insertObject:protoCls atIndex:0];
        }
        else if (!enable && [urlProtocolClasses containsObject:protoCls])
        {
            // 拦截完成后移除
            [urlProtocolClasses removeObject:protoCls];
        }
        sessionConfig.protocolClasses = urlProtocolClasses;
    }
    else
    {
        NSLog(@"[OHHTTPStubs] %@ is only available when running on iOS7+/OSX9+. "
              @"Use conditions like 'if ([NSURLSessionConfiguration class])' to only call "
              @"this method if the user is running iOS7+/OSX9+.", NSStringFromSelector(_cmd));
    }
}
3)实现步骤

利用 Objc 运行时 hook 掉 NSURLSessionConfiguration 的 defaultSessionConfiguration 属性和 ephemeralSessionConfiguration 属性设置,然后修改 configuration 的 protocolClassess 属性,插入我们自定义的 Protocol
在自定义的 NSURLProtocol 之类中实现如下方法:
+ canInitWithRequest: 在这里判断当前网络请求是否需要监控,如果不需要直接 return NO 即可。

      + canonicalRequestForRequest:   生成一个新的 request 请求,同时标识该请求已经处理过,防止死循环。

      - startLoading  将新的请求发送出去,设置对应的回调代理。

      - stopLoading   停止网络请求。

 3. 处理请求回调,实现需要进行处理的回调方法,处理完成后通过 self.client.urlProtocol 将回调方法传回至原来的 delegate。

 4. 至此,我们就完成了发送、接收等一系列操作,并且完美的将回调转发回了原来的代理方,剩下的就是我们在回调中收集完了请求的各种信息就好。

流程图如下:


网络拦截前后对比图.png
4)可能存在的问题及优化

流程并不复杂,从上图可以看到,使用了网络拦截之后的流程图比原本的多了一个 custom protocol(DLURLProtocol)和 custom session。custom potocol 用于拦截网络请求,custom session 用于发起新的请求。

这里可能会存在两个问题:

每个请求都会新创建一个 NSURLSession,对于网络请求这种很频繁的操作来说不是很友好;
新创建的 NSURLSession 如何确保超时、缓存、认证、cookies 等策略跟原始的 NSURLSession 保持一致,如果不一致是否会影响到既有的网络请求?

五、风险评估

针对可能存在的问题做相关梳理和验证~

关于第一点:每个请求都会创建一个 NSURLSession 这个很好解决,使用一个单例即可,从苹果的官方Demo CustomHTTPProtocol 中可以看到 Demux 这个类,通过阅读源码知道,该类的存在除了最大化复用 Session 之外,还将请求的发起和回调都放到了这个类进行处理,确保请求发起和回调都是在同一个线程和 Runloop Mode,至于为什么要这么做,文档中没有找到明确说明,不过后面踩坑的时候才发现,如果不这么做的话,在回调里面很容易就会遇到崩溃的情况,尽管你什么都没有做。

至于第二点:新创建的 NSURLSession 是否会影响到原来的网络请求策略?

思考:

根据苹果提供的Demo CustomHTTPProtocol 中可以看到,同样也是通过新创建一个 NSURLSession 发起请求的,那么它难道不会出现超时、缓存、认证等参数和原始请求不一致的情况么?

从逻辑上来说,要么就是要获取原始请求的 session,拿到对应的超时、缓存、认证等配置信息再发起请求;要么就是 Demux 中新创建的 session 对于请求发起方来说是透明的,这种透明包括不影响任何原始请求的参数配置!

针对以上猜想做相关验证:

5.1、超时验证:

验证1:原始请求设置超时为 5s,Demux 设置超时时间为 60s,手机网络设置为100% lost

验证结果:5s 触发超时

`2021``-``03``-``28` `18``:``44``:``11.307007``+``0800` `NSURLProtocolTest[``36460``:``8443172``] start a request...`

`2021``-``03``-``28` `18``:``44``:``16.381879``+``0800` `NSURLProtocolTest[``36460``:``8443172``] Task <CDCBC81D-E0C5-4A52-BDBF-5912AC0BCA32>.<``1``> finished with error [-``1001``] Error Domain=NSURLErrorDomain Code=-``1001` `"The request timed out."` `UserInfo={_kCFStreamErrorCodeKey=-``2102``, NSUnderlyingError=``0x2836d19e0` `{Error Domain=kCFErrorDomainCFNetwork Code=-``1001` `"(null)"` `UserInfo={_kCFStreamErrorCodeKey=-``2102``, _kCFStreamErrorDomainKey=``4``}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <CDCBC81D-E0C5-4A52-BDBF-5912AC0BCA32>.<``1``>, _NSURLErrorRelatedURLSessionTaskErrorKey=(`

`"LocalDataTask <CDCBC81D-E0C5-4A52-BDBF-5912AC0BCA32>.<1>"`

`), NSLocalizedDescription=The request timed out., NSErrorFailingURLStringKey=https:``//www.baidu.com, NSErrorFailingURLKey=[https://www.baidu.com](https://www.baidu.com/), _kCFStreamErrorDomainKey=4}`

验证结果:60s 触发超时

| 

`2021``-``03``-``28` `19``:``02``:``29.918506``+``0800` `NSURLProtocolTest[``36473``:``8448954``] start a request...`

`2021``-``03``-``28` `19``:``03``:``29.869946``+``0800` `NSURLProtocolTest[``36473``:``8448954``] Task <A533832C-C1E2-48B4-9C73-50447B930141>.<``1``> finished with error [-``1001``] Error Domain=NSURLErrorDomain Code=-``1001` `"The request timed out."` `UserInfo={_kCFStreamErrorCodeKey=-``2102``, NSUnderlyingError=``0x283d144b0` `{Error Domain=kCFErrorDomainCFNetwork Code=-``1001` `"(null)"` `UserInfo={_kCFStreamErrorCodeKey=-``2102``, _kCFStreamErrorDomainKey=``4``}}, _NSURLErrorFailingURLSessionTaskErrorKey=LocalDataTask <A533832C-C1E2-48B4-9C73-50447B930141>.<``1``>, _NSURLErrorRelatedURLSessionTaskErrorKey=(`

`"LocalDataTask <A533832C-C1E2-48B4-9C73-50447B930141>.<1>"`

`), NSLocalizedDescription=The request timed out., NSErrorFailingURLStringKey=https:``//www.baidu.com, NSErrorFailingURLKey=[https://www.baidu.com](https://www.baidu.com/), _kCFStreamErrorDomainKey=4}`

结论:Demux 中新创建的 NSURLSession 超时时间设置不影响到请求发起方。

5.2、缓存验证:

验证1:原始请求设置为不使用缓存 NSURLRequestReloadIgnoringLocalCacheData,Demux 中设置为使用缓存 NSURLRequestReturnCacheDataElseLoad ;通过 charles 抓包确认在收到 completed 的时候是否有真正发起请求。

验证结果:每次点击开始请求按钮的时候, charles 都能抓到请求数据包,且 response code 为 200。

验证2:原始请求设置为使用缓存 NSURLRequestReturnCacheDataElseLoad,Demux 中设置为不使用缓存 NSURLRequestReturnCacheDataElseLoad;通过 charles 抓包确认在收到 completed 的时候是否有真正发起请求。

验证结果:卸载App,首次点击请求按钮的时候可以在 charles 中抓到请求数据包;后面再次点击的时候就没有抓到相关请求数据包了,但却返回到了 completed 回调,且 response code 为 200

结论:Demux 中新创建的 NSURLSession 缓存设置不影响到请求发起方。

Snip20210712_6.png
5.3、认证策略验证:

验证1:由于条件限制,我们这里只做单向验证,即验证服务器证书。在请求方的回调 URLSession: didReceiveChallenge: completionHandler: 回调里面对服务器证书与本地正式的校验,校验通过则返回 completionHandler(NSURLSessionAuthChallengeUseCredential,credential);;然后在 DLURLProtocol 的 URLSession: didReceiveChallenge: completionHandler: 回调中直接设置为校验不通过

completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL);

验证结果:触发请求方的超时设置!

结论:会影响到请求方,尽管在请求方的 URLSession: didReceiveChallenge: completionHandler:回调里面调用了认证通过的completionHandler,一样会触发超时操作!

规避方法:网络监控所需要的信息采集不涉及到认证这块,可以直接将回调抛给请求方,由请求发起方进行处理。

5.4、耗时验证:

验证1:相隔10ms,异步轮流发起请求分别请求 www.baidu.comwww.sina.com.cnwww.taobao.com 这几个域名,加起来总共请求 100 次,然后计算使用 NSProtocol 和不使用 NSProtocol 的平均耗时。

验证结果:

//有接入:
2021-05-11 00:36:39.112549+0800 NSURLProtocolTest[90129:29124516] baidu, count:26 , avgDuration:324.019181
 
2021-05-11 00:36:39.112666+0800 NSURLProtocolTest[90129:29124516] sina, count:46  avgDuration:553.305805
 
2021-05-11 00:36:39.115587+0800 NSURLProtocolTest[90129:29124516] taobao, count:28  avgDuration:300.874954

//无接入:
 
2021-05-11 00:29:52.958785+0800 NSURLProtocolTest[90066:29117542] baidu, count:35 , avgDuration:306.175676
 
2021-05-11 00:29:52.958941+0800 NSURLProtocolTest[90066:29117542] sina, count:29  avgDuration:321.528200
 
2021-05-11 00:29:52.959113+0800 NSURLProtocolTest[90066:29117542] taobao, count:36  avgDuration:297.670796

结论:除去网络波动影响,耗时基本相近。
详细日志:(存放在百度网盘上面的网络监控文件夹)

六、参考链接

iOS 中的网络调试

爱奇艺全链路自动化监控平台的探索与实践

深度理解 NSURLProtocol

移动端APM网络监控与优化实践

URL Session Programming Guide

CustomHTTPProtocol

上一篇下一篇

猜你喜欢

热点阅读