解析OkHttp,参考源码3.14.x,如有错漏欢迎指出,博主只是菜鸟,第一次分析源码难免有错误,请各位大佬多指教
square/okhttp at okhttp_3.14.x
OKHTTP使用大概要经历以下过程:
java
//第一步创建OkHttpClient,之后的使用统一入口,直接操作client
OkHttpClient client = new OkHttpClient();
//第二步是创建Request 这里做的事情就是要把本次请求的数据填写好
//(包括请求方法、请求地址、请求头等参数) (Request的目的是说明要做什么)
MediaType JSON = MediaType.parse("application/json; charset=utf-8");
RequestBody body = RequestBody.create(JSON, json);
Request request = new Request.Builder()
.url(url)
.post(body)
.build();
//第三步根据请求生成Call任务 (Call的作用是根据Request生成任务,完成的职责是怎么做)
Call call = client.newCall(request)
//第四步执行任务,调用call的execute(同步执行)或者enqueue(异步)
call.enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
e.printStackTrace();
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//处理响应
}
});
}
1. OkHttpClient
OkHttpClient是OkHttp框架的统一使用入口,属于一种装饰模式(门面模式),屏蔽具体实现的细节,提供对外部的统一入口。OkHttpClient管理封装了连接池、线程池、拦截器等子系统,每个OkhttpClient都有自己独立的资源,所以在实际使用中建议使用单例模式创建,避免创建多个线程池、连接池等,发挥okhttp内部的连接复用,调度,控制效果。另外OkHttpClient还使用了构建者模式(builder模式),因为其创建时有许多参数可以配置,且有许多默认参数,所以使用构建者模式进行灵活配置,并且可以减少构造函数的数量。
另外对于cache、dns、proxy、connectionPool、authenticator,这些的配置本质是指定策略,将一些可变的算法或行为抽象为独立的接口,允许在构建 OkHttpClient时注入不同的具体策略,从而灵活地改变客户端的行为,属于策略模式的思想,但不属于经典实现。
2. Request和Response
OkHttp的请求和响应也用到构建者模式,因为根据http协议,请求报文和响应都有许多参数可以配置,所以很适合构建者模式使用。
3. Call
在构建Request之后,根据Request要生成具体任务(Call),其实用到的是工厂方法模式。为什么要用到工厂方法模式,理由是:
- OkHttpClient是一个门面,用于提供对外方便使用的接口,而Call是一个接口,用户代码依赖于 Call接口而非具体的 RealCall实现。这使得 OkHttp 内部可以灵活地改变或扩展 Call的实现(例如为 WebSocket 创建特殊类型的 Call),而不会影响上层使用者。
我们实际上需要一个实现了Call接口的对象RealCall,而复杂对象生成适合使用工厂模式,用new就可以完成创建的对象无需使用工厂模式,Call这个接口的实际功能是一次网络请求任务,属于复杂任务。RealCall需要绑定其所属的 OkHttpClient(内含连接池、调度器、拦截器链等复杂配置)以及原始的 Request对象。工厂模式将这部分组装逻辑隐藏在 OkHttpClient.newCall()方法内部,对使用者透明。实现解耦;
2.另外通过工厂方法创建,确保了每个 RealCall实例都能正确持有对其 OkHttpClient的引用,这是后续进行连接复用、调度器管理等操作的基础。同时,RealCall内部通过 executed标志位控制其只能被执行一次,工厂模式也有助于集中管理这种生命周期约束的初始化。
3.Call创建使用的工厂方法模式是由Call内定义Factory接口,OkHttpClient实现Call.Factory接口,这种实现将Call对象的创建逻辑与使用逻辑分离开。使用者(例如你的业务代码)只需要依赖Call.Factory接口和Call接口,而不需要关心具体的实现类是RealCall,这符合依赖倒置原则。
4. enqueue和execute执行
在任务创建之后,有两种执行方式,execute(同步执行)和enqueue(异步执行),通过源码可发现都是判断没有执行过后,先通知transmitter启动超时计时器(仅同步执行需要)和通知事件监听器有任务开始。
在同步调用 execute() 中,需要立即开始超时监听,确保整个调用过程(包括等待调度的时间)都在超时控制范围内。
callStart() 通知 EventListener 调用已开始,记录调用开始的准确时间点,包括调度等待时间
提供完整的调用生命周期监控(异步调用 enqueue在 AsyncCall.execute() 中会调用超时监听 timeoutEnter())
(可以发现同步队列将任务传给dispatcher后,自己调用getResponseWithInterceptorChain执行了,说明实际是在调用线程执行的,不是dispatcher调度)
(在源码中异步请求是new了一个AsyncCall,在传给dispatcher,AsyncCall是RealCall内部类,默认持有RealCall引用,另外存储了回调函数和统计此call目前对应主机已有连接数的计数器)
java
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
transmitter.timeoutEnter();
transmitter.callStart();
try {
client.dispatcher().executed(this);
return getResponseWithInterceptorChain();
} finally {
client.dispatcher().finished(this);
}
}
@Override public void enqueue(Callback responseCallback) {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
transmitter.callStart();
client.dispatcher().enqueue(new AsyncCall(responseCallback));
}
5. OkHttpClient内部的调度器(分发器)Dispatcher
最终任务提交给dispatcher,由dispatcher调度执行,我们看看dispatcher的内部构造。
5.1 内部成员:
java
private int maxRequests = 64;
private int maxRequestsPerHost = 5;
private @Nullable Runnable idleCallback;
/** Executes calls. Created lazily. */
private @Nullable ExecutorService executorService;
/** Ready async calls in the order they'll be run. */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** Running synchronous calls. Includes canceled calls that haven't finished yet. */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
5.1.1 maxRequests 、maxRequestsPerHost
成员maxRequests代表dispatcher同时执行最大的连接数,默认是64个,但是可以修改;maxRequestsPerHost代表同一个主机最多保持的连接数,默认5个,同样支持修改。
限制并发数量的原因如下:
-
每个连接都会消耗系统资源(文件描述符、内存、CPU等),无限制的连接可能导致系统资源被耗尽。
-
限制对单个主机的连接数,避免被误认为攻击行为,同时保护目标服务器不被过多并发请求压垮,确保多个客户端能够公平地访问服务器资源,属于客户端自律。
-
HTTP/1.1 和 HTTP/2 都支持连接复用,建立和维护连接都有开销,合理限制可以减少不必要的开销,过多连接反而降低性能
-
操作系统对单进程的TCP连接数有限制,可用端口数量有限
-
通常浏览器对单个域名的并发连接数限制在6-8个
5.1.2 idleCallback(空闲时回调)
idleCallback 是一个在 Dispatcher 变为空闲状态时被调用的回调函数,空闲状态定义为:当正在运行的调用数量返回到零时。作用是
-
资源清理,当所有网络请求完成后,可以执行一些清理操作释放相关的系统资源
-
应用状态管理,通知应用当前没有正在进行的网络请求,可用于更新应用的网络状态指示器
-
性能优化,在无网络活动时执行一些后台任务,暂停或恢复其他相关操作
5.1.3 executorService(线程池)
使用懒加载的方式初始化线程池,在第一次被获取时进行初始化。
java
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
线程池使用的参数是,没有核心线程、最大线程数可达到int整形最大值(相当于不限制),超时时间60s,使用同步工作队列。
为什么要这么配置参数:
- 只有在有任务需要执行时才创建线程,避免在没有请求时维持空闲线程,节省系统资源,符合网络请求的突发性特点
2.最大线程数不做限制,允许线程池根据需要创建足够多的线程,实际通过maxRequests参数控制并发数,将并发控制权交给应用层而非线程池本身
3.SynchronousQueue队列意味着希望获得最大并发量。因为无论如何,向线程池提交任务,往队列提交任务都会失败。而失败后如果没有空闲的非核心线程,就会检查如果当前的线程池中的线程数是否达到最大线程数,未达到最大线程则会新建线程执行新提交的任务。完全没有任何等待,唯一制约它的就是最大的线程数量。因此一般配合Integer.MAX_VALUE实现真正的无等待,加快响应速度
5.1.4 任务队列
同步任务有一个队列runningSyncCalls,存储正在运行的同步任务;异步任务有两个队列readyAsyncCalls和runningAsyncCalls,分别代表等待队列和执行队列。
这些队列都使用ArrayDeque的数据结构,在java中queue是接口,而需求的任务队列需要队头队尾操作,可选择LinkedList或者ArrayDeque,一个是基于双向链表,一个基于循环数组,因为ArrayDeque有更好的内存访问模式,在内存中连续存放,方便遍历时找到下个元素,所以选择了使用ArrayDeque。
5.2 同步执行过程
dispatcher内部的executed只是把同步任务记录到正在执行的同步队列,并没有涉及到dispatcher内部调度,而是由调用RealCall.execute的线程自己运行,执行过程是由拦截器链一步步将内部的拦截器走完(getResponseWithInterceptorChain),然后返回响应,最终dispatcher移除正在执行的记录。
java
synchronized void executed(RealCall call) {
runningSyncCalls.add(call);
}
5.3 异步执行过程
异步任务通过在dispatcher的enqueue方法中先被放入异步等待队列;
之后如果此任务不是websocket,会调用findExistingCallWithHost查找是否已经存在对同一主机的请求,如果存在,新请求会通过reuseCallsPerHostFrom方法复用已有请求的callsPerHost计数器,如果不存在,新请求会使用自己的计数器(确保同一个主机的请求使用同个计数器,这样连接数统计才正常);
promoteAndExecute完成的事情是从等待队列拿出目前能运行的任务(可以多个),然后执行,具体逻辑是遍历等待队列,如果此时运行的请求数小于maxRequests,对该主机的请求数小于maxRequestsPerHost,才会将请求从就绪队列移动到运行队列并执行。最终放入线程池执行,调用getResponseWithInterceptorChain经过拦截器链内部的所有拦截器的处理,然后得到响应。
java
void enqueue(AsyncCall call) {
synchronized (this) {
readyAsyncCalls.add(call);
// Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
// the same host.
if (!call.get().forWebSocket) {
//查询是否有请求相同主机的异步AsyncCall,如果有的话复用对应主机连接的计数器
AsyncCall existingCall = findExistingCallWithHost(call.host());
if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
}
}
//尝试调度执行任务
promoteAndExecute();
}
private boolean promoteAndExecute() {
assert (!Thread.holdsLock(this));
List<AsyncCall> executableCalls = new ArrayList<>();
boolean isRunning;
synchronized (this) {
for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) {
AsyncCall asyncCall = i.next();
//遍历异步等待队列,找出符合最大连接数和相同主机连接数限制的AsyncCall
if (runningAsyncCalls.size() >= maxRequests) break; // Max capacity.
if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; // Host max capacity.
i.remove(); //从等待队列移除
asyncCall.callsPerHost().incrementAndGet(); //对应请求主机连接数计数器递增
executableCalls.add(asyncCall); //加入本次可执行队列
runningAsyncCalls.add(asyncCall); //加入正在运行队列
}
isRunning = runningCallsCount() > 0;
}
for (int i = 0, size = executableCalls.size(); i < size; i++) {
AsyncCall asyncCall = executableCalls.get(i);
asyncCall.executeOn(executorService()); //本次可执行的AsyncCall调用executeOn执行
}
return isRunning;
}
void executeOn(ExecutorService executorService) {
assert (!Thread.holdsLock(client.dispatcher()));
boolean success = false;
try {
executorService.execute(this); //使用线程池执行,实际执行了AsyncCall的execute
success = true;
} catch (RejectedExecutionException e) {
InterruptedIOException ioException = new InterruptedIOException("executor rejected");
ioException.initCause(e);
transmitter.noMoreExchanges(ioException);
responseCallback.onFailure(RealCall.this, ioException);
} finally {
if (!success) {
client.dispatcher().finished(this); // This call is no longer running!
}
}
}
@Override protected void execute() {
boolean signalledCallback = false;
transmitter.timeoutEnter();
try {
//拦截器链中所有拦截器处理后返回Response
Response response = getResponseWithInterceptorChain();
signalledCallback = true;
responseCallback.onResponse(RealCall.this, response);
} catch (IOException e) {
if (signalledCallback) {
// Do not signal the callback twice!
Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);
} else {
responseCallback.onFailure(RealCall.this, e);
}
} catch (Throwable t) {
cancel();
if (!signalledCallback) {
IOException canceledException = new IOException("canceled due to " + t);
canceledException.addSuppressed(t);
responseCallback.onFailure(RealCall.this, canceledException);
}
throw t;
} finally {
client.dispatcher().finished(this);
}
}
}
6. 拦截器链
一个RealCall任务的执行是调用拦截器链进行处理的,拦截器链使用了责任链模式,责任链模式是一种行为设计模式,它允许你将请求沿着处理者链进行传递,直到有一个处理者能够处理该请求为止。在 OkHttp 中,每个拦截器都是一个处理者,请求会依次经过每个拦截器进行处理,最终到达网络或返回给应用程序,如果途中出现异常或者某些情况需要跳过,也方便进行处理。
java
Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
//将所有使用的拦截器按顺序存到List
interceptors.addAll(client.interceptors());
interceptors.add(new RetryAndFollowUpInterceptor(client));
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));
//根据List构建拦截器链
Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
originalRequest, this, client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis());
boolean calledNoMoreExchanges = false;
try {
Response response = chain.proceed(originalRequest); //开始执行拦截器链
if (transmitter.isCanceled()) {
closeQuietly(response);
throw new IOException("Canceled");
}
return response;
} catch (IOException e) {
calledNoMoreExchanges = true;
throw transmitter.noMoreExchanges(e);
} finally {
if (!calledNoMoreExchanges) {
transmitter.noMoreExchanges(null);
}
}
}
6.1 应用拦截器(OkHttpClient.interceptors)
在所有拦截器的最开始,是添加应用拦截器,可以看到将OkHttpClient中的interceptors添加到本次的拦截器List中,在创建OkHttpClient时传入的interceptors在这时用上了。
java
interceptors.addAll(client.interceptors());
那么应用拦截器有什么用?首先说一下拦截器链工作逻辑,其实很像是递归,第一个拦截器执行一些代码,然后转交第二个拦截器,不管其内部怎么做,等到第二个拦截器返回,再执行收尾工作,每个拦截器大致都是这种工作逻辑。
对于应用拦截器,由于是最外层的拦截器,所以能看到原始Request和Response,能感知完整的请求/响应生命周期。可以完成的事情主要有以下几点:
|---------------------------------------------|
| 1. 统一日志记录,记录请求和响应的详细信息,便于调试和监控 |
| 2. 统一身份验证,为所有请求添加认证头(如OAuth Token、API Key等) |
| 3. 请求/响应转换,统一处理请求参数、序列化格式等、统一解析响应数据、处理特定格式 |
| 4. 自定义缓存策略,实现应用级别的缓存逻辑,补充或替代OkHttp的默认缓存 |
| 5. 错误处理与重试,统一处理特定类型的错误,实现应用级别的重试逻辑 |
| 6. 性能监控与统计,记录请求耗时、成功/失败率等指标,用于性能分析 |
6.2 重试和重定向拦截器(RetryAndFollowUpInterceptor)
此拦截器处理请求的重试和重定向逻辑,是Okhttp内部五大内置拦截器的第一个。此拦截器主体是一个死循环,循环处理请求的重试、重定向、认证等,直到请求成功或者已经无法尝试恢复只能抛异常,其中最大重试次数定义MAX_FOLLOW_UPS为20次。
工作逻辑是先做好网络请求的准备工作,之后把request传给后续拦截器去完成,等response返回后,判断是否需要重试、重定向。
java
@Override public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
//拦截器链对象
RealInterceptorChain realChain = (RealInterceptorChain) chain;
//这个类是管理和使用连接的类,在http过程中一直会出现
Transmitter transmitter = realChain.transmitter();
//重定向次数
int followUpCount = 0;
Response priorResponse = null;
//不断循环直到请求满足条件
while (true) {
//为后续创建网络连接和流做准备工作,确保请求能够顺利发送
transmitter.prepareToConnect(request);
if (transmitter.isCanceled()) {
throw new IOException("Canceled");
}
Response response;
boolean success = false;
try {
//交给拦截器链中后续的拦截器处理,并接收返回Response,有点像递归
response = realChain.proceed(request, transmitter, null);
success = true;
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
// 路由连接失败处理,尝试能否重试、恢复
if (!recover(e.getLastConnectException(), transmitter, false, request)) {
throw e.getFirstConnectException();
}
continue;
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
// 网络通信失败处理,尝试能否重试、恢复
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, transmitter, requestSendStarted, request)) throw e;
continue;
} finally {
// The network call threw an exception. Release any resources.
if (!success) {
transmitter.exchangeDoneDueToException();
}
}
// Attach the prior response if it exists. Such responses never have a body.
//附加之前的响应,一般是重定向过程中的中间响应
//这些中间响应(如3xx重定向)不应该有body
if (priorResponse != null) {
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder()
.body(null)
.build())
.build();
}
//通过内部API获取当前响应关联的Exchange对象(OkHttp中连接和数据流的封装)
Exchange exchange = Internal.instance.exchange(response);
//从Exchange中获取当前连接的Route信息(包含URL、代理、协议等连接详情)
Route route = exchange != null ? exchange.connection().route() : null;
//根据当前响应和路由信息判断,确定是否需要后续请求,常见需要后续请求的情况
//HTTP 3xx重定向(如301永久重定向、302临时重定向)
//HTTP 401未授权(需要添加认证头)
//HTTP 407代理认证(需要添加代理认证头)
//HTTP 421 Misdirected Request(需要新的连接)
Request followUp = followUpRequest(response, route);
//如果不需要后续请求(followUp == null),则检查Exchange是否为双向通信,如果是双向通信
//(如WebSocket或duplex请求),则提前退出超时机制,最后返回当前响应,结束拦截器链
if (followUp == null) {
if (exchange != null && exchange.isDuplex()) {
transmitter.timeoutEarlyExit();
}
return response;
}
//需要后续请求的情况,检查后续请求的请求体是否为"一次性"(isOneShot返回true)
//一次性请求体是指只能发送一次的请求体(如某些流类型的请求体)
//如果是一次性请求体,则不进行后续请求,直接返回当前响应
RequestBody followUpBody = followUp.body();
if (followUpBody != null && followUpBody.isOneShot()) {
return response;
}
//关闭当前响应的body,释放资源
//如果Transmitter仍持有Exchange,则调用detachWithViolence()强制分离
//这确保了之前的连接资源被正确释放,为后续请求建立新连接做准备
closeQuietly(response.body());
if (transmitter.hasExchange()) {
exchange.detachWithViolence();
}
//检查后续请求次数限制,如果超出定义的最大值(20)则抛异常
//防止无限重定向或过多认证质询导致的循环请求
if (++followUpCount > MAX_FOLLOW_UPS) {
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
//将当前请求更新为后续请求,将当前响应保存为之前的响应,然后进入下一次循环处理
request = followUp;
priorResponse = response;
}
}
关于连接管理的Transmitter、Exchange、Route后面一起讲,这里先不展开,知道做了什么即可,重点看重试和重定向怎么做的。
6.2.1 重试(Recover)
重试要经过四个条件,都通过才可以重试。首先是OkHttpClient初始化时设置是否可以重试,默认可以重试,如果不允许直接返回false;检查请求是否已开始发送,请求体是不是一次性,如果都满足则不能重试;检查异常是否可能通过重试恢复;检查是否有可用路由。
检查异常是否可重试恢复的函数为isRecoverable,对于协议错误和证书错误返回false,这些时不能通过重试恢复的;如果是中断异常,只有SocketTimeoutException(socket超时)且请求还未开始发送这种情况可以恢复;其它类型异常都允许重试。
java
private boolean recover(IOException e, Transmitter transmitter,
boolean requestSendStarted, Request userRequest) {
// The application layer has forbidden retries.
//检查是否允许重试
if (!client.retryOnConnectionFailure()) return false;
// We can't send the request body again.
//如果请求体是一次性的,且连接已开始发送,不能恢复
if (requestSendStarted && requestIsOneShot(e, userRequest)) return false;
// This exception is fatal.
//检查异常是否可恢复
if (!isRecoverable(e, requestSendStarted)) return false;
// No more routes to attempt.
//检查是否还有可用路由(路由是dns解析域名得到的所有ip,再组合代理服务器(如果有)形成的)
//当前路由可用或者还有未尝试的路由才返回true
if (!transmitter.canRetry()) return false;
// For failure recovery, use the same route selector with a new connection.
return true;
}
private boolean isRecoverable(IOException e, boolean requestSendStarted) {
// If there was a protocol problem, don't recover.
//协议错误通常是由于客户端或服务器对HTTP协议的实现存在问题,
//如协议格式错误、协议版本不兼容等,这类错误重试也不会解决,直接放弃恢复
if (e instanceof ProtocolException) {
return false;
}
// If there was an interruption don't recover, but if there was a timeout connecting to a route
// we should try the next route (if there is one).
//当出现中断异常,只有SocketTimeoutException(socket超时)且请求还未开始发送时可以恢复
if (e instanceof InterruptedIOException) {
return e instanceof SocketTimeoutException && !requestSendStarted;
}
// Look for known client-side or negotiation errors that are unlikely to be fixed by trying
// again with a different route.
//当SSL握手过程中发生证书验证失败时(如证书无效、过期、不被信任等)
//证书问题属于安全性验证失败,重试也不会解决,直接放弃恢复
if (e instanceof SSLHandshakeException) {
// If the problem was a CertificateException from the X509TrustManager,
// do not retry.
if (e.getCause() instanceof CertificateException) {
return false;
}
}
//SSL对等验证异常,无法验证SSL对等方的身份,如证书固定错误(certificate pinning error)
//同样属于证书问题,重试无效
if (e instanceof SSLPeerUnverifiedException) {
// e.g. a certificate pinning error.
return false;
}
// An example of one we might want to retry with a different route is a problem connecting to a
// proxy and would manifest as a standard IOException. Unless it is one we know we should not
// retry, we return true and try a new route.
return true;
}
6.2.2 重定向和认证
根据服务器返回的状态码,有几类可进行重定向或者需要继续认证的:
第一类是需要认证,401是源服务器要求客户端认证、407是代理服务器要求客户端认证;
第二类是重定向,300-303直接进行重定向逻辑,307、308要求get和post才重定向;
第三类超时,408状态码表示服务器在等待客户端发送的完整请求时超时;
第四类服务器问题,503状态码强调"暂时性不可用",表示服务器本身没有"坏掉",只是暂时忙不过来或处于维护模式。
具体如何认证或重定向不细说了。
java
private Request followUpRequest(Response userResponse, @Nullable Route route) throws IOException {
if (userResponse == null) throw new IllegalStateException();
int responseCode = userResponse.code();
final String method = userResponse.request().method();
switch (responseCode) {
// 407 代理服务器要求客户端认证处理
case HTTP_PROXY_AUTH:
Proxy selectedProxy = route != null
? route.proxy()
: client.proxy();
if (selectedProxy.type() != Proxy.Type.HTTP) {
throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
}
return client.proxyAuthenticator().authenticate(route, userResponse);
// 401 源服务器要求客户端认证处理
case HTTP_UNAUTHORIZED:
return client.authenticator().authenticate(route, userResponse);
// 307 308 根据http规范如果非get或者post不自动重定向
case HTTP_PERM_REDIRECT:
case HTTP_TEMP_REDIRECT:
// "If the 307 or 308 status code is received in response to a request other than GET
// or HEAD, the user agent MUST NOT automatically redirect the request"
if (!method.equals("GET") && !method.equals("HEAD")) {
return null;
}
// fall-through
//300-303 直接进行重定向
case HTTP_MULT_CHOICE:
case HTTP_MOVED_PERM:
case HTTP_MOVED_TEMP:
case HTTP_SEE_OTHER:
// Does the client allow redirects?
if (!client.followRedirects()) return null;
String location = userResponse.header("Location");
if (location == null) return null;
HttpUrl url = userResponse.request().url().resolve(location);
// Don't follow redirects to unsupported protocols.
if (url == null) return null;
// If configured, don't follow redirects between SSL and non-SSL.
boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
if (!sameScheme && !client.followSslRedirects()) return null;
// Most redirects don't include a request body.
Request.Builder requestBuilder = userResponse.request().newBuilder();
if (HttpMethod.permitsRequestBody(method)) {
final boolean maintainBody = HttpMethod.redirectsWithBody(method);
if (HttpMethod.redirectsToGet(method)) {
requestBuilder.method("GET", null);
} else {
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
// When redirecting across hosts, drop all authentication headers. This
// is potentially annoying to the application layer since they have no
// way to retain them.
if (!sameConnection(userResponse.request().url(), url)) {
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
// 408 服务器在等待客户端发送的完整请求时超时。这通常发生在:
//客户端网络连接不稳定或缓慢
//服务器负载过高,无法及时处理请求
//请求头或请求体过大,传输时间过长
case HTTP_CLIENT_TIMEOUT:
// 408's are rare in practice, but some servers like HAProxy use this response code. The
// spec says that we may repeat the request without modifications. Modern browsers also
// repeat the request (even non-idempotent ones.)
if (!client.retryOnConnectionFailure()) {
// The application layer has directed us not to retry the request.
return null;
}
RequestBody requestBody = userResponse.request().body();
if (requestBody != null && requestBody.isOneShot()) {
return null;
}
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
// We attempted to retry and got another timeout. Give up.
return null;
}
if (retryAfter(userResponse, 0) > 0) {
return null;
}
return userResponse.request();
// 503 服务器暂时不可用
case HTTP_UNAVAILABLE:
if (userResponse.priorResponse() != null
&& userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
// We attempted to retry and got another timeout. Give up.
return null;
}
if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
// specifically received an instruction to retry without delay
return userResponse.request();
}
return null;
default:
return null;
}
}
6.3 桥接拦截器(BridgeInterceptor)
桥接拦截器的功能实际上是帮助我们构建符合http协议的请求报文,然后把报文交接给之后的拦截器,等待处理结果,接收响应报文然后构建Response返回。
6.3.1 整体流程
从代码上看,先从上个拦截器拿到用户Request,然后就是给request添加一些必要的请求头。(这里有个问题,我们在一开始创建http请求时已经构建Request了,为什么这里还要重新弄一遍? 理由是我们创建Request时使用构建者模式,所以会有未设置http必需字段的情况,这里其实就是帮我们做兜底,防止request对应报文不合法;另外就是一般我们创建Request不会指定cookies,而http是经常有cookies使用的,而okhttp可以自动管理cookies,一般就是在这里为我们无感处理cookies)
在后续拦截器返回响应后,对其进行处理,一个是提取缓存到cookieJar,确保缓存更新;然后是根据Response重新构建ResponseBuilder并指定其对应的Request为上一个拦截器传入的Request,后面将Response返回给上个拦截器时信息才能正确反映对应关系;之后如果okhttp自动添加了gzip压缩请求头,且返回的响应有响应头且gzip压缩,则需要解压缩并去除两个相关响应头防止误导用户。
java
@Override public Response intercept(Chain chain) throws IOException {+
//从上个拦截器传来的request重新构建builder
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
//如果本次请求有请求体,请求头必须有Content-Type,
//并且请求体长度不同必定有Content-Length/Transfer-Encoding请求头
RequestBody body = userRequest.body();
if (body != null) {
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
//必须有host请求头
if (userRequest.header("Host") == null) {
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
//必须有连接方式Connection,代表请求结束后是否关闭连接
if (userRequest.header("Connection") == null) {
requestBuilder.header("Connection", "Keep-Alive");
}
// If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
// the transfer stream.
//Accept-Encoding用于告知服务器客户端支持的内容编码(压缩)方式,不是必须的请求头
//如果用户请求中已经包含Accept-Encoding头,OkHttp不会覆盖它
//对于分块下载请求(Range头),不自动添加gzip支持
//如果OkHttp添加了Accept-Encoding: gzip,它会负责自动解压响应
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
//如果用户没有指定cookies,自动从CookieJar读取,如果有cookies内容,自动附加
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {
requestBuilder.header("Cookie", cookieHeader(cookies));
}
//添加用户代理UA,此请求头不是必须的,
//但是如果用户没有设置,okhttp默认根据自己的版本生成一个
//因为如果不带UA,很容易被服务器拒绝
if (userRequest.header("User-Agent") == null) {
requestBuilder.header("User-Agent", Version.userAgent());
}
//将检查处理后的request传给之后的拦截器处理并接受响应
Response networkResponse = chain.proceed(requestBuilder.build());
//从响应中的响应头尝试取出cookies并保存到cookieJar,维护缓存
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
//拿响应重新构建一个ResponseBuilder,修改其关联的Request为从上个拦截器传入的Request
//确保对上个拦截器,其拿到的响应能够正确反映出是对应其传入的Request
Response.Builder responseBuilder = networkResponse.newBuilder()
.request(userRequest);
//请求时自动添加了Accept-Encoding: gzip头 &&
//响应头包含Content-Encoding: gzip:服务器返回了gzip压缩的内容 &&
//响应体有内容
if (transparentGzip
&& "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
&& HttpHeaders.hasBody(networkResponse)) {
//创建解压流
GzipSource responseBody = new GzipSource(networkResponse.body().source());
//移除两个响应头,因为响应要被解压缩,所以这两个头信息会误导用户
Headers strippedHeaders = networkResponse.headers().newBuilder()
.removeAll("Content-Encoding")
.removeAll("Content-Length")
.build();
responseBuilder.headers(strippedHeaders);
String contentType = networkResponse.header("Content-Type");
//使用解压后的流创建新的响应体
responseBuilder.body(new RealResponseBody(contentType, -1L,
Okio.buffer(responseBody)));
}
return responseBuilder.build();
}
6.3.2 Cookie类
okhttp会自动进行cookie管理,管理cookie的类是CookieJar。CookieJar是一个接口,要求实现从response获取cookie的方法和从存储的cookie中拿出来添加到request的方法。
java
public interface CookieJar {
/**
* Saves {@code cookies} from an HTTP response to this store according to this jar's policy.
*
* <p>Note that this method may be called a second time for a single HTTP response if the response
* includes a trailer. For this obscure HTTP feature, {@code cookies} contains only the trailer's
* cookies.
*/
void saveFromResponse(HttpUrl url, List<Cookie> cookies);
/**
* Load cookies from the jar for an HTTP request to {@code url}. This method returns a possibly
* empty list of cookies for the network request.
*
* <p>Simple implementations will return the accepted cookies that have not yet expired and that
* {@linkplain Cookie#matches match} {@code url}.
*/
List<Cookie> loadForRequest(HttpUrl url);
}
看一下cookie类的实现,成员变量在下方列出。应该很容易理解,我们在为request附加cookie时,就是要看存储的这些cookie中,是不是其内部字段能与request的url匹配上。在cookieJar的loadForRequest方法需要做的就是匹配已有cookie,尝试添加到request。
java
public final class Cookie {
private final String name; // Cookie名称
private final String value; // Cookie值
private final long expiresAt; // 过期时间
private final String domain; // 域名
private final String path; // 路径
private final boolean secure; // 是否仅HTTPS
private final boolean httpOnly; // 是否仅HTTP
private final boolean persistent; // 是否持久化
private final boolean hostOnly; // 是否仅主机
}
cookie类除了几个成员变量,其它需要关注的就是如何匹配,内部的matches的函数就是完成匹配工作,代码上看是分了三步。
首先域名匹配,如果设置仅主机hostOnly == true,那就要求精确匹配域名,例如https://123.com/ 和https://123.com/可以匹配,其它不能匹配,如果hostOnly == false,那就是走子域名匹配domainMatch.
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| // 完全匹配 domainMatch("example.com", "example.com") // true |
| // 子域名匹配 domainMatch("www.example.com", "example.com") // true domainMatch("api.v1.example.com", "example.com") // true domainMatch("sub.example.com", "example.com") // true |
| // 域名不匹配 domainMatch("example.org", "example.com") // false |
| // 缺少点分隔符(错误的后缀匹配) domainMatch("myexample.com", "example.com") // false |
| // IP地址不允许子域名匹配 domainMatch("192.168.1.1", "168.1.1") // false |
| // 反向匹配(不允许) domainMatch("example.com", "www.example.com") // false |
第二步路径匹配,要cookie路径path囊括的范围要大于url的。
|-------------------------------------------------------------------------------------------------------------------------------|
| // 完全匹配 pathMatch(url("/foo"), "/foo") // true |
| // 根路径匹配所有子路径 pathMatch(url("/foo"), "/") // true pathMatch(url("/foo/bar"), "/") // true |
| // 前缀路径匹配 pathMatch(url("/foo/bar"), "/foo") // true(/foo/bar的第4个字符是/) pathMatch(url("/foo/bar/baz"), "/foo") // true |
| // 路径以斜杠结尾 pathMatch(url("/foo/bar"), "/foo/") // true |
| // 前缀相同但缺少分隔符 pathMatch(url("/foobar"), "/foo") // false(第4个字符是b,不是/) |
| // 路径不完整匹配 pathMatch(url("/foo"), "/foo/") // false(/foo不以/结尾) pathMatch(url("/foo"), "/foo/bar") // false(/foo不是/foo/bar的前缀) |
第三步安全协议匹配,cookie如果是http,且secure是true,不能匹配https。
java
/**
* Returns true if this cookie should be included on a request to {@code url}. In addition to this
* check callers should also confirm that this cookie has not expired.
*/
public boolean matches(HttpUrl url) {
// 1. 域名匹配
boolean domainMatch = hostOnly
? url.host().equals(domain) // 主机唯一:精确匹配
: domainMatch(url.host(), domain); // 域名模式:子域名匹配
if (!domainMatch) return false;
// 2. 路径匹配
if (!pathMatch(url, path)) return false;
// 3. 安全协议匹配
if (secure && !url.isHttps()) return false;
return true;
}
//子域名匹配
private static boolean domainMatch(String urlHost, String domain) {
if (urlHost.equals(domain)) {
return true; // As in 'example.com' matching 'example.com'.
}
if (urlHost.endsWith(domain)
&& urlHost.charAt(urlHost.length() - domain.length() - 1) == '.'
&& !verifyAsIpAddress(urlHost)) {
return true; // As in 'example.com' matching 'www.example.com'.
}
return false;
}
//路径匹配
private static boolean pathMatch(HttpUrl url, String path) {
String urlPath = url.encodedPath();
if (urlPath.equals(path)) {
return true; // As in '/foo' matching '/foo'.
}
if (urlPath.startsWith(path)) {
if (path.endsWith("/")) return true; // As in '/' matching '/foo'.
if (urlPath.charAt(path.length()) == '/') return true; // As in '/foo' matching '/foo/bar'.
}
return false;
}
6.3.3 cookieJar的实现类
okhttp提供了几个实现类,首先是默认的NO_COOKIES,直接在CookieJar中定义了,获取和存储直接是空实现,什么都不做,代表不使用cookie。
java
CookieJar NO_COOKIES = new CookieJar() {
@Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
}
@Override public List<Cookie> loadForRequest(HttpUrl url) {
return Collections.emptyList();
}
};
然后是JavaNetCookieJar,充当了OkHttp和Java标准库之间的桥梁,通过委托模式将Cookie管理委托给Java标准库的CookieHandler,与JDK的Cookie处理机制完全兼容。另外作为连接OkHttp和Java标准库的桥梁,也属于桥接模式。
根据这个角色定位,可以看见JavaNetCookieJar内部持有一个CookieHandler,CookieHandler是一个java标准库的接口,要求实现get、put方法,java本身的cookie处理是通过实现此接口的类,不同类对应不同的管理逻辑。
而JavaNetCookieJar实现CookieJar接口要求的获取cookie、存储cookie函数,实际上就是把okhttp的Cookie类转成CookieHandler可以接受的形式,委托给CookieHandler;或者从CookieHandler接收cookie,转成okhttp的Cookie类,具体的管理逻辑由CookieHandler决定,从而能够复用Java标准实现的cookie管理,实现无缝衔接。
java标准库对CookieHandler的实现是CookieManager类,里面有两个成员CookieStore和CookiePolicy。CookieStore也是接口,真正完成cookie的存取,默认有个InMemoryCookieStore实现,存储位置是在内存;CookiePolicy控制哪些Cookie需要存储,定了ACCEPT_ALL、ACCEPT_NONE、ACCEPT_ORIGINAL_SERVER三种,分别代表全部接收、都不接收、要求匹配域名才可以接收。
如果需要自定义Cookie逻辑,那有多种方法可以完成,首先CookieManager这个实现类允许传入CookieStore和CookiePolicy,可以自己指定存储逻辑,但是cookie接收逻辑只能使用CookiePolicy定义的三种;另外就是自己实现CookieHandler接口,然后OkHttp仍然使用JavaNetCookieJar,但是创建OkHttpClient的时候配置CookieJar,直接new一个JavaNetCookieJar并把自己实现CookieHandler的类作为参数传入,这种方式自定义程度高,cookie接收逻辑也可以自定义,如果是实现CookieHandler接口,那么可以适配各种http客户端,但是接口实现较为复杂,需要处理http头格式;最后一种就是实现CookieJar接口,同样自由度高,实现难度较低,避免了处理HTTP协议细节,但是只能适配okhttp。
java
/** A cookie jar that delegates to a {@link java.net.CookieHandler}. */
@EverythingIsNonNull
public final class JavaNetCookieJar implements CookieJar {
private final CookieHandler cookieHandler;
public JavaNetCookieJar(CookieHandler cookieHandler) {
this.cookieHandler = cookieHandler;
}
//保存cookie的方法
//1. Cookie转换:将OkHttp的Cookie对象转换为字符串格式
//2. 头信息构建:构建符合HTTP标准的Set-Cookie头
//3. 委托处理:调用CookieHandler.put()方法保存Cookie
//4. 异常处理:记录保存失败的错误日志
@Override public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
if (cookieHandler != null) {
List<String> cookieStrings = new ArrayList<>();
for (Cookie cookie : cookies) {
cookieStrings.add(cookie.toString(true));
}
Map<String, List<String>> multimap = Collections.singletonMap("Set-Cookie", cookieStrings);
try {
cookieHandler.put(url.uri(), multimap);
} catch (IOException e) {
Platform.get().log(WARN, "Saving cookies failed for " + url.resolve("/..."), e);
}
}
}
//读取cookie的方法
//1. 获取Cookie:调用CookieHandler.get()获取匹配的Cookie
//2. 头信息过滤:只处理Cookie和Cookie2头
//3. Cookie解析:将字符串头信息解析为OkHttp的Cookie对象
//4. 结果返回:返回不可修改的Cookie列表
@Override public List<Cookie> loadForRequest(HttpUrl url) {
// The RI passes all headers. We don't have 'em, so we don't pass 'em!
Map<String, List<String>> headers = Collections.emptyMap();
Map<String, List<String>> cookieHeaders;
try {
cookieHeaders = cookieHandler.get(url.uri(), headers);
} catch (IOException e) {
Platform.get().log(WARN, "Loading cookies failed for " + url.resolve("/..."), e);
return Collections.emptyList();
}
List<Cookie> cookies = null;
for (Map.Entry<String, List<String>> entry : cookieHeaders.entrySet()) {
String key = entry.getKey();
if (("Cookie".equalsIgnoreCase(key) || "Cookie2".equalsIgnoreCase(key))
&& !entry.getValue().isEmpty()) {
for (String header : entry.getValue()) {
if (cookies == null) cookies = new ArrayList<>();
cookies.addAll(decodeHeaderAsJavaNetCookies(url, header));
}
}
}
return cookies != null
? Collections.unmodifiableList(cookies)
: Collections.emptyList();
}
// 将字符串头信息解析为OkHttp的Cookie对象
private List<Cookie> decodeHeaderAsJavaNetCookies(HttpUrl url, String header) {
List<Cookie> result = new ArrayList<>();
for (int pos = 0, limit = header.length(), pairEnd; pos < limit; pos = pairEnd + 1) {
pairEnd = delimiterOffset(header, pos, limit, ";,");
int equalsSign = delimiterOffset(header, pos, pairEnd, '=');
String name = trimSubstring(header, pos, equalsSign);
if (name.startsWith("$")) continue;
// We have either name=value or just a name.
String value = equalsSign < pairEnd
? trimSubstring(header, equalsSign + 1, pairEnd)
: "";
// If the value is "quoted", drop the quotes.
if (value.startsWith("\"") && value.endsWith("\"")) {
value = value.substring(1, value.length() - 1);
}
result.add(new Cookie.Builder()
.name(name)
.value(value)
.domain(url.host())
.build());
}
return result;
}
}
6.4 缓存拦截器(CacheInterceptor)
缓存拦截器是帮助我们处理http缓存的,因为http协议定义了缓存机制,如果之前获取数据,服务器返回的响应中带了缓存相关字段,那么客户端可以对响应进行缓存,再次请求时可以先检查缓存是否可用,可用直接返回,节省网络带宽,加快速度。
(关于http缓存大致分为两类,强制缓存和协商缓存,一般第一次请求资源,服务器返回的响应中如果允许缓存,会是强制缓存;当强制缓存有效期过了,下次需要时可尝试向服务器发送请求并携带缓存协商相关字段,看之前的强制缓存是否能够继续使用,所以叫协商缓存。)(如果对缓存不了解,可参考我之前整理的一篇文章,第11点就是缓存 http协议基础-CSDN博客 )
回到缓存拦截器,其工作逻辑也很好理解,首先尝试从缓存中拿到候选的响应,然后根据缓存策略看能否使用,可使用缓存的话直接用缓存构造响应返回;如果缓存不能直接使用,有多种情况:没有缓存可用、缓存过期需要向服务器协商、策略上要求不使用缓存;这些就向服务器发请求,即交由之后的拦截器处理并接收response;收到response后就是尝试缓存到本地,更新现有缓存,构建返回response。
6.4.1 整体流程
接下来看代码,具体流程如下:
1.先从缓存拦截器的成员变量cache,尝试拿出候选的响应缓存。cache就是实际存取缓存的
2.根据本次请求和候选缓存的情况,构造缓存策略类。使用了简单工厂模式,最终出来的字段有两个,先是networkRequest,就是本次请求实际需要进行的网络请求Request,如果为空表示本次不需要进行网络请求;另外就是cacheResponse,代表本次可用的缓存response,如果为空代表本次没有可用缓存。如何生成这两个字段我会在后续讲,这里先了解流程即可
3.如果是cache中能找到候选缓存,但是缓存策略判断其不可用的情况(例如指定本次请求不使用缓存),做候选缓存关闭操作,防止泄漏
4.开始处理不需要使用网络请求的情况,如果networkRequest为空,且没有可用缓存,构造一个状态码为504的响应返回;如果有可用缓存(强制缓存),构建一个一模一样的response,但是这个response的cacheResponse成员有改变,是将可用缓存的body剥离掉的,其它不变的一个response。即原先的强制缓存是R,现在我们返回一个除了cacheResponse成员与R不一样,其它都一样的新变量R1,R1的cacheResponse是剥离掉body的R。
(为什么要这么做?首先Response是一次性的,如果直接把强制缓存返回,那么其之后被上个拦截器消费,存在cache中的强制缓存就失效了,所以需要重新构造一个进行返回;另外为什么需要将强制缓存的body去掉后,塞进新构造的response返回,要解答这个问题我们先了解一下一些必须的知识,Response中存储有networkResponse、cacheResponse成员,用于记录网络来源信息、记录缓存来源信息,仅仅只是记录而不是实际进行使用,不需要body,而必须将body去掉的原因是body通常包含流数据,如果缓存响应和网络响应都保留完整的body引用,会导致body对象无法被垃圾回收、多个地方同时持有body引用可能导致并发问题)
5.处理网络请求,将缓存策略生成的networkRequest交给下个拦截器执行,然后接收返回response
6.处理协商缓存请求成功,如果可用缓存cacheResponse非空,网络请求networkRequest也非空,返回响应是304,那么协商缓存成功,可复用之前的强制缓存。同样构造一个新response,cacheResponse字段和networkResponse字段存储的都是去掉body的响应。构造完新response后关闭网络请求拿到的response防止泄漏,然后记录缓存命中情况、更新缓存
7.处理其它情况的网络响应,并更新缓存,其中 POST、PATCH、PUT、DELETE、MOVE这5种方法会使缓存失效,需要移除
java
@Override public Response intercept(Chain chain) throws IOException {
//首先从缓存拿出候选的响应
Response cacheCandidate = cache != null
? cache.get(chain.request())
: null;
long now = System.currentTimeMillis();
//这里是缓存策略,使用CacheStrategy决定是否使用网络或缓存
//networkRequest非空则需要发起的网络请求(null表示不需要网络请求)
//cacheResponse是可用的缓存响应(null表示无缓存可用)
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;
//统计缓存使用情况,用于监控和性能分析
if (cache != null) {
cache.trackResponse(strategy);
}
//缓存中有数据,但缓存策略认为这些数据不可用,这时候做资源清理
//当cacheCandidate不为null时,它包含一个打开的body流(指向磁盘文件或内存数据)
//如果不及时关闭可能有以下后果:
//文件句柄泄漏:磁盘缓存文件无法被其他进程访问
//内存泄漏:内存缓存无法被垃圾回收
//系统资源耗尽:大量未关闭的流会消耗系统资源
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
//缓存策略要求不需发起网络请求,但是也没有可用缓存,返回504
if (networkRequest == null && cacheResponse == null) {
return new Response.Builder()
.request(chain.request())
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(Util.EMPTY_RESPONSE)
.sentRequestAtMillis(-1L)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
}
// If we don't need the network, we're done.
//如果不需要发起网络请求,且有可用的缓存响应(强制缓存)
//利用强制缓存响应,剥离掉body,重新构造一个新的response返回 要思考为什么???
if (networkRequest == null) {
return cacheResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.build();
}
//缓存策略需要进行网络请求,就调用之后的拦截器,并接收响应
Response networkResponse = null;
try {
networkResponse = chain.proceed(networkRequest);
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
// If we have a cache response too, then we're doing a conditional get.
//经过网络请求后,服务器的响应码为304(缓存未修改),这种说明之前过期的强制缓存还可以用
//构造返回响应同样剥离了cacheResponse、networkResponse的body
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {
Response response = cacheResponse.newBuilder()
.headers(combine(cacheResponse.headers(), networkResponse.headers()))
.sentRequestAtMillis(networkResponse.sentRequestAtMillis())
.receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
networkResponse.body().close();
// Update the cache after combining headers but before stripping the
// Content-Encoding header (as performed by initContentStream()).
//记录缓存命中情况和更新缓存
cache.trackConditionalCacheHit();
cache.update(cacheResponse, response);
return response;
} else {
closeQuietly(cacheResponse.body());
}
}
//其它情况构造response返回,因为需要记录好response对应的候选缓存响应与网络响应
//所以不能直接返回networkResponse
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
//如果启用了缓存
if (cache != null) {
//当响应有body且根据缓存策略可缓存时才执行网络响应写入缓存
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
// Offer this request to the cache.
CacheRequest cacheRequest = cache.put(response);
return cacheWritingResponse(cacheRequest, response);
}
//处理使缓存失效的HTTP方法,移除无效缓存,列出的方法有五种
// POST、PATCH、PUT、DELETE、MOVE
if (HttpMethod.invalidatesCache(networkRequest.method())) {
try {
cache.remove(networkRequest);
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
6.4.2 Cache和InternalCache
在缓存拦截器内部有一个cache成员,他是个InternalCache接口,可以完成缓存存取、更新、删除,记录缓存命中情况。而我们在创建OkhttpClient时指定的缓存是Cache类,Cache类内部存储了一个internalCache的实现,OkhttpClient只是通过构造函数指定Cache的文件、最大容量等信息,内部实现无需用户感知,并且内部实现整个缓存系统的工作,属于较为复杂的系统,所以对用户来说算是门面模式。
另外缓存拦截器只是依赖于InternalCache,而Cache类内部有一个此接口的实现,将Cache类这个完成了缓存系统工作的类适配到InternalCache接口,属于适配器模式。这种设计使得OkHttp能够:对外提供简单易用的缓存API,对内保持灵活的缓存策略实现,支持未来可能的缓存实现替换。
为什么Cache类不直接实现InternalCache接口,而是创建InternalCache成员?因为如果Cache类直接实现InternalCache接口,就会将内部API暴露给外部用户,违反包访问权限的设计意图,破坏OkHttp的模块化架构;(内部API是在不同版本实现可能变动的,如果用户可见,可能会直接实现InternalCache进行使用,后续版本变动可能会引发问题)
这里简单说一下,DiskLruCache是基于最近最少使用(LRU)算法实现的磁盘缓存,通过LiskHashMap实现LRU算法,第三个参数设置true使得LiskhashMap按访问顺序排序,最近被访问的元素会被移动到双向链表的末尾;所有公有方法通过synchorized加锁保证线程安全;事务写入机制,确保原子性;磁盘访问操作由线程池完成,线程池只有一个非核心线程,顺序执行任务队列的任务。
Cache的功能实现,在代码中可以发现Cache类是基于DiskLruCache实现的,并且内部记录了缓存命中的一些情况,且实现了http协议的条件缓存解析和多种缓存控制头。具体可以自行 了解,这里不细说。
6.4.3 CacheStrategy为什么用工厂创建
CacheStrategy是通过简单工厂创建的,因为需要根据传入的参数决策出是否进行网络请求和是否有缓存可使用。如果在构造函数做这些决策的事情并不符合构造函数的定位,构造函数应该只负责对象初始化,而不是业务决策。另外是意图不明确,用户无法知道构造函数内部执行了复杂的决策逻辑。
而如果使用工厂模式,可以清楚的知道创建的一个策略工厂,并进行调用获取到一个策略CacheStrategy,职责也明确;另外是创建多个相同的CacheStrategy,可以都从工厂获取,减少消耗;另外是扩展方便,修改工厂即可。
6.4.4 确定缓存策略
http协议关于缓存策略的字段实际上是请求头、响应头的几个字段控制,确定缓存策略就是处理这些字段。大致分5步:
1.进行基本的有效性检查,如果任何条件不满足,直接返回需要发送网络请求的策略;
2.检查请求的缓存控制头,如果请求明确要求不使用缓存或已有条件头,则发送网络请求;
3.计算缓存的各种时间参数,为后续新鲜度判断做准备;
4.新鲜度判断,如果缓存已存在时间 + 最小新鲜时间 < 新鲜生命周期 + 最大过期容忍时间
,说明缓存仍然有效,可以直接使用。(强制缓存)
5.缓存已过期,但可能仍然有效,则尝试构建条件请求。
java
private CacheStrategy getCandidate() {
// No cached response.
//没有候选响应
if (cacheResponse == null) {
return new CacheStrategy(request, null);
}
// Drop the cached response if it's missing a required handshake.
//https当时没有握手信息
if (request.isHttps() && cacheResponse.handshake() == null) {
return new CacheStrategy(request, null);
}
// If this response shouldn't have been stored, it should never be used
// as a response source. This check should be redundant as long as the
// persistence store is well-behaved and the rules are constant.
//响应不应该被缓存
if (!isCacheable(cacheResponse, request)) {
return new CacheStrategy(request, null);
}
//检查请求的缓存控制头,如果请求明确要求不使用缓存或已有条件头,则发送网络请求
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
// 缓存响应已存在的时间
long ageMillis = cacheResponseAge();
// 缓存的新鲜生命周期(有效期)
long freshMillis = computeFreshnessLifetime();
// 考虑请求的max-age限制
if (requestCaching.maxAgeSeconds() != -1) {
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
// 最小新鲜时间
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
// 最大过期容忍时间
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}
//缓存新鲜度判断
if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
// 缓存仍然新鲜,使用缓存
Response.Builder builder = cacheResponse.newBuilder();
if (ageMillis + minFreshMillis >= freshMillis) {
builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
}
//缓存接近过期,添加警告头
long oneDayMillis = 24 * 60 * 60 * 1000L;
if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());
}
// Find a condition to add to the request. If the condition is satisfied, the response body
// will not be transmitted.
String conditionName;
String conditionValue;
//如果缓存不新鲜但可能仍然有效,则构建条件请求(协商缓存)
// 使用ETag、Last-Modified或Served-Date作为条件
if (etag != null) {
conditionName = "If-None-Match";
conditionValue = etag;
} else if (lastModified != null) {
conditionName = "If-Modified-Since";
conditionValue = lastModifiedString;
} else if (servedDate != null) {
conditionName = "If-Modified-Since";
conditionValue = servedDateString;
} else {
// 无条件字段,发送普通请求
return new CacheStrategy(request, null); // No condition! Make a regular request.
}
Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);
//构造条件请求
Request conditionalRequest = request.newBuilder()
.headers(conditionalRequestHeaders.build())
.build();
return new CacheStrategy(conditionalRequest, cacheResponse);
}
6.5 连接拦截器(ConnectInterceptor)
连接拦截器的核心职责是建立与目标服务器的网络连接,然后就把请求和可用连接交给后续拦截器了,直接返回后续拦截器的响应。可以发现核心功能是在Transmitter类中实现的。
java
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
Transmitter transmitter = realChain.transmitter();
// 对于非GET请求(如POST、PUT等),会进行更严格的检查
// 这是因为这些请求通常涉及数据修改,需要确保连接是可靠的
boolean doExtensiveHealthChecks = !request.method().equals("GET");
//获取可用连接,属于连接拦截器的核心功能
Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks);
//把请求和连接交给后续的拦截器处理,直接返回其响应
return realChain.proceed(request, transmitter, exchange);
}
Tramsmitter类,作为应用层和网络层之间的桥梁,负责协调连接、请求和响应的管理。在早期的OkHttp版本中,这个功能由StreamAllocation类承担。从OkHttp创建Call开始,在 RealCall的构造函数内部,会同步创建一个专属的 Transmitter实例,由这个实例管理这次请求的生命周期,完成连接管理、超时控制、事件上报、资源清理等,一个Transmitter严格对应一个请求。
Transmitter类有四个主要功能:
1.连接管理协调,管理RealConnection的生命周期,协调连接池(RealConnectionPool)的使用,处理连接的获取、复用和释放。
2.请求/响应交换管理,创建和管理Exchange对象(单个HTTP请求/响应对),跟踪交换状态(请求完成、响应完成),处理交换过程中的异常。交换实际上就是数据通信。
3.超时控制,提供异步超时机制(AsyncTimeout),支持调用超时配置,处理WebSocket和双工调用的早期退出。
4.处理取消,支持异步取消操作,处理HTTP/2流的单独取消,在TLS握手期间处理取消。
Tramsmitter类关键字段如下所示
java
// 核心组件
private final OkHttpClient client; // OkHttp客户端实例
private final RealConnectionPool connectionPool; // 连接池
private final Call call; // 关联的调用
private final EventListener eventListener; // 事件监听器
private final AsyncTimeout timeout; // 超时控制器
// 请求和连接相关
private Request request; // 当前请求
private ExchangeFinder exchangeFinder; // 交换查找器
public RealConnection connection; // 当前连接(受连接池保护)
private @Nullable Exchange exchange; // 当前交换
// 状态标记
private boolean exchangeRequestDone; // 请求是否完成
private boolean exchangeResponseDone; // 响应是否完成
private boolean canceled; // 是否已取消
private boolean timeoutEarlyExit; // 是否提前退出超时
private boolean noMoreExchanges; // 是否不再有交换(不可用了)
6.5.1 连接池(RealConnectionPool)
先看成员,首先是一个线程池executor,用于在后台持续检查并清理连接池中那些闲置过久或超出数量限制的连接,及时释放系统资源,防止资源泄露,对应的清理任务是cleanupRunnable;
然后是连接池的两个参数,最大空闲连接数和连接的超时时间,线程池检查闲置过久或者超时数量限制,就是对应这两个参数对应的值;
连接队列connections是存储连接的双端队列,所有RealConnection对象都会被存放在这个队列中。RealConnection是 OkHttp 对底层 Socket 连接的封装。当有新的网络请求需要发起时,okhttp就是从连接队列尝试找到可复用的连接,减少连接建立开销。而后台连接清理也是看connections中的空闲连接数,如果连接没有关联transmitter代表空闲;
路由数据库routeDatabase是一个OkHttp内部使用的路由失败"黑名单",它专门用来记录那些曾经连接失败的路由(Route),从而帮助OkHttp在后续请求中智能地避开这些有问题的路线,提升连接效率和稳定性;如果没有其它可选路由,才会使用失败的路由进行尝试,如果尝试成功会把路由移出"黑名单";
运行标志cleanupRunning代表现在后台清理线程是否在工作,如果已经在工作了,那就不用再继续启动清理线程,理论上最多只有一个线程运行。
java
// 线程池执行器 - 用于后台清理任务
private static final Executor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS, new SynchronousQueue<>(),
Util.threadFactory("OkHttp ConnectionPool", true));
// 连接池配置
private final int maxIdleConnections; // 最大空闲连接数
private final long keepAliveDurationNs; // 连接保持活动时间(纳秒)
// 清理任务
private final Runnable cleanupRunnable = () -> {
while (true) {
long waitNanos = cleanup(System.nanoTime());
if (waitNanos == -1) return;
// 等待指定时间后再次清理...
}
};
// 连接存储
private final Deque<RealConnection> connections = new ArrayDeque<>(); // 连接队列
final RouteDatabase routeDatabase = new RouteDatabase(); // 路由数据库
boolean cleanupRunning; // 清理任务运行状态
6.5.1.1 为给定地址获取复用连接
步骤如下:遍历所有现有连接,检查连接是否支持多路复用(如需要),验证连接的是否可复用(通过isEligible()方法),如果找到合适连接,则通过acquireConnectionNoEvents()获取它。
如何检查是否可连接复用(isEligible)
大致分两个阶段,首先是基础条件与精确匹配。
1.容量与状态检查,连接上当前的活跃请求数(transmitters.size())不能超过其最大限制(allocationLimit)。连接必须允许创建新的请求(noNewExchanges为 false),例如,连接本身没有被关闭或标记为不可用 。
2.非HOST字段匹配,新请求的 Address与连接当前保存的 Address,除主机名(host)外的其他属性必须完全一致。这包括端口、SSL配置、代理设置、协议列表、认证信息等 。这是复用连接的基础,确保了两条请求的底层连接参数是兼容的。
3.HOST精确匹配(完美匹配),如果新请求的URL主机名与连接当前使用的主机名完全相同,则该连接是"完美匹配",可以被直接复用 。这是最常见且最高效的复用场景。
第二阶段是尝试合并,当主机名不完全相同时,OkHttp在HTTP/2协议下支持更高级的连接合并机制。这意味着即使主机名不同,只要满足严格的条件,连接仍可能被复用 。
1.协议要求:连接必须是HTTP/2连接,因为HTTP/2允许多个请求在一个连接上多路复用 。
2.IP地址与路由一致:新请求的IP地址和路由必须与连接的IP地址和路由匹配。这通常要求是直连(Proxy.Type.DIRECT),因为代理会隐藏真实的服务器IP 。
3.证书验证:服务器为当前连接颁发的SSL证书必须也适用于新的主机名。这通常意味着证书是一个通配符证书(如 *.example.com)或者包含了新的主机名(多域名证书)。
4.证书锁定匹配:如果配置了证书锁定(Certificate Pinning),新的主机名也必须通过证书检查。
如何获取可复用连接
acquireConnectionNoEvents这个方法的核心逻辑是建立 Transmitter和 RealConnection(代表一个实际的 TCP 连接)之间的所有权关系。因为Transmitter是负责管理一次请求整个生命周期的,可以代表这个请求,所以建立Transmitter和RealConnection的引用关系相当于为这次请求分配tcp连接。流程如下:
1.状态检查:首先,它会检查当前的 Transmitter是否已经持有一个连接 (this.connection != null)。如果已经持有,说明存在状态错误,会抛出异常。这确保了单个 Transmitter在同一时间只能管理一个连接。
2.建立关联:接着,它将传入的 RealConnection赋值给 Transmitter的 connection字段,完成了请求到连接的绑定。
3.记录引用:最后,它将当前的 Transmitter包装成一个 TransmitterReference(一个弱引用),并添加到连接的 transmitters集合中。这一步至关重要,它使得连接能够追踪到所有正在使用它的请求。
java
boolean transmitterAcquirePooledConnection(Address address, Transmitter transmitter,
@Nullable List<Route> routes, boolean requireMultiplexed) {
//一个断言,不用理会
assert (Thread.holdsLock(this));
//遍历连接队列
for (RealConnection connection : connections) {
//如果需要多路复用,那么候选连接需要支持多路复用才能继续往下判断
if (requireMultiplexed && !connection.isMultiplexed()) continue;
//验证此连接是否满足复用要求
if (!connection.isEligible(address, routes)) continue;
//满足要求通过acquireConnectionNoEvents获取连接
transmitter.acquireConnectionNoEvents(connection);
return true;
}
return false;
}
//RealConnection类方法
boolean isEligible(Address address, @Nullable List<Route> routes) {
// If this connection is not accepting new exchanges, we're done.
if (transmitters.size() >= allocationLimit || noNewExchanges) return false;
// If the non-host fields of the address don't overlap, we're done.
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
// If the host exactly matches, we're done: this connection can carry the address.
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
// At this point we don't have a hostname match. But we still be able to carry the request if
// our connection coalescing requirements are met. See also:
// https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
// https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
// 1. This connection must be HTTP/2.
if (http2Connection == null) return false;
// 2. The routes must share an IP address.
if (routes == null || !routeMatchesAny(routes)) return false;
// 3. This connection's server certificate's must cover the new host.
if (address.hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;
// 4. Certificate pinning must match the host.
try {
address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
} catch (SSLPeerUnverifiedException e) {
return false;
}
return true; // The caller's address can be carried by this connection.
}
//Transmitter类方法
void acquireConnectionNoEvents(RealConnection connection) {
assert (Thread.holdsLock(connectionPool));
if (this.connection != null) throw new IllegalStateException();
this.connection = connection;
connection.transmitters.add(new TransmitterReference(this, callStackTrace));
}
6.5.1.2 向连接池添加新连接
通过put方法向连接池添加一个新连接,如果后台清理线程此时没有运行,就通过线程池开起来,cleanupRunnable就是循环调用cleanup函数,cleanup函数进行清理的逻辑,并返回下一次进行清理的间隔(纳秒),-1表示不需要进一步清理。
java
void put(RealConnection connection) {
assert (Thread.holdsLock(this));
if (!cleanupRunning) {
cleanupRunning = true;
executor.execute(cleanupRunnable);
}
connections.add(connection);
}
6.5.1.3 清理方法cleanup
清理方法的逻辑如下:
1.遍历所有连接,统计使用中和空闲连接数量
2.找出空闲时间最长的连接
3.如果此连接满足清理标准(空闲时间超过keepAliveDurationNs或者空闲连接数超过maxIdleConnections),从连接队列移除
4.如果不满足清理标准,按情况返回下次清理的间隔
5.关闭被淘汰连接的socket,返回0立即进行下次清理
java
long cleanup(long now) {
int inUseConnectionCount = 0; //正在使用的连接数
int idleConnectionCount = 0; //空闲连接数
RealConnection longestIdleConnection = null; //空闲时间最长的连接
long longestIdleDurationNs = Long.MIN_VALUE; //此连接目前的已空闲时间(纳秒)
// Find either a connection to evict, or the time that the next eviction is due.
//加锁
synchronized (this) {
//遍历连接队列
for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
RealConnection connection = i.next();
// If the connection is in use, keep searching.
//清理泄漏的Transmitter并返回剩余活动引用数
if (pruneAndGetAllocationCount(connection, now) > 0) {
inUseConnectionCount++; //transmitter引用数大于0说明还在使用
continue;
}
idleConnectionCount++; //transmitter引用数为0,表示空闲
// If the connection is ready to be evicted, we're done.
//计算连接的已空闲时间
long idleDurationNs = now - connection.idleAtNanos;
//如果此连接已空闲时间超过目前发现最大空闲时间值,更新最大值和连接引用
if (idleDurationNs > longestIdleDurationNs) {
longestIdleDurationNs = idleDurationNs;
longestIdleConnection = connection;
}
}
//空闲时长最大的连接已经超过了定义的最大值
//或者 空闲连接数超过定义的最大值
if (longestIdleDurationNs >= this.keepAliveDurationNs
|| idleConnectionCount > this.maxIdleConnections) {
// We've found a connection to evict. Remove it from the list, then close it below (outside
// of the synchronized block).
//从连接队列移除
connections.remove(longestIdleConnection);
} else if (idleConnectionCount > 0) {
// A connection will be ready to evict soon.
//没有到达需要回收的标准,并且目前有空闲连接,预计此连接还有多久到达最大空闲时间
//返回此值作为下一次检查间隔
return keepAliveDurationNs - longestIdleDurationNs;
} else if (inUseConnectionCount > 0) {
// All connections are in use. It'll be at least the keep alive duration 'til we run again.
//没有到达需要回收的标准,且没有空闲连接,下次检查间隔是定义的最长连接空闲
return keepAliveDurationNs;
} else {
// No connections, idle or in use.
//目前没有连接在运行,修改后台清理线程运行状态,返回-1表示不需要下次清理
cleanupRunning = false;
return -1;
}
}
//清理实际连接,不在加锁块中
closeQuietly(longestIdleConnection.socket());
// Cleanup again immediately.
//如果是本次需要清理连接的情况,返回后马上尝试继续清理,或许还有可清理的连接
return 0;
}
顺便看一下pruneAndGetAllocationCount,其逻辑是遍历连接对应的transmitter引用列表,因为连接可被复用,所以代表请求的transmitter可能有多个;如果发现有无效的transmitter引用,则说明其泄漏了,通过将Reference<Transmitter>强制转换为TransmitterReference(这是Transmitter类的内部类,可访问到transmitter创建时的堆栈,transmitter内部有进行存储),记录警告信息,移除泄漏的引用,如果这是最后一个引用,标记连接为可立即淘汰并记录变为空闲的时间戳。其它情况最后返回有效的transmitter数。
java
private int pruneAndGetAllocationCount(RealConnection connection, long now) {
//先获取连接对应的transmitter(弱引用)列表,因为一个连接可能被多个请求复用
List<Reference<Transmitter>> references = connection.transmitters;
//遍历所有transmitter
for (int i = 0; i < references.size(); ) {
Reference<Transmitter> reference = references.get(i);
//如果这个transmitter非空,说明在使用,直接判断下一个
if (reference.get() != null) {
i++;
continue;
}
// We've discovered a leaked transmitter. This is an application bug.
//走到这里说明reference.get()为空,应该是发生泄漏,所以将其强制转换为
//TransmitterReference,其继承WeakReference<Transmitter>,但是多了个堆栈字段
//TransmitterReference在Transmitter类内部定义,堆栈也在内部存储,
//这样TransmitterReference可获取堆栈信息方便排查
TransmitterReference transmitterRef = (TransmitterReference) reference;
String message = "A connection to " + connection.route().address().url()
+ " was leaked. Did you forget to close a response body?";
Platform.get().logCloseableLeak(message, transmitterRef.callStackTrace);
//将这个泄漏的transmitter引用从列表移除
references.remove(i);
//标记连接不可以再创建新的Exchange(数据传输)了
connection.noNewExchanges = true;
// If this was the last allocation, the connection is eligible for immediate eviction.
//如果移除后,transmitter引用列表变空,记录此连接变为空闲的时间戳,然后返回0
if (references.isEmpty()) {
connection.idleAtNanos = now - keepAliveDurationNs;
return 0;
}
}
//返回transmitter引用数量(正被多少个请求使用)
return references.size();
}
6.5.1.4 ConnectionPool和RealConnectionPool
OkHttpClient创建的是ConnectionPool,而ConnectionPool内部持有RealConnectionPool,仅对外提供少数方法,为什么这么设计?
首先这是一个委托模式,内部其实是把接收到的外部调用转发给这个实例去执行。ConnectionPool充当了委托者,RealConnectionPool是实际执行者(Delegatee),ConnectionPool定义连接池的行为接口,而RealConnectionPool作为实际实现,职责单一明确,实现解耦。另外也体现门面模式的思想,封装内部的复杂实现,只为外部提供少数易用的接口。
那为什么不用ConnectionPool接口,RealConnectionPool实现此接口呢?
从功能上来说使用接口的实现也可做到职责分离、解耦、简化。之所以不使用这里感觉有几个原因,
首先是从使用难度来说,实体类的话可直接调用构造函数创建默认连接池,一般都是这么用,而使用接口的话需要实现各种方法,不够方便;
第二个原因是性能上使用具体类可以避免接口方法的虚方法调用开销,这在高频调用的网络库中很重要,并且JVM对具体类的方法调用有更好的内联优化机会;
第三个是连接池功能属于是稳定的,有什么功能明确且固定,所以不需要接口提供的多态性,okhttp设计也是需要多态性支持,功能不固定才会使用接口,其它就用具体类;
最后是添加新方法时,使用实体类的话可以很方便加一个方法,接口的话就会影响到之前所有使用的地方
java
public final class ConnectionPool {
final RealConnectionPool delegate;
/**
* Create a new connection pool with tuning parameters appropriate for a single-user application.
* The tuning parameters in this pool are subject to change in future OkHttp releases. Currently
* this pool holds up to 5 idle connections which will be evicted after 5 minutes of inactivity.
*/
public ConnectionPool() {
this(5, 5, TimeUnit.MINUTES);
}
public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
this.delegate = new RealConnectionPool(maxIdleConnections, keepAliveDuration, timeUnit);
}
/** Returns the number of idle connections in the pool. */
public int idleConnectionCount() {
return delegate.idleConnectionCount();
}
/** Returns total number of connections in the pool. */
public int connectionCount() {
return delegate.connectionCount();
}
/** Close and remove all idle connections in the pool. */
public void evictAll() {
delegate.evictAll();
}
}
6.5.2 连接(RealConnection)
RealConnection是OkHttp中负责管理实际网络连接的核心类,它实现了Connection接口,负责处理TCP连接、SSL/TLS握手、HTTP/2连接复用等关键功能。
其内部成员如下:
java
private static final int MAX_TUNNEL_ATTEMPTS = 21; //通过HTTP代理建立隧道连接的最大尝试次数
public final RealConnectionPool connectionPool; //所在连接池
private final Route route; //RealConnection对象所建立连接的具体路由信息
private Socket rawSocket; //底层TCP套接字
private Socket socket; //应用层套接字(可能是SSL套接字或原始套接字)
private Handshake handshake; // SSL握手信息
private Protocol protocol; //协议
private Http2Connection http2Connection; HTTP/2连接对象
private BufferedSource source; // 输入输出流
private BufferedSink sink;
boolean noNewExchanges; //不可以使用此连接创建新请求的标志
int routeFailureCount; //路由失败次数
int successCount; //成功次数
private int refusedStreamCount; // HTTP/2连接中被服务器拒绝的流(stream)数量
private int allocationLimit = 1; //并发流限制
final List<Reference<Transmitter>> transmitters = new ArrayList<>(); //transmitter引用队列
long idleAtNanos = Long.MAX_VALUE; //连接已空闲时长
内部方法的代码比较多,不细说了,想了解具体连接情况的可以自己翻源码,这里简单介绍一下。
工作流程
|----------------------------------------------------------------------------------|
| 1. 连接建立 调用connect()方法建立连接 根据路由配置选择直接连接或通过代理连接 处理SSL/TLS握手(如果需要) |
| 2. 协议协商 根据服务器支持选择HTTP/1.1或HTTP/2 对于HTTP/2,启动连接复用机制 |
| 3. 连接复用 通过isEligible()方法判断连接是否可复用 支持HTTP/2的连接合并(connection coalescing) 管理并发流数量限制 |
| 4. 健康检查 isHealthy()方法检查连接是否可用 定期验证连接的活跃状态 |
关键方法
connect(): 主要的连接建立方法
connectTunnel(): 处理HTTP代理隧道连接
connectSocket(): 建立底层TCP连接
establishProtocol(): 协商应用层协议
newCodec(): 创建对应的协议编解码器
isEligible(): 判断连接是否可复用
连接复用机制
相同主机的请求可以复用连接
HTTP/2支持多路复用,多个请求可以共享同一连接
通过证书验证确保连接安全性
支持连接合并,相同IP的不同域名可以共享连接
错误处理
trackFailure(): 跟踪连接失败情况
根据错误类型决定是否标记连接为不可用
管理路由失败计数,避免重复使用失败的路由
6.5.3 Exchange、ExchangeFinder、ExchangeCodec对象(数据通信对象)
Exchange是一次HTTP请求-响应的交换器,协调ExchangeCodec进行实际的数据传输,并负责事件监听,封装了单次HTTP交互的完整生命周期,是连接机制面向其他拦截器的接口。Transmitter使用的就是Exchange,传递给后续拦截器的也是Exchange;
ExchangeCodec是协议的编解码器,在已建立的连接上,负责将请求编码为字节流,并将响应字节流解码为响应对象,是一个抽象层,具体实现由连接使用的HTTP协议版本决定;
ExchangeFinder是连接的查找器,负责为新的HTTP请求寻找一个可复用的或新建的TCP连接,实现了复杂的连接复用逻辑,会进行多达5次尝试以获取最优连接,极大提升性能。对外返回这个连接的ExchangeCodec,然后Transmitter拿着ExchangeCodec去构造Exchange。
6.5.3.1 如何查找连接
ExchangeFinder通过find函数向外部提供一个可用连接的ExchangeCodec,而寻找可用连接是通过私有函数findHealthyConnection,其通过findConnection获取到一个候选连接,如果连接是新连接,不需要进行健康(可用)检测直接返回,如果是从连接池拿出的,调用RealConnection的isHealthy函数检查是否健康,如果不健康,将其标记为不再可用(noNewExchanged)
java
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
//循环获取,直到找到一个可用连接
while (true) {
//调用findConnection得到一个候选连接
RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
// If this is a brand new connection, we can skip the extensive health checks.
//连接从未成功处理过请求(全新连接)且不是HTTP/2多路复用连接,直接返回,不用再进行检查
//因为新连接刚建立,TCP握手和TLS握手都已完成,理论上是最健康的
//避免不必要的健康检查开销,提高性能
synchronized (connectionPool) {
if (candidate.successCount == 0 && !candidate.isMultiplexed()) {
return candidate;
}
}
// Do a (potentially slow) check to confirm that the pooled connection is still good. If it
// isn't, take it out of the pool and start again.
//检查连接是否健康
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
candidate.noNewExchanges();
continue;
}
return candidate;
}
}
//RealConnection.isHealthy
public boolean isHealthy(boolean doExtensiveChecks) {
// 基础检查:socket是否关闭或输入输出流是否关闭
if (socket.isClosed() || socket.isInputShutdown() || socket.isOutputShutdown()) {
return false;
}
// HTTP/2连接的特殊检查
if (http2Connection != null) {
return http2Connection.isHealthy(System.nanoTime());
}
// 深度健康检查(可选)
if (doExtensiveChecks) {
try {
int readTimeout = socket.getSoTimeout(); //保存原始超时设置
try {
socket.setSoTimeout(1); // 设置1ms超时
//检查输入流是否已经耗尽(EOF)
//如果流已耗尽,说明对端已关闭连接
//这是检测"半开连接"的关键
if (source.exhausted()) {
return false; // 流已耗尽,socket已关闭
}
return true;
} finally {
socket.setSoTimeout(readTimeout); // 恢复原始超时设置
}
} catch (SocketTimeoutException ignored) {
// 读取超时,说明socket是好的
} catch (IOException e) {
return false; // 无法读取,socket已关闭
}
}
return true;
}
查找候选连接的函数是findConnection,这个是核心部分,代码量有点多,这里就贴关键部分
候选连接第一优先级是现有连接,但是需要检查当前连接是否可用。这里有个问题,现有连接如果有一般都是上次findConnection获取连接后健康检查未通过,为什么要先尝试使用?理由是网络是动态的,可能上次检查失败是临时性问题,而现在恢复了,重新检查消耗资源不多,但是成功的话有较高收益,所以还是进行尝试了。
第二优先级是从连接池获取,调用RealConnectionpool.transmitterAcquirePooledConnection之前分析过此函数了,如果获取成功,后面将其返回,本次不考虑连接多路复用,因为检查多路复用的逻辑复杂耗时长,而大部分情况都可以只通过简单url匹配找到,第一次从连接池查询要求速度为主,所以不进行多路复用检查。在不成功时,检查是否有上次findConnection指定的本次要尝试的候选路由、或者上次findConnection获取的连接的路由url本次是否可重试,满足条件的话设置为本次需要创建连接时的路由。为什么要这么做?后面再细说。
第三优先级,如果从连接池获取不成功,之后第一种情况是需要进行路由列表生成,这种情况在路由列表生成后,第二次从连接池查找连接,考虑多路复用情况。为什么只有进行路由列表生成的情况才考虑查多路复用,因为路由列表生成会涉及dns等操作,耗时长,只有必须这么做的情况下查多路复用的收益才大于损失。第二次从连接池查找如果成功,后面将其返回,如果不成功,和另外的一种不需要进行路由列表生成情况一样处理。
如果不需要进行路由列表生成,之前第一次从连接池拿失败后,若有上一次findConnection指定的路由或者上次findConnection获取的连接的路由url本次是否可重试,那么不需要从路由列表取路由,否则从路由列表拿一个路由作为候选路由,然后创建新连接。后面进行tcp、tls握手和尝试将所用路由从路由失败黑名单移除。
第四优先级,竞争处理。由于多线程条件下,可能有同时创建连接的情况,如果创建连接后发现别的线程已经创建一个可以被我复用的连接,那么复用连接减少总数是更优选择。所以第四优先级是第三次从连接池获取连接,这次要求查询的连接必须可以多路复用,防止连接合并时出现错误,例如找到一个http/1.1连接,导致需要排队等待。如果找到了,那么进行多路复用,放弃本次创建的连接,但是创建时所用的路由设置为下一次findConnection的候选路由。如果没找到,那么使用本次创建的连接并加入连接池。
java
//1. 检查并释放不可用的现有连接
releasedConnection = transmitter.connection;
toClose = transmitter.connection != null && transmitter.connection.noNewExchanges
? transmitter.releaseConnectionNoEvents()
: null;
//2. 非空说明上面释放不可用连接时没有成功释放,即现有连接可用
if (transmitter.connection != null) {
// 如果当前调用已经有可用的连接,直接复用
result = transmitter.connection;
releasedConnection = null;
}
//3. 尝试从连接池获取连接
if (result == null) {
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, null, false)) {
//从连接池获取到可用的连接,不考虑多路复用情况,因为要求快速
foundPooledConnection = true;
result = transmitter.connection;
} else if (nextRouteToTry != null) {
//未能从连接池获取到可用的连接
// 如果有之前尝试过的路由,使用它
selectedRoute = nextRouteToTry;
nextRouteToTry = null;
} else if (retryCurrentRoute()) {
// 如果当前路由可以重试,使用当前路由
selectedRoute = transmitter.connection.route();
}
}
//4. 从连接池拿到可用连接直接返回
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
return result;
}
//5. 如果在从连接池获取失败后,上次调用findConnection没有指定本次需要使用的路由
// 同时侯选路由列表未创建或者侯选路由列表耗尽
// 那么由路由选择器生成侯选路由列表(一组候选路由)
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}
List<Route> routes = null;
synchronized (connectionPool) {
if (newRouteSelection) {
//6. 如果上面重新生成侯选路由列表了,这里再次尝试从连接池获取
//这次考虑连接合并,第一次从连接池尝试获取时没考虑连接合并
routes = routeSelection.getAll();
if (connectionPool.transmitterAcquirePooledConnection(
address, transmitter, routes, false)) {
foundPooledConnection = true;
result = transmitter.connection;
}
}
//7. 未进行连接合并尝试或者没成功,如果上次调用findConnection没有指定本次需要使用的路由
//这里从候选路由列表拿出一个作为尝试路由,创建新连接
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
result = new RealConnection(connectionPool, selectedRoute);
connectingConnection = result;
}
}
//8. 如果第二次从连接池取连接成功(有可以连接合并的情况),直接返回
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
//9. 对新创建的连接执行TCP+TLS握手(阻塞操作),
// 并且将连接的路由从连接失败黑名单移除
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
connectionPool.routeDatabase.connected(result.route());
//连接竞态处理,也是为什么findConnection会为下一次调用指定使用的路由的原因
Socket socket = null;
synchronized (connectionPool) {
connectingConnection = null;
//10. 第三次尝试从连接池获取连接,要求连接可多路复用
if (connectionPool.transmitterAcquirePooledConnection(address, transmitter, routes, true)) {
//如果连接池找到这种连接,使用找到的连接,放弃之前创建的连接,将其设置为不可用
result.noNewExchanges = true;
socket = result.socket();
result = transmitter.connection;
//本次创建连接使用的路由指定为下次尝试的路由
nextRouteToTry = selectedRoute;
} else {
//连接池没有可用连接,使用本次创建的,并加入连接池
connectionPool.put(result);
transmitter.acquireConnectionNoEvents(result);
}
}
为什么第一次查连接池失败后,要使用上次findConnction指定路由或者上次findConnection获取到的连接的路由?首先路由是有失败可能的,所以应该先尝试成功率高的,上次findConnection指定本次要尝试的路由的逻辑是在最后竞争时发现连接池有可多路复用连接,放弃已经创建的连接,然后将其路由进行指定,理论上刚成功创建的连接的路由成功率很高;其次尝试使用上次findConnection获取到的连接的路由,因为如果判断通过,说明连接未失败过,成功率也高。
6.5.3.2 路由如何生成
路由有三个组成部分,第一个是Address,代表目标地址和配置,一般包含以下信息:
主机名(hostname)和端口号 、DNS解析器(Dns接口)、Socket工厂、SSL/TLS配置(证书、验证器等)、认证信息、协议列表、连接规范、代理选择器。
Address只是描述"要去哪里"和"如何连接"。一般没有具体ip地址,或者ip地址和端口不一定是最后连接建立时使用的。
第二部分是代理Proxy,网络请求可以走代理,一般分无代理、http代理、socket代理三种。代理会影响最后实际连接建立时所用的ip和端口,无代理的话就是将Address的host进行dns解析获取ip,端口不变,然后通过ip+端口进行连接;
http代理是指定了代理服务器host和端口,最终使用代理服务器host解析的ip和代理服务器端口进行连接,连接上代理服务器后,需要告诉代理服务器要连接的目标服务器host和端口,在协议上如此规定,不过okhttp的实现是既对代理服务器host进行dns解析,也对目标服务器host进行dns解析,目的是找到可复用的连接进行性能优化,减少连接开销,在代理服务器连接之后,同样还是传目标服务器host而不是解析的ip,因为协议规定传host;另外https的ssl/tls握手需要让服务器返回ssl证书,这个在大部分情况下是绑定域名的;还有一个ip可能托管多个网站,无法确定要访问哪个。
socket代理是指定socket代理服务器和端口,需要对代理服务器host进行dns解析,在建立连接时使用代理服务器ip和端口,之后将目标服务器host与端口传给代理服务器,代理服务器进行dns解析并连上目标服务器。在连接建立后,通过socket数据收发经过代理服务器都是透传的。socket代理可以支持各种传输层使用tcp/udp的应用层协议。
第三部分是实际连接地址InetSocketAddress,可以理解为ip+端口,实际上还有些其它信息。描述"具体连接到哪个网络接口",如果是无代理的情况,那么是目标服务器ip+端口,如果是有代理情况,http代理是代理服务器ip+端口,socket代理是原始Address的host+端口。
在了解路由组成后,首先看Address哪里来?host,端口的,协议其实在url可以知道,其它的dns解析,ssl/tls配置,连接规范等在okhhtp里面都有默认,当然也可以自己指定;
然后代理,用户可以指定指定代理方式和代理服务器,如果不指定的话,okhttp会使用Java的ProxySelector.getDefault(),根据系统设置自动检测代理配置;
有了Address和代理,InetSocketAddress也就确定了。
6.5.4 EventListener
EventListener是一个抽象类,内部提供了很多事件监听回调,但都是空实现,但是会在合适的时间被调用,如果需要监听某个事件,可以直接继承EventListener重写某些回调或者实现EventListener的工厂接口。
6.6 网络拦截器(networkInterceptor)
这是另外一个可以由用户手动添加设置的拦截器,位于连接拦截器和请求服务器拦截器之间,到了网络拦截器这个阶段,已经建立起可用的连接,包括ssl/tls握手都已经完成,直接就可以发送和接收http报文了。
对于网络拦截器,能够访问真实的网络请求和响应、可以看到重定向和重试的每个网络请求、可以访问连接信息(如IP地址、TLS配置)。
因此常见可以在网络拦截器做的事情有:
-
网络层面修改请求头;
-
可以监控网络响应状态,时间等;
-
网络请求日志记录;
-
网络层认证处理;
-
网络错误统计;
-
重定向跟踪。
注:当连接不是http协议,而是websocket时,即使有指定,okhttp也不设置网络拦截器。
原因是:WebSocket不是传统的HTTP请求-响应模式,而是通过HTTP升级机制建立的,升级后连接变为全双工的WebSocket连接,并且是持久的长连接**,**可以双向发送消息,没有传统的请求-响应边界。网络拦截器的限制:设计为处理单个网络请求和响应,必须调用chain.proceed()恰好一次,适用于传统的HTTP请求-响应模式。
6.7 请求服务器拦截器(CallServerInterceptor)
OkHttp 拦截器链中的最后一个拦截器,负责实际与服务器进行网络通信。它的主要职责是发送 HTTP 请求到服务器,接收并处理服务器的响应,处理各种 HTTP 协议特性(如 100-continue、WebSocket 等)
代码流程:
1.首先发请求头;
2.请求体处理,如果请体不为空且请求的方法符合协议规定(get和head不允许有请求体),就需要处理请求体的发送;
请求体发送先处理100-continue的情况,(如果请求头有Expect: 100-continue的字段,这种请求一般是上传大文件,http协议要求先等待服务器响应,如果服务器返回100,继续再发请求体,并接收响应,这次的响应才是要返回的结果,如果没接收到100,那直接返回服务器响应,如4XX错误),这种直接读取响应头,先不发请求体。如果响应头中收到100状态码,之后responseBuilder为空,可继续发请求体,否则非空,不能继续发请求体。
如果是普通情况(请求头没有Expect: 100-continue的字段),或者请求头有Expect: 100-continue的字段但是响应头收到100,可继续发请求体。分为全双工和非全双工两种,全双工发送请求体,之后没有关闭流,非全双工发完会关闭流。
如果是请求头有Expect: 100-continue的字段但是响应头没收到100,不发送请求体,并且如果连接不是多路复用的,需要标记连接不可用避免误发请求体。
3.读取响应头,如果是之前100-continue的情况,发完请求头就读响应头,在这里是第二次读响应头,读到的是最终response的响应头。(当然可能有收到非预期状态码的情况,那种需要再读一次);
4.处理非预期状态码、构建response。首先是先创建一个response对象关联好request,记录好收发时间,然后处理意料之外的100状态码,我们请求体没有Expect:100-continue,但在HTTP/1.1协议中,服务器可以主动发送100-continue响应,这是协议允许的行为,服务器可以根据自己的策略决定是否发送100-continue,okhttp的处理是重新读一遍响应头,然后重新记录信息。
之后将响应体写入response并重新构建response对象,如果是websocket协议升级的情况,这种没有响应体,okhttp设置了EMPTY_RESPONSE防止响应体出现null;
- 最后检测,如果请求体或者响应头出现Connection: close,那么标记连接为不可用;检查状态码是否为204(No Content)或205(Reset Content),如果这些状态码的响应体长度大于0,抛出协议异常。
java
@Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Exchange exchange = realChain.exchange();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
//通过exchange发送请求头
exchange.writeRequestHeaders(request);
boolean responseHeadersStarted = false;
Response.Builder responseBuilder = null;
//这里判断request的方法是否为可携带请求体的种类,如果方法可携带请求体且有请求体
//执行接下来请求体发送的判断
if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100
// Continue" response before transmitting the request body. If we don't get that, return
// what we did get (such as a 4xx response) without ever transmitting the request body.
//如果请求头有Expect:100-continue,本次请求先不发请求体,而是先等待读取响应头
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
exchange.flushRequest();
responseHeadersStarted = true;
exchange.responseHeadersStart();
//读取响应头,如果响应头返回状态码100(可继续发送请求体),responseBuilder仍为null
responseBuilder = exchange.readResponseHeaders(true);
}
//处理请求体发送
if (responseBuilder == null) {
//针对全双工和非全双工,各自发送请求体
if (request.body().isDuplex()) {
//全双工
// Prepare a duplex body so that the application can send a request body later.
exchange.flushRequest();
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, true));
request.body().writeTo(bufferedRequestBody);
} else {
//非全双工
// Write the request body if the "Expect: 100-continue" expectation was met.
BufferedSink bufferedRequestBody = Okio.buffer(
exchange.createRequestBody(request, false));
request.body().writeTo(bufferedRequestBody);
//非全双工发送完请求体,关闭流
bufferedRequestBody.close();
}
} else {
//请求头有Expect:100-continue,并且服务器没有返回100就会走到这里,不能继续发请求体
//如果非多路复用的连接需要标记为不可用,避免误发请求体
exchange.noRequestBody();
if (!exchange.connection().isMultiplexed()) {
// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection
// from being reused. Otherwise we're still obligated to transmit the request body to
// leave the connection in a consistent state.
exchange.noNewExchangesOnConnection();
}
}
} else {
//没有请求体或者请求方法不能携带请求体,不继续发送了
exchange.noRequestBody();
}
if (request.body() == null || !request.body().isDuplex()) {
exchange.finishRequest();
}
if (!responseHeadersStarted) {
exchange.responseHeadersStart();
}
//除了请求头有Expect:100-continue,且服务器未返回100状态码的情况,都在这里读取响应头
//Expect:100-continue,服务器返回100的情况在这里是第二次读响应头,是属于最终响应的
if (responseBuilder == null) {
responseBuilder = exchange.readResponseHeaders(false);
}
//构造response,关联好request,记录好握手信息,收发时间等
Response response = responseBuilder
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
int code = response.code();
//处理100-continue的情况,这种是意料之外,即我们请求体没有Expect:100-continue
//在HTTP/1.1协议中,服务器可以主动发送100-continue响应,
//即使客户端没有明确设置"Expect: 100-continue"头。
//这是协议允许的行为,服务器可以根据自己的策略决定是否发送100-continue
if (code == 100) {
//okhttp的处理是再读一次,获取真实的响应码
// server sent a 100-continue even though we did not request one.
// try again to read the actual response
response = exchange.readResponseHeaders(false)
.request(request)
.handshake(exchange.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();
code = response.code();
}
exchange.responseHeadersEnd(response);
//为response写入响应体,重新构建response对象
if (forWebSocket && code == 101) {
//websocket 响应码101,代表协议升级,这种情况没有响应体
//okhttp设置了EMPTY_RESPONSE,防止出现null
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
//普通HTTP响应,打开并设置实际的响应体流
response = response.newBuilder()
.body(exchange.openResponseBody(response))
.build();
}
//处理连接关闭,检查请求头或响应头中是否包含Connection: close
//如果有的话标记连接不可复用
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
exchange.noNewExchangesOnConnection();
}
//处理协议错误
//检查状态码是否为204(No Content)或205(Reset Content)
//如果这些状态码的响应体长度大于0,抛出协议异常
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;
}
7.整体架构
7.1 顶层架构
对用户来说,感知到的就是顶层接口Call、OkhttpClient、Dispatcher。需要创建任务Call,交给机器OkHttpClient,机器默认使用Dispatcher程序执行。
同时允许用户对这三个接口进行定制化修改以满足特定需求,对于OkhttpClient的修改是全局配置上的调整,控制整个系统要启用什么,停用什么,某些地方要用什么策略之类;然后对Dispatcher的修改是控制任务调度的逻辑,管理并发数,优先级等;对于Call的修改,因为其是一个接口,可以对所有接口的函数做定制修改,满足测试或者特殊需求。
通过这种方式,使得框架简单易用,也在多个方面提供扩展可能。
7.2 为什么第一个拦截器是重试与重定向拦截器
OkHttp作用的是帮我们完成一次http请求的发起与响应的接收,而在内部工作逻辑是通过拦截器链顺序处理,每个环节完成一部分工作,走完所有拦截器也就完成了一次http请求的过程。所以要了解okhttp如何完成请求,就要了解http协议和拦截器各自完成协议的哪部分功能。
首先排除掉用户可选的两个额外拦截器,第一个拦截器是重试与重定向拦截器RetryAndFollowUpInterceptor,按http请求的流程来看,其实如果是我们自己做的话,一开始肯定按照流程进行对应的实现,首先dns找到ip,然后再根据端口,建立tcp连接,然后是可选的ssl/tls握手,完成建立可用连接后,才发送http报文,等待响应报文,最后构建响应数据。
那么根据这个逻辑我们需要建立连接,然后才发送数据,那么应该是连接拦截器,然后请求服务器拦截器;不过考虑到http协议有缓存响应的约定,所以先走缓存拦截器,如果有缓存的话可以直接返回;而桥接拦截器只是为了帮助我们构建http请求报文,算是优化项,但是既然有的话自然应该在缓存拦截器之前;那么最后这个重试与重定向拦截器,理论上应该是放最后,为什么要放最前?
我这边感觉是和使用的责任链模式有关,拦截器链是有点像递归的,当一个拦截器调用 proceed()方法后,流程会继续向下传递,直到最终发出请求并获得响应,这个响应再沿着链条原路返回给最初的调用者,重试与重定向拦截器被放置在链首,意味着它第一个接触到原始请求,也是最后一个接收到网络响应。这个位置赋予了它全局掌控的能力。它的主要工作并非在请求发出的阶段,而是在响应返回的阶段。当它从后续拦截器链收到响应或异常时,它开始判断:
重试:当链式调用过程中抛出如路由失败(RouteException)或I/O异常(IOException)时,该拦截器会捕获异常,并根据异常类型、客户端配置(是否允许重试)以及是否有备用路由等因素,决定是否重新开始整个请求流程
重定向:当收到如3xx的HTTP状态码时,它会解析Location头部,构建一个新的请求,然后再次重新开始整个请求流程
无论是重试还是重定向,其本质都是要重新发起一个完整的请求。这个新请求需要再次经历添加头部、缓存判断、建立连接等一系列过程。如果这个拦截器放在链尾,它将无法让请求重新走一遍这些必要流程。只有放在链首,它才能利用 while (true)循环,在需要时轻松地重启整个拦截器链,这正是实现重试和重定向功能最简洁、最可靠的架构。
架构总结:以责任链模式 为核心引擎,通过建造者 和策略模式 进行灵活配置,再用外观模式 提供简洁入口,最后由分发器 和连接池 通过享元模式等保障高性能。