OkHttp 简单使用与源码结构

OkHttp 使用方法简介

       1.创建⼀个 OkHttp 的实例:

1
OkHttpClient client = new OkHttpClient.Builder().build();

       2.创建 Request:

1
2
3
Request request = new Request.Builder()
.url("http://hencoder.com")
.build();

       3.创建 Call 并发起网络请求:

1
2
3
4
5
6
7
8
9
10
11
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {

}

@Override
public void onResponse(Call call, Response response) throws IOException {
Log.d("okhttp response", response.body().string());
}
});

OkHttp 源码总结

  • OkHttpClient 相当于配置中心,所有的请求都会共享这些配置(例如出错是否重试、共享的连接池)。OkHttpClient 中的配置主要有:

       1.Dispatcher dispatcher :调度器,用于调度后台发起的网络请求(通过线程池把请求放在后台线程进⾏等等),有后台总请求数(maxRequests)和单主机总请求数(maxRequestsPerHost)的控制。

1
2
3
4
public final class Dispatcher {
private int maxRequests = 64;//当总请求数达到64时便不再做新的请求了,开始等待。
private int maxRequestsPerHost = 5;//每一个主机的请求同时达到5个,也不再做新的请求。
……
1
如果想让所有的请求一个个按照顺序来执行,可以设置 setMaxRequests(1)

       2.List protocols :支持的应用层协议,即 HTTP/1.1、HTTP/2 等。

       3.List connectionSpecs :应⽤层支持的 Socket 设置,即使用明文
传输(用于 HTTP)还是某个版本的 TLS(用于 HTTPS)。

       4.List interceptors :大多数时候使用的 Interceptor 都应该配置到这里,类似如下代码:

1
2
3
4
5
6
7
8
9
10
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
// 前置工作
Response response = chain.proceed(chain.request());
//后置工作
return response;
}
}).build();

       5.List networkInterceptors :直接和网络请求交互的 Interceptor 配置到这里,例如如果你想查看返回的 301 报文或者未解压的 Response Body,需要在这⾥看。

       6.CookieJar cookieJar :管理理 Cookie 的控制器。OkHttp 提供了 Cookie 存取的判断⽀持(即什什么时候需要存 Cookie,什么时候需要读取 Cookie,但没有给出具体的存取实现。如果需要存取 Cookie,你得⾃己写实现,例如用 Map 存在内存里,或者用别的方式存在本地存储或者数据库。

1
2
3
4
5
6
7
8
9
10
11
12
CookieJar cookieJar = new CookieJar() {
Map<HttpUrl, List<Cookie>> cookiesMap = new HashMap<>();
@Override
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
cookiesMap.put(url,cookies);
}

@Override
public List<Cookie> loadForRequest(HttpUrl url) {
return cookiesMap.get(url);
}
};

       7.Cache cache :Cache 存储的配置。默认是没有,如果需要用,得⾃己配置出 Cache 存储的⽂件位置以及存储空间上限。

       8.CertificateChainCleaner certificateChainCleaner :从服务器获得的证书可能会包含很多内容,会包含好多无关的证书,certificateChainCleaner 会整理证书,形成一个证书序列,该序列第一个就是对方网站的证书,最后一个是信任的本地根证书。

       9.HostnameVerifier hostnameVerifier :主机名验证器,⽤于验证 HTTPS 握手过程中下载到的证书所属者是否和⾃己要访问的主机名一致。

       10.CertificatePinner certificatePinner :⽤于设置 HTTPS 握手过程中针对某个
Host 的 Certificate Public Key Pinner,即把网站证书链中的每一个证书公钥直接拿来提前配置进 OkHttpClient ⾥去,以跳过本地根证书,直接从代码⾥进⾏认证。这种⽤法⽐较少见,一般用于防止网站证书被⼈仿制。

       certificatePinner 用来做自签名时很有用。有时候一个证书由于是自签名或者证书机构未更新等原因,在本地验证不通过,假如是网站的开发者或者工作人员,可以拿到该证书的公钥,则可以将证书的信息记录在本地,对比网站下载的证书和本地证书是否一致即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String hostname = "hencoder.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.build();
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build();

Request request = new Request.Builder()
.url("https://" + hostname)
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
System.out.println("Failed!!!");
e.printStackTrace();
}

@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println("Success!!!");
}
});

       如上代码,在 client 中增加了一个 certificatePinner ,配置了一个网站的公钥信息,声明遇到“hencoder.com”时查看它的公钥是否是“sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=”,如果是则验证通过,否则失败,日志如下:

1
2
3
4
5
6
7
8
2019-11-28 15:57:55.359 4571-4613/com.example.httptest I/System.out: Failed!!!
2019-11-28 15:57:55.360 4571-4613/com.example.httptest W/System.err: javax.net.ssl.SSLPeerUnverifiedException: Certificate pinning failure!
2019-11-28 15:57:55.360 4571-4613/com.example.httptest W/System.err: Peer certificate chain:
2019-11-28 15:57:55.360 4571-4613/com.example.httptest W/System.err: sha256/+OAwmENjrBT/pI2PSOVO/I3OtHeKk7Y0PH9h8Z2z3Nw=: CN=hencoder.com
2019-11-28 15:57:55.360 4571-4613/com.example.httptest W/System.err: sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=: CN=Let's Encrypt Authority X3,O=Let's Encrypt,C=US
2019-11-28 15:57:55.360 4571-4613/com.example.httptest W/System.err: sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=: CN=DST Root CA X3,O=Digital Signature Trust Co.
2019-11-28 15:57:55.360 4571-4613/com.example.httptest W/System.err: Pinned certificates for hencoder.com:
2019-11-28 15:57:55.360 4571-4613/com.example.httptest W/System.err: sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

       可以看到 SSL 建立过程中验证失败的信息,并显示出了从网站获得的公钥信息,此处有三个:

1
2
3
sha256/+OAwmENjrBT/pI2PSOVO/I3OtHeKk7Y0PH9h8Z2z3Nw=
sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg=
sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=

       以及本地存的公钥信息:

1
sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

       可以将上面的三个公钥添加进去,即可验证成功,代码如下所示:

1
2
3
4
5
6
7
String hostname = "hencoder.com";
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add(hostname, "sha256/+OAwmENjrBT/pI2PSOVO/I3OtHeKk7Y0PH9h8Z2z3Nw="
, "sha256/YLh1dUR9y6Kja30RrAn7JKnbQG/uEtLMkBgFF2Fuihg="
, "sha256/Vjs8r4z+80wjNcr1YKepWQboSIRi63WsWXhIMN+eWys=")
.build();
……
1
经验证,上面日志中返回的三个公钥,只要有一个公钥匹配成功,即可验证通过。

       11.Authenticator authenticator :⽤于⾃动重新认证。配置之后,在请求收到401(权限不足,例如未登录)状态码的响应时,会直接调⽤ authenticator ,手动加入 Authorization header 之后自动重新发起请求。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
……
OkHttpClient client = new OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.authenticator(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) throws IOException {
//如果是Bearer授权时,此处执行真正的请求,例如调用刷新Token的接口,将Token加入header中
return response.request().newBuilder()
// .addHeader("Authorization","Basic d3k1MjFhbmdlbDoxMjM0NTY=")
.addHeader("Authorization","Bearer 此处是接口获取到的Token")
.build();
}
})
.build();
……

       Basic 和 Bearer 两种授权方式请参考登录与授权中的 Authorization 。

       12.boolean followRedirects :遇到重定向(状态码3xx)的要求时,是否自动跳转,默认为 true 。

       13.boolean followSslRedirects 在重定向时,如果原先请求的是 http ⽽重定向的目标是 https,或者原先请求的是 https ⽽重定向的目标是 http ,是否依然自动跳转。此外,如果 followRedirects 设置为 false ,便谈不上 followSslRedirects 是否自动跳转了。

       14.boolean retryOnConnectionFailure :在请求失败的时候是否自动重试。注意,⼤多数的请求失败并不不属于 OkHttp 所定义的“需要重试”,这种重试只适用于“同一个域名的多个 IP 切换重试”“Socket 失效重试”等情况。

       15.int connectTimeout :建⽴立连接(TCP 或 TLS)的超时时间。

       16.int readTimeout :发起请求到读到响应数据的超时时间。

       17.int writeTimeout :发起请求并被⽬标服务器接受的超时时间。(为什么?因为有时候对方服务器可能由于某种原因⽽不读取你的 Request。)

  • newCall(Request) ⽅法会返回一个 RealCall 对象,它是 Call 接口的实现。当调用 RealCall.execute() 的时候, RealCall.getResponseWithInterceptorChain() 会被调用,它会发起⽹络请求并拿到返回的响应,装进⼀个 Response 对象并作为返回值返回;RealCall.enqueue() 被调⽤的时候⼤同小异,区别在于 enqueue() 会使用 Dispatcher 的线程池来把请求放在后台线程进⾏,但实质上使用的同样也是
    getResponseWithInterceptorChain() 方法。

  • getResponseWithInterceptorChain() 方法做的事:OkHttp 在技术上的核心。把所有配置好的 Interceptor 放在一个 List 里,然后作为参数,创建⼀个 RealInterceptorChain 对象,并调用
    chain.proceed(request) 来发起请求和获取响应。

  • 在 RealInterceptorChain 中,多个 Interceptor 会依次调用⾃己的 intercept() ⽅法。这个⽅法会做三件事:

       1.对请求进⾏预处理;

       2.预处理之后,重新调用 RealIntercepterChain.proceed() 把请求交给下一个
Interceptor

       3.在下一个 Interceptor 处理完成并返回之后,拿到 Response 进⾏后续处理。

1
当然了,最后⼀个 Interceptor 的任务只有一个:做真正的⽹络请求并拿到响应。
  • 从上到下,每级 Interceptor 做的事:

       首先是开发者使用 addInterceptor(Interceptor) 所设置的,它们会按照开发者的要
求,在所有其它 Interceptor 处理之前,进⾏最早的预处理工作,以及在收到 Response 之后,做最后的善后工作。如果你有统一的 header 要添加,可以在这里设置;

       然后是 RetryAndFollowUpInterceptor :它负责在请求失败时的重试,以及重定向的自动后续请求。它的存在,可以让重试和重定向对于开发者是无感知的;

       BridgeInterceptor :它负责⼀些不影响开发者开发,但影响 HTTP 交互的一些额外预处理。例如,Content-Length 的计算和添加、gzip 的支持(Accept-Encoding: gzip)、gzip 压缩数据的解包,都发⽣在这⾥;

       CacheInterceptor :它负责 Cache 的处理。把它放在后⾯的⽹络交互相关
Interceptor 的前面的好处是,如果本地有了可用的 Cache,一个请求可以在没有发⽣实质⽹络交互的情况下就返回缓存结果,⽽完全不需要开发者做出任何的额外工作,让 Cache 更加⽆感知;

       ConnectInterceptor :它负责建⽴连接。在这⾥,OkHttp 会创建出⽹络请求所需要的 TCP 连接(如果是 HTTP),或者是建立在 TCP 连接之上的 TLS 连接(如果是 HTTPS),并且会创建出对应的 HttpCodec 对象(⽤于编码解码 HTTP 请求);

       然后是开发者使⽤ addNetworkInterceptor(Interceptor) 所设置的,它们的行为逻
辑和使用 addInterceptor(Interceptor) 创建的一样,但由于位置不同,所以这⾥创建的 Interceptor 会看到每个请求和响应的数据(包括重定向以及重试的⼀些中间请求和响应),并且看到的是完整原始数据,⽽不是没有加 Content-Length 的请求数据,或者 Body 还没有被 gzip 解压的响应数据。多数情况,这个⽅法不需要被使用;

       CallServerInterceptor :它负责实质的请求与响应的 I/O 操作,即往 Socket ⾥写⼊请求数据,和从 Socket ⾥读取响应数据。

       getResponseWithInterceptorChain()源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!forWebSocket) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(forWebSocket));

Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}

       图例:

OkHttp责任链

       如上图 Interceptor 组成一个链,整个链用来做网络请求,箭头1用来向网络发送信息,箭头2获取网络的返回。当结构复杂时,拆成链,每一个节点做不同的事,每一个节点就是一个拦截器(Interceptor)。源码中“ chain.proceed ”操作包含两部分( chain.proceed 代码之前的为前置工作,后面的为后置工作),以如图中的蓝色原点所示,在箭头1中,执行拦截器中的操作,并交给下一个点;在箭头2中,接受后面拦截器的返回,并继续执行一些操作,这两个部分为一个“ proceed ”操作。每一个拦截器基本上都包含两个部分:事前准备、交给下一个拦截器并等待回来,拿到返回做后续处理。当蓝色圆点的“ proceed ”操作结束,事实上它后面的圆点的“ proceed ”操作已经全部结束了。再以红色圆点为例,它先做前置工作,即“ proceed part1 ”的部分,一直需要等到后面的绿色、天蓝色、紫色圆点的前置工作和后置工作都结束( proceed part1 和 proceed part2 部分),以及粉红色圆点的工作结束,红色圆点继续做后置工作( proceed part2 )。这看上去很像一个递归操作。

       OkHttp 源码结构图:

OkHttp源码结构图

参考资料:
腾讯课堂 HenCoder

Fork me on GitHub