OkHttp3源码解析(二)五大拦截器原理分析

OkHttp3源码解析(一)分发器Dispatcher原理分析
OkHttp3源码解析(二)五大拦截器原理分析

从上一篇文章我们知道,

  • 同步请求,会执行
 @Override
    public Response execute() throws IOException {
        synchronized (this) {
            if (executed) throw new IllegalStateException("Already Executed");
            executed = true;
        }
        captureCallStackTrace();
        eventListener.callStart(this);
        try {
            client.dispatcher().executed(this);
            Response result = getResponseWithInterceptorChain();
            if (result == null) throw new IOException("Canceled");
            return result;
        } catch (IOException e) {
            eventListener.callFailed(this, e);
            throw e;
        } finally {
            client.dispatcher().finished(this);
        }
    }

  • 异步请求,会执行
 @Override
        protected void execute() {
            boolean signalledCallback = false;
            try {
                Response response = getResponseWithInterceptorChain();
                if (retryAndFollowUpInterceptor.isCanceled()) {
                    signalledCallback = true;
                    responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
                } else {
                    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 {
                    eventListener.callFailed(RealCall.this, e);
                    responseCallback.onFailure(RealCall.this, e);
                }
            } finally {
                client.dispatcher().finished(this);
            }
        }

它们里面有一句关键代码 Response response = getResponseWithInterceptorChain();,返回的响应结果都是通过getResponseWithInterceptorChain返回的。

1、拦截器责任链

OkHttp最核心的工作是在getResponseWithInterceptorChain()中进行,在进入这个方法分析之前,我们先来了解什么是责任链模式,因为此方法就是利用的责任链模式完成一步步的请求。

责任链顾名思义就是由一系列的负责者构成的一个链条,类似于工厂流水线。

2、责任链模式

在责任链模式中,每一个对象和其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。

3、OkHttp里的拦截器责任链

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

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

        return chain.proceed(originalRequest);
    }

一开始创建了一个List,往List里加入了7个拦截器,先看client.interceptors()和client.networkInterceptors(),这里的client就是OkHttpClient,一般和OkHttpClient相关的都是能自定义的或者更改的,这两个就是用户可以自己定义的两个拦截器,这两个拦截器一个是在拦截刚刚开始的时候,一个是在快结束的时候,自定义的拦截器我们先不关注,我们重点放在其他5个拦截器上面。我们先看下这5大拦截器的执行流程


image.png

当用户发起一次网络请求,一次Request的时候,依次会走下面5个拦截器

  • 1、retryAndFollowUpInterceptor
  • 2、BridgeInterceptor
  • 3、CacheInterceptor
  • 4、ConnectInterceptor
  • 5、CallServerInterceptor
    一层一层往下传递Request处理,CallServerInterceptor拦截器是负责和服务器通讯的,请求处理完之后,CallServerInterceptor拦截器会首先接收到响应,再一层一层往上传递Response,把响应结果传回到用户手里。在OkHttp中,这5大拦截器的执行流程就是使用到了责任链模式。用户不需要关心责任链里到底干了什么,只需要把Request传入到责任链里,当网络请求返回结果的时候,责任链会把Response返回给用户。

那么这个责任链在OkHttp里到底是怎么定义和实现的呢?

image.png

首先,有一个Chain接口,请求任务交给Chain即可

public interface Interceptor {
Response intercept(Chain chain) throws IOException;

interface Chain {
  Request request();

  Response proceed(Request request) throws IOException;

Chain接口是在Interceptor接口里定义的,这个Chain接口有个实现类RealInterceptorChain

 public final class RealInterceptorChain implements Interceptor.Chain {
 private final List<Interceptor> interceptors;
 private final StreamAllocation streamAllocation;
 private final HttpCodec httpCodec;
 private final RealConnection connection;
 private final int index;
 private final Request request;
 private final Call call;
 private final EventListener eventListener;
 private final int connectTimeout;
 private final int readTimeout;
 private final int writeTimeout;
 private int calls;

 public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation,
     HttpCodec httpCodec, RealConnection connection, int index, Request request, Call call,
     EventListener eventListener, int connectTimeout, int readTimeout, int writeTimeout) {
   this.interceptors = interceptors;
   this.connection = connection;
   this.streamAllocation = streamAllocation;
   this.httpCodec = httpCodec;
   this.index = index;
   this.request = request;
   this.call = call;
   this.eventListener = eventListener;
   this.connectTimeout = connectTimeout;
   this.readTimeout = readTimeout;
   this.writeTimeout = writeTimeout;
 }

在RealInterceptorChain类中有个集合 List<Interceptor> interceptors,存储拦截器,我们知道
Interceptor本身也是一个接口,Chain接口就是在Interceptor接口里定义的,这个interceptors集合存储了5大拦截器的实现类,


image.png

当我们执行整个拦截器责任链的时候,就会从这个interceptors集合中取出对应的拦截器去执行。标志各个拦截器对应位置的就是在RealInterceptorChain里的index这个成员变量。
我们来看看这个index是怎么改变拦截器的位置的

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

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

      return chain.proceed(originalRequest);
  }

还是这段代码,当我们把所有的拦截器放入到interceptors集合之后,会把这个集合传入给链条RealInterceptorChain,我们看到RealInterceptorChain的第四个参数是0,这个0就是index的初始数据,也就是一开始是从集合的第0个元素开始执行的。最后return chain.proceed(originalRequest);开始执行链条,所以我们看看是怎么执行的

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,

 ......

  // 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);
  Response response = interceptor.intercept(next);


  ......
  return response;
}

我们看到proceed方法里的关键代码会把index+1,传入到RealInterceptorChain里,新建一个链条next,先从interceptors里拿出当前index的链条interceptor,然后调用当前链条的intercept(next)方法,把新链条传入,我们知道链条里的第一个是RetryAndFollowUpInterceptor,我们看看RetryAndFollowUpInterceptor里的intercept方法是怎么实现的。

   @Override
  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;

      int followUpCount = 0;
      Response priorResponse = null;
      while (true) {
          if (canceled) {
              streamAllocation.release();
              throw new IOException("Canceled");
          }

          Response response;
          boolean releaseConnection = true;
          try {
              //todo 请求出现了异常,那么releaseConnection依旧为true。
              response = realChain.proceed(request, streamAllocation, null, null);
              releaseConnection = false;
          } catch (RouteException e) {
              //todo 路由异常,连接未成功,请求还没发出去
              //The attempt to connect via a route failed. The request will not have been sent.
              if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
                  throw e.getLastConnectException();
              }
              releaseConnection = false;
              continue;
          } catch (IOException e) {
              //todo 请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
              // ConnectionShutdownException只对HTTP2存在。假定它就是false
              //An attempt to communicate with a server failed. The request may have been sent.
              boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
              if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
              releaseConnection = false;
              continue;
          } finally {
              // We're throwing an unchecked exception. Release any resources.
              //todo 不是前两种的失败,那直接关闭清理所有资源
              if (releaseConnection) {
                  streamAllocation.streamFailed(null);
                  streamAllocation.release();
              }
          }
          //todo 如果进过重试/重定向才成功的,则在本次响应中记录上次响应的情况
          //Attach the prior response if it exists. Such responses never have a body.
          if (priorResponse != null) {
              response = response.newBuilder()
                      .priorResponse(
                              priorResponse.newBuilder()
                                      .body(null)
                                      .build()
                      )
                      .build();
          }
          //todo 处理3和4xx的一些状态码,如301 302重定向
          Request followUp = followUpRequest(response, streamAllocation.route());
          if (followUp == null) {
              if (!forWebSocket) {
                  streamAllocation.release();
              }
              return response;
          }

          closeQuietly(response.body());

          //todo 限制最大 followup 次数为20次
          if (++followUpCount > MAX_FOLLOW_UPS) {
              streamAllocation.release();
              throw new ProtocolException("Too many follow-up requests: " + followUpCount);
          }

          if (followUp.body() instanceof UnrepeatableRequestBody) {
              streamAllocation.release();
              throw new HttpRetryException("Cannot retry streamed HTTP body", response.code());
          }
          //todo 判断是不是可以复用同一份连接
          if (!sameConnection(response, followUp.url())) {
              streamAllocation.release();
              streamAllocation = new StreamAllocation(client.connectionPool(),
                      createAddress(followUp.url()), call, eventListener, callStackTrace);
              this.streamAllocation = streamAllocation;
          } else if (streamAllocation.codec() != null) {
              throw new IllegalStateException("Closing the body of " + response
                      + " didn't close its backing stream. Bad interceptor?");
          }

          request = followUp;
          priorResponse = response;
      }
  }

我们先直接看关键代码response = realChain.proceed(request, streamAllocation, null, null);,我们知道此时realChain的index是1了,这时候又执行链条的proceed方法,就调用到了下标是1的拦截器了,然后又开始了前面的流程,这样就实现了0,1,2,3...,责任链里每一个拦截器的执行。到此,我们整个责任链的执行流程已经通了,我们接下来看看每个拦截器到底是怎么工作的。

5大拦截器介绍及源码分析

  • 1、retryAndFollowUpInterceptor:重试拦截器在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。

  • 2、BridgeInterceptor:桥接拦截器在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的行为(如:GZIP压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。

  • 3、CacheInterceptor:缓存拦截器顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。

  • 4、ConnectInterceptor:连接拦截器在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后不进行额外的处理。

  • 5、CallServerInterceptor:请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。

1、retryAndFollowUpInterceptor 重试与重定向拦截器

  • 怎么判断是否可以进行重试呢?

我们直接进入到拦截器RetryAndFollowUpInterceptor的intercept方法里

@Override
  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;

      int followUpCount = 0;
      Response priorResponse = null;
      while (true) {
          if (canceled) {
              streamAllocation.release();
              throw new IOException("Canceled");
          }

          Response response;
          boolean releaseConnection = true;
          try {
              //todo 请求出现了异常,那么releaseConnection依旧为true。
              response = realChain.proceed(request, streamAllocation, null, null);
              releaseConnection = false;
          } catch (RouteException e) {
              //todo 路由异常,连接未成功,请求还没发出去
              //The attempt to connect via a route failed. The request will not have been sent.
              if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {
                  throw e.getLastConnectException();
              }
              releaseConnection = false;
              continue;
          } catch (IOException e) {
              //todo 请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)
              // ConnectionShutdownException只对HTTP2存在。假定它就是false
              //An attempt to communicate with a server failed. The request may have been sent.
              boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
              if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;
              releaseConnection = false;
              continue;
          } finally {
              // We're throwing an unchecked exception. Release any resources.
              //todo 不是前两种的失败,那直接关闭清理所有资源
              if (releaseConnection) {
                  streamAllocation.streamFailed(null);
                  streamAllocation.release();
              }
          }
          //todo 如果进过重试/重定向才成功的,则在本次响应中记录上次响应的情况
          //Attach the prior response if it exists. Such responses never have a body.
          if (priorResponse != null) {
              response = response.newBuilder()
                      .priorResponse(
                              priorResponse.newBuilder()
                                      .body(null)
                                      .build()
                      )
                      .build();
          }
  }

我们直接看到关键代码while死循环的try-catch代码块里,一开始执行response = realChain.proceed,将我们的请求一层一层发给后面4个拦截器,在这个执行过程中可能会发生异常,
我们看到有两种异常RouteException(路由异常)和IOException(IO异常),RouteException可以简单的理解客户端和服务端在建立连接的时候发生异常,导致连接不成功,请求没发出去;IOException表示连接成功了,请求发出去了,但是和服务器通信失败了(Socket流读写数据的时候失败了,例如超时了)。如果发生以上这两种异常的话就会被catch住,然后调用recover方法去判断是否去重试,如果判断不去重试就会直接抛出异常;如果判断满足重试条件的话就会调用到continue方法,跑出catch代码块,继续去执行try里面的方法realChain.proceed又去重新执行一遍后面的拦截器,全部去重新执行一遍。所以我们进入recover方法看看是怎么判断是否去重试的

   private boolean recover(IOException e, StreamAllocation streamAllocation,
                          boolean requestSendStarted, Request userRequest) {
      streamAllocation.streamFailed(e);

      //todo 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试
      //The application layer has forbidden retries.
      if (!client.retryOnConnectionFailure()) return false;

      //todo 2、由于requestSendStarted只在http2的io异常中为true,先不管http2
      //We can't send the request body again.
      if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)
          return false;

      //todo 3、判断是不是属于重试的异常
      //This exception is fatal.
      if (!isRecoverable(e, requestSendStarted)) return false;

      //todo 4、不存在更多的路由
      //No more routes to attempt.
      if (!streamAllocation.hasMoreRoutes()) return false;

      // For failure recovery, use the same route selector with a new connection.
      return true;
  }

一开始先去判断1、OKhttpClient是不是设置了允许重试(默认是允许重试的),
如果你设置了不允许重试,直接返回false,然后进入第二个判断,因为第二个判断只在http2中,我们先不管,第三个判断这个异常是不是属于可以重试的异常,具体在isRecoverable方法里进行(稍后讲),最后判断存在不存在更多的路由,如果不存在返回false,路由的意思就是DNS解析域名的时候可能会返回多个ip地址,如果第一个ip地址不满足,再试下一个,以此类推。现在我们进入isRecoverable看看那些异常是属于可以重试的异常。

private boolean isRecoverable(IOException e, boolean requestSendStarted) {
      // If there was a protocol problem, don't recover.
      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).
      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.
      if (e instanceof SSLHandshakeException) {
          // If the problem was a CertificateException from the X509TrustManager,
          // do not retry.
          if (e.getCause() instanceof CertificateException) {
              return false;
          }
      }
      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;
  }

这里四个if就是依次判断1、不是协议异常。2、不是Socket超时异常。3、SSL证书没有问题。4、SSL证书校验没有问题。如果满足这四个条件,就是属于可以重试的异常。属于网络通信的基础知识这里就不展开讲了。到这里是否去重试的源码解读完了,接下来看看什么时候去重定向。

  • 怎么判断去重定向的呢?
    如果前面的代码执行完,没有去重试或者请求成功了,就会继续执行intercept后面的代码,调用followUpRequest判断需不需要去重定向,如果followUpRequest返回一个重定向的Request,会去请求重定向的Request,如果不需要重定向,就直接返回response,
 //todo 处理3和4xx的一些状态码,如301 302重定向
          Request followUp = followUpRequest(response, streamAllocation.route());
          if (followUp == null) {
              if (!forWebSocket) {
                  streamAllocation.release();
              }
              return response;
          }

followUpRequest方法里就是各种响应码的判断我就不一一解释了,都是属于HTTP的基础知识,我这里给出一张表格说明下


image.png

需要注意的一个点就是重定向最大的限制是20次,如果你大于20次就不帮我们重试了。

 //todo 限制最大 followup 次数为20次
            if (++followUpCount > MAX_FOLLOW_UPS) {
                streamAllocation.release();
                throw new ProtocolException("Too many follow-up requests: " + followUpCount);
            }

到这里重试和重定向拦截器所有的流程都讲完了

2、BridgeInterceptor 桥接拦截器

BridgeInterceptor,连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设置请求内容长度,编码,gzip压缩,cookie等,获取响应后保存Cookie等操作。这个拦截器相对比较简单。

补全请求头:

请求头 说明
Content-Type 请求体类型,如:application/x-www-form-urlencoded
Content-Length/Transfer-Encoding 请求体解析方式
Host 请求的主机站点
Connection: Keep-Alive 保持长连接
Accept-Encoding: gzip 接受响应支持gzip压缩
Cookie cookie身份辨别
User-Agent 请求的用户信息,如:操作系统、浏览器等

在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:

1、保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的CookieJar不提供实现

2、如果使用gzip返回的数据,则使用GzipSource包装便于解析。

总结

桥接拦截器的执行逻辑主要就是以下几点

对用户构建的Request进行添加或者删除相关头部信息,以转化成能够真正进行网络请求的Request
将符合网络请求规范的Request交给下一个拦截器处理,并获取Response
如果响应体经过了GZIP压缩,那就需要解压,再构建成用户可用的Response并返回

3、CacheInterceptor 缓存拦截器

CacheInterceptor,在发出请求前,判断是否存在缓存。如果存在则可以不请求,直接使用缓存的响应。 (只会存在Get请求的缓存)

 @Override
    public Response intercept(Chain chain) throws IOException {
        //todo 通过url的md5数据 从文件缓存查找 (GET请求才有缓存)
        Response cacheCandidate = cache != null
                ? cache.get(chain.request())
                : null;

        long now = System.currentTimeMillis();

        //todo 缓存策略:根据各种条件(请求头)组成 请求与缓存
        CacheStrategy strategy =
                new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();
        Request networkRequest = strategy.networkRequest;
        Response cacheResponse = strategy.cacheResponse;

        if (cache != null) {
            cache.trackResponse(strategy);
        }

        if (cacheCandidate != null && cacheResponse == null) {
            closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
        }

        //todo 没有网络请求也没有缓存
        //If we're forbidden from using the network and the cache is insufficient, fail.
        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();
        }

        //todo 没有请求,肯定就要使用缓存
        //If we don't need the network, we're done.
        if (networkRequest == null) {
            return cacheResponse.newBuilder()
                    .cacheResponse(stripBody(cacheResponse))
                    .build();
        }

        //todo 去发起请求
        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.
        if (cacheResponse != null) {
            //todo 服务器返回304无修改,那就使用缓存的响应修改了时间等数据后作为本次请求的响应
            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());
            }
        }

        //todo 走到这里说明缓存不可用 那就使用网络的响应
        Response response = networkResponse.newBuilder()
                .cacheResponse(stripBody(cacheResponse))
                .networkResponse(stripBody(networkResponse))
                .build();
        //todo 进行缓存
        if (cache != null) {
            if (HttpHeaders.hasBody(response) && CacheStrategy.isCacheable(response,
                    networkRequest)) {
                // Offer this request to the cache.
                CacheRequest cacheRequest = cache.put(response);
                return cacheWritingResponse(cacheRequest, response);
            }

            if (HttpMethod.invalidatesCache(networkRequest.method())) {
                try {
                    cache.remove(networkRequest);
                } catch (IOException ignored) {
                    // The cache cannot be written.
                }
            }
        }

        return response;
    }

步骤为:

1、从缓存中获得对应请求的响应缓存

2、创建CacheStrategy ,创建时会判断是否能够使用缓存,在CacheStrategy 中存在两个成员:networkRequestcacheResponse。他们的组合如下:

networkRequest cacheResponse 说明
Null Not Null 直接使用缓存
Not Null Null 向服务器发起请求
Null Null okhttp直接返回504
Not Null Not Null 发起请求,若得到响应为304(无修改),则更新缓存响应并返回

3、交给下一个责任链继续处理

4、后续工作,返回304则用缓存的响应;否则使用网络响应并缓存本次响应(只缓存Get请求的响应)

拦截器中只需要判断CacheStrategynetworkRequestcacheResponse的不同组合就能够判断是否允许使用缓存。真正复杂的代码在CacheStrategy中,这里就不展开讲了,我直接把大概流程总结一下

  • 1、如果从缓存获取的Response是null,那就需要使用网络请求获取响应;
  • 2、如果是Https请求,但是又丢失了握手信息,那也不能使用缓存,需要进行网络请求;
  • 3、如果判断响应码不能缓存且响应头有no-store标识,那就需要进行网络请求;
  • 4、如果请求头有no-cache标识或者有If-Modified-Since/If-None-Match,那么需要进行网络请求;
  • 5、如果响应头没有no-cache标识,且缓存时间没有超过极限时间,那么可以使用缓存,不需要进行网络请求;
  • 6、如果缓存过期了,判断响应头是否设置Etag/Last-Modified/Date,没有那就直接使用网络请求否则需要考虑服务器返回304;

并且,只要需要进行网络请求,请求头中就不能包含only-if-cached,否则框架直接返回504!

image.png

缓存拦截器本身主要逻辑其实都在缓存策略中,拦截器本身逻辑非常简单,在发出请求前,判断是否存在缓存。如果存在则可以不请求;如果不存在则使用网络的响应。这里涉及到http的缓存机制的对比缓存原理,主要是判断是否有缓存和304的判断,我大致用流程图表示下,再不懂的同学自己去看http的原理和基础吧

Http缓存机制及原理
Http缓存机制及原理-无缓存

Http缓存机制及原理-有缓存

4、ConnectInterceptor 连接拦截器

image.png

在连接拦截器中,我们需要关心的有三个点:1、连接池 2、正常连接 3、代理连接。
我们知道,如果我们要发起一次http请求,我们肯定要建立一个socket连接,socket连接只要没有close掉就是可以重复使用的。如果说我们第一次与一台服务器建立了socket连接,这个socket允许我们保持长连接,没有被关闭,我们就要把这个连接保持进连接池中,如果下一次我们与同一台服务器建立连接的话,我们可以尝试直接从连接池中将这个连接拿出来,与这台服务器建立连接。这就是连接池的概念。其实就是用来复用socket连接的。下面,我们来看看连接池的定义。

public final class ConnectionPool {
  /**
   * Background threads are used to cleanup expired connections. There will be at most a single
   * thread running per connection pool. The thread pool executor permits the pool itself to be
   * garbage collected.
   */
  private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
      Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
      new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));

首先定义了一个线程池executor,这就不多说了,和上一篇文章里的几乎一样,可以理解成一个无限制的线程池就行了,每次都会新建线程执行新提交的任务。完全没有任何等待。我们再看到它的构造方法


public ConnectionPool() {
    this(5, 5, TimeUnit.MINUTES);
  }

  public ConnectionPool(int maxIdleConnections, long keepAliveDuration, TimeUnit timeUnit) {
    this.maxIdleConnections = maxIdleConnections;
    this.keepAliveDurationNs = timeUnit.toNanos(keepAliveDuration);

    // Put a floor on the keep alive duration, otherwise cleanup will spin loop.
    if (keepAliveDuration <= 0) {
      throw new IllegalArgumentException("keepAliveDuration <= 0: " + keepAliveDuration);
    }
  }

可以看到这个连接池默认最多保持5个空闲状态的连接,连接的默认保活时间为5分钟。如果我们想往连接池里面加入一个连接,我们只需要调用它的put方法,拿的话调用get方法,把adress传递进来,他会循环连接池里面所有的连接,连接池中是否存在和传进来的连接完全相同(dns/代理/域名/端口/证书等等),如果完全相同就可以复用,具体判断在isEligible方法里。

@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.isEligible(address, route)) {
        streamAllocation.acquire(connection, true);
        return connection;
      }
    }
    return null;
  }

下面我贴出ConnectInterceptor的代码

/** Opens a connection to the target server and proceeds to the next interceptor. */
public final class ConnectInterceptor implements Interceptor {
  public final OkHttpClient client;

  public ConnectInterceptor(OkHttpClient client) {
    this.client = client;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
}

虽然代码量很少,实际上大部分功能都封装到其它类去了,这里只是调用而已。

首先我们看到的StreamAllocation这个对象是在第一个拦截器:重定向拦截器创建的,但是真正使用的地方却在这里。

"当一个请求发出,需要建立连接,连接建立后需要使用流用来读写数据";而这个StreamAllocation就是协调请求、连接与数据流三者之间的关系,它负责为一次请求寻找连接,然后获得流来实现网络通信。

这里使用的newStream方法实际上就是去查找或者建立一个与请求主机有效的连接,返回的HttpCodec中包含了输入输出流,并且封装了对HTTP请求报文的编码与解码,直接使用它就能够与请求主机完成HTTP通信。

StreamAllocation中简单来说就是维护连接:RealConnection——封装了Socket与一个Socket连接池。

总结

这个拦截器中的所有实现都是为了获得一份与目标服务器的连接,在这个连接上进行HTTP数据的收发。

5、CallServerInterceptor 请求服务器拦截器

CallServerInterceptor,利用HttpCodec发出请求到服务器并且解析生成Response

首先调用httpCodec.writeRequestHeaders(request); 将请求头写入到缓存中(直到调用flushRequest()才真正发送给服务器)。然后马上进行第一个逻辑判断

  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();
                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都和一个请求头有关: Expect: 100-continue。这个请求头代表了在发送请求体之前需要和服务器确定是否愿意接受客户端发送的请求体。所以permitsRequestBody判断为是否会携带请求体的方式(POST),如果命中if,则会先给服务器发起一次查询是否愿意接收请求体,这时候如果服务器愿意会响应100(没有响应体,responseBuilder 即为nul)。这时候才能够继续发送剩余请求数据。

但是如果服务器不同意接受请求体,那么我们就需要标记该连接不能再被复用,调用noNewStreams()关闭相关的Socket。

后续代码为:

 if (responseBuilder == null) {
            realChain.eventListener().responseHeadersStart(realChain.call());
            responseBuilder = httpCodec.readResponseHeaders(false);
        }

        Response response = responseBuilder
                .request(request)
                .handshake(streamAllocation.connection().handshake())
                .sentRequestAtMillis(sentRequestMillis)
                .receivedResponseAtMillis(System.currentTimeMillis())
                .build();

这时responseBuilder的情况即为:

1、POST方式请求,请求头中包含Expect,服务器允许接受请求体,并且已经发出了请求体,responseBuilder为null;

2、POST方式请求,请求头中包含Expect,服务器不允许接受请求体,responseBuilder不为null

3、POST方式请求,未包含Expect,直接发出请求体,responseBuilder为null;

4、POST方式请求,没有请求体,responseBuilder为null;

5、GET方式请求,responseBuilder为null;

对应上面的5种情况,读取响应头并且组成响应Response,注意:此Response没有响应体。同时需要注意的是,如果服务器接受 Expect: 100-continue这是不是意味着我们发起了两次Request?那此时的响应头是第一次查询服务器是否支持接受请求体的,而不是真正的请求对应的结果响应。所以紧接着:

int code = response.code();
        if (code == 100) {
            // server sent a 100-continue even though we did not request one.
            // try again to read the actual response
            responseBuilder = httpCodec.readResponseHeaders(false);

            response = responseBuilder
                    .request(request)
                    .handshake(streamAllocation.connection().handshake())
                    .sentRequestAtMillis(sentRequestMillis)
                    .receivedResponseAtMillis(System.currentTimeMillis())
                    .build();

            code = response.code();
        }


如果响应是100,这代表了是请求Expect: 100-continue成功的响应,需要马上再次读取一份响应头,这才是真正的请求对应结果响应头。

然后收尾

  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))
                    .build();
        }

        if ("close".equalsIgnoreCase(response.request().header("Connection"))
                || "close".equalsIgnoreCase(response.header("Connection"))) {
            streamAllocation.noNewStreams();
        }

        if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
            throw new ProtocolException(
                    "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
        }

        return response;

forWebSocket代表websocket的请求,我们直接进入else,这里就是读取响应体数据。然后判断请求和服务器是不是都希望长连接,一旦有一方指明close,那么就需要关闭socket。而如果服务器返回204/205,一般情况而言不会存在这些返回码,但是一旦出现这意味着没有响应体,但是解析到的响应头中包含Content-Lenght且不为0,这表响应体的数据字节长度。此时出现了冲突,直接抛出协议异常!

总结

在这个拦截器中就是完成HTTP协议报文的封装与解析。

OkHttp总结

整个OkHttp功能的实现就在这五个默认的拦截器中,所以先理解拦截器模式的工作机制是先决条件。这五个拦截器分别为: 重试拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求服务拦截器。每一个拦截器负责的工作不一样,就好像工厂流水线,最终经过这五道工序,就完成了最终的产品。

但是与流水线不同的是,OkHttp中的拦截器每次发起请求都会在交给下一个拦截器之前干一些事情,在获得了结果之后又干一些事情。整个过程在请求向是顺序的,而响应向则是逆序。

当用户发起一个请求后,会由任务分发起Dispatcher将请求包装并交给重试拦截器处理。

1、重试拦截器在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。

2、桥接拦截器在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的行为(如:GZIP压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。

3、缓存拦截器顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。

4、连接拦截器在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后不进行额外的处理。

5、请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。

在经过了这一系列的流程后,就完成了一次HTTP请求!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容