蘑菇街App Chromium网络栈实践

本文为『移动前线』群在5月26日的分享总结整理而成,转载请注明来自『移动开发前线』公众号。

嘉宾介绍

蘑菇街移动端资深开发工程师,曾就职于腾讯,百度,微软。专注移动端浏览器内核移植,开发以及优化。现在负责蘑菇街移动端跨平台组件开发。

为什么需要自有网络库

首先要介绍为什么需要一个自有的网络库,在应用开发过程中,为了节约开发成本,最直接的方式是使用系统提供的网络API,这种方案虽然能暂时节约开发成本,但是长期过程中会带来一些问题,例如无法解决系统提供库中存在的bug,无法添加自己对网络的优化等等。同时由于各个平台API不同,对各平台网络模块的维护也需要消耗一定资源。为了解决这些问题,我们需要一个自有的网络库,并且这个网络库能做到跨平台,并且相对于系统网络库有更好灵活性,更易于功能的扩展以及性能的优化。

蘑菇街目前自有网络栈是基于Chromium网络库改造而来。Chroium网络库本身针对网络性能有很多优化,因此在使用过程中会比使用系统网络库拥有更好的性能体验。我们针对Chromium网络库和系统网络库做了测试,得到了如下数据:

蘑菇街App Chromium网络栈实践_第1张图片

同时Chromium网络库对网络协议支持完善,并且且易于扩展,同时是一个跨平台库,这些优点也是选择Chromium网络库作为蘑菇街自有网络库的基础。

  • 协议支持

    目前Chromium网络库支持HTTP,HTTPS,SPDY,QUIC,HTTP2.0等协议,在目前所有网络库中所支持的协议是比较完全的,并且会随着标准的演进,继续更新。

  • 易用性

    Chromium网络库中的主要接口是URLRequest 和 URLRequestContext,接口简单明了,非常易于接入,但是由于接口都是异步接口,在希望使用同步调用的时候,需要进行一些封装。

  • 扩展性

    Chromium网络库安装网络层次进行了代码划分,因此可以根据需求在不同网络层进行扩展,满足业务需求。例如:可以方便的扩展域名解析过程,TSL加密过程。

  • 跨平台

    Chromium网络库代码使用C++编写,有非常强的跨平台能力,但是由于Chromium有自己编译体系,因此移植各个平台需要重新编写编译脚本。

网络库结构

蘑菇街App Chromium网络栈实践_第2张图片

当URLRequest被上层调用,而启动请求的时候,URLRequest会根据URL的scheme(URL请求类型,如http://,ftp://等)来决定需要创建什么类型的请求。URLRequest会创建的是一个URLRequestJob子类的一个对象,来处理相应的请求。例如scheme为http://时会创建URLRequestHttpJob,来处理Http请求。

当URLRequestHttpJob被创建后,首先从CookieManager中获取跟该URL相关联的Cookie信息。之后会通过HttpTransactionFactory创建一个HttpTransaction类的对象来开启一个HTTP连接的任务。通常情况下,HttpTransactionFactory对应的是一个它的子类HttpCache的实例。HttpCache类会使用本地磁盘缓存机制,如果该请求对应的回复已经在磁盘缓存中,那么无需再建立HttpTransaction来发起连接,直接从磁盘中获取即可。如果磁盘中没有,同时如果目前该URL请求对应的HttpTransaction已经建立,那么只要等待它的回复即可。这些条件都不满足后,实际上才会真正创建HttpTransaction。

HttpNetworkTransaction使用HttpNetworkSession来管理连接会话。HttpNetworkSession通过它的成员HttpStreamFactory来建立TCP Socket连接,之后创建HttpStream对象。HttpStreamFactory将和网络之间的数据读写交给自己新创建的一个HttpStream对象来处理。

最后进行套接字的建立。Chromium中的跟服务器建立连接的套接字是StreamSocket,它是一个抽象类,在不同太平上有分别不同的实现。

基于自有网络路能解决的问题

有了自有网络库之后,我们就能解决掉很多使用系统网络库无法解决的问题:

  • DNS劫持

    DNS劫持是移动网络经常遇见的问题,通常方案是采用HTTP协议访问自有DNS服务器,获取域名,IP映射,在访问域名的时替换成IP进行访问,但是在访问HTTPS服务的时候,无法直接替换,这个时候自有网络库就能发挥能力,再实现网络库的域名解析时,不使用系统的域名解析过程,而是使用自己现实的域名解析方案,从而获取正确域名解析,并且可以将这个过程提前,避免在建连过程中访问域名解析服务,从而提高连接速度。

    蘑菇街的网络库中就在网路库的HostResolverImpl类中添加了ExternalResolver,通过ExternalResolver将链接所对应的IP返回,如果ExternalResolver所执行的HTTP DNS失败之后,会采用正常DNS解析。

    蘑菇街App Chromium网络栈实践_第3张图片

  • 代理转发

    在电商类应用中,为了适用业务的快速迭代,会使用混合开发,这样就是使用的系统WebView,然后系统WebView有自己网络库实现,因此很多针对网络库的定制和优化讲无法使用。解决这类问题,可以使用基于自有网络库实现的代理服务,将WebView的网络请求代理到自有网络库上,再进行转发。

    蘑菇街在处理系统WebView请求的时候,为系统的WebView设置代理,将请求发送至本地端口。同时在网络库中实现了一个Http Proxy Server,能转发所监听端口的http,https请求,所有接收到的http,https请求,可以经过自己的网络库转发出去,这样所有自有网络库的修改,优化都可以生效。

  • 网络调试

    网络调试是网络开发过程中一个非常棘手的事情,自定义网络库有着非常大的灵活性,在自定义网络库的过程中可以实现Chrome Dev Tool的协议与Chrome浏览器进行通信,这样就能通过Chrome浏览器的Dev Tool进行网络调试,能直观的看到网络数据,以及耗时等信息。

    蘑菇街的网络库接口封装形式与HttpURLConnection一致,这样基于facebook提供的stetho开源库,使用其中的com.facebook.stetho.urlconnection这个包,将stetho接入的自己的网络库以实现Android与Chrome浏览器的通信。

  • 自定义协议

    HTTP协议在使用过程中有着不少缺陷,例如:HTTP协议非长连接,每次请求需要重新握手,这是一个非常消耗时间的过程。为了解决HTTP在之前设计过程中的不足之处,出现了很多解决方案,如SDPY,HTTP2.0等。但是此类的在部署,以及标准话过程中并不完善,因此自定义协议是更符合业务需求的。例如:蘑菇街针对现有业务场景,对TLS进行了改造,更换SSL加密算法,将RSA跟换为ECDHE,ECDHE加密算法较RSA速度更快。

自有网络库实现过程

Chromium网络库剥离

Chromium的网络库虽然非常强大,但是将Chromium网络库进行改造是一个非常艰难的过程。Chromium代码都是基于C++实现,并且有自己的编译体系,模块之间也有引用。使用Chromium网络库的第一步就是将网络库从Chromium庞大的代码中剥离出来。Chromium网络库依赖了非常多的第三库以及内部模块,编译过程就需要将这些库单独剥离出来编译。整个Chromium网络库依赖了以下库:

  • base:Chromium基础类库

  • nss:加密库

  • icu:Unicode支持库

  • zlib:压缩解压库

  • protobuf:Protocol Buffers

  • modp_b64:base64库

  • brotli:brotli压缩算法库

  • url_lib:url解析库

针对不同平台,需要建立不同的编译工程,例如Android iOS,在Chromium的编译过程中,这两个平台的编译都可以采用Chromium的编译系统进行编译,但是编译独立的网络库模块的时候,iOS会出现问题:

  • 编译过程问题

    其原因是单独编译出来的网络库与openssl库中有同名函数冲突,解决这个问题需要修改openssl,通过在头文件中通过宏定义修改函数名,替换掉库中所有同名函数。

  • 运行问题

    通过Chromium编译系统编译出来的iOS端网络库,会存在无法运行的情况,其原因是xcode编译与Chromium的编译有冲突,解决这个问题,需要根据Chromium网络库的编译文件,生成xcode工程进行编译。

相对于iOS平台,Android平台的编译相对简单,可以直接使用Chromium编译系统进行编译。直接使用ninja编译cronet模块就能生成相应的网络库。

网络库封装接入

在使用Chromium网络库过程中,首先需要对网络栈进行封装,不同的平台需要实现各自平台的Adapter层。

以Android平台为例,Chromium网络库提供了基础C++接口,为了方便Android平台应用使用,需要将Chromium网络库封装成Android系统常用网络库接口(HttpURLConnection),这样能无需进行大规模改动,就能非常方便的进行接入。

对于Chromium网络库是一个异步网络库,对于应用开发来说异步网络库不易于使用,因此需要将异步网络转化层同步接口,同时不能损失网络库的高效性。

包大小问题解决

Chromium网络库编译封装完成之后,在接入应用的时候需要考虑网络库的包大小,为了减小让网络库的大小对应用的体积的影响,蘑菇街的网络库使用了动态加载机制,蘑菇街自有网络库在线下载,动态安装。应用首次安装后,首先使用系统网络库,同时也会去下载蘑菇街自有网络库,在下载完成之后,通过蘑菇街的动态加载框架,动态加载网络库。

蘑菇街网络库架构

蘑菇街网络库被分为三层,最低层为协议支持层,提供对基本协议,自定义协议的支持。协议层上层为扩展层,是蘑菇街对网络库的扩展,已经定制,最上层为平台层,提供网络库在各个平台上的封装。

QA环节

Q:使用自定义网络库有哪些弊端?

A:包大小会增加,但是可以以动态加载方式解决。初始接入成本较高,需要改动底层代码,但是后期回报也会很高。

Q:请问技术开源吗?

A:chromium整个代码是开源的,我们自己的改动暂时没有开源,未来可能会考虑将自己代码开源出来

Q:支持本地缓存吗?无网可以访问吗?

A:支持本地缓存,并且我们讲本地缓存作为单独模块独立出来,可以提供给应用端其他模块使用。无网络情况下是不支持访问的。

Q:整套方案涉及到的库加起来,客户端体积增加有多大?

A:整个方案下来的库加起来增加了3m左右。iOS上经过stripe,以及手工剥离无用代码,整个网络库也是在3m左右。

Q:在iOS 平台,系统7及以下不支持动态库,是降级到系统库还是有别的办法?

A:在iOS上是不支持动态加载,整个网络库会直接链接到应用中。

Q:选择这种方案的初衷是什么,比起其他三方网络库有什么特别,就本次分享看来更优之处主要是跨平台意见方便用chrome调试,还有其他的目的么?

A:这个方案初衷是希望提升网络性能,并且能有更高的灵活性。对于其他第三方网络栈,chromium的网络栈有更多的协议支持,更高性能cache,还有预期功能,比如http预链接,dns预取等功能,不仅仅是方便调试。

Q:以前没接触这快,想咨询一下,这部分内容能应用到WebView的优化吗?

A:WebView优化有很多方面,如果有自己能修改内核的话,能做的就非常多,比如网络,渲染性能等等。如果是使用系统WebView的话,可以单独剥离Chromium网络栈优化之后,和我们做的那样使用chromium网络栈实现代理服务,通过走自己的网络栈来优化网络。

Q:为了减小让网络库的大小对应用的体积的影响,使用了动态加载机制,蘑菇街自有网络库在线下载,动态安装。—下载之后不也增加了应用的大小吗?并且也会增加网络请求,那么这么做的好处又是什么呢?

A:应用大小在动态加载之后会增加,但是对于应用的安装包来说是不会增加的,好处自然是能获得更好的网络性能,并且能扩展自己应用在的网络栈上面的需求。

Q:总的来说表述的基本上都对,对 TLS 进行改造说的就有点牵强了。TLS  在 Handshake 时有个Prefer Ciphers,这个在Server端可配置.... 优先EECDH即可…那么我的问题来了:蘑菇街有计划在自己业务中使用QUIC这类基于UDP的协议么? 我看你们RTT耗时非常长啊 ,蘑菇街在移动端有对SSL Session Cache或者SSL Session Ticket做过修改或者优化么? 或者已经做了那些优化?

A:我们现在正在调研使用spdy协议,spdy的tls握手过程比较耗时,目前正在尝试优化tls握手过程,希望能做到1-RTT或者0-RTT。

Q:上面提到网络架构有三层,是不是指最下面的协议支持层主要是chrominum项目中的代码,而蘑菇街实现自身需求的部分主要在扩展层中,后期如果chrominum代码版本更新了,只需更新最下面的协议支持层即可?另外扩展层对协议支持层的扩展,主要用什么设计模式实现的?

A:我们目前协议支持层在chromium源码中,主要扩展是在外层,这样会比较方便升级。协议支持层的改动,还是基本chromium代码本身进行修改

Q:因为现在的应用大部分做原生的,只是部分用HTML,所以用的都是系统的webview,现在主要是想对webview优化,加快访问速度,除了单独剥离Chromium网络栈优化,还能给分享一下其他优化点吗?

A:如果不对webview动手的话,能做的优化有限,我们目前使用自己的内核,所以能做系统webview不能做的优化。使用系统webview的话,可以根据需求做predict,prefetch。

Q:直接使用IP发请求是否可以避免dns劫持的问题?

A:直接使用ip时能避免劫持,但是在访问https的时候,需要域名不能直接使用ip,所以我们在域名解析的代码进行了hook插入自己的代码。

Q:启动后从服务器端下载动态库可能存在失败的情况,这时肯定是要用系统网络库,我猜测,蘑菇街app业务层调用的是自定义的一套网络访问接口,下面有基于chromium这一套和用系统接口的两套实现,在动态库没有正确加载时使用系统库的实现,对上层透明,是这样吗?

A:基本是你所说的,但是我们没有使用自定义的网络接口,我们用的还是希望标准的网络接口,我们在中间做了一层封装,会根据情况选择网络库。

你可能感兴趣的:(蘑菇街App Chromium网络栈实践)