导语: 最近一直在优化 rn 的下载速度,尝试过
bundle
分包,资源压缩等一系列手段,发现优化后的bundle
(200kb,gzip:67.2k)下载时间也需要300ms
左右的时间。最后我们选择使用QUIC
协议来尝试bundle
下载,下文主要简单介绍QUIC
协议的使用。
# QUIC 协议是干嘛的?
我想这篇文章: QUIC 分享 (opens new window)对于 QUIC 和 http2 的区别
以及 QUIC 如何做多路复用和握手
讲的非常详细了。
建议查看如下内容之前,先阅读上文,知晓 QUIC
和 http1.0
,http1.1
,http2.0
的区别。
QUIC
(Quick UDP Internet Connections,读 quick)是由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 的依赖包?
- 编译系统要求
- 64 位 Mac 10.12.6+
- Xcode 11.4+.
- 最新的 JDK
- 获取源码
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
- 编译
进入 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,那么直接使用官方自带的库,也许是最好的选择.
- 添加依赖
dependencies {
implementation 'com.google.android.gms:play-services-cronet:16.0.0'
}
- 检查 Cronet 是否可用
CronetProviderInstaller.installProvider(reactContext).addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
if (task.isSuccessful()) {
// 初始化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 中使用
- 项目中有使用到
Retrofit
你可以使用Retrofit.Builder.callFactory
去自定义一个 call;
一个简单的例子: RNCronetOkHttpCall.java (opens new window)
- 没有
Retrofit
在没有Retrofit
的工程中国,你可以使用 okhttp 的interceptor
去处理 cronet。
一个简单的例子: RNCronetInterceptor.java (opens new window)
- 初始化
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 引擎之后,在使用 NSURLSession
和 NSURLConnection
时会自动使用。
# 服务端支持 QUIC
nginx 官方 nginx-quic: nginx-quic (opens new window), 编译步骤参考在 nginx 中支持 HTTP3.0/QUIC (opens new window)
基于
cloudflare
的分支版本nginx
: 对于 HTTP3.0/QUIC
,Cloudflare 始终走在了前列,借助于自家维护的开源项目 quic,从nginx
中拉出了一个分支来开发,并编译出了对HTTP3.0
支持的nginx
服务器
# 如何测试 QUIC 是否生效
在文章之处已经提过,使用 QUIC 协议需要客户端和服务端支持,如果您已经部署到 quic 协议,客户端也已经实现,您可以使用如下几种方案检查是否 quic 生效:
- 使用
WireShark
(推荐,也是最可靠的方式),如果使用 chrome 浏览器或者网上的其他方式,很可能抓包时显示的是 h2 协议,因为 quic 协议和浏览器支持的版本也是有关系的。
- Firefox(tips: 笔者并未尝试成功,但是有同学尝试成功过)
- 启动 Firefox Nightly
- 输入
about:config
- 搜索
network.http.http3.enabled
并设置为enable
- 重启浏览器并打开开发者工具
- 查看对应协议
- 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};
}
其中 IETF
的 QUIC
版本和 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)
# 源码
# 参考
- 本文链接: https://mrgaogang.github.io/net/quic.html
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 许可协议。转载请注明出处!