okhttp组件学习

目录

  1. 简介
  2. 网络请求队列 Dispatcher
  3. Socket连接池 ConnectionPool
  4. 缓存管理
  5. HTTPS

简介

OkHttp是一个HTTP客户端组件,要求Android2.3以上或者JDK1.7以上,最近重新读其源码,对感兴趣的部分记录一下.

网络请求队列 Dispatcher

  • Call#execute()同步请求前会进入Dispacher#runningSyncCalls队列,完成后remove

  • Call#enqueue()异步请求会进入Dispatchder#runningAsyncCalls队列,这里有限制条件:maxRequests<=64 && maxRequestsPerHost<=5,不满足则放入Dispatcher#readyAsyncCalls,这些值可通过#setMaxRequests()#setMaxRequestsPerHost()设置.

  • 异步执行的线程池默认设置为:

CorePoolSize MaximumPoolSize KeepAliveTime WorkQueue
0 Integer.MAX_VALUE 60s SynchronousQueue
  • Dispatcher#setIdleCallback()用于设置在同步请求队列和异步请求队列均为空时执行一次callback

Socket连接池 ConnectionPool

HTTP和HTTP/2请求相同地址时会共享一个Connection.

ConnectionPool#maxIdleConnections用于标识连接池中最多可空闲的连接数,默认是5

ConnectionPool#keepAliveDurationNs标识连接可空闲的最大时间,默认是5min

通过ConnectionPool#cleanupRunnable对过期连接进行remove操作.

缓存管理

  • CacheInterceptor

用于request缓存实现,其中对request获取缓存response是通过CacheStrategy实现的,发送request时可通过设置CacheControl(也就是头部Cache-Control字段)实现缓存控制,其中内置两种策略:

FORCE_NETWORK FORCE_CACHE
强制网络请求 强制仅使用缓存
  • 判断缓存的response是否可用

1.如果request Header和response Header存在Cache-Control:no-store则不使用缓存(如果response HTTP Code为302/307时,还需判断Header是否存在Expires或者Cache-Control值是不是max-age/public/private)

2.如果request Header存在If-Modified-SinceIf-None-Match则需要进行Conditional Request

3.如果response Header存在Cache-control: immutable则使用缓存

4.根据Refreshness判断

a.根据RFC7234-4.2.3计算response的current_age

b.计算response的freshness_time. 计算该值所使用字段的优先级为Cache-Control:max-age > Date > Last-Modified; 如果request设置了Cache-Control:max-age则freshness_time取上述两个值的最小值

c.request的Cache-Control:min-fresh值min_fresh_value(无则为0)

d.如果response没有设置Cache-control: must-revalidate则取requestCache-Control: max-stale值max_stale_value(无则为0)

如果满足current_age + min_fresh_value < freshness_time + max_stale_value则直接使用缓存

5.上述条件不满足则发送Condition request. Header condition取值的优先级为:If-None-Match:<Etag-Value> > If-Modified-Since:<Last-Modified-Value> > If-Modified-Since:Date-Value>

  • Cache本地存储

本地Cache的底层实现是DiskLruCache,包括一个日志文件(journal)和若干存储文件组成.日志文件的逻辑结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
libcore.io.DiskLruCache     // "magic number"
1 // cache version
201105 // application version
2 // value count
// 空行
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

每一行日志记录由三部分组成:

1.State

state description
DIRTY 缓存文件开始被编辑的状态(create/update)
CLEAN 缓存文件更新完成的状态(commit)
READ 读取缓存文件
REMOVE 移除该缓存文件

文件操作采用事务处理方式,即修改文件前先写入一个同名tmp文件,当所有内容写完后将tmp 文件的扩展名去掉以覆盖原有文件.所以通常DIRTY后会跟着CLEAN/REMOVE,如果不存在,则表明这是一条无效记录.

2.Key: OKHttp使用url的md5值

3.optional state-specific values

OkHttp设置value count为2(对应第四行),所以是两个值. 该值表示缓存的文件大小(单位:byte).两个缓存的文件名和内容:

文件名 内容
<Key_value>.0 包括request/response Header等的METADATA
<Key_value>.1 response body

Cache初始化时读取日志文件,使用LinkedHashMap在内存中生成缓存文件的索引,其中Key是url的md5值,Value是保存缓存文件File的Entry对象.
LinkedHashMap初始化时设置accessOrder为true,当缓存文件超过客户端设置的最大值时,只需要remove头部对象就实现了LRU算法.
同时,当日志文件记录条数大于阈值(2000)并且记录条数大于LinkedHashMap的size时需要进行重建,其逻辑是遍历LinkedHashMap,根据其是否正在编辑来写入DIRTY或CLEAN记录.

另外,DiskLruCache#get()返回一个DiskLruCache#Snapshot对象,表示缓存Entry的某一版本,由Entry成员变量sequenceNumber表示.修改数据时会比较Snapshot#sequenceNumber与对应的Entry#sequenceNumber,如果不相等则说明缓存版本已失效,无法进行编辑.

HTTPS

1.通过ConnectionSpec可以指定tls版本号和支持的cipher suites,当前版本配置参考ConnectionSpec#MODERN_TLS,其中默认允许tls1.0~tls1.3,而且允许协商失败时尝试明文传输.

2.提供Certificate Pinning能力,简单的说就是对服务端提供证书的公钥进行校验,官方Demo如下:

1
2
3
4
5
6
7
8
public CertificatePinning() {
client = new OkHttpClient.Builder()
.certificatePinner(new CertificatePinner.Builder()
.add("publicobject.com",
"sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=") // 服务端证书的公钥hash值
.build())
.build();
}

3.自定义信任证书
底层实现是基于JSSE框架,使用自有证书构建X509TrustManager对象即可,官方Demo如下:

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
59
// generate X509Manager by custom certification
private X509TrustManager trustManagerForCertificates(InputStream in)
throws GeneralSecurityException {
CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
Collection<? extends Certificate> certificates = certificateFactory.generateCertificates(in);
if (certificates.isEmpty()) {
throw new IllegalArgumentException("expected non-empty set of trusted certificates");
}

// Put the certificates a key store.
char[] password = "password".toCharArray(); // Any password will work.
KeyStore keyStore = newEmptyKeyStore(password);
int index = 0;
for (Certificate certificate : certificates) {
String certificateAlias = Integer.toString(index++);
keyStore.setCertificateEntry(certificateAlias, certificate);
}

// Use it to build an X509 trust manager.
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, password);
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
if (trustManagers.length != 1 || !(trustManagers[0] instanceof X509TrustManager)) {
throw new IllegalStateException("Unexpected default trust managers:"
+ Arrays.toString(trustManagers));
}
return (X509TrustManager) trustManagers[0];
}

private KeyStore newEmptyKeyStore(char[] password) throws GeneralSecurityException {
try {
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream in = null; // By convention, 'null' creates an empty key store.
keyStore.load(in, password);
return keyStore;
} catch (IOException e) {
throw new AssertionError(e);
}
}

// client
X509TrustManager trustManager;
SSLSocketFactory sslSocketFactory;
try {
trustManager = trustManagerForCertificates(trustedCertificatesInputStream());
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] { trustManager }, null);
sslSocketFactory = sslContext.getSocketFactory();
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}

client = new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustManager)
.build();