导语: 最近一直在优化 rn 的下载速度,尝试过 bundle 分包,资源压缩等一系列手段,发现优化后的 bundle(200kb,gzip:67.2k)下载时间也需要 300ms 左右的时间。最后我们选择使用 QUIC 协议来尝试 bundle 下载,下文主要简单介绍 QUIC 协议的使用。

# QUIC 协议是干嘛的?

我想这篇文章: QUIC 分享 (opens new window)对于 QUIC 和 http2 的区别以及 QUIC 如何做多路复用和握手讲的非常详细了。

建议查看如下内容之前,先阅读上文,知晓 QUIChttp1.0,http1.1,http2.0的区别。

QUIC(Quick UDP Internet Connections,读 quick)是由 Google 提出的一种基于 UDP 改进的低时延的互联网传输层.

QUIC 相比 http2.0 有如下优点:

  • 支持 0-RTT 连接(最多 1-RTT)
  • 用户域的拥塞控制,协议可快速部署、更新
  • UDP 天然无队头阻塞问题
  • 连接迁移
  • 前向纠错

那么项目如何接入 QUIC 协议呢?

要使用 QUIC 协议需要客户端和服务端支持,客户端需要使用 Google 工具Cronet,下文将详细介绍如何编译客户端包,以及如何使用。

# Cronet 客户端依赖包编译

# 什么是 Cronet ?

这可能是最好的 Cronet 文档说明:

Cronet is the networking stack of Chromium put into a library for use on mobile. This is the same networking stack that is used in the Chrome browser by over a billion people. It offers an easy-to-use, high performance, standards-compliant, and secure way to perform HTTP requests. Cronet has support for both Android and iOS.

大致的意思是 Cronet 是一个移动端谷歌浏览器使用的网络请求库,拥有易用、高性能、安全和高标准的特点,目前以及在 android 和 iOS 中使用。

Cronet 支持如下功能:

  • 支持 http2 协议
  • 支持 quic 协议,默认情况如何服务端不支持 quic 协议,将会采用降级策略降级至 http2
  • 支持 Brotli 压缩
  • 支持 Metrics
  • 支持缓存,日志记录
  • 等等

# 如何编译 Android 和 iOS 的依赖包?

  1. 编译系统要求
  • 64 位 Mac 10.12.6+
  • Xcode 11.4+.
  • 最新的 JDK
  1. 获取源码

iOS 的必须在 mac 下编译,Android 必须要 linux 平台下编译。

  • 安装 depot_tools 工具 在任意目录下载 depot_tools 源码,并将该项目根目录添加到 PATH(添加到~/.bashrc 或者 ~/.zshrc, 记得source ~/.zshrc),从而可以使用其工具比如 fetch
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
export PATH="$PATH:/path/to/depot_tools"
  • 拉取 chromium 源码 这里需要区分是拉取 Android 平台的,还是 iOS 平台的
mkdir chromium
# 记得 --no-history --nohooks提高拉取速度,此处拉取大约需要30min
fetch --no-history --nohooks  android      # fetch --no-history  --nohooks  ios
  1. 编译

进入 chromium/src 目录,执行下面命令,

# 下载依赖
~/chromium/src $ gclient sync
~/chromium/src $ ln -s /path/to/components/cronet/tools/cr_cronet.py ./cr_cronet.py
~/chromium/src $ ./cr_cronet.py gn --out_dir=out/Cronet
# 构建iOS模拟器包
~/chromium/src $ ninja -C out/Cronet cronet_package
# 构建release包
~/chromium/src $ ./cr_cronet.py gn -i --release --out_dir=out/Cronet-iphoneos-release

对于 release 版本的 Cronet 包,需要在构建的时候添加--release选项,另外 iOS 版本会区分模拟器和真机,区别是 cpu 架构不一样,模拟器使用 x86 架构,真机使用的 ARM 架构。 上述 cr_cronet.py 命令默认生成模拟器版本的库,生成真机的库需要添加 -i 选项,同时必须具有 iOS 开发者证书,并在 xcode 中配置好。

至此你可以在src/out/Cronet/Static目录下已然生成Cronet.framework

# 如何使用 Cronet

# Cronet 在 Android 中使用

如果你的项目么有使用到 okhttp,那么直接使用官方自带的库,也许是最好的选择.

  1. 添加依赖
dependencies {
    implementation 'com.google.android.gms:play-services-cronet:16.0.0'
}
  1. 检查 Cronet 是否可用
CronetProviderInstaller.installProvider(reactContext).addOnCompleteListener(new OnCompleteListener<Void>() {
  @Override
  public void onComplete(@NonNull Task<Void> task) {
    if (task.isSuccessful()) {
      // 初始化Cronet代码
    }
  }
});
  1. 初始化 Cronet 引擎,并发送请求

发送请求大致分为如下几步:

  • 创建并配置CronetEngine单实例
  • 提供请求回调的实现
  • 创建 Executor 对象来管理网络请求任务
  • 创建和配置 UrlRequest 对象

第一步: 创建并配置CronetEngine单实例

 private static synchronized CronetEngine getCronetEngine(Context context) {
        // Lazily create the Cronet engine.
        if (cronetEngine == null) {
            CronetEngine.Builder myBuilder = new CronetEngine.Builder(context);
            // Enable caching of HTTP data and
            // other information like QUIC server information, HTTP/2 protocol and QUIC protocol.
            cronetEngine = myBuilder
                    .enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISABLED, 100 * 1024)
                    .addQuicHint("stgwhttps.kof.qq.com", 443, 443)
                    //.enableHttp2(true)
                    .enableQuic(true)
                    .build();
            //    .setUserAgent("clb_quic_demo")
        }
        return cronetEngine;
}

第二步: 提供请求回调的实现

方法详细回调,请见Android Developer Cronet (opens new window)

class SimpleUrlRequestCallback extends UrlRequest.Callback {

        private ByteArrayOutputStream bytesReceived = new ByteArrayOutputStream();
        private WritableByteChannel receiveChannel = Channels.newChannel(bytesReceived);
        private ImageView imageView;
        public long start;
        private long stop;
        private Activity mainActivity;

        SimpleUrlRequestCallback(ImageView imageView, Context context) {
            this.imageView = imageView;
            this.mainActivity = (Activity) context;
        }

        @Override
        public void onRedirectReceived(
                UrlRequest request, UrlResponseInfo info, String newLocationUrl) {
            android.util.Log.i(TAG, "****** onRedirectReceived ******");
            request.followRedirect();
        }

        @Override
        public void onResponseStarted(UrlRequest request, UrlResponseInfo info) {
            android.util.Log.i(TAG, "****** Response Started ******");
            android.util.Log.i(TAG, "*** Headers Are *** " + info.getAllHeaders());

            request.read(ByteBuffer.allocateDirect(32 * 1024));
        }

        @Override
        public void onReadCompleted(
                UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) {
            android.util.Log.i(TAG, "****** onReadCompleted ******" + byteBuffer);
            byteBuffer.flip();
            try {
                receiveChannel.write(byteBuffer);
            } catch (IOException e) {
                android.util.Log.i(TAG, "IOException during ByteBuffer read. Details: ", e);
            }
            byteBuffer.clear();
            request.read(byteBuffer);
        }

        @Override
        public void onSucceeded(UrlRequest request, UrlResponseInfo info) {

            stop = System.nanoTime();

            android.util.Log.i(TAG,
                    "****** Cronet Request Completed, the latency is " + (stop - start));

            android.util.Log.i(TAG,
                    "****** Cronet Request Completed, status code is " + info.getHttpStatusCode()
                            + ", total received bytes is " + info.getReceivedByteCount());
            // Set the latency
            ((MainActivity) context).addCronetLatency(stop - start, 0);

            byte[] byteArray = bytesReceived.toByteArray();
            final Bitmap bimage = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length);
            mainActivity.runOnUiThread(new Runnable() {
                public void run() {
                    imageView.setImageBitmap(bimage);
                    imageView.getLayoutParams().height = bimage.getHeight();
                    imageView.getLayoutParams().width = bimage.getWidth();
                }
            });
        }

        @Override
        public void onFailed(UrlRequest var1, UrlResponseInfo var2, CronetException var3) {
            android.util.Log.i(TAG, "****** onFailed, error is: " + var3.getMessage());
        }
    }

此处代码参考CLB quic demo (opens new window)

第三步: 创建 Executor 对象来管理网络请求任务

可以使用 Executor 类执行网络任务。要获取 Executor 的实例,请使用返回 Executor 对象的 Executors 类的任一静态方法。以下示例展示了如何使用 newSingleThreadExecutor() 方法创建 Executor 对象:

Executor executor = Executors.newSingleThreadExecutor();

第四步: 创建和配置 UrlRequest 对象

 UrlRequest.Callback callback = new SimpleUrlRequestCallback(holder.mImageViewCronet,this.context);
UrlRequest.Builder builder = cronetEngine.newUrlRequestBuilder(
ImageRepository.getImage(position), callback, executor);
// Measure the start time of the request so that
        // we can measure latency of the entire request cycle
((SimpleUrlRequestCallback) callback).start = System.nanoTime();
// Start the request
builder.build().start();

# Cronet 在有 okhttp 的 Android 中使用

  1. 项目中有使用到Retrofit

你可以使用Retrofit.Builder.callFactory去自定义一个 call;

一个简单的例子: RNCronetOkHttpCall.java (opens new window)

  1. 没有 Retrofit

在没有Retrofit的工程中国,你可以使用 okhttp 的interceptor去处理 cronet。

一个简单的例子: RNCronetInterceptor.java (opens new window)

  1. 初始化 Cronet 引擎,并发送请求

同上

# Cronet 在 iOS 中使用

利用前一个构建步骤生成的Cronet.framework导入到 xcode 中即可使用。

使用方式如下:

#include <Cronet/Cronet.h>
...
[Cronet setHttp2Enabled:YES];
[Cronet setQuicEnabled:YES];
[Cronet setBrotliEnabled:YES];
[Cronet setHttpCacheType:CRNHttpCacheTypeDisk];
[Cronet addQuicHint:@"www.google.com" port:443 altPort:443];
[Cronet start];
[Cronet registerHttpProtocolHandler];

没错,在 iOS 中使用就是如此简单,当我们初始化了 Cronet 引擎之后,在使用 NSURLSessionNSURLConnection时会自动使用。

# 服务端支持 QUIC

# 如何测试 QUIC 是否生效

在文章之处已经提过,使用 QUIC 协议需要客户端和服务端支持,如果您已经部署到 quic 协议,客户端也已经实现,您可以使用如下几种方案检查是否 quic 生效:

  1. 使用WireShark(推荐,也是最可靠的方式),如果使用 chrome 浏览器或者网上的其他方式,很可能抓包时显示的是 h2 协议,因为 quic 协议和浏览器支持的版本也是有关系的。

  1. 使用检查工具http3-test (opens new window)

  2. 使用检查工具HTTP/3 Test (opens new window)

  1. Firefox(tips: 笔者并未尝试成功,但是有同学尝试成功过)
  • 启动 Firefox Nightly
  • 输入about:config
  • 搜索network.http.http3.enabled并设置为enable
  • 重启浏览器并打开开发者工具
  • 查看对应协议
  1. cURL

首先必须升级到cURL 到最新的版本 (opens new window),

# 测试
curl --http3 https://www.google.com -I

# QUIC 版本

我们知道,cronet 的版本和 QUIC 协议的版本是有对应关系的,目前implementation 'com.google.android.gms:play-services-cronet:16.0.0'版本缩支持的 QUIC 协议版本如下:

constexpr std::array<QuicTransportVersion, 6> SupportedTransportVersions() {
  return {QUIC_VERSION_IETF_DRAFT_29,
          QUIC_VERSION_IETF_DRAFT_27,
          QUIC_VERSION_51,
          QUIC_VERSION_50,
          QUIC_VERSION_46,
          QUIC_VERSION_43};
}

其中 IETFQUIC 版本和 Google QUIC 版本对应关系如下:

 QUIC_VERSION_43 = 43,  // PRIORITY frames are sent by client and accepted by
                         // server.
  // Version 44 used IETF header format from draft-ietf-quic-invariants-05.
  // Version 45 added MESSAGE frame.
  QUIC_VERSION_46 = 46,  // Use IETF draft-17 header format with demultiplexing
                         // bit.
  // Version 47 added variable-length QUIC server connection IDs.
  // Version 48 added CRYPTO frames for the handshake.
  // Version 49 added client connection IDs, long header lengths, and the IETF
  // header format from draft-ietf-quic-invariants-06
  QUIC_VERSION_50 = 50,  // Header protection and initial obfuscators.
  QUIC_VERSION_51 = 51,  // draft-29 features but with GoogleQUIC frames.
  // Number 70 used to represent draft-ietf-quic-transport-25.
  QUIC_VERSION_IETF_DRAFT_27 = 71,  // draft-ietf-quic-transport-27.
  // Number 72 used to represent draft-ietf-quic-transport-28.
  QUIC_VERSION_IETF_DRAFT_29 = 73,  // draft-ietf-quic-transport-29.
  // Version 99 was a dumping ground for IETF QUIC changes which were not yet
  // yet ready for production between 2018-02 and 2020-02.

数据来源: google source (opens new window)

# 源码

# 参考

【未经作者允许禁止转载】 Last Updated: 10/14/2021, 11:20:21 AM