OkHttp(四) – 核心拦截器

前面分析了okhtt底层请求代码,了解到请求的处理是通过拦截器链来进行的。框架总共提供了5个核心的拦截器,每个拦截器都有其特定的功能,后面将会逐个分析。除此之外我们还可以在系统拦截器之前或之后扩展自己的拦截器,,下图所示为拦截器工作链条:

OkHttp(四) - 核心拦截器

用户自定义拦截器,只需实现intercept方法,并在其中调用chain的proceed方法即可。下面将按照请求顺序重点介绍框架提供的拦截器

1. RetryAndFollowUpInterceptor

此拦截器提供两个功能:

  • 错误恢复
  • 重定向

这两个功能是在intercept方法中得以体现的,该方法中有一个while(true)的循环。当后续的请求发生异常会调用类中的recover方法,recover中依据一定规则,如是否是fatal异常,是否配置了失败重试等来决定是否重试;当返回的响应是重定向,则会调用followUpRequest方法,生成重定向请求,再重新发起请求。无论错误恢复还是重定向实际都是通过循环来实现的。下面贴出intercept方法中的关键代码:

while (true) {
      try {
       //执行请求
        response = ((RealInterceptorChain) chain).proceed(request, streamAllocation, null, null);
        releaseConnection = false;
      } catch (RouteException e) {
        // The attempt to connect via a route failed. The request will not have been sent.
        //如果能够恢复则重试,否则抛出异常
        if (!recover(e.getLastConnectException(), false, request)) {
          throw e.getLastConnectException();
        }
        releaseConnection = false;
        continue;
      } catch (IOException e) {
        // An attempt to communicate with a server failed. The request may have been sent.
        boolean requestSendStarted = !(e instanceof ConnectionShutdownException);
        //如果能够恢复则重试,否则抛出异常
        if (!recover(e, requestSendStarted, request)) throw e;
        releaseConnection = false;
        continue;
      } finally {
        ......
      }
      ......
      //判断响应是否有重定向,如果有则重定向
      Request followUp = followUpRequest(response);

      if (followUp == null) {
        if (!forWebSocket) {
          streamAllocation.release();
        }
        return response;
      }
      ......
      request = followUp;
      priorResponse = response;
    }
复制代码

2. BridgeInterceptor

此拦截器主要的主要有两个:

  • 请求头处理(添加一些默认的请求等)
  • 响应及响应头处理(添加默认的响应头及响应体处理)

此拦截器中的intercept方法比较简单,就是设置一些默认的请求头或响应头等,就不单独说了,看代码便可一目了然。

@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");
    }
    //设置cookie
    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());

    HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers());

    Response.Builder responseBuilder = networkResponse.newBuilder()
        .request(userRequest);
    //解析gzip压缩
    if (transparentGzip
        && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding"))
        && HttpHeaders.hasBody(networkResponse)) {
      GzipSource responseBody = new GzipSource(networkResponse.body().source());
      Headers strippedHeaders = networkResponse.headers().newBuilder()
          .removeAll("Content-Encoding")
          .removeAll("Content-Length")
          .build();
      responseBuilder.headers(strippedHeaders);
      responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody)));
    }

    return responseBuilder.build();
  }

复制代码

3. CacheInterceptor

从名字中就可以看出此拦截器是提供缓存相关功能的。okhttp底层的缓存采用的是lru算法,具体的实现类是okhttp3.internal.cache.DiskLruCache,这里就不具体展开说了。拦截器根据请求头中或响应头中的有关缓存的设置来决定缓存策略。具体的实现稍显繁琐,但理解起来并不复杂,具体参考其代码。

@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 (cache != null) {
     cache.trackResponse(strategy);
   }

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

   // 如果不需要网络请求,而缓存又不存在,则返回一个504的失败响应
   if (networkRequest == null && cacheResponse == null) {
     return new Response.Builder()
         .request(chain.request())
         .protocol(Protocol.HTTP_1_1)
         .code(504)
         .message("Unsatisfiable Request (only-if-cached)")
         .body(Util.EMPTY_RESPONSE)
         .sentRequestAtMillis(-1L)
         .receivedResponseAtMillis(System.currentTimeMillis())
         .build();
   }

   // 如果不需要网络请求,返回缓存的响应
   if (networkRequest == null) {
     return cacheResponse.newBuilder()
         .cacheResponse(stripBody(cacheResponse))
         .build();
   }
   
   //不走缓存,执行网络请求
   Response networkResponse = null;
   try {
   //将请求传到到下一个拦截器进行处理
     networkResponse = chain.proceed(networkRequest);
   } finally {
     // If we are crashing on I/O or otherwise, do not leak the cache body.
     if (networkResponse == null && cacheCandidate != null) {
       closeQuietly(cacheCandidate.body());
     }
   }

   if (cacheResponse != null) {
     //如果返回的响应代码为HTTP_NOT_MODIFIED,则从缓存中提取内容返回
     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();
       //更新缓存相关内容
       cache.trackConditionalCacheHit();
       cache.update(cacheResponse, response);
       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);
       return cacheWritingResponse(cacheRequest, response);
     }
     
     if (HttpMethod.invalidatesCache(networkRequest.method())) {
     //缓存失效
       try {
         cache.remove(networkRequest);
       } catch (IOException ignored) {
         // The cache cannot be written.
       }
     }
   }

   return response;
 }
复制代码

4. ConnectInterceptor

此拦截器负责与远程服务之间建立连接,来看看具体的代码,虽然拦截方法代码不多,但是方法的调用链却非常深:

@Override public Response intercept(Chain chain) throws IOException {
    RealInterceptorChain realChain = (RealInterceptorChain) chain;
    Request request = realChain.request();
    //返回的streamAllocation对象是在RetryAndFollowUpInterceptor拦截器中创建的
    //streamAllocation = new StreamAllocation(client.connectionPool(), createAddress(request.url()), callStackTrace);
    StreamAllocation streamAllocation = realChain.streamAllocation();

    // We need the network to satisfy this request. Possibly for validating a conditional GET.
    boolean doExtensiveHealthChecks = !request.method().equals("GET");
    //在newStream建立socket连接,返回HttpCodec对象,此对象用于对流的解析
    HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
    RealConnection connection = streamAllocation.connection();

    return realChain.proceed(request, streamAllocation, httpCodec, connection);
  }
复制代码

关键注释都已在代码中给出,这里就不具体解释了。网络请求可以抽象为底层的连接即:connection;在connection之上进行数据通信,可以把交换的数据抽象为流,即stream;在上层,将我们的发起的请求调用抽象为call。StreamAllocation对象就是负责协调管理这三者之间的关系。代码中还出现了一个对象:HttpCodec,此对象用于对流进行解析。streamAllocation创建了一条流就等效于与远程服务建立了一条通信链路,我们可以在这条链路上进行数据通信。深入newStram()方法:

public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
    int connectTimeout = client.connectTimeoutMillis();
    int readTimeout = client.readTimeoutMillis();
    int writeTimeout = client.writeTimeoutMillis();
    boolean connectionRetryEnabled = client.retryOnConnectionFailure();

    try {
      //与远程请求地址通过socket建立连接
      RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
          writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
      HttpCodec resultCodec = resultConnection.newCodec(client, this);

      synchronized (connectionPool) {
        codec = resultCodec;
        return resultCodec;
      }
    } catch (IOException e) {
      throw new RouteException(e);
    }
  }
复制代码

具体的连接建立过程就是在此方法中进行的,此过程比较简单就不深入跟踪,这里给出时序图,如下图所示。总之最后还是通过socket与远程的请求建立连接。

OkHttp(四) - 核心拦截器

5. CallServerInterceptor

连接建立好后,就要进行数据通信了,此拦截器的作用就是发送请求数据并从服务端获取响应数据。几行代码胜过千言万语:

@Override public Response intercept(Chain chain) throws IOException {
    ......
    Request request = realChain.request();
    //发送请求头
    httpCodec.writeRequestHeaders(request);

    Response.Builder responseBuilder = null;
    if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {
       ......
      if (responseBuilder == null) {
        // Write the request body if the "Expect: 100-continue" expectation was met.
        //创建请求体sink,也就是将请求体写入到一个缓冲buffer中
        Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
        BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);
        //将请求体中的内容写入BufferedSink
        request.body().writeTo(bufferedRequestBody);
        //将内容写到远程请求端
        bufferedRequestBody.close();
      } 
      ......
    }
    //完成请求发送
    httpCodec.finishRequest();

    //获取响应头
    if (responseBuilder == null) {
      responseBuilder = httpCodec.readResponseHeaders(false);
    }
    ......
    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))
          .build();
    }
    ......
    return response;
  }
复制代码

上面的代码将数据通信的核心代码提炼了出来,可以发现通信过程为:发送请求头–>发送请求体(如果有) –> 获取响应头 –> 获取响应体。

总结

至此我们就将okhttp中发送请求到获取响应的流程分析完了。各个拦截器各司其职,每个都有自己独立需要完成的功能,通过调用链模式组合在一起,降低了耦合性并具有很好的扩展性,此设计值得学习和借鉴。

原文 

https://juejin.im/post/5d463e5fe51d4561ef6c0a64

本站部分文章源于互联网,本着传播知识、有益学习和研究的目的进行的转载,为网友免费提供。如有著作权人或出版方提出异议,本站将立即删除。如果您对文章转载有任何疑问请告之我们,以便我们及时纠正。

PS:推荐一个微信公众号: askHarries 或者qq群:474807195,里面会分享一些资深架构师录制的视频录像:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系。还能领取免费的学习资源,目前受益良多

转载请注明原文出处:Harries Blog™ » OkHttp(四) – 核心拦截器

赞 (0)
分享到:更多 ()

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址