Android项目引入QUIC实践

2,322 阅读7分钟

一、背景

QUIC 是快速 UDP 网络连接( Quick UDP Internet Connections )的缩写,是由 Google 于 2013 年提出,并研发的一种基于 UDP 协议的低时延多路并发传输协议。经过几年的发展,在 2018 年的 IETF 会议中,HTTP-over-QUIC,或者更确切的说 HTTP2-over-QUIC 协议,被正式重命名为 HTTP3 ,开始了 HTTP3 的标准化过程。

QUIC 实现主要有 2 个分支:

Google 的 gQUIC:QUIC 最初由 Google 开发,现在 Google 版本的 QUIC 称为 gQUIC,常用的如 gQUIC 的 39、43、44、46 版本,国内甚至有用更早的版本。

IETF 的 iQUIC:区别于 Google 的 gQUIC,由 IETF 主导,HTTP3 标准基于 iQUIC,当前最新版为草案-29,常用的有 27,28,29 版本。草案-27 后 iQUIC 的变动已经非常小,趋于稳定,有望在最近 1-2 年内正式发布。

QUIC 与 TCP/HTTP 方案相比,有以下优势:

  • 通过减少往返次数,缩短连接建立时间,极大的降低延迟,实现 0-RTT
  • 基于 UDP 协议的多路复用,解决 HTTP2 在 TCP 层面的队头阻塞问题
  • 使用一个随机数代替 IP + PORT 来标识连接,实现连接迁移
  • 改善网络拥塞控制,可插拔和灵活定制流控算法
  • 使用“前向纠错”恢复丢失的包,以减少超时重传

基于以上优势,由部门服务端同学发起,共同推进了接入QUIC协议的工作。

二、Android 上引入QUIC

在Android上我们使用 Cronet 库作为 QUIC 的网络库,Cronet是 Chromium 网络堆栈,可作为库提供给 Android 应用。Cronet 利用多种技术来减少延迟并提高应用正常运行所需的网络请求吞吐量。

  • Protocol support Cronet 本身支持 HTTP 协议、HTTP/2 协议和 QUIC 协议。
  • Request prioritization 该库允许您为请求设置优先级标记。服务器可以使用优先级标记来确定处理请求的顺序。
  • Resource caching Cronet可以使用内存或磁盘缓存来存储在网络请求中检索到的资源。后续请求将自动从缓存中提供。
  • Asynchronous requests 默认情况下,使用Cronet Library发出的网络请求是异步的。在等待请求返回时,不会阻止您的工作线程。
  • Data compression Cronet使用Brotli压缩数据格式支持数据压缩,有研究表明,对于文本文件,相同的压缩质量下,brotlin 通常比 gzip 高出了20%的压缩率

简介:developer.android.com/guide/topic…

1、引入

引入Cronet库

海外

如果你负责的App是面向海外的,默认用户的手机上都有GooglePlay,那么我建议使用官方推荐的方式,即通过gms安装的方式:

依赖库引入:

implementation "com.google.android.gms:play-services-cronet:17.0.1"

通过谷歌服务加载依赖:

    private void initCronetProvider() {
        Task<Void> installTask = CronetProviderInstaller.installProvider(App.getContext());
        installTask.addOnCompleteListener(new OnCompleteListener<Void>() {

            @Override
            public void onComplete(@NonNull Task<Void> task) {
                if (task.isSuccessful()) {
                    // cronet install success
                } else {
                    // cronet install failed
                }
            }
        });
    }

Cronet提供了一个DEMO: github.com/GoogleChrom… 具体如何写,可以参考这个代码。

国内

如果是在国内,由于众所周知的原因无法依赖上面的方式,就需要开发者自行引入相关的依赖,对应的包体积也会增加2-3M。

implementation "org.chromium.net:cronet-common:101.4951.41"
implementation "org.chromium.net:cronet-embedded:101.4951.41"

注意这里cronet 库具有非常多的版本,具体使用哪个版本建议各个端协调沟通清楚。

2、与OkHttp结合

由于我们项目中使用的网络库都是及用途OkHttp构建的,代码逻辑众多,如果要完全抛弃OkHttp的API,目前看几乎是不可能的,所以最后的办法一定是基于当前的OkHttp接口来改造。

好在Cronet团队提供了一个库,使得OkHttp用户可以使用Cronet作为他们的传输层,从中受益于QUIC/HTTP3支持或连接迁移等特性。该库也可以与其他基于OkHttp的库一起使用,例如Retrofit、Coil等。

implementation "com.google.net.cronet:cronet-okhttp:0.1.0"

github地址: github.com/google/cron…

3、代码调用

构建CronetEngine对象

private static CronetEngine createDefaultCronetEngine(Context context) {
    // Cronet makes use of modern protocols like HTTP/2 and QUIC by default. However, to make
    // the most of servers that support QUIC, one must either specify that a particular domain
    // supports QUIC explicitly using QUIC hints, or enable the on-disk cache.
    //
    // When a QUIC hint is provided, Cronet will attempt to use QUIC from the very beginning
    // when communicating with the server and if that fails, we fall back to using HTTP. If
    // no hints are provided, Cronet uses HTTP for the first request issued to the server.
    // If the server indicates it does support QUIC, Cronet stores the information and will use
    // QUIC for subsequent request to that domain.
    //
    // We recommend that QUIC hints are provided explicitly when working with servers known
    // to support QUIC.
    return new CronetEngine.Builder(context)
            // The storage path must be set first when using a disk cache.
            .setStoragePath(context.getFilesDir().getAbsolutePath())

            // Enable on-disk cache, this enables automatic QUIC usage for subsequent requests
            // to the same domain across application restarts. If you also want to cache HTTP
            // responses, use HTTP_CACHE_DISK instead. Typically you will want to enable caching
            // in full, we turn it off for this demo to better demonstrate Cronet's behavior
            // using net protocols.
            .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 100 * 1024)

            // HTTP2 and QUIC support is enabled by default. When both are enabled (and no hints
            // are provided), Cronet tries to use both protocols and it's nondeterministic which
            // one will be used for the first few requests. As soon as Cronet is aware that
            // a server supports QUIC, it will always attempt to use it first. Try disabling
            // and enabling HTTP2 support and see how the negotiated protocol changes! Also try
            // forcing a new connection by enabling and disabling flight mode after the first
            // request to ensure QUIC usage.
            .enableHttp2(true)
            .enableQuic(true)

            // Brotli support is NOT enabled by default.
            .enableBrotli(true)

            // As noted above, QUIC hints speed up initial requests to a domain. Multiple hints
            // can be added. We don't enable them in this demo to demonstrate how QUIC
            // is being used if no hints are provided.

            // .addQuicHint("storage.googleapis.com", 443, 443)
            // .addQuicHint("www.googleapis.com", 443, 443)
            .build();
}

developer.android.com/guide/topic… 文档中推荐CronetEngine 使用单例。

注意:建议您仅创建 CronetEngine 的一个实例。单个实例可以发送多个异步请求。此外,存储目录不支持多个 CronetEngine 实例并发访问。如需了解详情,请参阅 setStoragePath()。

与OkHttp结合 addInterceptor(CronetInterceptor.newBuilder(cronetEngine).build()) 需要注意的是,需要将 Cronet 拦截器添加到最后,否则后续的拦截器将被跳过。

4、小结

以上接入的工作就全部做完了,接入的工作量不大。 需要注意的是单纯客户端调整是不行的,服务端也需要做相应的调整。

三、关于Cronet

Cronet是一个对Chromium的网络模块的封装库

  • 支持android/iOS移动平台
  • 可以无缝对接到各个平台的常见网络库同时也拥有自己的API
  • 支持HTTP协议,同时支持QUIC协议

既然Cronet能够同时Http协议和QUIC协议,那么他是如何做出选择使用哪种协议进行通信的呢?

Cronet建连协议选择原理

首次请求

(网图,侵删)

quic____1.png 此处的首次请求和后续请求均是指的相同域名下的请求

  • 首次请求,客户端发送正常的HTTP协议请求,此过程与正常HTTP请求的唯一区别是,携带了支持QUIC协议的信息,告诉Server端,Client支持QUIC请求
  • Server端收到请求后,判断自身是否支持QUIC协议
  • 如果Server端支持QUIC协议,Server端直接在response header中添加一个响应头: alt-syc alt-syc携带的是什么信息呢?主要包含三部分信息
    • quic=ip:port 告诉客户端QUIC建连的IP和端口
    • ma=xxxxx 主要用来标识服务是否可用(是否在有效期)
    • v=xx,xx 标识Server端支持的QUIC版本
  • 如果Server端不支持QUIC协议,则直接按照HTTP协议返回,response header中不包含:alt-syc头部信息

第二次请求

(网图,侵删)

quic____2.png

  • 客户端第二次请求,Cronet网络库同时发出基于TCP的HTTP连接和基于UDP的QUIC连接
    • TCP连接基于正常的DNS解析进行建连
    • QUIC连接是基于第一次请求时Server端在alt-syc头信息中标注的ServerIP和端口号进行发起的,而不走DNS解析
  • Server接收两种连接后均会做出反馈给客户端
  • 客户端等待两个连接的响应,Cronet会按照时间维度判断哪个连接首先建立成功。
    • 如果QUIC连接首先完成,则后续请求均走此QUIC连接;
    • 如果HTTP连接首先完成,则后续直接按照HTTP连接与Server通信;

总结

看完这两个过程,不难发现,Cronet创建QUIC连接时,其实是不依赖于本地DNS解析的,正常需要建立QUIC连接的IP和端口均是有Server端提前告知的,因此Cronet对HTTP和QUIC协议的选择可以归纳为以下几点: 首次请求,基于DNS建立HTTP连接,并下发QUIC的建连信息 第二次请求,采用竞速模式,以时间维度则其优。 QUIC建连时,不依赖本地DNS解析,而是按照Server下发信息进行建连