揭秘网络框架第三篇: OkHttp 核心机制深入学习(彻底理解五个拦截器)

    • OkHttp 核心机制深入学习

OkHttp 核心机制深入学习

第一节基础部分我们了解到,HTTP 请求信息、响应信息中有很多是重复的,通信过程中会很多种异常情况,根据响应码的不同,也需要做不同的处理。

那么多情况要处理,放到一两个类里想必会十分冗余,这个时候就是表演技术的时候了, OkHttp 采用了一种清晰、低耦合的分层责任链模式。

上一小节我们了解到, OkHttp 内置了 5 个拦截器,在每一个拦截器里,分别对请求信息和响应值做了处理,每一层只做当前相关的操作,这五个拦截器分别是:

揭秘网络框架第三篇: OkHttp 核心机制深入学习(彻底理解五个拦截器)

他们的作用分别如下:

  1. RetryAndFollowUpInterceptor :取消、失败重试、重定向
  2. BridgeInterceptor :把用户请求转换为 HTTP 请求;把 HTTP 响应转换为用户友好的响应
  3. CacheInterceptor :读写缓存、根据策略决定是否使用
  4. ConnectInterceptor :实现和服务器建立连接
  5. CallServerInterceptor :实现读写数据

掌握了这五个拦截器,我们就熟悉了 OkHttp 的核心,来挨个了解一下吧!

取消、失败重试、重定向

RetryAndFollowUpInterceptor 是内置拦截器中的第一个,也是我们接触最早的一个。在前面的 AsyncCall.execute() 方法中,通过拦截器链拿到响应值后,首先调用了 RetryAndFollowUpInterceptor.isCanceled() 方法判断当前请求是否取消:

//AsyncCall.execute()
@Override protected void execute() {
  boolean signalledCallback = false;
  try {
    Response response = getResponseWithInterceptorChain();
    if (retryAndFollowUpInterceptor.isCanceled()) {    //判断是否取消请求
      signalledCallback = true;
      responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
    } 
    //...
}

拦截器最核心的就是拦截方法 intecept(Chain) ,我们直接看 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 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释放连接
      streamAllocation.release();
      throw new IOException("Canceled");
    }

    Response response;
    boolean releaseConnection = true;
    try {
      response = realChain.proceed(request, streamAllocation, null, null);
      releaseConnection = false;    //请求过程中,只要发生未处理的异常,releaseConnection 就会为true,一旦变为true,就会将StreamAllocation释放掉
    } catch (RouteException e) {
      // 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) {
      // 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 {
      if (releaseConnection) {
        streamAllocation.streamFailed(null);
        streamAllocation.release();
      }
    }

    //关联前一个响应
    if (priorResponse != null) {
      response = response.newBuilder()
          .priorResponse(priorResponse.newBuilder()
                  .body(null)
                  .build())
          .build();
    }

    //根据 code 和 method 判断是否需要重定向请求
    Request followUp = followUpRequest(response, streamAllocation.route());

    if (followUp == null) {    //不需要重定向时直接返回结果
      if (!forWebSocket) {
        streamAllocation.release();
      }
      return response;
    }

    closeQuietly(response.body());

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

    //通常发生请求重定向时,url 地址将会有所不同
    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;
  }
}

可以看到, RetryAndFollowUpInterceptor 的拦截操作中做了这么几件事:

  1. 首先创建了流引用管理的类 StreamAllocation,这个类非常重要,我们后面介绍
  2. 然后在一个 while 循环中调用拦截器链的 proceed() 方法
  3. 拿到响应的过程中如果出现远端异常、IO 异常,就 continue 请求(即失败重试)
  4. 如果成功拿到响应,就释放当前连接
  5. 然后在 followUpRequest() 方法中判断是否需要重定向,是的话就再请求

当不需要重定向时, followUpRequest() 会返回空 。我们来看下 followUpRequest() 方法如何决定是否需要重定向:

private Request followUpRequest(Response userResponse, Route route) throws IOException {
  if (userResponse == null) throw new IllegalStateException();
  int responseCode = userResponse.code();

  final String method = userResponse.request().method();
  switch (responseCode) {    //根据响应码做相关操作
    case HTTP_PROXY_AUTH:    //407,代理服务器验证
      Proxy selectedProxy = route != null
          ? route.proxy()
          : client.proxy();
      if (selectedProxy.type() != Proxy.Type.HTTP) {
        throw new ProtocolException("Received HTTP_PROXY_AUTH (407) code while not using proxy");
      }
      return client.proxyAuthenticator().authenticate(route, userResponse);

    case HTTP_UNAUTHORIZED:    //401,未验证
      return client.authenticator().authenticate(route, userResponse);

    case HTTP_PERM_REDIRECT:
    case HTTP_TEMP_REDIRECT:
      // "If the 307 or 308 status code is received in response to a request other than GET
      // or HEAD, the user agent MUST NOT automatically redirect the request"
      if (!method.equals("GET") && !method.equals("HEAD")) {
        return null;
      }
      // fall-through
    case HTTP_MULT_CHOICE:
    case HTTP_MOVED_PERM:
    case HTTP_MOVED_TEMP:
    case HTTP_SEE_OTHER:
      // Does the client allow redirects?
      if (!client.followRedirects()) return null;

      String location = userResponse.header("Location");
      if (location == null) return null;
      HttpUrl url = userResponse.request().url().resolve(location);

      // Don't follow redirects to unsupported protocols.
      if (url == null) return null;

      // If configured, don't follow redirects between SSL and non-SSL.
      boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme());
      if (!sameScheme && !client.followSslRedirects()) return null;

      // Most redirects don't include a request body.
      Request.Builder requestBuilder = userResponse.request().newBuilder();
      if (HttpMethod.permitsRequestBody(method)) {
        final boolean maintainBody = HttpMethod.redirectsWithBody(method);
        if (HttpMethod.redirectsToGet(method)) {
          requestBuilder.method("GET", null);
        } else {
          RequestBody requestBody = maintainBody ? userResponse.request().body() : null;
          requestBuilder.method(method, requestBody);
        }
        if (!maintainBody) {
          requestBuilder.removeHeader("Transfer-Encoding");
          requestBuilder.removeHeader("Content-Length");
          requestBuilder.removeHeader("Content-Type");
        }
      }
      //...
      return requestBuilder.url(url).build();

    case HTTP_CLIENT_TIMEOUT:
      //...

      if (userResponse.priorResponse() != null
          && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
        // We attempted to retry and got another timeout. Give up.
        return null;
      }

      if (retryAfter(userResponse, 0) > 0) {
        return null;
      }

      return userResponse.request();

    case HTTP_UNAVAILABLE:
      if (userResponse.priorResponse() != null
          && userResponse.priorResponse().code() == HTTP_UNAVAILABLE) {
        // We attempted to retry and got another timeout. Give up.
        return null;
      }

      if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {
        // specifically received an instruction to retry without delay
        return userResponse.request();
      }

      return null;

    default:
      return null;
  }
}

在第一章我们介绍了一些比较冷门的响应码,这里就见到用处了。

followUpRequest() 根据响应码做了这些事:

  • 如果 code 是 401 or 407 会调用我们构造 OkHttpClient 时传入的 Authenticator ,做鉴权处理操作,返回处理后的结果
  • 如果 code 是 307 or 308 重定向,且方法不是 GET 或 HEAD,就返回 null
  • 如果 code 是 300、301、302、303,且构造 OkHttpClient 时设置允许重定向,就从当前响应头中取出 Location 即新地址,然后构造一个新的 Request 再请求一次
  • 如果 code 是 408 超时,且上一次没有超时,就再请求一次
  • 如果 code 是 503 服务不可用,且上一次不是 503,就再请求一次

如果要处理 401 或者 407,我们就要在构造 OkHttpClient 时传入自定义的 Authenticator ,比如这样:

private OkHttpClient getOkHttpClient() {
        return new OkHttpClient.Builder()
                .authenticator(new Authenticator() {
                    @Nullable
                    @Override
                    public Request authenticate(final Route route, final Response response) throws IOException {
                        //这里根据响应做一些鉴权、新请求构造操作
                        return null;
                    }
                })
                .build();
    }

followUpRequest() 方法结束之后,如果返回不为 null,说明要重新请求,就会把和这次请求地址不同的连接释放掉,创建新连接。

在这里我们频繁看到 StreamAllocation ,它主要用于管理客户端与服务器之间的连接,同时管理连接池,以及请求成功后的连接释放等操作,我们讲连接拦截器时介绍。

OK,至此我们了解了第一个拦截器 RetryAndFollowUpInterceptor ,小结一下它做的事:

  1. 创建一个 StreamAllocation
  2. 发起请求
  3. 请求异常时会重试
  4. 根据响应码做鉴权、重定向和重试
  5. 重定向时如果地址不一致会释放连接
  6. 另外也保存是否取消的状态值,在重试、请求得到响应后都会判断是否取消

请求、响应转换

内置的拦截器中第二个是 BridgeInterceptor 。Bridge,桥,什么桥?连接用户请求信息和 HTTP 请求的桥梁。

BridgeInterceptor 负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应。

来看下它的拦截方法:

// 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) {    //根据请求体添加 header
    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) {    //添加 Host header
    requestBuilder.header("Host", hostHeader(userRequest.url(), false));
  }

  if (userRequest.header("Connection") == null) {    //添加连接信息 header
    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()) {    //从本地加载 cookie 信息
    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);

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

可以看到就是在请求前添加一下 header,请求后做完解压缩处理等,然后移除一些 header。

我们记录一下都具体添加了哪些信息吧,请求前:

  • 如果这个请求有请求体,就添加 Content-Type , Content-Length
  • 如果这个请求没有 Host ,就通过 url 来获取 Host 值添加到 Header 中
  • 如果这个请求没有接收的数据类型 Accept-Encoding ,且没指定接收的数据范围,就添加默认接受格式为 gzip
  • CookieJar 中根据 url 查询 Cookie 添加到 Header
  • 如果当前没有,就添加 User-Agent 信息

发起请求后:

  • 解析响应 Header 中的 Cookie
  • 如果想要数据的格式是 gzip,就创建 GzipSource 进行解压,同时移除 Content-EncodingContent-Length

这个拦截器做的很简单,用一个图就可以描述它的功能:

揭秘网络框架第三篇: OkHttp 核心机制深入学习(彻底理解五个拦截器)

缓存处理

揭秘网络框架第三篇: OkHttp 核心机制深入学习(彻底理解五个拦截器)

第三个拦截器是缓存处理拦截器 CacheInterceptor ,它的重要性用一句话来描述: 快的请求就是不请求,直接用缓存。

缓存用得好,响应快的不得了,但是如果一不小心用错了缓存,会导致在对的时间遇到错的数据,遗憾终生。

这一节我们来看看 OkHttp 的缓存处理是如何做的,不过在这之前先再补补一些基础知识。

HTTP 缓存策略

在 HTTP 协议中,定义了一些缓存相关的 Header:

  • Cache-Control
  • Etag , If-None_match
  • LastModified , If-Modified-Since
  • Expired

首先看下 Cache-Control ,即缓存策略,它的值关系到客户端是否使用缓存,请求和响应分别有这些值:

  • 请求:
    • no-cache:不使用缓存的实体,直接从服务器去取
    • only-if-cached:表示不进行与网络相关的交互,只返回已经缓存且 满足要求 的数据,否则的话返回 504 错误。
    • max-age:缓存可以使用的最大时间
    • max-stale:已过期的缓存在多少时间内仍可以继续使用(类似保质期),可以接受过去的对象,但是过期时间必须小于 max-stale 值
    • min-fresh:最小新鲜度,只接受其新鲜生命期大于当前 Age 跟 min-fresh 值之和的缓存对象
  • 响应:
    • public:可以用 Cached 内容回应任何用户
    • private:只能用缓存内容回应先前请求该内容的那个用户
    • no-cache:缓存服务器不能对资源进行缓存
    • no-store:不缓存
    • max-age:本响应包含的对象的过期时间

不同于拦截器设置缓存,CacheControl 是针对 Request 的,所以它可以针对每个请求设置不同的缓存策略。

剩下的那些缓存相关 Header 使用规则如下图所示:

揭秘网络框架第三篇: OkHttp 核心机制深入学习(彻底理解五个拦截器)

HTTP 定义的规范,首先会根据 CacheControl 来判断是否使用缓存,如果使用缓存,就去判断当前缓存是否新鲜,是否新鲜这样判断:

  1. 如果缓存信息里有 Etag ,就向服务器发送带 If-None-Match 的请求,服务器进行决策
  2. 如果没有 Etag 就看有没有 Last-Modified ,有的话向服务器发送带 If-Modified-Since 的请求,由服务器进行决策

服务器验证缓存有效性后,如果缓存仍可以使用,就返回 304;如果 code 不是 304,客户端就需要从响应里拿数据,同时更新缓存。

服务器返回的响应头里可能有 Expired ,这个值表示当前响应将在什么时候过期,对于过期了的对象,只有在跟服务器验证了其有效性后,才能用来响应客户请求。例如: Expires:Sat, 23 May 2009 10:02:12 GMT

OkHttp 的缓存策略

在基本了解 HTTP 协议中定义的缓存策略后,我们来看看 OkHttp 的缓存拦截器是如何实现的:

public final class CacheInterceptor implements Interceptor {
  final InternalCache cache;

  public CacheInterceptor(InternalCache cache) {
    this.cache = cache;
  }

  @Override public Response intercept(Chain chain) throws IOException {
    Response cacheCandidate = cache != null
        ? cache.get(chain.request())
        : null;        //先去缓存拿,拿到了用不用得看情况
    //...
    return response;
  }

首先可以看到的是,缓存拦截器会先去 InternalCache 里找有没有缓存。

这个 InternalCache 唯一的实现是在 OkHttp.Cache 中,它就是一个包装类,还是调用的 OkHttp.Cache 的方法,我们直接看 Cache.get() 方法:

public final class Cache implements Closeable, Flushable {
  private static final int VERSION = 201105;
  private static final int ENTRY_METADATA = 0;
  private static final int ENTRY_BODY = 1;
  private static final int ENTRY_COUNT = 2;

  final InternalCache internalCache = new InternalCache() {
    @Override public Response get(Request request) throws IOException {
      return Cache.this.get(request);
    }
    //...
  };

  final DiskLruCache cache;
  public Cache(File directory, long maxSize) {
    this(directory, maxSize, FileSystem.SYSTEM);
  }

  Cache(File directory, long maxSize, FileSystem fileSystem) {
    this.cache = DiskLruCache.create(fileSystem, directory, VERSION, ENTRY_COUNT, maxSize);
  }

  public static String key(HttpUrl url) {
    return ByteString.encodeUtf8(url.toString()).md5().hex();
  }

  @Nullable Response get(Request request) {
    String key = key(request.url());
    DiskLruCache.Snapshot snapshot;
    Entry entry;

    snapshot = cache.get(key);
    if (snapshot == null) {
        return null;
    }
    //...
    entry = new Entry(snapshot.getSource(ENTRY_METADATA));
    Response response = entry.response(snapshot);

    //...
    return response;
  }

从上面的代码可以看到的是,OkHttp 中的缓存使用的是基于文件系统的磁盘缓存,缓存的 key 是 url 的 md 值。

由于调用方可以 针对某个请求是否要使用缓存进行配置 (通过给 Request 配置 CacheControl 属性),比如这样:

Request request = new Request.Builder()
       .cacheControl(new CacheControl.Builder().noCache().build())
       .url("http://publicobject.com/helloworld.txt")
       .build();

因此我们从 cache 拿到缓存响应后,还需要做这几件事:

  1. 看当前调用方允不允许使用缓存(判断 request.cacheControl() 的值)
  2. 允许使用缓存的话,验证这个缓存还有没有效

CacheInterceptor 中,是通过 CacheStrategy 来判断缓存能否使用的。

CacheInterceptor.intercept() 中我们可以看到,去 cache 里拿到缓存响应后,接着又调用了 CacheStrategy

//CacheInterceptor.intercept()
@Override public Response intercept(Chain chain) throws IOException {
  Response cacheCandidate = cache != null
      ? cache.get(chain.request())
      : null;        //去缓存拿缓存响应

  //判断缓存能否使用
  CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();    /
  Request networkRequest = strategy.networkRequest;
  Response cacheResponse = strategy.cacheResponse;
    //...
}

这个 CacheStrategy 如何判断缓存能否使用的呢?我们通过一张图来解释:

揭秘网络框架第三篇: OkHttp 核心机制深入学习(彻底理解五个拦截器)

如图所示, CacheStrategy 的工厂方法构造需要两个参数:请求信息和拿到的缓存响应。

//两个参数:请求信息和拿到的缓存响应
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get();

在其内部,它会根据用户对当前请求设置的 CacheControl 和缓存响应的时间、ETag 、 LastModified 或者 ServedDate 等 Header 进行判断,最后输出两个值 :

//加工后拿到两个值,根据这两个值得情况决定是请求网络还是直接返回缓存
Request networkRequest = strategy.networkRequest;
Response cacheResponse = strategy.cacheResponse;

根据这两个值是否为空的四种情况,有不同的处理,分别如下:

1. networkRequestcacheResponse 都是空,表示调用端要求只用缓存,但缓存不可用了,只好返回 504 响应

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

504 Gateway Timeout

作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应。

2. networkRequest 为空,但 cacheResponse 不为空,就直接返回缓存响应

// If we don't need the network, we're done.
if (networkRequest == null) {
  return cacheResponse.newBuilder()
      .cacheResponse(stripBody(cacheResponse))
      .build();
}

3. networkRequest 不为空,表示不用缓存或者缓存有效性需要验证,这时就需要请求网络了

  • 如果 cacheResponse 不为空,且请求的响应码是 304,表示缓存还可以用,就直接返回 cacheResponse **
  • 否则就返回网络请求的响应

下面的代码就是这种情况的逻辑:

“`

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());

}

}

原文 

https://xiaozhuanlan.com/topic/8453126709

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

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

转载请注明原文出处:Harries Blog™ » 揭秘网络框架第三篇: OkHttp 核心机制深入学习(彻底理解五个拦截器)

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

评论 0

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