Cronet介绍

Cronet是Chromium的网络模块,位Chromium提供网络支持。其是一个支持多平台的网络库(Android/iOS/Mac/Windows/Linux)。 Cronet利用多种技术来减少延迟并提高应用程序需要工作的网络请求的吞吐量。

Cronet Library每天处理数百万人使用的应用程序请求,例如YouTube,Google App,Google Photos和Maps - Navigation&Transit。

Cronet具有以下特点:

  • 1.Protocol support
    Cronet本身支持HTTP,HTTP / 2和QUIC协议。
  • 2.Request prioritization
    该库允许您为请求设置优先级标记。服务器可以使用优先级标记来确定处理请求的顺序。
  • 3.Resource caching
    Cronet可以使用内存或磁盘缓存来存储在网络请求中检索到的资源。后续请求将自动从缓存中提供。
  • 4.Asynchronous requests
    默认情况下,使用Cronet Library发出的网络请求是异步的。在等待请求返回时,不会阻止您的工作线程。
  • 5.Data compression
    Cronet使用Brotli压缩数据格式支持数据压缩。

为什么移动端要接入Cronet网络库

随着移动端的发展,追求更好的网络体验是大家都更加向往的,同样google也面临这个问题。于是google自研了QUIC网络协议,作为google最重要的网络库cronet自然也就是天生的支持了QUIC协议。因此,业界如果要支持QUIC协议,那么要么选择Cronet网络库,要么自研网络库,能够支持QUIC。

如何使用Cronet

最初使用cronet是一个非常繁琐的过程,因为需要编译cronet的源码,特别是在android上使用,需要使用linux系统进行编译,这个过程是非常通过的。

为了让大家更便捷的接入Cronet网络库,google也给到了编译好的,android/iOS平台下的网络库。https://console.cloud.google.com/storage/browser/chromium-cronet

Android上的具体使用,请参考官方文档:https://developer.android.com/guide/topics/connectivity/cronet/start

如何结合OkHttp网络库使用

面前大部分项目网络库使用的都是okhttp,那么如果从底层更便捷的切换到cronet呢? 具体接入详情请参考以下链接:https://mp.weixin.qq.com/s/MUCSsgLbn3XBz7jgmdWk6Q

如何支持HttpDns

业界比较常见的做法是:修改cronet源码,将HostResolver部分通过回调形式暴露出来。 这种方式原理上讲是没有问题的,但是弊端在于工作量太大了,会话费大量的人力物力成本,而且会对后续的版本升级带来一部分成本。那么有没有,无需修改源码,就可以直接支持httpDns的方案呢?

大家都知道Chrome上是有一个—host-resolver-rules命令,可以将Chrome的网络代理到某台指定机器上,那么android和iOS平台有没有类似的API呢?

经过一段时间cronet源码的分析发现,android和iOS平台同样是有此类API进行暴露的,只不过官方介绍很简单。(iOS下也有一个类似的API,此处以android的实现为例做介绍)

1
2
3
4
5
6
7
8
9
10
/**
* Sets experimental options to be used in Cronet.
*
* @param options JSON formatted experimental options.
* @return the builder to facilitate chaining.
*/
public Builder setExperimentalOptions(String options) {
mBuilderDelegate.setExperimentalOptions(options);
return this;
}

虽然有这个API,他如何使用呢? 带着疑问,我们去反馈源码发现有两处单元测试代码,使用到了此API。

源码出处:https://chromium.googlesource.com/chromium/src/+/HEAD/components/cronet/android/test/javatests/src/org/chromium/net/ExperimentalOptionsTest.java

https://chromium.googlesource.com/chromium/src/+/HEAD/components/cronet/android/test/src/org/chromium/net/CronetTestUtil.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Test
@MediumTest
@Feature({"Cronet"})
@OnlyRunNativeCronet
// Tests that NetLog writes effective experimental options to NetLog.
public void testNetLog() throws Exception {
File directory = new File(PathUtils.getDataDirectory());
File logfile = File.createTempFile("cronet", "json", directory);
JSONObject hostResolverParams = CronetTestUtil.generateHostResolverRules();
JSONObject experimentalOptions =
new JSONObject().put("HostResolverRules", hostResolverParams);
mBuilder.setExperimentalOptions(experimentalOptions.toString());
CronetEngine cronetEngine = mBuilder.build();
cronetEngine.startNetLogToFile(logfile.getPath(), false);
String url = Http2TestServer.getEchoMethodUrl();
TestUrlRequestCallback callback = new TestUrlRequestCallback();
UrlRequest.Builder builder =
cronetEngine.newUrlRequestBuilder(url, callback, callback.getExecutor());
UrlRequest urlRequest = builder.build();
urlRequest.start();
callback.blockForDone();
assertEquals(200, callback.mResponseInfo.getHttpStatusCode());
assertEquals("GET", callback.mResponseAsString);
cronetEngine.stopNetLog();
assertFileContainsString(logfile, "HostResolverRules");
assertTrue(logfile.delete());
assertFalse(logfile.exists());
cronetEngine.shutdown();
}

/**
* Generates rules for customized DNS mapping for testing hostnames used by test servers,
* namely:
* <ul>
* <li>{@link QuicTestServer#getServerHost}</li>
* </ul>
* @param destination host to map to
*/
public static JSONObject generateHostResolverRules(String destination) throws JSONException {
StringBuilder rules = new StringBuilder();
for (String domain : TEST_DOMAINS) {
rules.append("MAP " + domain + " " + destination + ",");
}
return new JSONObject().put("host_resolver_rules", rules);
}

看到这里我们就完全可以通过org.chromium.net.ExperimentalCronetEngine.Builder#setExperimentalOptions 这个API,片面的实现httpDns的功能,那么需要做两步工作:

  1. 每次创建请求,通过Factory生成ExperimentalCronetEngine对象,
  2. 每次请求为ExperimentalCronetEngine设置setExperimentalOptions将我们host解析规则设置过去。

具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public NRCronetEngine create(String host, Call call, boolean enableHttpDns) {
NRCronetEngine nrCronetEngine = null;
if (enableHttpDns) {
//优先走httpdns或者Debug界面配置host map数据时,单独设置HostResolverRules
List<InetAddress> dnsList = null;
Dns dns = NtesDnsProvider.getInstance().provideDns();
if (dns != null) {
try {
dnsList = dns.lookup(host);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
JSONObject hostResolverRules = getHostResolverRules(host, dnsList);
if (hostResolverRules != null) {
try {
JSONObject options = new JSONObject().put("HostResolverRules", hostResolverRules);
mOptionsEngineBuilder.setExperimentalOptions(options.toString());
} catch (JSONException e) {
e.printStackTrace();
}
nrCronetEngine = new NRCronetEngine(mOptionsEngineBuilder.build());
} else {
nrCronetEngine = createNoOptionsEngine();
}
} else {
//未开启httpDns时,使用单例的engine对象
nrCronetEngine = createNoOptionsEngine();
}
return nrCronetEngine;
}

private JSONObject getHostResolverRules(String host, List<InetAddress> dnsList) {
if (TextUtils.isEmpty(host) || DataUtils.isEmpty(dnsList)) {
return null;
}
if (DataUtils.valid(dnsList)) {
StringBuilder hostRuleStr = new StringBuilder();
//目前测试看,即使此处设置多个dnsIp, cronet默认也只是用第一个,且第一个失败后不会自动重试第二个
for (int i = 0, size = dnsList.size(); i < size; i++) {
InetAddress inetAddress = dnsList.get(i);
if (inetAddress != null) {
String hostAddress = inetAddress.getHostAddress();
if (!TextUtils.isEmpty(hostAddress)) {
hostRuleStr.append("MAP ").append(host).append(" ").append(hostAddress).append(",");
}
}
}
JSONObject host_rule = null;
try {
host_rule = new JSONObject().put("host_resolver_rules", hostRuleStr);
} catch (JSONException e) {
e.printStackTrace();
}
return host_rule;
}
return null;
}

—host-resolver-rules 参数的语法规则

以逗号分隔的rules列表,用于控制主机名的映射方式

例如:

  • MAP * 127.0.0.1 强制将所有主机名映射到127.0.0.1
  • MAP *.google.com proxy 强制所有google.com子域名解析到”proxy”.
  • MAP test.com [::1]:77 强制”test.com”解析为IPv6环回地址. 也将强制生成的套接字地址端口为77.
  • MAP * baz, EXCLUDE www.google.com 把所有地址重新映射到“baz”, 除了”www.google.com".
    这些映射适用于网络请求中的端点主机. 网络请求包括TCP连接和直连的主机解析器, 以及HTTP代理连接中的CONNECT方式, 以及在SOCKS代理连接中的端点主机.

总结对比

总结对比两种方式实现HttpDns功能的优缺点

Item 优点 缺点
源码编译暴露HostResolver回调 更加彻底的解决dns解析问题 难度极大,复杂性高,不易维护升级
使用setExperimentalOptions API 现有API,无需编译源码,难度等级0 1. 会产生对个Engine对象;2. 不支持host解析到多IP