已授权发表至《iOS成长之路3期·WWDC17内参》


本文将介绍 iOS11 下网络层(NSURLSession)的一些变化。这篇文章分4部分来总结了Session 707和709:

  1. iOS11中网络层新出的功能
  2. iOS11中网络层优化的功能
  3. iOS11中网络层的最佳实践
  4. 苹果对网络层未来的规划


因为这一次很多内容其实是苹果用新的技术来给网络层做的优化,所以新功能优化很难界定。我这边采用的判断标准是:


  1. 如果是需要工程师代码配合的优化,就算是新功能
  2. 如果是不需要工程师代码配合的,哪怕是应用了新技术,我也把它归类为优化




1. iOS11中网络层新出的功能



1.1 Network Extension Framework有新API


提供了两个新类:NEHotSpotConfiguration、NEDNSProxyProvider。


  • NEHotSpotConfiguration

NEHotSpotConfiguration可以让你的智能设备在链接手机App之后,能够很方便地通过在手机App上的操作来实现热点的链接。

例如你买了一个网络摄像头,你想要连上摄像头的Wi-Fi热点去配置这个摄像头的话,以前要这么操作:

图1

现在用NEHotSpotConfiguration就能很方便地搞定事情了:

图2

当然,这套API也可以被拿来模拟各种网络环境,在测试App的时候很有用。


  • NEDNSProxyProvider

NEDNSProxyProvider可以用来设置你的手机如何跟DNS做交互。你可以自己发DNS请求,也可以自己基于不同的协议去做DNS查询。例如DNS over TLS,DNS over HTTP。




1.2 可以进行多路多协议的网络操作(Multipath Protocols for Mobile Devices)



在之前的iOS版本中,网络如果要从Wi-Fi模式变成Cellular(2/3/4G)模式,那就是先断掉Wi-Fi然后再连上Cellular,然后数据包就从原来的Wi-Fi链路迁移到Cellular链路去发送。


在链路切换过程中链接肯定就断掉了,为了解决这个问题,苹果爸爸搞了Multipath Protocols for Mobile Devices(移动设备多路协议),这使得移动设备的TCP包可以在这两个(多个)链路上随意切换着发(同时开启两个流量链路),而不必断线重连。


效果就是:Wi-Fi和Cellular可以共存,相互辅助。有三个模式,前两个可选,第三个只能自己私底下玩:



  • Handover Mode(高可靠模式)


这种模式下优先考虑的是链接的可靠性。只有在Wi-Fi信号不好的时候,流量才会走Cellular。如果Wi-Fi信号好,但是Wi-Fi很慢,这时候也不会切到Cellular链路。


这种模式在beta版中已经支持了。



  • Interactive Mode(低延时模式)


这种模式下优先考虑的是链接的低延时。系统会看Wi-Fi快还是Cellular快。如果Cellular比Wi-Fi快,哪怕此时Wi-Fi信号很好,系统也会把流量切到Cellular链路。


这种模式在未来的beta版会支持。



  • Aggregation Mode(混合模式)


在这种模式下,Wi-Fi和Cellular会同时起作用。如果Wi-Fi是1G带宽,Cellular也是1G带宽,那么你的设备就能享受2G带宽。


嗯,很好很强大。但你只能拿来玩,不能生产环境中使用。


因为苹果爸爸不想让你用。其实他说的理由是希望开发者自己好好考虑1和2用哪个,因为如果3也能用的话,苹果爸爸知道你们就完全不会考虑1和2了,直接用3了。反正用户流量不是开发者掏钱。


这个模式只能够在系统设置里面自己开启来玩,不能像1和2那样可以让开发者在应用中通过NSURLSessionConfiguration.multipathServiceType自己选。



需要注意的是,Multipath Protocols for Mobile Devices这个功能同时也需要服务端支持MPTCP(Multipath TCP)才行,如果服务端不支持的话,光客户端支持没用。linux起了一个项目在做这个事情,项目地址:https://multipath-tcp.org。有兴趣的同学可以自己去看一下。




1.3 ProgressReporting协议



iOS11提供了ProgressReporting协议,并且NSURLSessionTask实现了这个协议,让你能够获得progress对象,这个progress对象可以以0~1.0的方式告诉你当前进度,而不用你自己去拿到已获得的数据量去除以需要获得的数据总量从而得出进度。因为有的时候你并不一定能够拿到数据总量。然后这个progress对象跟NSURLSessionTask的绑定是双向的:你调用progress对象的cancel、pause、resume也会使得task变为cancel、pause、resume,反之亦然。


图3



1.4 URLSessionStreamTask 和 authentication proxies


你需要使用URLSessionStreamTask去替代之前的NSInputStream/NSOutputStream,它支持:



  1. 使用host和port来进行TCP/IP连接
  2. 支持基于STARTTLS的安全握手协议
  3. 支持Navigation of authenticating HTTPS Proxies。事实上就是:如果你是通过proxy访问的网络,当proxy问你要证书的时候,iOS11会自动帮你从keychain里面找到证书给出去。




1.5 URLSession Adaptable Connectivity API



以前进行网络调用时,如果网络不通,那么系统就会报个错告诉你网络不通。这时候你要么轮询要么让用户手动retry,然后网络通了请求才能发送出去。


现在你可以这么做:如果请求发送的时候网络不通,那么这个请求就会等到网络通了的时候再发出去。于是你就不用轮询了,用户也不用手动retry了。


具体做法:把NSURLSessionConfiguration的waitsForConnectivity设置成YES,拿它去生成NSURLSession去使用就好了。




1.6 URLSessionTask Scheduling API


这其实算是一种优化,但还是需要工程师代码配合的,它主要解决了三个问题:


  1. 没必要因为创建NSURLSessionTask而进行额外的后台加载
  2. 当后台请求创建但还没发出去的时候,这个被创建的请求有可能因为上下文变化的原因导致这个请求无意义
  3. iOS系统并不知道什么时候去发起你的请求才是最合适的



原先你要做后台数据加载的时候,流程是这样的:

图4

现在iOS11里,苹果把头两个步骤合并为一个步骤了:

图5


所以当应用在前台的时候,你就可以创建这个NSURLSessionTask,iOS11里面就不会在后台额外launch一次去创建NSURLSessionTask。这就解决了问题1。


iOS11在NSURLSessionTask的delegate里面提供了一个新的方法:urlSession:task:willBeginDelayedRequest:completionHandler:。系统在发起请求之前会调一个这个回调,然后在这个completionHandler里面你告诉系统这个请求是否要发出去,是否要修改。从而解决了问题2。


iOS11也给了NSURLSessionTask一个property:earliestBeginDate。系统在earlistBeginDate之前是不会发起这个请求的。你给这个task设置一个earliestBeginDate,就解决了问题3。


最后小胖子又补充了一下,你可以通过设置NSURLSessionTask的countOfBytesClientExpectsToSend和countOfBytesClientExpectsToReceive来让系统更好地调度你的后台网络任务。




2. iOS11中网络层优化的功能




2.1 Explicit Congestion Notification(ECN,显式拥塞通知)




先说一下ECN的好处:


  1. 可以最大化地使用网络带宽
  2. 减少包重发的次数,从而降低延迟,可以提高用户体验


然后说说ECN到底是什么:


显式那就有隐式。在以前,TCP/IP发现拥塞的方法是看有没有丢包情况,如果有,那它就认为当前网络有拥塞。于是TCP/IP就会降低发包速率,避免拥塞。这个过程我们可以理解为隐式拥塞通知显式拥塞通知就是TCP/IP会收到打上拥塞标记的数据包,TCP/IP发现这个拥塞标记的话,就认为网络出现了拥塞,从而降低数据吞吐量,最终避免恶化网络拥塞现象。


因为通过丢包来发现网络拥塞(隐式拥塞通知)是一件非常消耗成本的事情。接收方在发现丢包之后,需要重新要求发送方来发送之前丢的包,这就额外占用了资源。所以大家就想能不能不用通过丢包的方式来表达网络拥塞的状态,从而让发送端降低发包速率,缓解拥塞。


所以就有了显式拥塞通知。在支持ECN的链路上,如果出现拥塞现象的话,链路不会丢包,而是会在包的header里打上一个标记。接收方拿到这个带着标记的包之后,就认为网络有拥塞现象了,于是就可以降低发包速率,从而缓解网络拥塞。


最后,ECN是需要SQM算法(Smart Queue Management,智能队列管理)支持的。


这个功能在iOS10.3的时候就已经有了。当链路支持显式拥塞通知的时候,iOS10.3上会有50%的链接使用显式拥塞通知来表达网络链路的拥塞状态。从iOS11开始,在支持显式拥塞通知的的链路上,100%的链接都会使用这个技术来表达网络链路的拥塞状态。




2.2 iOS11里的网络操作被移动到User Space去了



原来网络操作都是内核去处理的,现在由每个App各自去处理了。


其实这一段我没有听太懂,说是这么做能够减少更多的上下文切换,从而匀出更多的时间让CPU去处理UI方面的事情,但我感觉好像本质上没什么变化?后来群里AloneMonkey同学说:user到内核会有软中断,涉及到user space的上写文参数到kernel space的映射拷贝,会有比较大的消耗,所以一般会尽量减少这种切换。因此事实上这种优化能够降低不必要的开销,从而提高应用执行的效率。


这么一来的话,以前使用Network Kernel Extension(OS X下)的同学,就不要用了,将来会被deprecate掉。换成Network Extension Framework就好。




2.3 Brotli Compression



iOS11支持Brotli压缩算法(RFC7932)。需要在HTTPS下才能使用,HTTP请求里的Header的Content-Encoding的值是br就表示使用了Brotli压缩算法。这套压缩算法相比gzip的压缩效率提高了15%。苹果浏览器Safari使用了NSURLSession,所以Safari也支持了Brotli。




2.4 Public Suffix List Updates



iOS11更新了Public Suffix List。


补充一下,Public Suffix List能够带来的主要好处有三个:避免超级cookie导致的隐私泄漏、让你的地址栏上的public suffix部分可以高亮、可以更好地对历史URL进行排序。所谓的Public Suffix就是域名的后半部分:.com .co .uk(不完全举例)这些。这个列表告诉了客户端(往往是各大厂商的浏览器)如何去区分域名的边界。


关于Public Suffix List Updates的具体介绍可以看https://publicsuffix.org




3. iOS11中网络层的最佳实践



3.1 IPv6



苹果爸爸在说:IPv6各种好,大家快来用。如果你不支持IPv6,爸爸就不让你上架。


要支持IPv6的话,老老实实用NSURLSession或者CFNetwork就OK了。


不要做的事情:


  1. 不用历史遗留的IPv4 API
  2. 不要直接用IPv4的地址做链接,应该用域名去做请求
  3. 发包前不要做各种检查:比如你在建立链接之前想看一下我当前这个设备是不是IPv4的地址,这种做法就不行
  4. 不要直接使用socket去发起请求



之前我们team也有App上架被拒,原因说的是IPv6不通过。但事实上如果你非常确认以上这些不要做的事情你没做,那么很有可能就是苹果审核人他网络不好,连不到你的服务器(现在还是有一大部分应用的审核在美国)。这类错误苹果都会在邮件中说你不支持IPv6,所以不要被它迷惑了。



3.2 不要引入其他的网络库,要使用苹果自己的API


苹果并不是在说AFNetworking、Alamofire不能用。这些第三方库本质上还是基于NSURLSession,也就是苹果的API去开发的。所以用它们没问题。


苹果的意思是不希望你使用别的基于Socket开发的网络库,例如:ACE、Asio这些。因为苹果的NSURLSession针对自家设备的特点,结合各种网络条件,针对电量、临时/后台请求等做了一系列优化。若是你不用NSURLSession去做网络请求,那这些优化就都没了,苹果后续新版本给到的新功能也会用不上了。




3.3 注意timeoutIntervalForResource和timeoutIntervalForRequest的区别



timeoutIntervalForResource是表示数据没有在指定的时间里面加载完,默认值是7天。


timeoutIntervalForRequest是表示在下载过程中,如果某段时间之内一直都没有接收到数据,那么就认为超时。


举个例子就是,如果你要下一个10G的数据,timeoutIntervalForResource设置成7天的话,你的网速特别慢:0.1k/s,7天都没下载完,那就超时了。虽然整个过程中,你一直在源源不断地下载。


如果你要下一个10G的数据,timeoutIntervalForRequest设置为20秒的话,下的过程中有超过20s的时间段并没有数据过来,那么这时候就也算超时。




3.4 一般来说一个App就一个NSURLSession就够了



以前迁移NSURLConnection到NSURLSession的时候,会有人每次都创建新的NSURLSession,但事实上这是没必要的。各个并行的NSURLSessionTask可以共享同一个NSURLSession。真正会使用多个NSURLSession的情况,老头就举了个例子:如果你用safari去开隐私模式的窗口访问网络,那么每个窗口就是一个新的NSURLSession,从而避免数据泄漏。


最后,如果你使用了多个NSURLSession的话,记得清理就好,不清理的话苹果是会产生内存泄漏的。




3.5 NSURLSession的delegate方法和快手block方法不要同时使用



如果你用了block,那么delegate就不会回调了。这事情仅有两个特例是两个都回调的:taskIsWaitingForConnectivity和didReceiveAuthenticateChallenge。




4. 苹果对网络层未来的规划



4.1 TLS1.3



苹果要把网络库整体迁移到支持TLS1.2,年底TLS1.3的标准应该能出来。现在基于TLS1.3草稿的实现可以弄下来自己测试着玩了。


最新的TLS1.3草稿已经出到21了:draft-ietf-tls-tls13-21


TLS1.3提供了加密的HTTP链接,也就是HTTPS。相对于TLS1.2来说,TLS1.3在安全和执行效率上都有提高。


在安全上,TLS 1.3放弃了很多原来TLS 1.2上的加密算法,毕竟都是上世纪90年代的算法了,现在那些算法安全性已经不高了。例如SHA-1和RC4就已经不用了。


在速度上,TLS 1.3主要是通过减少握手次数来实现速度提升的。TLS 1.2上的两轮握手在TLS 1.3上只需要一轮就可以建立连接了。



4.2 QUIC


Google搞了个QUIC,苹果在跟进。QUIC可以理解成UDP实现的TCP+TLS+HTTP/2集合体。主要是提高了数据传输效率和链接效率:


  1. 极大降低了链接建立的时间
  2. 增强了拥塞控制机制
  3. 无阻塞的多路传输
  4. 通过数据冗余传送来实现的错误控制机制(接收端会识别重复数据从而将其筛掉,最终使得错误率尽可能低)
  5. 链接迁移(由于QUIC是UDP实现的,因此一个“链接”并不要求一定是端对端,可以几台设备同时处理一个“链接”上的数据)


目前QUIC的开发才刚刚开始,项目网站提供了玩具客户端和玩具服务端给大家玩:Playing with QUIC





总结

我个人比较喜欢的是设备支持了多路多协议的网络操作,这个功能可以极大地提高应用体验。不过也会有用户认为这个功能会导致额外的流量消耗,这就比较矛盾了。


我个人比较讨厌的是不允许delegate和block同时使用的这一项,相信大家也没少被这个事情坑过。因为delegate和block都有各自的适用场景,苹果这么一做,哪怕API调用时的业务场景是一样的,但只要适用场景不同,就也得创建多个NSURLSession了,否则使用起来就很别扭。我可以理解苹果这么做的目的是认为有些事件不适合做两次:delegate一次,block又一次。但这种做法其实并没有必要,应该由使用者来决定在delegate和block都有实现时,应该如何处理:是两个都调、只调block不调delegate、只调delegte不调block。


总的来说这一轮苹果对NSURLSession的改造都很中规中矩,即便是给的新功能,绝大多数也都是偏优化方向。




参考





评论系统我用的是Disqus,不定期被墙。所以如果你看到文章下面没有加载出评论列表,翻个墙就有了。




本文遵守CC-BY。

请保持转载后文章内容的完整,以及文章出处。本人保留所有版权相关权利

如果您觉得文章有价值,可以通过支付宝扫描下面的二维码捐助我。


Comments

comments powered by Disqus