okhttp连接池_okhttp源码解析

OkHttp 源码解析

OkHttp是一个高效的客户端HTTP请求框架,在Android客户端开发被广泛使用。

最近学习了HTTP协议的相关知识,学习一下OKHttp是如何使用HTTP协议请求网络的。

学习OKhttp源码就要先看全局,然后根据使用需要再研究细节,体会其精妙之处。
了解它的原理,并加以实践,可以实现一个简单的HTTP请求客户端。

实现HTTPS请求-响应思路

根据TCP/IP五层网络模型自下而上分别是物理层、数据链路层、网络层、传输层、应用层。

我们在实际开发中接触最多的就是传输层协议TCP/UDP和应用层协议HTTP,如果是HTTPS在传输层和应用层之间还有安全接口SSL/TLS协议。

836a22717f42982e3473e953d2a2af5d.png

请求-响应思路如下,整个OkHTTP整体实现思路基本也是一样的,具体的实现细节则需要探寻源码了。

  1. 根据URL主机名,通过DNS协议获取IP地址。
  2. 通过三次握手客户端和服务端建立TCP连接。
  3. TLS/SSL协议握手,确定加密方式、密钥等,保证传输安全。
  4. 通过HTTP协议构建请求,发送请求内容。
  5. 服务端处理返回,读取HTTP响应内容,渲染展示。

OkHttp使用的设计模式

OkHttp实现请求-响应的整体过程主要运用了两种设计模式Builder模式责任链模式

Builder模式

Builder模式主要用于构建复杂对象,将创建过程和参数表示进行解耦。

不是所有的复杂对象都适合使用Builder模式,比如数据实体Modle对象。

Builder模式适用条件:

  1. 如果复杂对象中很多参数只能内部创建,不能由调用者外部构建参数,传入参数值。
  2. 参数创建顺序不同导致结果不同,比如装配汽车,先要把车身组建好再安装车轮,而不是先安装车轮。
  3. 相同的参数可以被多个对象使用,最终构建的对象是不同的。比如不同品牌的电脑都是用英特尔的CPU。

Builder模式把创建细节封装在内部,可以定制的参数再开放给外部,交由外部创建,最终按照一定的顺序进行创建对象。

使用Builder模式创建的对象,内部都有一个Builder对象用于实现创建细节。

OkHttp中的很多对象都使用了Builder模式,比如RequestReponseOkHttpClient等等,在OkHttp源码中可以细细探究。

责任链模式

OkHttpClient

OkHttpClient是Http请求-响应的客户端类,可以理解成HTTP协议中的客户端,如浏览器,负责管理一个应用中所有HTTP请求和响应。
HTTP客户端需要有自己的武器库,通过组合使用,实现客户端请求响应的功能。

下面一一介绍OkHttpClient武器库中的武器,在分析源码中,再看看他们是如何应用的。

Dispatcher

调度器,负责请求的调度,管理线程池和请求队列,默认的最大请求量为64。
Okhttp分为同步请求和异步请求。
同步请求:使用当前线程请求,会堵塞当前线程。
异步请求:使用另外一个线程请求,当请求完成,将响应结果通过回调函数传递。

Dispatcher主要是对异步请求的调度,使用线程池中的线程发起请求,并通过异步请求队列进行管理。Dispatcher通过同步请求队列管理请求,但是线程调度由外部负责。

private ExecutorService executorService;
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();

private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();

Dns

Dns负责将调用Socket中的方法,将主机名解析成IP地址。

SocketFactory

socket工厂类,可以使用多种创建方式完成socket的创建。
socket是操作系统提供给的网络接口,我们将应用层的数据,交给Socket,由操作系统实现TCP/IP协议栈,完成数据的传输。

SSLSocketFactory

SSLSocket工厂类,SSLSocket在socket基础上,实现TLS/SSL协议的安全接口。

ConnectionSpec

SSLSocket的连接配置,HTTPS完成三次握手连接之后,还要进行TLS/SSL协议握手。ConnectionSpec根据TLS版本不同,配置不同的连接参数,主要包括客户端支持的TLS版本和密码套件。

CertificatePinner

证书限定,限制信任哪些证书和证书颁发机构。客户端只信任配置的数字证书,其他数字证书一律不信任,防止黑客使用中间机构颁发的数字证书伪装骗取信任,获取数据,破坏传输安全。
不过可能会出现服务端证书更新,由于证书固定导致不被客户端信任,所以服务端如果没有要求,一般情况下不使用证书限定。

CertificateChainCleaner

证书清除器,服务器传递的证书????

HostnameVerifier

在TLS握手过程中,如果URL的主机名和请求服务器的主机名不匹配,则需要使用验证接口进行校验,数字证书中包含主机名可以通过证书校验,或者其他校验方式验证。

Authenticator

身份认证器,如果服务端返回401状态码,则客户端要向服务器验证自己的身份。

OkHttpClient包含目标服务器身份认证authenticator和代理服务器身份认证proxyAuthenticator

ConnectionPool

连接池,如果客户端和请求的服务器已经建立连接,OkHttp不会新建连接,而是复用连接池中已有连接对象,以减少网络延迟。

Protocol

枚举类,使用的协议管理,如HTTP/1.0、HTTP/1.1、HTTP/2等

HTTP_1_0("http/1.0"),
HTTP_1_1("http/1.1"),
SPDY_3("spdy/3.1"),
HTTP_2("h2"),
H2_PRIOR_KNOWLEDGE("h2_prior_knowledge"),
QUIC("quic");

Proxy

表示请求代理设置,有三种类型

  • DIRECT,直连模式,直接和目标服务器通信,中间没有代理服务器
  • HTTP, 代理模式,通过HTTP服务器和目标服务器通信。HTTP代理服务器根据HTTP协议转发HTTP数据包。
  • SOCKS,代理模式,通过Socks服务器和目标服务器通信,Socks代理只是简单地转发数据包(传输层),而不必关心是何种应用协议(比如FTP、HTTP和NNTP请求)。

ProxySelector

代理选择器,根据你要连接的URL自动选择最合适的代理,如果需要编写定制代理选择器,需要继承并实现select方法。

CookieJar

Cookie管理,如果服务器返回的响应中返回cookie,则会CookieJar则会保存在本地。
当请求服务器时,请求头会带上对应域名下保存的cookie。
默认情况不保存cookie,配置对应属性才能保存。

Cache

缓存器,将响应结果保存到本地,主要通过DiskLruCache实现HTTP的缓存。

Interceptor

拦截器,拦截请求和响应,通过拦截请求和响应的各个阶段,对请求参数和响应结果进行处理。

OkHttp提供了RetryAndFollowUpInterceptorBridgeInterceptorCacheInterceptorConnectInterceptorNetWorkInterceptorCallServerInterceptor官方拦截,负责请求和响应的处理。

我们也可以编写自定义拦截器,实现接口Interceptor,对请求和结果进行处理。

EventListener.Factory

EventListener工厂类,负责创建EventListenerEventListener负责监听HTTP或HTTPS从请求开始到响应结束的各个阶段,通过回调函数获取请求、响应等对象。

public void callStart(Call call);
public void dnsStart(Call call, String domainName);
public void dnsEnd(Call call, String domainName, List<InetAddress> inetAddressList);
public void connectStart(Call call, InetSocketAddress inetSocketAddress, Proxy proxy);
public void secureConnectStart(Call call);
public void secureConnectEnd(Call call, @Nullable Handshake handshake);
public void connectEnd(Call call, InetSocketAddress inetSocketAddress, Proxy proxy,
    @Nullable Protocol protocol) ;
public void connectFailed(Call call, InetSocketAddress inetSocketAddress, Proxy proxy,
@Nullable Protocol protocol, IOException ioe) ;
public void connectionAcquired(Call call, Connection connection);
public void connectionReleased(Call call, Connection connection);
public void requestHeadersStart(Call call);
public void requestHeadersEnd(Call call, Request request);
public void requestBodyStart(Call call);
public void requestBodyEnd(Call call, long byteCount);
public void responseHeadersStart(Call call);
public void responseHeadersEnd(Call call, Response response);
public void responseBodyStart(Call call);
public void responseBodyEnd(Call call, long byteCount);
public void callEnd(Call call);

分析源码

请求过程

了解OkHttpClient中的参数,下面深入源码OkHttp如何完成一次HTTP的请求和响应的。

Okhttp有同步和异步两种方式,请求过程基本一致,但是异步请求需要Dispatcher调度。
我们先通过同步请求了解OkHttp的请求过程,再分析异步请求的调度处理。

由于OkHttp中需要处理的东西比较多,因此会对源码做删减,只关注核心。
请求流程如下图所示,

发起请求

同步请求代码如下所示,没有配置任何参数,是一个最小化同步请求客户端。

首先创建Request对象,传入请求url,这里请求的是百度,使用get请求方法。
创建OkHttpClient对象,调用netCall方法创建一个Call对象,并发起请求。

Call对象负责获取请求参数,发起请求,接收响应结果,返回给客户端,如同浏览器打开了一个新窗口,输入网址发起请求,接收结果渲染页面。Call只是一个接口,OkHttp中真正使用的实现类RealCall

调用RealCall中的execute()方法执行,开始发起请求。

Request request = new Request.Builder()
        .url("百度一下,你就知道")
        .get()
        .build();
OkHttpClient okHttpClient = new OkHttpClient();
Response response = okHttpClient.newCall(request).execute();

RealCall中的execute()方法,执行步骤:

  1. 调用监听函数,告诉调用者请求已经开始
  2. 使用Dispather调度器,将Call加入到同步队列当中,就如同浏览器的窗口管理,如果浏览器关闭,统一将所有请求断开,关闭窗口。
  3. 调用getResponseWithInterceptorChain()通过责任链模式将多个拦截器连接,自上而下处理请求,自下而上处理响应。
  4. 返回响应结果,关闭Call对象。

getResponseWithInterceptorChain()方法完成了OkHttp的请求和响应的具体工作。

// RealCall
public Response execute() throws IOException {
    eventListener.callStart(this);

    client.dispatcher().executed(this);
    Response result = getResponseWithInterceptorChain();

    client.dispatcher().finished(this);
  }

// Dispatcher
synchronized void executed(RealCall call) {
  runningSyncCalls.add(call);
}

责任链处理请求

RealCall中的getResponseWithInterceptorChain()方法将自定义的拦截器和OkHttp提供的拦截器依次添加到拦截器容器中。
创建RealInterceptorChain责任链对象,传入参数为拦截器容器,将要调用的拦截器索引,请求对象和超时时间,其他参数初始化时无法提供所以暂时为空。

最后调用责任链对象中proceed方法依次调用拦截器处理请求。

Response getResponseWithInterceptorChain() throws IOException {
  List<Interceptor> interceptors = new ArrayList<>();
  interceptors.addAll(client.interceptors());
  interceptors.add(retryAndFollowUpInterceptor);
  interceptors.add(new BridgeInterceptor(client.cookieJar()));
  interceptors.add(new CacheInterceptor(client.internalCache()));
  interceptors.add(new ConnectInterceptor(client));
  if (!forWebSocket) {
    interceptors.addAll(client.networkInterceptors());
  }
  interceptors.add(new CallServerInterceptor(forWebSocket));

  Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
      originalRequest, this, eventListener, client.connectTimeoutMillis(),
      client.readTimeoutMillis(), client.writeTimeoutMillis());

  return chain.proceed(originalRequest);
}

RealInterceptorChain中的proceed()方法又创建了一个责任链对象,根据索引获取当前处理请求的拦截器,调用拦截器中的intercept()方法处理请求。Interceptor中的intercept()方法,处理完请求之后,又调用了责任链对象的proceed()方法。

// RealInterceptorChain中的proceed
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
    RealConnection connection) throws IOException {
  ...
  RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
      connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
      writeTimeout);
  Interceptor interceptor = interceptors.get(index);
  Response response = interceptor.intercept(next);

  ...
  return response;
}

// Interceptor中的intercept方法
public Response intercept(Chain chain) throws IOException {
    // 处理请求
    ...

    response = realChain.proceed(request, streamAllocation, null, null);

    // 处理响应
    ...
    return response;
  }

看到这里,大脑就比较混乱了,下面整理思绪,细细分析一下。
责任链模式的核心就是将对象顺着责任链依次加工,如下图所示,可以责任链对象看成工厂,拦截器是车间。

  1. 责任链对象Chain调用当前拦截器的intercept()方法,并将整个责任链对象传递过去。
  2. 拦截器可以使用Chain中的所有参数如RequestCall等进行处理,表示车间可以使用工厂已有的所有原料。
  3. 拦截器处理完毕,再调用责任链对象的proceed()方法,再将处理完毕的参数再回传给Chain,表示车间将原料加工成半成品或产生了新产品,交给工厂。
  4. proceed()方法中又创建了新的责任链对象,但是整个拦截器链没有发生变化,只是原料换成了半成品和新产品,所以可看成工厂更新了原料,并且使用下一个拦截器加工。
  5. 按照上述做法,责任链按照顺序依次调用拦截器对参数进行处理,完成HTTP请求和响应。

接下来按照顺序分析每个拦截器的intercept()方法是如何处理请求参数,最终完成HTTP请求。

f4ca0424e591d0e4cae0b46984418c3c.png

RetryAndFollowUpInterceptor处理请求

RetryAndFollowUpInterceptor是处理参数的第一个拦截器,主要工作如下:

创建了StreamAllocation对象,负责协调请求、连接和流之间的关系,请求通过新建或复用连接,在连接上流按照一定的逻辑顺序传输数据,调用StreamAllocation对象完成具体工作由其他拦截器处理。

接下来进入一个while死循环,调用Chainproceed执行调用下一个拦截器处理,回传新建StreamAllocation对象。

如果接下来的拦截器处理出错,由于死循环,会从头开始,请求会继续重试。
如果catch异常处理中,恢复失败,则抛出异常终止重试。

public Response intercept(Chain chain) throws IOException {
    Request request = chain.request();
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Call call = realChain.call();
    EventListener eventListener = realChain.eventListener();

    StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
        createAddress(request.url()), call, eventListener, callStackTrace);
    this.streamAllocation = streamAllocation;

    while (true) {
      Response response;
      boolean releaseConnection = true;
      try {
        response = realChain.proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
          if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
          throw e.getFirstConnectException();
        }
        releaseConnection = false;
        continue;
      }catch (IOException e) {
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      }
    }
    // 响应结果处理
    ...
  }

BridgeInterceptor处理请求

BridgeInterceptorRequest对象进行加工,补充请求头信息,并将传入的参数对象转换需要的头部字段。

我们在开发时构建的Request对象仅仅传入了url和确定请求方式,而HTTP请求需要的其他头部字段,都需要在BridgeInterceptor中添加。
下面看看为请求头添加了哪些头部字段

  1. 判断是否请求上传实体,如果有上传,调用者传入了contentType,将其转换为头部字段Content-Type字符串。
    实体长度确定,添加Content-Length说明实体长度。
    实体长度不确定,添加Transfer-Encoding:chunked说明分块阐述。
  2. 添加Host说明请求服务器的主机名。
  3. 添加Connection:Keep-Alive使用长连接。
  4. 判断是否请求部分资源,如果不是添加Accept-Encoding:gzip,期望服务器对返回资源进行压缩。
  5. 加载域名下的本地cookie,如果存在则添加Cookie:本地cookie信息
  6. 添加User-Agent,说明客户端的信息。

Request对象头部信息添加完毕,传递给责任链对象,调用下一个拦截器进行处理。

@Override public Response intercept(Chain chain) throws IOException {
  Request userRequest = chain.request();
  Request.Builder requestBuilder = userRequest.newBuilder();
  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");
    }
  }
  if (userRequest.header("Host") == null) {
    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) {
    transparentGzip = true;
    requestBuilder.header("Accept-Encoding", "gzip");
  }
  List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url());
  if (!cookies.isEmpty()) {
    requestBuilder.header("Cookie", cookieHeader(cookies));
  }
  if (userRequest.header("User-Agent") == null) {
    requestBuilder.header("User-Agent", Version.userAgent());
  }
  Response networkResponse = chain.proceed(requestBuilder.build());

  // 处理响应
  return responseBuilder.build();
}

缓存控制

OkHttp根据HTTP协议处理客户端缓存,所以想要了解OkHttp中对缓存的处理,需要先了解HTTP协议的缓存机制。

OkHttp中缓存处理的组件主要有CacheCacheStrategyCacheIntercepterCache:负责缓存的存取等操作。CacheStrategy:负责缓存策略的制定。CacheIntercepter:负责将请求和将资源缓存到客户端。客户端和服务器根据一定的机制(策略CacheStrategy ),在需要的时候使用缓存的数据作为网络响应,节省了时间和宽带。

下面详细看看OkHttp是如何进行缓存控制的,对上面的类一一分析。

CacheIntercepter

Http请求如果不需要使用缓存,则直接调用下一个拦截器处理。CacheStrategy主要制定4种缓存策略,具体策略如何生成的,下面会具体分析

networkRequestcacheResponse缓存策略NULLNULL只能使用本地缓存,但是无缓存可用,又不向服务器请求新资源NULLNOT NULL使用本地缓存,不向服务器请求新资源NOT NULLNOT NULL不使用本地缓存,向服务器请求新资源NOT NULLNOT NULL使用本地缓存,向服务器验证缓存是否可用

根据缓存策略做出不同处理。
如果第1种策略返回状态码504(Unsatisfiable Request (only-if-cached)),只能使用缓存,但是客户端无缓存可用。
如果第2种策略,则直接返回客户端缓存的资源。
第3或第4种策略,则不管是否使用缓存,都要向服务器请求,则交由下一个拦截器进行处理。

@Override public Response intercept(Chain chain) throws IOException {
  Response cacheCandidate = cache != null
      ? cache.get(chain.request())
      : null;
  long now = System.currentTimeMillis();
  CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
  Request networkRequest = strategy.networkRequest;
  Response cacheResponse = strategy.cacheResponse;
  ...
  // 第一种
  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 (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());
    }
  }

  // 处理响应
  ...
  return response;
}

CacheStrategy

CacheInterceptor中通过下面代码创建CacheStrategy缓存策略。

cacheCandidate会先从cache中根据请求获取缓存资源,如果存在直接赋值,如果不存在则为null。

调用CacheStrategyFactory()方法创建工厂,调用get()方法,通过工厂模式创建CacheStrategy对象。

Response cacheCandidate = cache != null
    ? cache.get(chain.request())
    : null;

CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

CacheStrategyFactory()方法中,如果cacheResponse不为空,即在客户端缓存的资源。
根据cacheResponse对象的响应头,获取跟缓存控制相关头部的值。

sentRequestMillis:表示上次客户端向服务器请求该资源的时间。receivedResponseMillis:表示上次客户端从服务端收到包含该资源响应的时间。servedDate:表示服务端返回资源生成响应报文的时间。expires:表示缓存资源过期时间。lastModified:表示缓存资源被服务器修改的时间。etag:表示缓存资源的实体标签。ageSeconds:表示缓存资源在代理服务器中存储的时间,如果没有代理服务器值接近为0。

public Factory(long nowMillis, Request request, Response cacheResponse) {
  this.nowMillis = nowMillis;
  this.request = request;
  this.cacheResponse = cacheResponse;
  if (cacheResponse != null) {
    this.sentRequestMillis = cacheResponse.sentRequestAtMillis();
    this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis();
    Headers headers = cacheResponse.headers();
    for (int i = 0, size = headers.size(); i < size; i++) {
      String fieldName = headers.name(i);
      String value = headers.value(i);
      if ("Date".equalsIgnoreCase(fieldName)) {
        servedDate = HttpDate.parse(value);
        servedDateString = value;
      } else if ("Expires".equalsIgnoreCase(fieldName)) {
        expires = HttpDate.parse(value);
      } else if ("Last-Modified".equalsIgnoreCase(fieldName)) {
        lastModified = HttpDate.parse(value);
        lastModifiedString = value;
      } else if ("ETag".equalsIgnoreCase(fieldName)) {
        etag = value;
      } else if ("Age".equalsIgnoreCase(fieldName)) {
        ageSeconds = HttpHeaders.parseSeconds(value, -1);
      }
    }
  }
}

调用Factory中的get方法获取并创建缓存策略对象CacheStrategy
通过调用getCandidate()方法根据上面获取的缓存字段确定不同的缓存策略,返回对应CacheStrategy对象。

public CacheStrategy get() {
  CacheStrategy candidate = getCandidate();
  if (candidate.networkRequest != null && request.cacheControl().onlyIfCached()) {

    return new CacheStrategy(null, null);
  }
  return candidate;
}

getCandidate()代码比较复杂,需要分步来看

第一部分代码,如果在以下情况下,缓存策略为不使用缓存资源,向服务器请求最新资源,返回CacheStrategy(request, null)

  1. 如果客户端没该请求下的缓存资源。
  2. 如果请求为HTTPS,但是缺少TLS/SSL的握手信息。
  3. 如果不能使用缓存。
  4. 如果使用缓存之前需要验证,但是请求头中的条件控制头部If-Modified-SinceIf-None-Match都为空。
private CacheStrategy getCandidate() {
  if (cacheResponse == null) {
    return new CacheStrategy(request, null);
  }

  if (request.isHttps() && cacheResponse.handshake() == null) {
    return new CacheStrategy(request, null);
  }

  if (!isCacheable(cacheResponse, request)) {
    return new CacheStrategy(request, null);
  }
  CacheControl requestCaching = request.cacheControl();
  if (requestCaching.noCache() || hasConditions(request)) {
    return new CacheStrategy(request, null);
  }
}

第二部分代码如下所示,如果缓存不需要向服务器验证且在使用时间范围内,则可以使用缓存,返回CacheStrategy(null, cacheResponse)

CacheControl保存就是HTTP协议中的头部字段Cache-Control中的值。

ageMillis:表示缓存资源产生的时间,从服务器产生资源返回给客户端起,包括停留在代理服务器的时间、传输花费的时间、客户端存储的时间。freshMillis:表示缓存资源可以存储的时间,从服务器产生资源开始算,超过这个时间段,则会过期,相当于保质期。minFreshMillis:表示请求头要求缓存资源在指定时间内不能过期maxStaleMillis:表示请求头要求缓存如果过期了,还能使用的时间。

然后通过判断,如果缓存资源使用不要向服务器验证,通过公式ageMillis + minFreshMillis < freshMillis + maxStaleMillis判断是否使用缓存。
就是判断缓存是否过期了,如果过期了就不使用,没过期则使用。

举个例子,ageMillis=10,minFreshMillis=3, freshMillis = 20, maxStaleMillis = 2。
缓存已经产生了10秒,客户端要求缓存资源在3秒后也不能过期。
缓存过期时间为20秒,并且如果过期了,在2秒内还能使用。

所以即使3秒后,缓存也才存在了13秒,没有超过缓存的使用期限22秒,所以客户端可以使用缓存。

private CacheStrategy getCandidate() {
  CacheControl responseCaching = cacheResponse.cacheControl();

  long ageMillis = cacheResponseAge();
  long freshMillis = computeFreshnessLifetime();
  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());
  }
}

第三部分,如果缓存需要向服务器验证是否可以使用,客户端需要构建条件请求头部。
等待服务器验证,验证通过则客户端使用服务器缓存,验证不通过服务器返回最新资源,返回CacheStrategy(conditionalRequest, cacheResponse)

conditionNameconditionValue为条件请求头部字段和对应值。
相关字段已经在HTTP协议缓存控制介绍了,最后构建对应的请求头。

String conditionName;
  String conditionValue;
  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);
  }
  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);

ConnectInterceptor

ConnectInterceptor负责和服务端建立连接,打开传输流。

拦截器中代码确实不长,建立连接的核心代码为HttpCodec httpCodec=streamAllocation.newStream(client, chain, doExtensiveHealthChecks);

HttpCodec从连接对象中获取输入流和输出流,负责对对HTTP请求报文进行编码写入输入流,从输出流获取字节进行HTTP解码获取响应报文。

最后将连接对象connection和http编解码对象httpCodec交给下一个拦截器处理。

下面我们将分析streamAllocation中的newStream方法。

@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);
  RealConnection connection = streamAllocation.connection();
  return realChain.proceed(request, streamAllocation, httpCodec, connection);
}

StreamAllocation对象中调用findHealthyConnection()findHealthyConnection()方法中进入一个死循环,调用findConnection()

如果连接池中可以复用当前请求连接,直接复用,并检查连接是否可用,如果可用直接返回,不可用继续寻找。

如果successCount=0,即连接还没有被使用过,是一个全新的可用连接直接返回。

下面分析findConnection()方法,了解其寻找并建立连接的思路。

 public HttpCodec newStream(
     OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
  ...
   boolean connectionRetryEnabled = client.retryOnConnectionFailure();
   try {
     RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
         writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
     HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
     synchronized (connectionPool) {
       codec = resultCodec;
       return resultCodec;
     }
   } catch (IOException e) {
     throw new RouteException(e);
   }
 }

 private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
    int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
    boolean doExtensiveHealthChecks) throws IOException {
    while (true) {
      RealConnection candidate = findConnection(connectTimeout, readTimeout, writeTimeout,
        pingIntervalMillis, connectionRetryEnabled);

      synchronized (connectionPool) {
        if (candidate.successCount == 0) {
          return candidate;
        }
      }

      if (!candidate.isHealthy(doExtensiveHealthChecks)) {
        noNewStreams();
        continue;
      }

      return candidate;
  }
}

findConnection代码很长,分为两步,复用已有的连接或建立新连接。

先看findConnection()方法中使用的局部变量

result:表示获取并返回的连接对象。selectedRoute:选择路由,网络数据包客户端->代理服务器->源服务器之间的传输路径,包含客户端、代理服务器、源服务器的InetSocketAddress对象,可以简单理解为把客户端、代理服务器和源服务器的IP地址连接起来,如同你要从福州到合肥,路径为福州->武夷山->黄山->合肥。releasedConnection:表示需要释放的连接对象。toClose:需要关闭的socket对象。

boolean foundPooledConnection = false;
RealConnection result = null;
Route selectedRoute = null;
Connection releasedConnection;
Socket toClose;

首先来如何复用已有的连接。

releasedConnection对象获取当前的连接对象。
调用releaseIfNoNewStreams()方法,如果当前连接存在没有被释放,并且不能创建新的连接流了,如HTTP2多个请求共用一个传输流,每个请求不能创建新流,则释放当前连接,返回对应套接字关闭。

如果当前连接可用,没有被释放,则作为当前请求的连接对象。
如果当前连接不可用,根据目标主机名从连接池中获取连接对象。

最终找到可用连接对象直接返回。

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
    int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {

    synchronized (connectionPool) {
      ...

      releasedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      if (this.connection != null) {
        result = this.connection;
        releasedConnection = null;
      }
      if (!reportedAcquired) {
        releasedConnection = null;
      }

      if (result == null) {
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
    }
    closeQuietly(toClose);

    ...

    if (result != null) {
      return result;
    }
}

第二部分,根据连接主机名无法从连接池获取缓存连接对象,那再根据路由从连接池中获取。路由保存了传输端点的IP地址,一个IP地址可以绑定多个主机名,所以有可能目标主机名虽然变化了但是目标IP地址没变,还可以使用缓存连接对象。

routeSelector为路由选择器,调用routeSelector.next()方法。
路由选择器使用DNS对象,获取到了目标服务器或代理服务器的所有IP地址。
根据目标服务器IP地址或代理服务器IP地址创建多个路由,表示客户端和目标服务器所有传输路径。
添加完毕之后,返回路由集合routeSelection

routeSelection中获取所有路由,依次遍历,根据每个路由从连接池中获取缓存连接对象。

如果找到,foundPooledConnection=true表示从连接池中找到连接对象,直接跳出循环,最终返回缓存连接对象。foundPooledConnection=false表示没有从连接池中找到连接对象,那么先创建一个新的连接对象,并且从路由集合中选择一个路由,建立TCP连接,传输数据包。

private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
      int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
    boolean newRouteSelection = false;
    if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
      newRouteSelection = true;
      routeSelection = routeSelector.next();
    }

    synchronized (connectionPool) {
      if (canceled) throw new IOException("Canceled");

      if (newRouteSelection) {
        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);
          if (connection != null) {
            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);
      return result;
    }
}

创建完新的连接对象,result为RealConnection类型,调用connect()方法和目标服务器建立TCP连接,如果为HTTPS建立TCS连接。
连接建立完毕,将连接对象添加到连接池中,如果HTTP协议版本为HTTP/2,则释放该连接,使用RealConnection中的Http2Connection连接对象。

最后返回连接对象。

result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
    connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
  reportedAcquired = true;

  Internal.instance.put(connectionPool, result);

  if (result.isMultiplexed()) {
    socket = Internal.instance.deduplicate(connectionPool, address, this);
    result = connection;
  }
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;

CallServerInterceptor

CallServerInterceptor主要将请求报文进行编码,添加到输出流中。

调用HttpCodec对象的writeRequestHeaders()方法,将请求头编码为一行字符串,添加到输出流中。
如果请求头部字段Expect100-continue,一般用于POST请求提交请求体。
客户端先发送请求, 包含一个Expect:100-continue, 询问服务端是否愿意接受数据,所以调用httpCodec.flushRequest();将输入流刷新,将请求头(不包含请求体)发送给服务端,获取响应构造器responseBuilder

如果不需要提前发送请求向服务器验证,如果有请求体,对请求体也进行编码,并添加到输出流中,最后刷新输出流,发送给服务端。

  @Override public Response intercept(Chain chain) throws IOException {
    ...

    realChain.eventListener().requestHeadersStart(realChain.call());
    httpCodec.writeRequestHeaders(request);
    realChain.eventListener().requestHeadersEnd(realChain.call(), request);

    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
      if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }

      if (responseBuilder == null) {
        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()) {
        streamAllocation.noNewStreams();
      }
    }

    httpCodec.finishRequest();

    // 处理响应
    ...
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值