一、OkHttp的优点
- 1、拦截器,责任链模式,简化逻辑。每层只需要关注自己的责任,各层之间通过约定的接口/协议进行合作
- 2、默认缓存拦截器会获取 请求头、响应头 Cache-control 配置 可由前后端配合控制缓存的有效性。
- 3、连接池,避免了频繁创建和回收请求连接,节省资源。
- 4、使用 GZIP压缩,减少传输的数据量
二、OkHttp执行请求的整个流程
OkHttpClient.Builder > OkHttpClient > + Request > RealCall > execute / enqueue >
拦截器链 > CallServerInterceptor请求网络
Okhttp 内部 socket 连接成功后,会获取 socket 的输入、输出流,用Okio包装,
封装成一个 HttpCodeC 对象,用于操作请求信息的写入、和返回信息的读取
三、OkHttp中的拦截器
责任链模式:
它包含了一些命令对象和一系列的处理对象,每一个处理对象决定它能处理哪些命令对象,
它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。
该模式还描述了往该处理链的末尾添加新的处理对象的方法。
Interceptor 把实际的网络请求、缓存、透明压缩等功能都统一了起来,每一个功能都只是一个
Interceptor,它们再连接成一个 Interceptor.Chain,环环相扣,最终圆满完成一次网络请求。
1)首先调用的是,在配置 OkHttpClient 时设置的 interceptors;
2)RetryAndFollowUpInterceptor:负责失败重试以及重定向的
3)BridgeInterceptor:负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应的
4)CacheInterceptor:负责读取缓存直接返回、更新缓存的
5)ConnectInterceptor:负责和服务器建立连接的
6)networkInterceptors:配置 OkHttpClient 时设置的
7)CallServerInterceptor:负责向服务器发送请求数据、从服务器读取响应数据的
拦截器链会逐个调用,如果中间没有拦截器完成处理,会一直往下调用 CallServerInterceptor 执行真正的网络请求
请求的结果会返回给上一个调用的拦截器,逐级返回。
这个过程类似View的事件机制。
四、OkHttp中的同步请求与异步请求的理解及其源码
1、同步请求
String run(String url) throws IOException {
Request request = new Request.Builder()
.url(url)
.build();
Response response = client.newCall(request).execute();
return response.body().string();
}
final class RealCall implements Call {
@Override
public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed"); // (1)
executed = true;
}
try {
client.dispatcher().executed(this); // (2)
Response result = getResponseWithInterceptorChain(); // (3)
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this); // (4)
}
}
}
1)检查这个 call 是否已经被执行了,每个 call 只能被执行一次,
如果想要一个完全一样的 call,可以利用 call#clone 方法进行克隆。
2)利用 client.dispatcher().executed(this) 来进行实际执行,
dispatcher 是刚才看到的 OkHttpClient.Builder 的成员之一,
它的文档说自己是异步 HTTP 请求的执行策略,现在看来,同步请求它也有掺和。
3)调用 getResponseWithInterceptorChain() 函数获取 HTTP 返回结果,
从函数名可以看出,这一步还会进行一系列“拦截”操作。
4)最后还要通知 dispatcher 自己已经执行完毕。
dispatcher 这里我们不过度关注,在同步执行的流程中,
涉及到 dispatcher 的内容只不过是告知它我们的执行状态,
比如开始执行了(调用 executed),比如执行完毕了(调用 finished),在异步执行流程中它会有更多的参与。
2、建立连接 ConnectInterceptor
@Override
public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
建立连接就是创建了一个 HttpCodec 对象,它将在后面的步骤中被使用。
Http1Codec:对应 HTTP/1.1 的实现
Http2Codec:对应 HTTP/2 的实现
在 Http1Codec 中,它利用 Okio 对 Socket 的读写操作进行封装,
Okio 对 java.io 和 java.nio 进行了封装,让我们更便捷高效的进行 IO 操作。
1)首先获取 StreamAllocation 对象,StreamAllocation中封装了 连接池 ConnectionPool
ConnectionPool 连接池中有一个队列,保存了 RealConnection 对象
2)根据请求信息,先从连接池中找到能用的 RealConnection,
找不到则创建新的 RealConnection对象并返回
3)利用 RealConnection 的输入输出(BufferedSource 和 BufferedSink)创建 HttpCodec 对象
4)RealConnection的 BufferedSource 和 BufferedSink 是在 Socket 连接成功后创建的
public class RealConnection extends xxx implements Connection {
private void connectSocket(int connectTimeout, int readTimeout, Call call,
EventListener eventListener) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
eventListener.connectStart(call, route.socketAddress(), proxy);
rawSocket.setSoTimeout(readTimeout);
try {
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
ce.initCause(e);
throw ce;
}
// The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
// More details:
// https://github.com/square/okhttp/issues/3245
// https://android-review.googlesource.com/#/c/271775/
try {
//【BufferedSource】内部封装的是 socket.getInputStream(),用于接收服务器返回的数据
source = Okio.buffer(Okio.source(rawSocket));
//【BufferedSink】内部封装的是 socket.getOutputStream(),用于向服务器发送数据
sink = Okio.buffer(Okio.sink(rawSocket));
} catch (NullPointerException npe) {
if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
throw new IOException(npe);
}
}
}
}
HttpCodec 中,持有了RealConnection 的输入输出(BufferedSource 和 BufferedSink)
就可以 发送数据到服务器 、 接收服务器数据 了。
3、发送和接收数据 CallServerInterceptor
@Override
public Response intercept(Chain chain) throws IOException {
HttpCodec httpCodec = ((RealInterceptorChain) chain).httpStream();
StreamAllocation streamAllocation = ((RealInterceptorChain) chain).streamAllocation();
Request request = chain.request();
long sentRequestMillis = System.currentTimeMillis();
/*【1】httpCodec中持有 RealConnection的 BufferedSource 和 BufferedSink 对象
* 可以通过 BufferedSink把数据发送给服务器
* 这里是把 request header 发送给服务器
*/
httpCodec.writeRequestHeaders(request);
//【2】如果有 requetsBody,就向服务器发送
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);
bufferedRequestBody.close();
}
httpCodec.finishRequest();
//【3】读取 response header,先构造一个 Response 对象
Response response = httpCodec.readResponseHeaders()
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
//【4】如果有 response body,就在【3】的基础上加上body,构造一个新的 Response 对象
if (!forWebSocket || response.code() != 101) {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();
}
// 省略部分检查代码
return response;
}
核心工作都由 HttpCodec 对象完成,HttpCodec 实际上是用Okio
4、异步请求
核心线程 0,最大线程数 Integer.MAX_VALUE,闲置时间60秒
readyAsyncCalls 等待执行请求队列
runningAsyncCalls 正在执行的请求队列
Okhttp:默认最大同时请求数
Okhttp:一个 host 最多能同时有 5个请求
private int maxRequests = 64;// 默认最大同时请求数
private int maxRequestsPerHost = 5;//一个 host 最多能同时有 5个请求
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
System.out.println(response.body().string());
}
});
// RealCall#enqueue
@Override
public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
// Dispatcher#enqueue
public final class Dispatcher {
//核心线程 0,最大线程数 Integer.MAX_VALUE,闲置时间60秒。
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
synchronized void enqueue(AsyncCall call) {
/* dispatcher 在异步执行时发挥的作用了,
* 如果当前还能执行一个并发请求,那就立即执行,否则加入 readyAsyncCalls 队列。
* 而正在执行的请求执行完毕之后,会调用 promoteCalls() 函数,
* 来把 readyAsyncCalls 队列中的 AsyncCall “提升”为 runningAsyncCalls,并开始执行。
*/
if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
runningAsyncCalls.add(call);
executorService().execute(call);
} else {
readyAsyncCalls.add(call);
}
}
}
Okhttp默认线程池:核心线程 0,最大线程数 Integer.MAX_VALUE,闲置时间60秒。
final class AsyncCall extends NamedRunnable {
private final Callback responseCallback;
AsyncCall(Callback responseCallback) {
super("OkHttp %s", redactedUrl());
this.responseCallback = responseCallback;
}
...
//execute() 是在父类的 run()中调用的
@Override
protected void execute() {
boolean signalledCallback = false;
try {
//这里开始调用拦截器链,一个个拦截器开始执行
Response response = getResponseWithInterceptorChain();
//执行完成后,把结果通过 callback 回调到上层
if (retryAndFollowUpInterceptor.isCanceled()) {
signalledCallback = true;
responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
} else {
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
}
} catch (IOException e) {
...
} finally {
/* 执行完毕后会调用 Diaspatcher的 finish()
* finish()方法中会把当前请求从 runningAsyncCalls 队列中移除
*/
client.dispatcher().finished(this);
}
}
}
5、返回数据的获取
同步(Call#execute() 执行之后)或者异步(Callback#onResponse() 回调中)请求完成之后,
我们就可以从 Response 对象中获取到响应数据了,
包括 HTTP status code,status message,response header,response body 等。
这里 body 部分最为特殊,因为服务器返回的数据可能非常大,
所以 必须通过数据流的方式 来进行访问
(当然也提供了诸如 string() 和 bytes() 这样的方法将流内的数据一次性读取完毕),
而响应中其他部分则可以随意获取。
响应 body 被封装到 ResponseBody 类中,该类主要有两点需要注意:
1)每个 body 只能被消费一次,多次消费会抛出异常
2)body 必须被关闭,否则会发生资源泄漏
由 HttpCodec#openResponseBody 提供具体 HTTP 协议版本的响应 body,
而 HttpCodec 则是利用 Okio 实现具体的数据 IO 操作
这里有一点值得一提,OkHttp 对响应的校验非常严格,
HTTP status line 不能有任何杂乱的数据,否则就会抛出异常。
6、HTTP 缓存
CacheInterceptor 位于 ConnectInterceptor 之前,
在建立 连接之前,会先检查相应是否已经被缓存、缓存是否可用。
如果是,则直接返回缓存的数据。否则进行后面的流程,并在返回之前把网络数据写入缓存。
OkHttp内置封装了一个 Cache 类,
它会解析 请求头、响应头 Cache-Control,来控制是否使用缓存、以及缓存是否有效
它利用 DiskLruCache ,用磁盘上的有限大小空间进行缓存,按 LRU算法进行缓存淘汰。
如果要自定义缓存策略,实现 InternalCache 接口,在构造 OkHttpClient 时设置。
7、总结
1)OkHttpClient 实现 Call.Factory,负责为 Request 创建 Call;
2)RealCall 为具体的 Call 实现,其 enqueue() 异步接口通过 Dispatcher 利用 ExecutorService 实现,
最终进行网络请求时和同步 execute() 接口一致,都是通过 getResponseWithInterceptorChain() 函数实现;
3)getResponseWithInterceptorChain() 中利用 Interceptor 链条,分层实现缓存、透明压缩、网络 IO 等功能;
五、OkHttp中涉及到的设计模式
Builder模式、责任链模式、外观模式(门面模式)、策略模式(缓存这块)
六、OkHttp底层网络请求实现,socket还是URLConnection
Socket
在 RealConnection#connect() 中会创建并连接 socket
七、HttpDns
使用自己信任的dns Http服务器,做dns域名解析,降低被劫持的风险。
定义:
HttpDns,是对DNS解析的另一种实现方式,将域名解析的协议由 DNS协议 换成 Http协议。
原理:
客户端直接访问HttpDNS接口,获取业务在域名配置管理系统上配置的访问延迟最优的IP
(基于容灾考虑,还是保留次选使用运营商LocalDNS解析域名的方式)
HttpDns优势:
绕过运营商的本地域名解析服务器,避免本地域名解析服务器缓存/劫持/插入广告
提供HttpDns解析服务的厂商:提供httpdns解析服务的有:
阿里云HttpDNS:Get请求,返回一个json
腾讯 DNSPod D+:Get请求,返回结果不是json,有免费版本
1、使用Interceptor,直接将域名替换为ip地址
1)优点
对Dns的控制偏上层,可更加细化,控制灵活。
容灾处理更容易
2)缺点 一切跟域名有关的处理全部失效
在Https下处理SSL证书会出现校验问题
ip访问时出现Cookie校验问题。
2、使用OkHttp提供的dns接口,新建Dns子类,实现lookup()方法。
1)优点
Https下不会存在证书校验问题,保证流程正常执行
种 Cookie 时不会存在问题
2)缺点
时机过于底层,容灾控制都不方便。
okhttp自身存在缓存,一旦dns自身ttl过期,okhttp缓存有可能还在使用,会存在一定的风险。
3)实现
compile 'com.qiniu:happy-dns:0.2.13'
compile 'com.squareup.okhttp3:okhttp:3.9.0'
public class HttpDns implements Dns {
private DnsManager dnsManager;
public HttpDns() {
IResolver[] resolvers = new IResolver[1];
try {
resolvers[0] = new Resolver(getByName("119.29.29.29"));
dnsManager = new DnsManager(NetworkInfo.normal, resolvers);
} catch (UnknownHostException e) {
e.printStackTrace();
}
}
@Override
public List<InetAddress> lookup(String hostname) throws UnknownHostException {
if (dnsManager == null) //当构造失败时使用默认解析方式
return Dns.SYSTEM.lookup(hostname);
try {
String[] ips = dnsManager.query(hostname); //获取HttpDNS解析结果
if (ips == null || ips.length == 0) {
return Dns.SYSTEM.lookup(hostname);
}
List<InetAddress> result = new ArrayList<>();
for (String ip : ips) { //将ip地址数组转换成所需要的对象列表
result.addAll(Arrays.asList(getAllByName(ip)));
}
return result;
} catch (IOException e) {
e.printStackTrace();
}
//当有异常发生时,使用默认解析
return Dns.SYSTEM.lookup(hostname);
}
}
OkHttpClient okHttpClient = new OkHttpClient.Builder().dns(new HttpDns()).build();
推荐阅读:
拆轮子系列-拆OkHttp