OKHttp(二)intercepter拦截器

上一篇我们说完了dispatcher分发器,我们知道了请求任务是如何分发出去的,那响应是如何获取到的呢?再看一下RealCall中的同步方法execute():

    @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拦截器链得到网络请求响应
            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);
        }
    }

可以看到一个方法getResponseWithInterceptorChain(),点进去看看

    Response getResponseWithInterceptorChain() throws IOException {
        // 创建一个拦截器list.
        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);
    }

这里添加了一大堆拦截器,Okhttp默认有5个拦截器

  • RetryAndFollowUpInterceptor 第一个接触到请求,最后接触到响应;负责判断是否需要重新发起整个请求
  • BridgeInterceptor 补全请求,并对响应进行额外处理
  • CacheInterceptor 请求前查询缓存,获得响应并判断是否需要缓存
  • ConnectInterceptor 与服务器完成TCP连接
  • CallServerInterceptor 与服务器通信;封装请求数据与解析响应数据(如:HTTP报文)

我们先说一下拦截器链是如何工作的,看一张图


拦截器责任链.png

如果上面看不懂先看这张

拦截器链举例.png

在网络请求发起后逐一经过各个拦截器处理Request,把Request发送到服务器端,由服务器端处理后反馈Response,再次逐一经过各个拦截器处理,最后拿到返回结果,完成一次网络请求。这就是拦截器的责任链模式。
看代码,getResponseWithInterceptorChain()中的这句:

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

        return chain.proceed(originalRequest);

new了一个RealInterceptorChain对象,传入 interceptors 拦截器集合,后面有一个参数index :0,这个index就是拦截器集合的下标,意味着现在处理到第几个拦截器了,后面传入originalRequest 也就是Request对象,最后
return chain.proceed(originalRequest)
来看看 chain.proceed()方法,它是接口Interceptor.chain声明的方法,实现类是RealInterceptorChain

@Override public Response proceed(Request request) throws IOException {
    return proceed(request, streamAllocation, httpCodec, connection);
  }

参数是Request ,返回值类型是Response,然后调用重载方法;

public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,
      RealConnection connection) throws IOException {
    if (index >= interceptors.size()) throw new AssertionError();

    calls++;

    // If we already have a stream, confirm that the incoming request will use it.
    if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must retain the same host and port");
    }

    // If we already have a stream, confirm that this is the only call to chain.proceed().
    if (this.httpCodec != null && calls > 1) {
      throw new IllegalStateException("network interceptor " + interceptors.get(index - 1)
          + " must call proceed() exactly once");
    }

    // 重点在这里,经过一系列异常判断后,又new了一个RealInterceptorChain
    // 传入interceptors, index + 1, request等等
    RealInterceptorChain next = new RealInterceptorChain(interceptors, streamAllocation, httpCodec,
        connection, index + 1, request, call, eventListener, connectTimeout, readTimeout,
        writeTimeout);
    // 获取到第一个拦截器,这里index是0,拦截器链从这里开始顺序执行
    Interceptor interceptor = interceptors.get(index);
    // 调用 interceptor的intercept方法, 拿到Response 
    Response response = interceptor.intercept(next);

    // 下面拿到 Respone 又是一顿异常判断

    // Confirm that the next interceptor made its required call to chain.proceed().
    if (httpCodec != null && index + 1 < interceptors.size() && next.calls != 1) {
      throw new IllegalStateException("network interceptor " + interceptor
          + " must call proceed() exactly once");
    }

    // Confirm that the intercepted response isn't null.
    if (response == null) {
      throw new NullPointerException("interceptor " + interceptor + " returned null");
    }

    if (response.body() == null) {
      throw new IllegalStateException(
          "interceptor " + interceptor + " returned a response with no body");
    }

    return response;
  }

此时的index值是0,这里既是入口也是出口,类似举例中的政府发放补贴那个位置,然后调用 Interceptor 的 intercept 方法,这里是第一个拦截器,相当于举例中的省长拦截器,把补贴发放到省长手里,由省长开始第一次拦截处理。
再看一下 Interceptor 中的 intercept 方法,Interceptor是接口,拦截器必须实现它,这里我们找一个实现类 RetryAndFollowUpInterceptor 看一下

    @Override
    public Response intercept(Chain chain) throws IOException {

        Request request = chain.request();
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        ......
        Response priorResponse = null;
        while (true) {
            ......
            try {
                // 这里处理完又调回 RealInterceptorChain 的proceed方法
                response = realChain.proceed(request, streamAllocation, null, null);
                releaseConnection = false;
            } catch (RouteException e) {
               ......
            }
            ......
            return response;
        }
    }

看到这里就很明朗了,拦截器无非就一个套路,在intercept方法中

public Response intercept(Chain chain){
     // todu 在这里处理request
     // 再次回到拦截器链的proceed方法,把request传递给下一个拦截器
     response = chain.proceed(request, streamAllocation, null, null);
     // todo 在这里处理response
     // 返回response给上一个拦截器
     return response;
}

这时回头再看看我们举得例子,类比拦截器处理过程

1、拿到补贴(Request)
2、克扣一点(处理Request)
3、把剩下的补贴发到下一级(传递给下个拦截器)
4、拿到下一级的收据反馈(拿到Response)
5、改一下数字(处理Response)
6、把收据反馈给上一级(返回Response)
拦截结束。

责任链中每一个拦截器有着它自己的职责(单一职责原则),接下来分析每个拦截器负责的功能。

1、RetryAndFollowUpInterceptor 重试及重定向拦截器

负责判断用户是否取消了请求,出现异常后是否需要重试;在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。看代码:

    @Override
    public Response intercept(Chain chain) throws IOException {

        Request request = chain.request();
        RealInterceptorChain realChain = (RealInterceptorChain) chain;
        Call call = realChain.call();
        EventListener eventListener = realChain.eventListener();

        /**
         * 管理类,维护了 与服务器的连接、数据流与请求三者的关系。真正使用的拦截器为 ConnectIntercepter
         */
        StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
                createAddress(request.url()), call, eventListener, callStackTrace);
        this.streamAllocation = streamAllocation;
        // 重试次数
        int followUpCount = 0;
        Response priorResponse = null;
        // while循环表示只要没达到限制条件(如异常情况或者最大重试次数20次)就一直重试
        while (true) {
            // canceled表示请求是否被用户主动取消,如果被取消直接抛异常退出
            if (canceled) {
                streamAllocation.release();
                throw new IOException("Canceled");
            }

            Response response;
            boolean releaseConnection = true;
            try {
                // 请求出现了异常,那么releaseConnection依旧为true。
                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.getLastConnectException();
                }
                releaseConnection = false;
                continue;
            } catch (IOException e) {
                // 请求发出去了,但是和服务器通信失败了。(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.
                // 不是前两种的失败,那直接关闭清理所有资源
                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();
            }
            // 处理3和4xx的一些状态码,如301 302重定向
            Request followUp = followUpRequest(response, streamAllocation.route());
            if (followUp == null) {
                if (!forWebSocket) {
                    streamAllocation.release();
                }
                return response;
            }

            closeQuietly(response.body());

            // 限制最大 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());
            }
            // 判断是不是可以复用同一份连接
            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;
        }
    }

重试

下面这句代码是被 try catch 包裹的
response = realChain.proceed(request, streamAllocation, null, null);
在请求过程中可能会出现2种异常情况

  • RouteException 它是OkHttp自定义的一个 RuntimeException,如果请求时需要通过一个代理服务器转发请求,在和代理服务器连接的时候出错,就会抛出一个RouteException。
  • IOException 和服务器通信失败,比如socket流正在读写数据的时候断开连接,抛出异常。

这两个catch代码块中都有continue关键字,说明出现这两种异常都可以触发重试机制,但是中间还有个if 判断 recover() 方法返回值 ,只有它返回 true 才能重试,如果返回 false 直接抛出IOException,我们看看recover():

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

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

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

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

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

先说一下第4点,hasMoreRoutes()表示有没有更多路线,比如一个域名dns可能解析出多个IP或者存在多个代理服务器的情况,如果拥有多个路线,就可以换一个路线重试请求 。
再说一下第3点,判断是不是属于重试的异常,调用 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).
        // 如果只是InterruptedIOException 不重试, 如果是超时异常SocketTimeoutException 可重试
        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;
    }

看了这么一堆, 总结一张图


重试机制.png

重定向
如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重定向的判断。重定向的判断位于 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) {
            // 407 客户端使用了HTTP代理服务器,在请求头中添加 “Proxy-Authorization”,让代理服务器授权
            case HTTP_PROXY_AUTH:
                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);
            // 401 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 “Authorization” 
            case HTTP_UNAUTHORIZED:
                return client.authenticator().authenticate(route, userResponse);
            // 308 永久重定向 
            // 307 临时重定向
            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"
                // 如果请求方式不是GET或者HEAD,框架不会自动重定向请求
                if (!method.equals("GET") && !method.equals("HEAD")) {
                    return null;
                }
                // fall-through
            // 300 301 302 303 
            case HTTP_MULT_CHOICE:
            case HTTP_MOVED_PERM:
            case HTTP_MOVED_TEMP:
            case HTTP_SEE_OTHER:
                // Does the client allow redirects?
                // 如果用户不允许重定向,那就返回null
                if (!client.followRedirects()) return null;
                // 从响应头取出location 
                String location = userResponse.header("Location");
                if (location == null) return null;
                // 根据location 配置新的请求 url
                HttpUrl url = userResponse.request().url().resolve(location);

                // Don't follow redirects to unsupported protocols.
                // 如果为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向
                if (url == null) return null;

                // If configured, don't follow redirects between SSL and non-SSL.
                // 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)
                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();
                    /**
                     *  重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的      方法都要改为GET请求方式,
                     *  即只有 PROPFIND 请求才能有请求体
                     */
                 //请求不是get与head
                if (HttpMethod.permitsRequestBody(method)) {
                    final boolean maintainBody = HttpMethod.redirectsWithBody(method);
                    // 除了 PROPFIND 请求之外都改成GET请求
                    if (HttpMethod.redirectsToGet(method)) {
                        requestBuilder.method("GET", null);
                    } else {
                        RequestBody requestBody = maintainBody ? userResponse.request().body() :
                                null;
                        requestBuilder.method(method, requestBody);
                    }
                    // 不是 PROPFIND 的请求,把请求头中关于请求体的数据删掉
                    if (!maintainBody) {
                        requestBuilder.removeHeader("Transfer-Encoding");
                        requestBuilder.removeHeader("Content-Length");
                        requestBuilder.removeHeader("Content-Type");
                    }
                }

                // When redirecting across hosts, drop all authentication headers. This
                // is potentially annoying to the application layer since they have no
                // way to retain them.
                // 在跨主机重定向时,删除身份验证请求头
                if (!sameConnection(userResponse, url)) {
                    requestBuilder.removeHeader("Authorization");
                }

                return requestBuilder.url(url).build();
            // 408 客户端请求超时 
            case HTTP_CLIENT_TIMEOUT:
                // 408's are rare in practice, but some servers like HAProxy use this response
                // code. The
                // spec says that we may repeat the request without modifications. Modern
                // browsers also
                // repeat the request (even non-idempotent ones.)
                // 408 算是连接失败了,所以判断用户是不是允许重试
                if (!client.retryOnConnectionFailure()) {
                    // The application layer has directed us not to retry the request.
                    return null;
                }

                if (userResponse.request().body() instanceof UnrepeatableRequestBody) {
                    return null;
                }

                if (userResponse.priorResponse() != null
                        && userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {
                    // We attempted to retry and got another timeout. Give up.
                    return null;
                }
                // 如果服务器告诉我们了 Retry-After 多久后重试,那框架不管了。
                if (retryAfter(userResponse, 0) > 0) {
                    return null;
                }

                return userResponse.request();
            // 503 服务不可用 和408差不多,但是只在服务器告诉你 Retry-After:0(意思就是立即重试) 才重请求
            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;
        }
    }

整个是否需要重定向的判断内容很多,记不住,这很正常,关键在于理解他们的意思。如果此方法返回空,那就表示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的Request,但是需要注意的是,我们的followup在拦截器中定义的最大次数为20次。


重定向.png

总结
RetryAndFollowUpInterceptor是整个责任链中的第一个,这意味着它会是首次接触到Request与最后接收到Response的角色,在这个拦截器中主要功能就是判断是否需要重试与重定向。

重试的前提是出现了RouteException或者IOException。一旦在后续的拦截器执行过程中出现这两个异常,就会通过recover方法进行判断是否进行连接重试。

重定向发生在重试的判定之后,如果不满足重试的条件,还需要进一步调用followUpRequest根据Response 的响应码(当然,如果直接请求失败,Response都不存在就会抛出异常)。followup最大发生20次。

2、BridgeInterceptor 桥接拦截器

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

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

基本上就是将Request补全请求头的操作, 就是下面这张表:


补全请求头.png

然后得到响应后,主要干两件事情:

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

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

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

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

3、CacheInterceptor 缓存拦截器

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

    @Override
    public Response intercept(Chain chain) throws IOException {
        // 通过url的md5数据 从文件缓存查找 (GET请求才有缓存)
        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) {
            // 服务器返回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());
            }
        }

        // 走到这里说明缓存不可用 那就使用网络的响应
        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;
    }

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。
流程图:


缓存.png

总结
主要流程就是查询本地是否有当前Request的缓存,如果有缓存并且缓存有效(没有过期),使用缓存,否则发起网络请求。

4、ConnectInterceptor 连接拦截器

功能是打开与目标服务器的连接,以及Socket连接的缓存。

    @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连接池ConnectionPool 。
ConnectionPool 连接池,类似线程池,只不过一个是缓存线程,一个是缓存Socke连接,内部维护了一个队列,当我们发起一次网络请求和服务器建立了一个Socket连接,就将该连接缓存到连接池中,等下一次向同一主机发起请求时复用该连接。
复用条件:
1、

        if (allocations.size() >= allocationLimit || noNewStreams) return false;

连接到达最大并发流或者连接不允许建立新的流;如http1.x正在使用的连接不能给其他人用(最大并发流为:1)或者连接被关闭;那就不允许复用;
2、

        if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
        // If the host exactly matches, we're done: this connection can carry the address.
        if (address.url().host().equals(this.route().address().url().host())) {
            return true; // This connection is a perfect match.
        }

DNS、代理、SSL证书、服务器域名、端口完全相同则可复用;
如果上述条件都不满足,在HTTP/2的某些场景下可能仍可以复用(http2先不管)。

所以综上,如果在连接池中找到个连接参数一致并且未被关闭没被占用的连接,则可以复用。

连接池.png

当然连接是有有效期的,ConnectionPool中创建了一个线程池用于定期清理无效连接


清理连接.png

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

5、CallServerInterceptor 请求服务拦截器

利用HttpCodec发出请求到服务器并且解析生成Response。
说白了就是将请求头、请求体按照报文格式拼装成字符串发送给服务器,并将返回的数据解析成Response对象返回给上一层拦截器。

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

拦截器总结

整个OkHttp功能的实现就在这五个默认的拦截器中,所以先理解拦截器模式的工作机制是先决条件。这五个拦截器分别为: 重试拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求服务拦截器。每一个拦截器负责的工作不一样,就好像工厂流水线,最终经过这五道工序,就完成了最终的产品。
但是与流水线不同的是,OkHttp中的拦截器每次发起请求都会在交给下一个拦截器之前干一些事情,在获得了结果之后又干一些事情。整个过程在请求向是顺序的,而响应向则是逆序。

用户也可以自定义自己的拦截器在请求过程中去处理自己想要的业务逻辑,比如打印Log、统一封装Header等等。

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

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

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

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

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

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

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

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

推荐阅读更多精彩内容