OkHttpClient是目前开发 android 应用使用最广泛的网络框架,最近看了阿里的 httpdns 里面对于 dns 的处理,我们团队就想调研一下在项目中有什么利弊,并且框架中是否对 socket 的连接是怎么缓存的。下面就带着问题去分析一下这个框架:
new OkHttpClient.Builder().build();
这个建造者的每个传参在源码分析2中有详细的讲解
发送请求的只有这一个方法,就顺着点进去看下是怎么设计的
java copyable"> @Override public Call newCall(Request request) {
return RealCall.newRealCall(this, request, false /* for web socket */);
}
复制代码
java copyable">/**
*注意这个方法是没有声明 public 的,只能在包内部调用,所以用户无法直接调用.
*并在这里eventListenerFactory为每个 call 设置了监听
**/
static RealCall newRealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
RealCall call = new RealCall(client, originalRequest, forWebSocket);
call.eventListener = client.eventListenerFactory().create(call);
return call;
}
//私有构造方法,只能通过上面的静态方法调用,这样的好处是,防止框架使用者传参有误导致无法正常使用
private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
this.client = client;
this.originalRequest = originalRequest;
this.forWebSocket = forWebSocket;
this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
}
复制代码
使用者只需要传入一个 Request 就可以使用了
java copyable">//request 也是通过构造者生成的, 整个请求的头,行,体都从这里传入,并多了一个 tag 作为标识
public static class Builder {
HttpUrl url;
String method;
Headers.Builder headers;
RequestBody body;
Object tag;//使用者可以设置一个唯一标识,拿到 Request 的时候分不出是哪个请求
//默认设定请求使用了 GET 方法,并传入空 Headers
public Builder() {
this.method = "GET";
this.headers = new Headers.Builder();
}
}
复制代码
HttpUrl url,
String method
通过方法url()
传入,使用HttpUrl对 url 封装,方便获取 url 的各个部分信息。可以直接调用get(),head(), post(body),delete(),put(body)
等设置 methord 并传入 Body,也可以使用method(String method,RequestBody body)
直接输字符串,这个方法里进行了校验,传参出错抛出异常
Headers
Headers.Builder中使用 addLenient,add 添加一行 header,可以是“key:value”这样的字符串,也可以是两个参数 key,value。 调用 add 检测中文或不可见字符会抛出异常,而addLenient 不会检测。 还有一个 set 会覆盖已经存在所有name一样的值
Headers 中用一个数组保存数据,偶数位是 key,奇数位是 value,里面的大部分方法是处理这个数组的。 除了用 buidler 生成header,还可以调用静态方法of
传入一个 map 或者[key1,value1,key2,value2...]生成
java copyable"> String get(String name) //获取 key=name 的 value
public List<String> values(String name) //和 get 方法类似,如果有多个 key=name 的条目返回所有的 value。(http 协议的header不限制顺序,不排重)
public Date getDate(String name)//内部调用get(name)并转成时间
public int size() //header 的条数,保存了key、value数组的一半
public String name(int index) //获取指定位置的 name,index相对于 header 的条数,不是数组的 index
public String value(int index)//获取指定位置的value,index相对于 header 的条数,不是数组的 index
public Set<String> names()//获取所有的 name,由于是 set 重复的只算一条
public long byteCount() //整个 header 要占用的长度,包含分割的“: ”和回车换行
public Map<String, List<String>> toMultimap()//把这个 Headers 转成 map,由于可能有多个相同的 key,value 要用 list返回
复制代码
RequestBody
框架中两个实现类:FormBody上传普通的参数请求,MultipartBody上传文件。RequestBody中三个静态 create 方法传入MediaType和ByteString、byte[]、File 满足了大部分请求需要,并且传给MultipartBody。
如果需要自定义需要至少实现contentType,writeTo,分别传入数据类型和写入。如果有断点续传的要求复写contentLength。
java copyable"> public abstract MediaType contentType();
public long contentLength() throws IOException {
return -1;
}
public abstract void writeTo(BufferedSink sink) throws IOException;
复制代码
java copyable">/**出来下面三个主要方法的调用还有一些获取 name 和 value 的方法,和 builder 里添加name 和 value的方法,容易理解不做解释。
**/
public final class FormBody extends RequestBody {
private static final MediaType CONTENT_TYPE = MediaType.parse("application/x-www-form-urlencoded");
@Override public MediaType contentType() {
return CONTENT_TYPE;//返回了x-www-form-urlencoded的类型,适用于带参数的接口请求
}
@Override public long contentLength() {
return writeOrCountBytes(null, true);//调用了writeOrCountBytes
}
@Override public void writeTo(BufferedSink sink) throws IOException {
writeOrCountBytes(sink, false);//调用了writeOrCountBytes
}
private long writeOrCountBytes(@Nullable BufferedSink sink, boolean countBytes) {
long byteCount = 0L;
Buffer buffer;
if (countBytes) {//只需要获取 body 的长度,contentLength
buffer = new Buffer();
} else {
buffer = sink.buffer();//写入数据 writeTo
}
for (int i = 0, size = encodedNames.size(); i < size; i++) {
if (i > 0) buffer.writeByte('&');//第一个请求参数前不拼'&'
buffer.writeUtf8(encodedNames.get(i));
buffer.writeByte('=');//拼接请求体
buffer.writeUtf8(encodedValues.get(i));
}
if (countBytes) {//如果是 contentLength 调用需要返回长度
byteCount = buffer.size();
buffer.clear();
}
return byteCount;
}
复制代码
MultipartBody和FormBody计算 body 长度和写入数据的方法类似,但是MediaType类型比较多,看服务器需要哪种类型的,我们公司服务器是MediaType.parse("multipart/form-data")的,在 builder 中添加了多个part,writeTo的时候用boundary分割开。
java copyable">public static final class Builder {
private final ByteString boundary;//每个 part 的分割线,基本上都是一个够长的随机串
private MediaType type = MIXED;//默认类型MediaType.parse("multipart/mixed")
private final List<Part> parts = new ArrayList<>();//保存了所有的 part
public Builder(); //随机生成了一个boundary,没有特殊需求不要修改
public Builder(String boundary); //自定义一个boundary
public Builder setType(MediaType type);//自定义类型
public Builder addPart(Headers headers, RequestBody body); //添加一个 part 如果上传文件可以自定义 headers, FormBody.create(MultipartBody.FORM, file)
public Builder addFormDataPart(String name, String value); //添加一个字符串的 body
/**添加普通 header 的文件.
*@params name 是服务器接受 file 的字段名;
*@params filename 本地的文件名
*@params body 传FormBody.create(MultipartBody.FORM, new File(filePath))
**/
public Builder addFormDataPart(String name, @Nullable String filename, RequestBody body) ;
复制代码
同步请求 execute() 异步请求 enqueue()
使用字段 executed 防止一个 RealCall 调用 enqueue 或者 execute 多次,调用 eventListener.callStart
开始请求,两个方法都调用到了client.dispatcher()
(okHttpClient.Builder中设置见 源码解析2) 里,其中enqueue使用了NamedRunnable, 最终都调用到了getResponseWithInterceptorChain();
发送请求的核心方法。
java copyable">public abstract class NamedRunnable implements Runnable {
protected final String name;
//在子类AsyncCall的传参是“OkHttp%s”,redactedUrl()这个方法最终生成了一个和请求url相似的string。
public NamedRunnable(String format, Object... args) {
this.name = Util.format(format, args);//拼接成一个字符串,标识okhttp的请求
}
/**在执行请求时,替换了线程名,子线程的名字可能是dispatcher或者用户定义的线程池设置的。调试更方便
*@methord execute 倒了个方法名,就是run方法。okhttp框架的方法名起得都很怪异,java原生 execute 是在origin thread调用
**/
@Override public final void run() {
String oldName = Thread.currentThread().getName();
Thread.currentThread().setName(name);
try {
execute();
} finally {
Thread.currentThread().setName(oldName);
}
}
protected abstract void execute();
}
复制代码
getResponseWithInterceptorChain
设计思路是使用RealInterceptorChain
保存了所有参数,相比旧版使用ApplicationInterceptorChain
调用HttpEngine
完成请求,新版依次调用拦截器interceptors
的list生成请求,并把框架使用者设置的拦截器插入进来,可以在请求过程中拿到并修改包含request和response的所有值,提高了扩展性,并且这种链式依次调用应该会更容易理解。
client.interceptors()、client.networkInterceptors()
(源码分析2) 在OkHttpClient.Builder
里传入,分别在 建立连接前 和 request 写入服务器前 注入。 networkInterceptors 这个名字很到位,表明了是在网络连接之后的注入的拦截器,最终写入数据到socket中并获取服务器返回的流,形成了一条数据调用链。
java copyable">Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());//OkHttpClient.Builder.addInterceptor(Interceptor interceptor)
interceptors.add(retryAndFollowUpInterceptor);//错误处理,和其他的拦截器相比 这个拦截器先初始化了,这样设计我觉得是处理 cancel 用的
interceptors.add(new BridgeInterceptor(client.cookieJar()));//拼接http协议 组装header
interceptors.add(new CacheInterceptor(client.internalCache()));//缓存处理
interceptors.add(new ConnectInterceptor(client));//使用连接池建立连接
if (!forWebSocket) {//forWebSocket 在 httpclient.newCall时是 false
interceptors.addAll(client.networkInterceptors());//添加OkHttpClient.Builder.addNetworkInterceptor(Interceptor interceptor)
}
interceptors.add(new CallServerInterceptor(forWebSocket));//想服务器发起请求
Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest, this, eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis());
return chain.proceed(originalRequest);//开始请求
}
复制代码
Interceptor.Chain 它只有一个子类 RealInterceptorChain(旧版有两个子类) 实际上就是一个保存了所有参数的类,里面只有一个有用的方法 Response proceed(Request request)
。像是 Interceptor 对应的 bean,并且每一个拦截器都有一个 chain。
java copyable">/**链式调用下一个拦截器,直到CallServerInterceptor发出请求,不在调用proceed。
*@params index 标识调用到第几个拦截器了,当然不会超出interceptors.size(),逻辑上看是不会抛出AssetionError的
*@params calls 标识和拦截器对应的Chain调用了几次,如果用户定义的interceptor调用了多次 chain.proceed会抛出异常
**/
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
RealConnection connection) throws IOException {
if (index >= interceptors.size()) throw new AssertionError();
calls++;
// If we already have a stream, confirm that the incoming request will use it.
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {//connect不支持这个请求
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) + " must retain the same host and port");
}
// If we already have a stream, confirm that this is the only call to chain.proceed().
if (this.httpCodec != null && calls > 1) {//如果用户定义的interceptor调用了多次 chain.proceed会抛出异常
throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) + " must call proceed() exactly once");
}
// Call the next interceptor in the chain.
RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec, connection, index + 1, request, call, eventListener, connectTimeout, readTimeout, writeTimeout);
Interceptor interceptor = interceptors.get(index);//arraylist的下一个拦截器
Response response = interceptor.intercept(next);//这里的 interceptor如果命名为next更好理解一点
// Confirm that the next interceptor made its required call to chain.proceed().
if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {//请求已经完成了,确保proceed必须仅仅调用一次,如果没有调用 index+1小于size,如果调用多次,calls会大于1
throw new IllegalStateException("network interceptor " + interceptor + " must call proceed() exactly once");
}
// Confirm that the intercepted response isn't null.
if (response == null) {//已经走完了所有的拦截器,这时候必须有response
throw new NullPointerException("interceptor " + interceptor + " returned null");
}
if (response.body() == null) {//response必须有相应体。分开判断。提示不同的错误日志,更好调试
throw new IllegalStateException( "interceptor " + interceptor + " returned a response with no body");
}
return response;
}
}
复制代码
RetryAndFollowUpInterceptor
如果用户没有设置拦截器,第一个进入了RetryAndFollowUpInterceptor
,通过 followRedirects、followSslRedirects(源码分析2)控制302,401等响应码逻辑。
java copyable">/**使用StreamAllocation找到一个可用的 connect,并且传给后面的拦截器继续处理,
*处理完成生成了 response,通过响应码判断是否还需要后续请求,
*如果需要后续请求,判断StreamAllocation这个连接是否可以复用或者 new 一个新的。
*@methord createAddress 可能是因为变量太多,不容易通过方法传递弄个Address类,没有任何逻辑
*@field canceled 如果取消请求了,这里抛出了IOException异常
*@constant MAX_FOLLOW_UPS最多可以重定向20
**/
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
RealInterceptorChain realChain = (RealInterceptorChain) chain;//3.9版本只有这一中拦截器,如果用户定义自己的拦截器要继承RealInterceptorChain了
Call call = realChain.call();
EventListener eventListener = realChain.eventListener();
//处理流和连接池的类
streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), call, eventListener, callStackTrace);
int followUpCount = 0;//重定向多少次了
Response priorResponse = null;//如果有重定向,记录上一次返回值
while (true) {//如果不 return 或抛出异常 会一直重试下去
if (canceled) {//调用了 Call.cancel,如果这个 call 在子线程的队列里还没有发出去就可以 cancel 掉
streamAllocation.release();
throw new IOException("Canceled");
}
Response response;//记录请求结果
boolean releaseConnection = true;//执行后面的拦截器的抛出的异常没有抓到,是连接不到服务器或者服务器出错,要释放 StreamAllocation
try {//执行后续的拦截器(下面一一介绍),当所有的拦截器执行完,就有了 response
response = realChain.proceed(request, streamAllocation, null, null);
releaseConnection = false;
} catch (RouteException e) {
/* recover() 这个方法判断如果连接出现问题是否可以重试
*先判断 Okhttpclient.retryOnConnectionFailure(),
*在判断是否可以通过重试解决的Exception,
*如果有更多的连接方案返回 true */
if (!recover(e.getLastConnectException(), false, request)) {
throw e.getLastConnectException();//路由认证失败抛出异常
}
releaseConnection = false;
continue;//掉用了streamAllocation.hasMoreRoutes,去找下一个可用的 connection
} catch (IOException e) {
// 和RouteException类似,重试请求还是抛出异常
boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
if (!recover(e, requestSendStarted, request)) throw e;
releaseConnection = false;
continue;
} finally {
if (releaseConnection) {//如果有请求过程中抛出了异常,connection 不可用了,要释放掉
streamAllocation.streamFailed(null);
streamAllocation.release();
}
}
if (priorResponse != null) {//如果重试了一次或多次,要把第一个响应的除了 body 部分返回给调用者
response = response.newBuilder()
.priorResponse(priorResponse.newBuilder().body(null).build())
.build();
}
Request followUp = followUpRequest(response);
if (followUp == null) {//根据响应码判断是否需要后续请求,不需要后续处理则返回 response
if (!forWebSocket) {
streamAllocation.release();
}
return response;
}
closeQuietly(response.body());//body 不需要复用给priorResponse,则关掉
if (++followUpCount > MAX_FOLLOW_UPS) {//如果重新发起请求次数超过20次就不在重试了
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (followUp.body() instanceof UnrepeatableRequestBody) {//返回来的 requestbody 继承了 UnrepeatableRequestBody。
streamAllocation.release();
throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
}
//通过方法sameConnection判断是不是要请求同一个 url,如果不是统一要 new 一个新的StreamAllocation
if (!sameConnection(response, followUp.url())) {
streamAllocation.release();
streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(followUp.url()), call, eventListener, callStackTrace);
} else if (streamAllocation.codec() != null) {//当前的streamAllocation没有设置协议类型,正常不会进入到这里
throw new IllegalStateException("Closing the body of " + response + " didn't close its backing stream. Bad interceptor?");
}
request = followUp;//继续请求这个
priorResponse = response;//保存这次请求的结果,需要里面的 request 和 header
}
}
复制代码
300-399之间的请求码是重定向,需要重新请求,拼接到方法里了,实际这部分代码也可以在 builder 里暴露出来提供默认实现。注意第一次408超时,只会再重试1次 client.proxyAuthenticator().authenticate(route, userResponse);
参见源码分析2
client.authenticator().authenticate(route, userResponse);
参见源码分析2
java copyable">//当请求失败的时候,通过这个方法判断状态码,是不是要继续请求,这里只做简单的分析
private Request followUpRequest(Response userResponse) throws IOException {
..........
switch (responseCode) {
case HTTP_PROXY_AUTH://407 设置了代理,并且需要登录代理才能继续下面的访问
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);
case HTTP_UNAUTHORIZED://401服务器需要授权,这里返回一个登录服务器的请求
return client.authenticator().authenticate(route, userResponse);
case HTTP_PERM_REDIRECT://308
case HTTP_TEMP_REDIRECT://307
// "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
case HTTP_MULT_CHOICE://300
case HTTP_MOVED_PERM://301
case HTTP_MOVED_TEMP://302
case HTTP_SEE_OTHER://303
if (!client.followRedirects()) return null;//如果设置成不支持重定向直接返回当前 response。
String location = userResponse.header("Location");
if (location == null) return null;//这里的判空很严谨,按理说300的响应码都会包含这个字段
HttpUrl url = userResponse.request().url().resolve(location);
if (url == null) return null;
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)) {//判断是不是要修改 body,除了 post、get 都需要
final boolean maintainBody = HttpMethod.redirectsWithBody(method);// 如果是PROPFIND需要保留 body,其他的都不需要
if (HttpMethod.redirectsToGet(method)) {//PROPFIND需要把方法给为 get
requestBuilder.method("GET", null);
} else {//根据maintainBody是否保留 body
RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
requestBuilder.method(method, requestBody);
}
if (!maintainBody) {//body 都不要了,这些 header 里标识 body 信息的都不要了
requestBuilder.removeHeader("Transfer-Encoding");
requestBuilder.removeHeader("Content-Length");
requestBuilder.removeHeader("Content-Type");
}
}
if (!sameConnection(userResponse, url)) {//认证过一次就不要再次认证了
requestBuilder.removeHeader("Authorization");
}
return requestBuilder.url(url).build();
case HTTP_CLIENT_TIMEOUT://请求超时,只重试一次
if (!client.retryOnConnectionFailure()) {
return null;
}
if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
return null;
}
if (userResponse.priorResponse() != null && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
return null;//如果上次也是请求超时,这次还是就不在重试了
}
return userResponse.request();
default:
return null;
}
}
复制代码
BridgeInterceptor
根据body 中的信息设置 header 中的一些参数,比如数据长度,数据类型。
使用 OkHttpClient.Builder 设置过来的 CookieJar(源码分析2) 接口设置 cookie,如果需要支持设置 cookie 请调用Builder cookieJar(CookieJar cookieJar)
,默认不支持cookie, 这样设计可能是考虑为客户端接口使用,基本不用 cookie,少了处理 cookie 的逻辑性能上更好一些。
其中 Connection:Keep-Alive 对 socket 连接缓存,socket内部会每隔一段时间检测是否和服务器连通,比如断网了,检测不到服务器在线,发送 RST 包断开连接。
请求头中的 Host 是为了让服务器定位哪个域名下的服务的,只有很少的大公司会一个 ip 下的主机部署一套域名的服务,往往是一台主机承载了很多个域名的服务,http 的请求头里只有要请求的路径。为了区分把请求下发给哪个域名需要在 header 设置 Host,框架中已经自动搞定了。大家可以试试,不带 host 的请求有的是可以走通的,但大部分是会报错的
gzip 框架内部是自动支持压缩的,这样可以减少传输的数据量。注意如果框架使用者在 Header 中设置了Accept-Encoding:gzip,框架不会自动解压,会把原始数据在 response 直接返回。保证响应内容和请求是对应的
java copyable">public Response intercept(Chain chain) throws IOException {
Request userRequest = chain.request();
Request.Builder requestBuilder = userRequest.newBuilder();
RequestBody body = userRequest.body();
if (body != null) {//如果是 post 请求,会设置 body
MediaType contentType = body.contentType();
if (contentType != null) {//请求体的数据类型,form 格式的 header 如 application/x-www-form-urlencoded
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {//数据长度,服务器根据数据长度读取 body
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {//输出的内容长度不能确定
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
}
if (userRequest.header("Host") == null) {//设置 host,一个ip会对应几个域名的情况,服务器判断header 里的 host,判断应该去哪个域名下查找数据
requestBuilder.header("Host", hostHeader(userRequest.url(), false));
}
if (userRequest.header("Connection") == null) {//建立连接消耗性能,所有在服务器和客户端都有缓存,要设置这个字段
requestBuilder.header("Connection", "Keep-Alive");
}
boolean transparentGzip = false;
if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) {//使用 gzip 传输数据较少很大的数据量
transparentGzip = true;
requestBuilder.header("Accept-Encoding", "gzip");
}
List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
if (!cookies.isEmpty()) {//如果实现了 cookieJar 接口,会给 header 写入 cookie
requestBuilder.header("Cookie", cookieHeader(cookies));
}
if (userRequest.header("User-Agent") == null) {//设置了默认的 user-agent:okhttp/3.9.0
requestBuilder.header("User-Agent", Version.userAgent());
}
Response networkResponse = chain.proceed(requestBuilder.build());//继续执行后续的请求,直到服务器返回数据
//这个方法里只是调用了cookieJar.saveFromResponse(url, cookies);
HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
//这里给码农返回的是传进来的 request,并不是添加了很多默认值之后的 request
Response.Builder responseBuilder = networkResponse.newBuilder().request(userRequest);
/**处理 gzip 的逻辑,框架使用者并没有设置 gzip 的,框架自动添加了压缩逻辑;
*并且服务器的响应头说返回了 gzip 的数据;
*这里解析了 body 中的数据并返回了没压缩的数据。
*清楚 header 中的Encoding和Length,因为码农拿到的是解压后的数据
**/
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")//已经不是 gzip 压缩之后的数据了
.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();
}
复制代码
CacheInterceptor
主要处理了缓存逻辑,如果需要支持 Cache-Control、Pragma 响应头使用 cache,可以使用框架中自带的 Cache 类OkHttpClient.Builder().cache(new Cache(file,maxSize)
。默认不支持缓存
InternalCache(源码分析2) 获取或者保存缓存的接口,这里只处理存取的逻辑。
判断缓存是否可用的逻辑在CacheStrategy中,CacheStrategy里面对header进行了一系列的计算,这些计算并没有使用接口暴露出来,这样设计因为里面完全遵循了http协议,只要协议里所有的header参数都处理了不会有任何其他实现类,则不需要抽取为接口
java copyable">Response intercept(Chain chain) throws IOException {
Response cacheCandidate = cache != null? cache.get(chain.request()): null;//设置了InternalCache,根据请求获取一个缓存的 response
long now = System.currentTimeMillis();//http 的协议需要根据当前时间是否判断使用缓存
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();//判断 header 的缓存有效期
Request networkRequest = strategy.networkRequest;//根据 header 判断是否需要发起请求
Response cacheResponse = strategy.cacheResponse;//根据 header 判断是不是使用缓存的 response
if (cache != null) {//记录使用了这个更新,在 cache 中的实现只是对使用缓存情况计数
cache.trackResponse(strategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}//在 cache 中找到了 request 缓存的 response,但是根据 header 中的时间策略,这个 response 已经超时失效了
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {//request 的 cacheControl只允许使用缓存 onlyIfCached 为 true,响应投中包含only-if-cached
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 (networkRequest == null) {//缓存还在有效期内,直接返回了缓存
return cacheResponse.newBuilder()//返回了缓存的 respose
.cacheResponse(stripBody(cacheResponse))
.build();
}
Response networkResponse = null;
try {//如果没有缓存或者缓存失效,后面继续请求网络
networkResponse = chain.proceed(networkRequest);
} finally {
if (networkResponse == null && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());//networkResponse是空肯定是抛异常了,没有返回值
}
}
if (cacheResponse != null) {
if (networkResponse.code() == HTTP_NOT_MODIFIED) {//如果有缓存并且网络返回了304,还是要使用缓存数据
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();
cache.trackConditionalCacheHit();//使用了一次缓存,记录一下
cache.update(cacheResponse, response);//更新缓存,主要是 header 变了
return response;
} else {//缓存失效了,释放掉
closeQuietly(cacheResponse.body());
}
}
Response response = networkResponse.newBuilder()
.cacheResponse(stripBody(cacheResponse))
.networkResponse(stripBody(networkResponse))
.build();
if (cache != null) {//下面是存储缓存的
if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response, networkRequest)) {
CacheRequest cacheRequest = cache.put(response);//更新缓存,header/body都变了
return cacheWritingResponse(cacheRequest, response);
}
if (HttpMethod.invalidatesCache(networkRequest.method())) {//不支持缓存方法,要清除掉缓存,post 不支持
try {
cache.remove(networkRequest);//
} catch (IOException ignored) {
// The cache cannot be written.
}
}
}
return response;
}
复制代码
CacheStrategy.Factory
只是根据requestHeader对 CacheStrategy 需要的参数做解析,这里叫 Factory 并不合适,因为它不是工厂,还不如弄个静态方法parse,所有的逻辑处理都在get()
里(下一个分析),如果对http协议缓存处理部分有一定了解,就不用看Factory的分析了
java copyable"> public Factory(long nowMillis, Request request, Response cacheResponse) {
this.nowMillis = nowMillis;//当前时间,判断当前客户端的时间是不是可以直接使用缓存,如果客户端的时间是错的,会对缓存逻辑有影响
this.request = request;//码农的request
this.cacheResponse = cacheResponse;//上一次的缓存
if (cacheResponse != null) {
this.sentRequestMillis = cacheResponse.sentRequestAtMillis();//缓存的请求时间 在CallServerInterceptor中给这两个字段赋值
this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();//请求的响应时间
Headers headers = cacheResponse.headers();
for (int i = 0, size = headers.size(); i < size; i++) {//拿到缓存的response的header,找里面和缓存时间相关的值
String fieldName = headers.name(i);
String value = headers.value(i);
if ("Date".equalsIgnoreCase(fieldName)) {//原始服务器消息发出的时间,如果服务器没有返回这个字段就使用框架中保存的receivedResponseMillis。 Date:Tue,17 Apr 2017 06:46:28GMT
servedDate = HttpDate.parse(value);
servedDateString = value;
} else if ("Expires".equalsIgnoreCase(fieldName)) {//缓存数据的过期时间 Expires: Fri, 30 Oct 2018 14:19:41
expires = HttpDate.parse(value);
} else if ("Last-Modified".equalsIgnoreCase(fieldName)) {//请求的数据的最后修改时间,下次请求使用If-Modified-Since传给服务器,如果数据没变化,服务器返回304。Last-modified:Tue,17 Apr 2017 06:46:28GMT
lastModified = HttpDate.parse(value);
lastModifiedString = value;
} else if ("ETag".equalsIgnoreCase(fieldName)) {//数据的唯一标识,下次请求使用If-None-Match传给服务器,如果数据没变化,服务器返回304。Etag:"a030f020ac7c01:1e9f"
etag = value;
} else if ("Age".equalsIgnoreCase(fieldName)) {//从原始服务器到代理缓存形成的估算多少秒 Age: 12
ageSeconds = HttpHeaders.parseSeconds(value, -1);
}
}
}
}
复制代码
Factory.get()
主要是处理header中缓存的逻辑,判断传进来的cacheResponse是不是可以直接使用,又分为两个主要方法。
getCandidate()
方法里主要是计算缓存过期时间,还有一小部分根据cache设置requestHeader的逻辑,是主要的分析方法。 response.cacheControl()
方法对responseHeader服务器返回的字段(Cache-Control、Pragma)解析,把字符串转换为Bean,CacheControl里也有一个Budiler,这里有点鸡肋了,实际这个只是在header中parse就好了,绝对用不到new Builder出来。逻辑并不复杂,有兴趣自己看一下。
getCandidate
java copyable"> private CacheStrategy getCandidate() {
if (cacheResponse == null) {//如果没有chache直接返回request策略,继续请求网络
return new CacheStrategy(request, null);
}
if (request.isHttps() && cacheResponse.handshake() == null) {//如果是https请求,要有证书认证的信息,如果没有继续请求网络
return new CacheStrategy(request, null);
}
if (!isCacheable(cacheResponse, request)) {//这个方法里根据响应码判断响应的数据是否可以使用缓存
return new CacheStrategy(request, null);
}
CacheControl requestCaching = request.cacheControl();
if (requestCaching.noCache() || hasConditions(request)) {//如果reseponse中nocache,或者If-Modified-Since、If-None-Match字段则进行网络请求
return new CacheStrategy(request, null);
}
CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {//缓存的响应头 Cache-Control的值为immutable,标识永久可以使用的缓存
return new CacheStrategy(null, cacheResponse);
}
long ageMillis = cacheResponseAge();//根据header中date、age和当前时间,计算cacheResponse距离当前时间已经过了多久了
/*根据maxage和expires计算缓存数据还能用多久时间,
*还根据上一次修改时间lastModified距返回数据时间Date的差10%时间以内,HTTP RFC 建议这段时间内数据是有效的*/
long freshMillis = computeFreshnessLifetime();
if (requestCaching.maxAgeSeconds() != -1) {//在computeFreshnessLifetime中用if elseif优先去了maxAge的时间,这里的if判断是多余的
freshMillis = Math.min(freshMillis, SECONDS.toMillis(requestCaching.maxAgeSeconds()));
}
long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {//响应头的min-fresh 最小刷新间隔
minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}
long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {//如果不是must-revalidate过期之后必须请求新数据,max-stale允许过期多少秒之后,仍然可以使用缓存
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()) {//缓存的数据已经超过24小时了,也给一个警告,http协议不推荐设置超过24小时的缓存
builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
}
return new CacheStrategy(null, builder.build());//缓存还有效直接把缓存扔给了码农
}
// 客户端不能决定缓存是否可以使用,让服务器去决定,那就需要带一些参数,下面这些都是根据缓存头中服务器返回的数据标识,设置本次请求头应该带的参数,服务器知道客户端现在缓存是什么时候返回的
String conditionValue;
if (etag != null) {//注意这里使用的是elseif,优先级依次是 etag-lastModifity-date
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);//有缓存,但是判断不出来是否可以直接使用,让服务器去决定,如果服务器返回304,就不用返回请求体了,服务器决定使用缓存
}
复制代码
ConnectInterceptor
在intercept中使用连接池创建链接,并根据协议获取对应的HttpCodec的实现类,主要逻辑都在newStream、connection这两个方法中。
java copyable"> @Override public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
Request request = realChain.request();
StreamAllocation streamAllocation = realChain.streamAllocation();
boolean doExtensiveHealthChecks = !request.method().equals("GET");
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);//http 协议格式的接口,直接影响了协议版本
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
复制代码
StreamAllocation
newStream 初始化connectTimeout、readTimeout、writeTimeout等全局变量,并调用findHealthyConnection找到一个可用的链接 RealConnection,在方法内 while (true) 从连接池里找到没有被占用successCount=0,并且没有close的链接,关键方法是findConnection。
java copyable">/**找到一个connect。**/
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;//是否找到了可用的RealConnection
RealConnection result = null;//和foundPooledConnection对应,将要使用的connect
Route selectedRoute = null;//生成RealConnection用到的Address、Proxy存到这个类中
Connection releasedConnection;//将要释放的链接
Socket toClose;
synchronized (connectionPool) {
if (released) throw new IllegalStateException("released");
if (codec != null) throw new IllegalStateException("codec != null");
if (canceled) throw new IOException("Canceled");
releasedConnection = this.connection;//默认全局保存的connection是不可用的了,需要释放,从newStream进来是null
toClose = releaseIfNoNewStreams();//判断connection是否需要释放,如果已经不可用把connection置空,并返回sockte准备释放
if (this.connection != null) {//经过上面releaseIfNoNewStreams的方法判断,connection还可用没有置空
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {//没有需要释放的 连接
releasedConnection = null;
}
if (result == null) {//不能直接使用connection,从缓存池里去一个,这里route是空,先当没有代理处理
Internal.instance.get(connectionPool, address, this, null);//里面调用了acquire,如果缓存池里有可用的,就赋值给connection
if (connection != null) {//使用了缓存池中的 connect 不用重新创建。
foundPooledConnection = true;
result = connection;
} else {//缓存池中没取到,需要重新创建连接,如果有代理,使用route取一个有代理的试试
selectedRoute = route;
}
}
}
closeQuietly(toClose);
if (releasedConnection != null) {//释放掉不可用的
eventListener.connectionReleased(call, releasedConnection);
}
if (foundPooledConnection) {//上面已经从连接池缓存中找到了可用的
eventListener.connectionAcquired(call, result);
}
if (result != null) {
return result;
}
boolean newRouteSelection = false;//缓存中没有返回一个可用的RealConnection,需要生成一个
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {//判断刚才的全局保存的route是否为空,如果还没有赋值 就使用routeSelector找一个
newRouteSelection = true;
routeSelection = routeSelector.next();//routeSelector中保存了所有的代理(可能多个),从中找到一个可用的并调用 dns,找到要连接的 ip,准备建立 socket 连接
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {//Selection中返回了所有可用的route,从头往后找,越靠前的代理连通性越高
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
Internal.instance.get(connectionPool, address, this, route);//掉用了connectPool.get在方法isEligible中判断是否有和 address、route对应的缓存 Connection,如果有调用 StreamAllocation.acquire给全局变量connection赋值
if (connection != null) {//上面的Internal.instance.get取到了缓存的 connection直接返回
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
if (!foundPooledConnection) {//没有取到缓存
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);//缓存中没有,只能创建一个新的
acquire(result, false);
}
}
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);//如果缓存中取到了,调用监听,这个方法应该在上面的 if (!foundPooledConnection) 前面就好理解了
return result;//和上面的 if 比 这里 return 了,不会创建 socket连接了
}
//这里是调用socket.connect的主要方法,并调用connectTls方法对 https进行 Handshake并调用 HostnameVerifier.verify 验证证书, 只有新创建的才会进入这里,在RealConnection中会详细分析
result.connect( connectTimeout, readTimeout, writeTimeout, connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {//把这个RealConnection缓存到连接池里
reportedAcquired = true;
Internal.instance.put(connectionPool, result);
if (result.isMultiplexed()) {//是http2.0协议
socket = Internal.instance.deduplicate(connectionPool, address, this);//调用了releaseAndAcquire把connection重新赋值,并释放旧的socket
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
复制代码
RealConnection
HostnameVerifier.verify验证tls证书 (源码分析2)
java copyable"> public void connect(int connectTimeout, int readTimeout, int writeTimeout,
boolean connectionRetryEnabled, Call call, EventListener eventListener) {
if (protocol != null) throw new IllegalStateException("already connected");
RouteException routeException = null;
List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();//选择https的算法用
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
if (route.address().sslSocketFactory() == null) {
if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {//判断 ssl 的安全性,不支持 CLEARTEXT
throw new RouteException(new UnknownServiceException("CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
if (!Platform.get().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException("CLEARTEXT communication to " + host + " not permitted by network security policy"));
}
}
while (true) {
try {
if (route.requiresTunnel()) {//如果是https 并且含有代理,需要先创建代理通道
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);//先发出一个请求创建通道
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break;
}
} else {//只创建一个socket
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
establishProtocol(connectionSpecSelector, call, eventListener);//判断http协议版本,链接socket,并建立 https 使用的加密通道,使用HostnameVerifier.verify验证tls证书
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
} catch (IOException e) {//创建链接失败,释放资源
closeQuietly(socket);
closeQuietly(rawSocket);
socket = null;
rawSocket = null;
source = null;
sink = null;
handshake = null;
protocol = null;
http2Connection = null;
eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);
if (routeException == null) {
routeException = new RouteException(e);
} else {
routeException.addConnectException(e);
}
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException;
}
}
}
if (route.requiresTunnel() && rawSocket == null) {//链接太多了
ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: " + MAX_TUNNEL_ATTEMPTS);
throw new RouteException(exception);
}
if (http2Connection != null) {
synchronized (connectionPool) {
allocationLimit = http2Connection.maxConcurrentStreams();//限制http2.0同一个connection的最大的连接数
}
}
}
复制代码
CallServerInterceptor
已经在上面的步骤创建好了连接,下面就可以直接写入数据了,使用Http1Codec按照 http1.1的格式写入数据到 socket 中,使用Http2Codec按照 http2的格式写入数据,注意这里返回的 Response持有的 Body是已流的形式存在的,使用完后一定要调用 close,否则连接池等资源不能释放。
java copyable">public Response intercept(Chain chain) throws IOException {
RealInterceptorChain realChain = (RealInterceptorChain) chain;
HttpCodec httpCodec = realChain.httpStream();//在ConnectInterceptor中建立连接后赋值,
StreamAllocation streamAllocation = realChain.streamAllocation();
RealConnection connection = (RealConnection) realChain.connection();
Request request = realChain.request();
long sentRequestMillis = System.currentTimeMillis();
realChain.eventListener().requestHeadersStart(realChain.call());
httpCodec.writeRequestHeaders(request);//把 request 中的 header按照 http 协议的各个版本拼接好写入到 socket 中
realChain.eventListener().requestHeadersEnd(realChain.call(), request);
Response.Builder responseBuilder = null;
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.
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
httpCodec.flushRequest();//如果 get 请求数据大于1k 当前请求不能把数据全部写入,需要分次
realChain.eventListener().responseHeadersStart(realChain.call());
responseBuilder = httpCodec.readResponseHeaders(true);
}
if (responseBuilder == null) {
// Write the request body if the "Expect: 100-continue" expectation was met.
realChain.eventListener().requestBodyStart(realChain.call());
long contentLength = request.body().contentLength();
CountingSink requestBodyOut = new CountingSink(httpCodec.createRequestBody(request, contentLength));//对写入的数据大小计数
BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
request.body().writeTo(bufferedRequestBody);//写入请求体中的数据
bufferedRequestBody.close();
realChain.eventListener()
.requestBodyEnd(realChain.call(), requestBodyOut.successfulCount);
} else if (!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.
streamAllocation.noNewStreams();//释放连接
}
}
httpCodec.finishRequest();//请求数据全部写入完成,等待服务器响应
if (responseBuilder == null) {
realChain.eventListener().responseHeadersStart(realChain.call());//这个 listener的位置在服务器响应之前,并不是已经开始读取 response 了
responseBuilder = httpCodec.readResponseHeaders(false);//读取响应头
}
Response response = responseBuilder
.request(request)
.handshake(streamAllocation.connection().handshake())
.sentRequestAtMillis(sentRequestMillis)
.receivedResponseAtMillis(System.currentTimeMillis())
.build();//生成了请求的结果
realChain.eventListener() .responseHeadersEnd(realChain.call(), response);//在responseHeadersStart和responseHeadersEnd之间包含了读取响应头和服务器处理请求的时间
int code = response.code();//响应码
if (forWebSocket && code == 101) {//应当继续发送请求,请求被服务器接受但是需要更多的请求数据 服务器才能处理
// Connection is upgrading, but we need to ensure interceptors see a non-null response body.
response = response.newBuilder()
.body(Util.EMPTY_RESPONSE)
.build();
} else {
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))//读取响应体到FixedLengthSource 或者ChunkedSource 中 使用完必须调用调用close,否则资源不能释放。
.build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
|| "close".equalsIgnoreCase(response.header("Connection"))) {
streamAllocation.noNewStreams();//响应头中包含了close需要释放连接
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {//204、205不需要返回请求体
throw new ProtocolException(
"HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;//已经完成了返回了这次请求的响应
复制代码
add by dingshaoran