一起学习Android开源框架&解析Okhttp的五种拦截器(第五部分)

711 阅读14分钟

这是我参与8月更文挑战的第5天,活动详情查看:8月更文挑战

上文我们已经分析了Okhttp的拦截器,那么下面我将和大家一起分析Okhttp系统内部的那些拦截器的具体作用,在责任链模式下,又会碰撞出什么火花呢?

五种拦截器源码分析

RetryAndFollowInterceptor重定向拦截器

从名字上可以看出就应该知道该拦截器的作用是进行失败重连的;打个比方,如果我们想要失败重连,就可以在OKhttpClient进行配置,但是有一点我们需要注意,不是所有的网络请求失败后都可以重连,有一定的限制范围的.因此,Okhttp内部就会进行检查网络请求异常和行为码判断,如果符合相应条件,才会去执行重连的,这也就是重定向拦截器存在的意义

下面我们看下它的源码,首先看下intercept方法

  1. 先看初始化部分
  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;
  • 这里创建了一个streamAllocation对象,这个其实用来建立执行http请求所需要那些网络的组件 ,分配stream流
  • 需要注意的是虽然streamAllocation对象已经创建好,但是并没有使用到,真正使用到的是ConnectInterceptor(后面会细说),主要是用于获取服务端Connection的连接和用于服务端进行数据传输的输入输出IO流,会依次通过拦截器链传递给ConnectInterceptor拦截器使用的

对此,还有个问题值得思考,我们知道在整个OkHttp拦截器链的过程中,可以理解为一个递归,内部会通过**RealInterceptorChain()**这个类负责所有拦截器并串联起来,所以说只有当所有拦截器执行完之后才会被返回Repsonse;但是在平时开发过程中,网络并不是一直稳定的,肯定会有不同程度的问题,有可能网络中断或者失败,那么response返回的code就不是正常的200了,已经出现异常了,那么就要用到RetryAndFollowInterceptor拦截器,它是怎么进行拦截的呢?

  1. 可以看下intercept方法中的while循环
 private static final int MAX_FOLLOW_UPS = 20;
while(true) {
	....
	if (++followUpCount > MAX_FOLLOW_UPS) {
        streamAllocation.release();
        throw new ProtocolException("Too many follow-up requests: " + followUpCount);
      }
}
  • 这里代码过多就只展示部分了,所有的逻辑都在这个while死循环当中,同学可以去详细看下,我就不多说了
  • 我们看下,这里面会对重试次数判断,也就是这里不可能无限制的去重试网络请求,这里MAX_FOLLOW_UPS的值设置为20,换而言之,如果okhttp内部超过了20次的重试请求之后释放它的straeamAllocation对象,当然也不会再去请求了,这也是这个拦截器的核心流程,它有个跳出的逻辑

RetryAndFollowInterceptor总结

  1. 创建StreamAllocation对象,用来建立Okhttp请求所需要所有的网络组件,用来分配Stream
  2. 调用RealInterceptorChain.proceed()方法进行实际的网络请求
  3. 根据异常结果或者响应结果判断是否要重新请求
  4. 调用下一个拦截器,对response进行处理,返回给上一个拦截器

BridgeInterceptor桥接拦截器

对于BridgeInterceptor桥接拦截器,它负责的主要内容就是设置内容长度,编码方式,压缩等等,主要就是添加头部

看下intercept方法

  1. 首先,初始化工作
 @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");
    }

    // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing
    // the transfer stream.
    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());
    }
  ....
}
  • 这里这么多行代码,其实主要作用就是给一个普通的request添加很多头部信息,成为一个可以发送网络请求的request
  • 具体来看,从代码当中,可以很清晰的看出,会从原来空的contentType添加请求头,比如说Content-Type,Content-Length,Transfer-Encoding,Host主机,Connection Keep-Alive(在一定时间内保持连接状态),这些代码所做到的就是初始化工作,添加header头部信息
  1. 转化可用的Repsonse
 Response networkResponse = chain.proceed(requestBuilder.build());
 HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());
  • 这里调用了RealInteceptor的proceed方法,向服务器发送一个请求,服务器获取请求操作后会返回客户端response响应
  • 接着调用HttpHeaders的静态方法receiveHeaders,将我们的网络请求和服务器返回给我们的Repsonse转化为用户可以使用的Respsonse
  1. 可以看下receiveHeaders的具体实现
  public static void receiveHeaders(CookieJar cookieJar, HttpUrl url, Headers headers) {
    if (cookieJar == CookieJar.NO_COOKIES) return;

    List<Cookie> cookies = Cookie.parseAll(url, headers);
    if (cookies.isEmpty()) return;

    cookieJar.saveFromResponse(url, cookies);
  }

其实这里内部主要就是使用CookieJar保存下响应报文的信息,其中HTTP的Cookie就是用来保存用户的一些信息的

  1. Content-Encoding支持gzip
Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);

    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      String contentType = networkResponse.header("Content-Type");
      responseBuilder.body(new RealResponseBody(contentType, -1L, Okio.buffer(responseBody)));
    }
     return responseBuilder.build();
  • 这里就去重新构建响应Repsonse
  • 接着利用transparentGzip可以让响应头Content-Encoding对Gzip格式进行支持,并且解压成用户可用的响应Repsonse,其中对于GzipSource就是以解压的方式去读取流数据
  • 最后将响应response返回,进行到下一个拦截器

BridgeInterceptor总结

  1. 是负责将用户构建的一个Request请求转化为能够进行网络访问的请求
  2. 将这个符合网络请求的Request进行网络请求
  3. 将网络请求回来的响应Response转化为用户可用的Response(Gzip压缩,Gzip解压)

CacheInterceptor缓存拦截器

该拦截器的主要负责工作在于从缓存中去拿取响应结果,如果没有的话,才会通过网络请求去获取响应

下面来看下Cache类的源码实现

在解析CacheInterceptor之前,我们首先来使用下Okhttp如何使用缓存的,其实使用起来非常简单,调用cache方法中的Cache类,传入文件目录和大小两个参数,这样就设置好了所需要的缓存路径,代码如下

 private fun cacheRequest() {
        val client = OkHttpClient.Builder()
            .cache(Cache(File("cache"), 24 * 1024 * 1024)).build()
        val request = Request.Builder().url("https://www.baidu.com")
            .get().build()
        
        val call = client.newCall(request)
       kotlin.runCatching {
           val response = call.execute()
           response.close()
       }.onFailure {
           println(it)
       }

    }
  1. 首先我们可用看到有个internalCache这个接口
final InternalCache internalCache = new InternalCache() {
    @Override public Response get(Request request) throws IOException {
      return Cache.this.get(request);
    }

      @Override public CacheRequest put(Response response) throws IOException {
      return Cache.this.put(response);
    }
    ....
  };

可以看到它里面所有的方法,无论是get,put也好,都是通过Cache类实现的

  1. 在看下put方法的具体实现
  @Nullable CacheRequest put(Response response) {
     .....
    if (HttpHeaders.hasVaryAll(response)) {
      return null;
    }

    Entry entry = new Entry(response);
    DiskLruCache.Editor editor = null;
    try {
      editor = cache.edit(key(response.request().url()));
      if (editor == null) {
        return null;
      }
      entry.writeTo(editor);
      return new CacheRequestImpl(editor);
    } catch (IOException e) {
      abortQuietly(editor);
      return null;
    }
  }
  • 这里会创建一个Entry实例,写入缓存的部分(里面包含了很多url,header,协议方法等等),已经封装了
  • 还有个非常值得注意的DiskLruCache,看到这里才明白,整个okhttpd的缓存做了那么多封装呀,最终还是交给了这个DiskLruCache缓存算法来实现的,这里暂不详细看了,但是需要了解的是Okhttp内部是维护了一个清理的线程池的,由它来实现对缓存的自动清理和管理
  • 定义好edit对象后,通过cache.edit方法,传入一个key值,这个值就是将我们网络的url转化为对应的key,里面做了md5的加密处理,然后得到md5的16进制表示形式
  • 在做好所有工作后,调用entry.writeTo()方法,就是将我们的缓存写入到磁盘上

在这里或许大家都有个疑问,这个writeTo方法传入的editor对象,而这个editor中的key只保存了url,那我们既然把请求的头部和响应的头部都保存好了,那么最关键响应的body我们在哪里保存了呢?

  1. 看下最后的return的CacheRequestImpl类
private final class CacheRequestImpl implements CacheRequest {
    private final DiskLruCache.Editor editor;
    private Sink cacheOut;
    private Sink body;
    boolean done;

    CacheRequestImpl(final DiskLruCache.Editor editor) {
      this.editor = editor;
      this.cacheOut = editor.newSink(ENTRY_BODY);
      this.body = new ForwardingSink(cacheOut) {
        ....
      };
    }
    ...
  • 可以看到内部有个body,这就是我们的响应主体,同时这里还有一个editor,这里通过DiskLruCache.Editor来书写我们的body
  • 这里CaheRequestImpl实现了个CacheRequest接口,这就是暴露给CacheInterceptor缓存拦截器的,可以直接根据这个CacheRqeustImpl这个实现类来更新和写入缓存数据

CacheInterceptor的创建传入的client.internalCache()方法的返回值,可以看下internalCache方法

 InternalCache internalCache() {
    return cache != null ? cache.internalCache : internalCache;
  }

这里主要就是如果在OkhttpClient.Builder创建的时候传入了Cache参数或者InternalCache参数,那么设置internalCache和Cache会置为null,也就是相互抵消了,如果都没有传入相关的设置,它们默认都为null,具体可看如下代码

  void setInternalCache(@Nullable InternalCache internalCache) {
      this.internalCache = internalCache;
      this.cache = null;
    }

    /** Sets the response cache to be used to read and write cached responses. */
    public Builder cache(@Nullable Cache cache) {
      this.cache = cache;
      this.internalCache = null;
      return this;
    }

看下CacheInterceptor的Intercept方法

  1. 获取缓存策略
  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 (cache != null) {
      cache.trackResponse(strategy);
    }
  • 这里一开始就去获取候选的缓存,如果没有的返回null,根据这个结果去设置CacheStrategy缓存策略,这里有networkRequest和cacheResponse两个对象,分别就是网络请求和返回的缓存响应,接下来就是对相关条件进行筛选判断,正式步入缓存拦截器的负责工作
  1. 一系列的条件判断逻辑
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();
    }
  • 如果networkRequest为null同时cacheResponse为null,即代表网络请求和缓存都没有的时候,直接会返回504错误的Response响应码
 if (networkRequest == null) {
      return cacheResponse.newBuilder()
          .cacheResponse(stripBody(cacheResponse))
          .build();
    }
  • 如果网络请求为null,不允许网络请求的时候,但是有缓存,这里直接返回缓存的响应
 if (cacheResponse != null) {
      if (networkResponse.code() == HTTP_NOT_MODIFIED) {
        Response response = cacheResponse.newBuilder()
            .headers(combine(cacheResponse.headers(), networkResponse.headers()))
            .sentRequestAtMillis(networkResponse.sentRequestAtMillis())
            .receivedResponseAtMillis(networkResponse.receivedResponseAtMillis())
            .cacheResponse(stripBody(cacheResponse))
            .networkResponse(stripBody(networkResponse))
            .build();
        networkResponse.body().close();

        // Update the cache after combining headers but before stripping the
        // Content-Encoding header (as performed by initContentStream()).
        cache.trackConditionalCacheHit();
        cache.update(cacheResponse, response);
        return response;
      } else {
        closeQuietly(cacheResponse.body());
      }
    }

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.
        }
      }
    }
  • 如果networkRequest不为空的时候,表示需要使用网络,则会时候就会调用拦截器链剩下的拦截器继续处理得到networkResponse响应
  • 如果这时候缓存响应存在的话,并且有效的话,就需要更新缓存,反之,没有缓存响应的话,直接写入缓存就可以了

ConnectInterceptor连接拦截器

该拦截器主要的工作在于获取一个目标请求的连接,正式开启网络请求

直接看它的intercept方法

  @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对象就是前面在重定向拦截器传入的,然后再创建了HTTPCodec和RealConnection对象
  • 最后我们会调用拦截器链的proceed方法,之前我们已经非常熟悉了,这个时候就已经设置好了整个网络连接拦截器

这里有个疑问这个HTTPCodec到底是什么东西呢?我们来具体看下它的代码

HttpCodec

可以看到它是对HTTP协议的抽象实现, Http1Codec和 Http2Codec实现了这个接口,对应的是HTTP1.1/HTTP2两个版本的实现

public interface HttpCodec {
  int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
  ...
}

  • 在HttP1Codec代码中实现了对java.io和java.nio的封装,对此,它对Socket进行了封装
  • 主要是用来编码我们的request,以及解码我们的repsonse

看下newStream方法

下面代码先后创建了RealConnection和HttpCodec对象,然后用了一个同步代码块返回Httpcodec对象

 public HttpCodec newStream(
      OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
    int connectTimeout = chain.connectTimeoutMillis();
    int readTimeout = chain.readTimeoutMillis();
    int writeTimeout = chain.writeTimeoutMillis();
    int pingIntervalMillis = client.pingIntervalMillis();
    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);
    }
  }

这里总的来说就做了两件事情

  • 调用了findHelathyConnection方法生成RealConnection对象,进行实际的网络连接
  • 接着通过realConnection来生成HttpCodec对象,然后在同步代码块中返回
  1. 看下findHealthyConnection方法
 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;
      }

  • 可以看到这里又调用一个findConnection方法来获取RealConnection
  • 同步代码块中可以看到如果这个RealConnection的successCount为0的时候,意味着整个网络连接就结束了
  • 如果是不健康的连接(例如Socket连接没有关闭,输入输出流没有关闭)就会调用noNewStreams()方法,并且循环执行findConnection()方法
  1. 看下findConnection()方法

由于findConnection()方法过于冗长,这里只展示部分核心代码

      releasedConnection = this.connection;
      toClose = releaseIfNoNewStreams();
      if (this.connection != null) {
        result = this.connection;
        releasedConnection = null;
      }
  • 尝试复用这个connection给releaseconnection的变量
  • 去判断这个可复用的连接是否为空,如果不为空就将这个connection赋值给我们的result

如果不能复用的时候,这个时候result是空的

 if (result == null) {
        Internal.instance.get(connectionPool, address, this, null);
        if (connection != null) {
          foundPooledConnection = true;
          result = connection;
        } else {
          selectedRoute = route;
        }
      }
  • 这个时候会从连接池获取一个实际的realconnection,将它赋值给我们的result
    result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
        connectionRetryEnabled, call, eventListener);
     Internal.instance.put(connectionPool, result);
  • 获取realConnection后,调用connect来进行实际的网络连接,然后需要将它反正连接池中

简单来说,findConnection()会去尝试去获取connection,能复用就去复用,不能的话就从连接池当中获取新的connection来进行连接,最后需要把这个新的connection放入连接池当中

ConnectInterceptor总结

  1. 弄一个RealConnection
  2. 选择不同的连接方式
  3. ConnectInterceptor获取Inteceptor传过来的StreamAllocation,streamAllocation.newStream()
  4. 将刚才创建的用于网络IO的RealConnection对象,以及对于与服务器交互中最为关键的HttpCodec等对象传递给后面的拦截器
  5. 最后调用CallServerInterceptor来完成整个okhttp网络请求操作

CallServerInterceptor调用服务拦截器

这个拦截器主要是负责向我们的服务器发起一个真正的网络请求,然后接收服务器返回的读取响应,最后再返回

intercept()方法

  1. 定义了五个对象
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    HttpCodec httpCodec = realChain.httpStream();
    StreamAllocation streamAllocation = realChain.streamAllocation();
    RealConnection connection = (RealConnection) realChain.connection();
    Request request = realChain.request();
  • RealInterceptorChain: 拦截器链,所有拦截器都要通过这个拦截器链链接在一起,然后完成响应的功能
  • HttpCodec:包括Htpp1.1协议和Http1.2协议(本文主要讲的是Http1.1),编码request和解码response
  • StreamAllocation:建立http请求所需要的所有网络组件,分配Stream
  • RealConnection:把客户端与服务器之间的连接已经抽象成了connection,这个就是抽象连接的具体实现
  • Request:网络请求
  1. 核心部分
    realChain.eventListener().requestHeadersStart(realChain.call());
    httpCodec.writeRequestHeaders(request);
    realChain.eventListener().requestHeadersEnd(realChain.call(), request);
  • 这里httpCodec调用了writeRequestHeaders()方法,并且传入我们的网络请求,目的就是向socket写入请求的头部信息
  1. 处理特殊情况
if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {
        httpCodec.flushRequest();
        realChain.eventListener().responseHeadersStart(realChain.call());
        responseBuilder = httpCodec.readResponseHeaders(true);
      }
  • 这里的判断就是询问服务器是否发送带有请求体的信息,如果该请求头部可以添加Expect,以及100-continue,那么就会返回给我们一个100的响应码,客户端继续发送请求,跳过写入body,直接获取响应信息,正常情况不一定会走
  1. 写入body信息 正常情况下,如果没有上述情况下,会走以下流程
request.body().writeTo(bufferedRequestBody);
...
httpCodec.finishRequest();
  • 调用body中的writeTo()方法向socket当中写入body信息
  • httpCodec的finishRequest()方法调用表明了完成了http请求的写入工作
  1. 请求读取相应工作
if (responseBuilder == null) {
      realChain.eventListener().responseHeadersStart(realChain.call());
      responseBuilder = httpCodec.readResponseHeaders(false);
    }

 ...
 
  if (forWebSocket && code == 101) {
      response = response.newBuilder()
          .body(Util.EMPTY_RESPONSE)
          .build();
    } else {
      //建造者模式创建了一个response
      response = response.newBuilder()
          .body(httpCodec.openResponseBody(response))
          .build();
    }  

      streamAllocation.noNewStreams();
  • 调用readResponseHeaders方法读取http请求的头部信息
  • 调用noNewStreams()就是禁止新的流创建,关闭IO流,关闭connection

最后检查一下,code响应码是否是204或者205,如果是的话会抛出一个异常

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

至此,我们完成了整个repsonse的获取工作

尾言

至此okhttp的源码解析就告一段落了,简单梳理下整个okhttp中一次网络请求的大致过程

  1. Call对象对请求的封装
  2. dispatcher对请求的分发
  3. getResponseWithInterceptors()方法,拦截器链的使用,五个拦截器的链式调用,层层递进
  • RetryAndFollowInterceptor
  • BridgeInterceptor
  • CacheInterceptor
  • ConnectInterceptor
  • CallServerInterceptor

okhttp还有很多内容值得去深究,这里就不过多解析了,接下来我将开始分析Retrofit,同是网络框架,相对于Okhttp它又会有怎样的不同呢?持续学习ing....