本文为『移动前线』群在 5 月 26 日的分享总结整理而成,转载请注明来自『移动开发前线』公众号。
嘉宾介绍
蘑菇街移动端资深开发工程师,曾就职于腾讯,百度,微软。专注移动端浏览器内核移植,开发以及优化。现在负责蘑菇街移动端跨平台组件开发。
为什么需要自有网络库
首先要介绍为什么需要一个自有的网络库,在应用开发过程中,为了节约开发成本,最直接的方式是使用系统提供的网络 API,这种方案虽然能暂时节约开发成本,但是长期过程中会带来一些问题,例如无法解决系统提供库中存在的 bug,无法添加自己对网络的优化等等。同时由于各个平台 API 不同,对各平台网络模块的维护也需要消耗一定资源。为了解决这些问题,我们需要一个自有的网络库,并且这个网络库能做到跨平台,并且相对于系统网络库有更好灵活性,更易于功能的扩展以及性能的优化。
蘑菇街目前自有网络栈是基于 Chromium 网络库改造而来。Chroium 网络库本身针对网络性能有很多优化,因此在使用过程中会比使用系统网络库拥有更好的性能体验。我们针对 Chromium 网络库和系统网络库做了测试,得到了如下数据:
同时 Chromium 网络库对网络协议支持完善,并且且易于扩展,同时是一个跨平台库,这些优点也是选择 Chromium 网络库作为蘑菇街自有网络库的基础。
-
协议支持
目前 Chromium 网络库支持 HTTP,HTTPS,SPDY,QUIC,HTTP2.0 等协议,在目前所有网络库中所支持的协议是比较完全的,并且会随着标准的演进,继续更新。
-
易用性
Chromium 网络库中的主要接口是 URLRequest 和 URLRequestContext,接口简单明了,非常易于接入,但是由于接口都是异步接口,在希望使用同步调用的时候,需要进行一些封装。
-
扩展性
Chromium 网络库安装网络层次进行了代码划分,因此可以根据需求在不同网络层进行扩展,满足业务需求。例如:可以方便的扩展域名解析过程,TSL 加密过程。
-
跨平台
Chromium 网络库代码使用 C++ 编写,有非常强的跨平台能力,但是由于 Chromium 有自己编译体系,因此移植各个平台需要重新编写编译脚本。
网络库结构
当 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 解析。
-
代理转发
在电商类应用中,为了适用业务的快速迭代,会使用混合开发,这样就是使用的系统 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:基本是你所说的,但是我们没有使用自定义的网络接口,我们用的还是希望标准的网络接口,我们在中间做了一层封装,会根据情况选择网络库。
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论