彻底掌握网络通信(十九)走进OkHttp3的世界(四)拦截器深入分析

彻底掌握网络通信(一)Http协议基础知识
彻底掌握网络通信(二)Apache的HttpClient基础知识
彻底掌握网络通信(三)Android源码中HttpClient的在不同版本的使用
彻底掌握网络通信(四)Android源码中HttpClient的发送框架解析
彻底掌握网络通信(五)DefaultRequestDirector解析
彻底掌握网络通信(六)HttpRequestRetryHandler解析
彻底掌握网络通信(七)ConnectionReuseStrategy,ConnectionKeepAliveStrategy解析
彻底掌握网络通信(八)AsyncHttpClient源码解读
彻底掌握网络通信(九)AsyncHttpClient为什么无法用Fiddler来抓包
彻底掌握网络通信(十)AsyncHttpClient如何发送JSON解析JSON,以及一些其他用法
彻底掌握网络通信(十一)HttpURLConnection进行网络请求的知识准备
彻底掌握网络通信(十二)HttpURLConnection进行网络请求概览
彻底掌握网络通信(十三)HttpURLConnection进行网络请求深度分析
彻底掌握网络通信(十四)HttpURLConnection进行网络请求深度分析二:缓存
彻底掌握网络通信(十五)HttpURLConnection进行网络请求深度分析三:发送与接收详解
彻底掌握网络通信(十六)走进OkHttp3的世界(一)引言
彻底掌握网络通信(十七)走进OkHttp3的世界(二)请求/响应流程分析
彻底掌握网络通信(十八)走进OkHttp3的世界(三)详解Http请求的连接,发送和响应

  一个Http请求是由拦截器组顺序执行,并冒泡返回给上层调用者;在这个过程中,参加的拦截器主要有:

  1. RetryAndFollowUpInterceptor
  2. BridgeInterceptor
  3. CacheInterceptor
  4. ConnectInterceptor
  5. CallServerInterceptor

针对每一个拦截器的大致作用,我们在前面的分析已经有了大致的了解;这篇我们主要深入了解下各个拦截器以及http背后的知识


RetryAndFollowUpInterceptor 详解

  1. 主要作用
    这个拦截器主要作用是从一个失败的连接中恢复并对某些连接进行重定向

  2. 在Http中如何表示一个重定向连接
    通过Location字段表示

  3. 核心代码分析

 @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 {
        response = realChain.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(), streamAllocation, false, request)) {
          throw e.getFirstConnectException();
        }
        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 {
        // We're throwing an unchecked exception. Release any resources.
        if (releaseConnection) {
          streamAllocation.streamFailed(null);
          streamAllocation.release();
        }
      }

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

      Request followUp;
      try {
        followUp = followUpRequest(response, streamAllocation.route());
      } catch (IOException e) {
        streamAllocation.release();
        throw e;
      }

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

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

第1行:获取发送的请求request
第2行:构建RealInterceptorChain,通过该主链可以找到下一个拦截器
第7行:传入request,call等参数构建 StreamAllocation
第11行:初始化重定向次数为0
第12行:设置一个Response变量priorResponse,因为某些请求需要重定向,顾我们需要对上一次的响应保存,来构建一个新的response来进行解析
第13行:开启死循环,对Response进行判断,如果需要重定向则重新发起连接请求
第14行:以一个请求被cancel之后,会抛出IOException的异常
第19行:构建Response变量response,用于保存当前请求的响应
第22行:让下一个拦截器开始执行,下一个拦截器返回response返回给RetryAndFollowUpInterceptor,并最终返回给上层调用者
第26行:当一个连接发生异常的时候,我们将尝试重新连接,recover方法返回false,表示不可恢复该连接,如果返回true,表示可恢复该连接;比如有些致命的异常是不可恢复的,如ProtocolException,SSLHandshakeException等
第39行:当一个请求被正常执行的时候,releaseConnection是为false的,顾此段代码不会被执行
第46行:第一次执行的时候,priorResponse为null,但是如果一个请求为重定向请求,则会将当前重定向请求的相应保存在priorResponse
第47行:根据上一次重定向返回的响应重新构建response
第54行:构建一个Request变量followUp,该变量表示一个重定向的连接
第56行:根据followUpRequest方法获得的response来判断该请求的响应能否创建一个重定向的请求,如果followUpRequest返回null,则表示该请求不需要重定向;举例:一个响应头中状态码为308,307,300,303等的时候,如果okhttpclient设置了不允许重定向,则followUpRequest方法返回为null,如果响应头中没有Location字段,则返回为null … …
   308:状态码表示:这个请求和以后的请求都应该被另一个URI地址重新发送
   307:状态码表示:这也是一个重定向的状态码,对于get请求,则继续发送请求,对于post请求的重定向,则不继续,需要用户确认
   303:状态码表示:临时重定向,发送Post请求,收到303,直接重定向为get,发送get请求,不需要向用户确认
   300:状态码表示:被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向

第58行:当出现异常的时候,关闭socket连接,释放资源
第62~第66行:一个请求如果最终被执行,则返回当前response给上层
第71~74行:当重定向次数大于20次,则关闭socket,释放资源并抛出ProtocolException异常
第81~89行:重新创建StreamAllocation,用于重定向再次发起请求.


BridgeInterceptor 详解

  1. 该类主要作用有两个
    第一个是对上层的http请求做补全处理,如补充头信息
    第二个是对返回的响应做处理,然后返回给RetryAndFollowUpInterceptor,接着返回给上层

  2. 核心代码

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

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

第9行:  添加Content-Type头信息
第14行: 添加Content-Length头信息
第15行: 移除Transfer-Encoding头信息,Transfer-Encoding含义为分块编码,表达方式为Transfer-Encoding: chunked,允许客户端请求分成多块发送给服务器或者服务器响应分成多块发送给客户端。分块传输编码只在HTTP协议1.1版本(HTTP/1.1)中提供。数据分解成一系列数据块,并以一个或多个块发送,这样服务器可以发送数据而不需要预先知道发送内容的总大小。因为我们提供了Content-Length字段可以准确判断传输大小,顾此处就不需要Transfer-Encoding头信息了
第35行 : 添加Accept-Encoding头信息,说明客户端支持gzip类型编码
第47行: 通过下一个拦截器的执行,获得http请求的响应networkResponse
第49行: 通过响应头信息,将cookie保存至cookieJar;Cookie总是保存在客户端中,可以理解为一个键值对,客户端发送cookie给服务器的时候,只是发送对应的名称和值,如Cookie: name=value; name2=value2;从服务器端发送cookie给客户端,是对应的Set-Cookie。包括了对应的cookie的名称,值,以及各个属性。如Set-Cookie: made_write_conn=1295214458; Path=/; Domain=.169it.com;在receiveHeaders方法中,我们应该注意到,如果

cookieJar == CookieJar.NO_COOKIES

则我们不会将服务端响应中的cookie进行保存;同时在OkHttpClient.java中,cookieJar 默认为CookieJar.NO_COOKIES,顾默认情况下,客户端是不会有保存cookie的操作的;我们可以通过如下代码来设置客户端的cookie的存储机制

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .cookieJar(new CookieJar() {
                    @Override
                    public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
                      
                    }

                    @Override
                    public List<Cookie> loadForRequest(HttpUrl url) {
                                    
                    }
                })
                .build();

ok,我们继续分析BridgeInterceptor的核心代码

第55行:如果服务端的响应中,Content-Encoding内容编码格式如果gzip,则将响应转为GzipSource格式;其中GzipSource的包名为

package okio

这里我们简单将GzipSource理解为将服务端响应解压到GzipSource当中即可;

Accept-Encoding :描述的是客户端接收的编码格式
Content-Encoding:描述的是正文的编码格式,目的是优化传输,例如用 gzip 压缩文本文件,能大幅减小体积。内容编码通常是选择性的,例如 jpg / png 这类文件一般不开启,因为图片格式已经是高度压缩过的,再压一遍没什么效果不说还浪费 CPU。
流程如下:
1:客户端发送请求时,通过Accept-Encoding带上自己支持的内容编码格式列表;
2:服务端在接收到请求后,从中挑选出一种用来对响应信息进行编码,并通过Content-Encoding来说3:明服务端选定的编码信息
4:客户端在拿到响应正文后,依据Content-Encoding进行解压。服务端也可以返回未压缩的正文,但这种情况不允许返回Content-Encoding

第67行:返回response给上层

BridgeInterceptor 中值得学习的地方

  1. 如果服务端响应中Content-Encoding为gzip,客户端会使用okio来解压缩正文

CacheInterceptor 详解

  1. 缓存当中If-Modified-Since和Last-Modified解释
       Last-Modified:是服务端返回给客户单的一个头字段,用来描述改请求内容的最后修改时间;
       If-Modified-Since:是客户端发送给服务端的一个头字段,用来表示从这个时间段开始,被请求内容是否发生变化
    具体流程为
       1)客户端第一次请求服务端页面,服务端响应报文中有Last-Modified表示正文最后修改时间,此处假设为:2019年3月10日10:05:100
        2)客户端第二次请求服务端页面,客户端带上请求字段If-Modified-Since,此处假设为2019年3月10日10:05:100
        3)服务端根据If-Modified-Since字段的值,并对比本身Last-Modified的值,如果大于If-Modified-Since,则说明正文已经被修改,则返回200给客户端;如果小于If-Modified-Since,则说明正文未被修改,返回304给客户端;

  2. 如何设置缓存

 OkHttpClient client = new OkHttpClient.Builder()
                .cache(new Cache(new File("填入缓存地址"),1024*1024))
                .build();
  1. 在介绍CacheInterceptor核心代码之前,我们先看下缓存拦截器涉及到的一个重要类:Cache.java
    如上面的代码,我们通过.cache的方法,创建了一个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);
  }

第六行:在创建Cache实例的同时,我们也创建了一个DiskLruCache的实例,简单说下DiskLruCache,DiskLruCache常用方法如下

方法含义
DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)打开一个缓存目录,如果没有则首先创建它,directory:指定数据缓存地址; appVersion:APP版本号,当版本号改变时,缓存数据会被清除; maxSize:最大可以缓存的数据量
Editor edit(String key)通过key可以获得一个DiskLruCache.Editor,通过Editor可以得到一个输出流,进而缓存到本地存储上
Snapshot get(String key)通过key值来获得一个Snapshot,如果Snapshot存在,则移动到LRU队列的头部来,通过Snapshot可以得到一个输入流InputStream

Cache.java中有一个重要成员: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);
    }

    @Override public void remove(Request request) throws IOException {
      Cache.this.remove(request);
    }

    @Override public void update(Response cached, Response network) {
      Cache.this.update(cached, network);
    }

    @Override public void trackConditionalCacheHit() {
      Cache.this.trackConditionalCacheHit();
    }

    @Override public void trackResponse(CacheStrategy cacheStrategy) {
      Cache.this.trackResponse(cacheStrategy);
    }
  };

可见internalCache不是直接面向开发者的,Cache.java类通过internalCache来操作缓存
  第3行:通过internalCache的get方法,调用Cache的get方法,Cache的get方法实际上是对DiskLruCache相关操作,通过获取DiskLruCache的快照DiskLruCache.Snapshot来获得缓存的响应
  第6行:通过internalCache的put方法,调用Cache的put方法,Cache的put方法实际上是对DiskLruCache相关操作,通过获取DiskLruCache的DiskLruCache.Editor来保存响应到DiskLruCache,在put方法中,我们应该注意一点:对于非GET的请求,Cache.java是不会进行缓存的

  internalCache的其他方法本质上都是调用Cache对应方法,Cache的相关方法通过对DiskLruCache的操作,来完成缓存的增删改查;

   4. 现在回归到 CacheInterceptor 核心代码上
 @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 wasn't applicable. Close it.
    }

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

    // If we don't need the network, we're done.
    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());
      }
    }

    // If we have a cache response too, then we're doing a conditional get.
    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());
      }
    }

    Response response = networkResponse.newBuilder()
        .cacheResponse(stripBody(cacheResponse))
        .networkResponse(stripBody(networkResponse))
        .build();

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

第2行:因为在构造CacheInterceptor的时候是通过RealCall.java中new CacheInterceptor(client.internalCache())方法创建,这里的client.internalCache()代码为

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

顾第2行中的cache != null,同时CacheInterceptor.java中InternalCache cache实现者为Cache.java中的internalCache
  第3行:调用Cache.java中的internalCache中的get方法,调用Cache.java的get方法返回Response cacheCandidate
  第8行:通过Factory方法,构建Factory对象,如果在第3行获得的响应不为空,则我们可以获得缓存请求的过期时间,上次是否被修改,过期时间等信息;最后调用Factory的get方法完成CacheStrategy实例的创建
CacheStrategy有两个重要的成员

  1. public final @Nullable Request networkRequest; //发送给服务端的call
  2. public final @Nullable Response cacheResponse; //缓存的响应

  第21行~第31行:特殊情况处理,返回一个空的response,那什么时候networkRequest会为null同时cacheResponse为null?
在第8行中,我们通过调用Factory方法,传入当前发送给网络的request,然后调用其get方法来构建CacheStrategy,当构建CacheStrategy传入的第一个参数为null,则networkRequest为null;我们可以通过阅读CacheStrategy源码229行来进行分析;通过CacheStrategy源码229行分析之后,当返回一个空的response的时候,会在头部加上Warning字段

  第34行,当有缓存,但是当前发送网络的networkRequest为null,则将当前缓存返回
  第42行,将请求交给下一个拦截器,返回Response networkResponse
  第51行~70行:当缓存不为空,同时返回的响应状态码为304,表明服务器正文并没有发生变化,顾我们封装好缓存返回给上层,并更新本地缓存
  第72行~75行:使用当前网络响应返回Response response给上层
  第77行~91行:如果OkHttpClient设置了缓存策略,将响应流信息写入到缓存文件中,即我们之前提到的Cache.java的DiskLruCache中

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值