OkHttp源码解析(二)五大拦截器

一.五大拦截器总体介绍

在前面分发器的介绍中,可以看到通过getResponseWithInterceptorChain这一个方法,就可以获得响应。这个方法里面是这样的
OkHttp源码解析(二)五大拦截器_第1张图片
除了两个用户可以自己添加的拦截器之外,剩下的五个是默认的拦截器。他们之间是用责任链模式连接在一起

1.责任链模式

概念:责任链上的处理者负责处理请求,客户只需要将请求发送到责任链即可,无须关心请求的处理细节和请求的传递,所以职 责链将请求的发送者和请求的处理者解耦了

比如点个外卖,它的流程是这样的
OkHttp源码解析(二)五大拦截器_第2张图片
我不必关心内部是具体怎么给我做的饭,美团外卖只管提供平台和送餐,饭店只管接单和派单,厨师只管接收菜和做菜。他们之间是一个U型结构,类似于Android的事件分发。

这五大拦截器之间也是责任链模式,具体可用一张图表示
OkHttp源码解析(二)五大拦截器_第3张图片

2.五大拦截器的责任链模式抽取

我们建立这几个类和接口
OkHttp源码解析(二)五大拦截器_第4张图片
上面五个是拦截器,Chain是链条类,负责串联这几个拦截器,Interceptor是接口,MyClass是程序入口。我们进入MyClass
OkHttp源码解析(二)五大拦截器_第5张图片
这里首先创建了五个拦截器,然后通过Chain将五大拦截器串起来,index设为0,然后调用chainprocessd方法
OkHttp源码解析(二)五大拦截器_第6张图片
看下第一个拦截器的intercept方法
OkHttp源码解析(二)五大拦截器_第7张图片
然后调用chain(索引值从1开始,也就是从第二个拦截器开始)的processd方法。再以此类推,直到调用了第五个拦截器的intercept方法
OkHttp源码解析(二)五大拦截器_第8张图片
然后依次返回,一直到第一个。
也就是说先调用chainprocessd方法,找到第一个拦截器,然后调用其intercept方法,然后再调用chainprocessd方法,找到第二个拦截器,然后调用其intercept方法,然后再调用chainprocessd方法,以此类推,直到找到第五个拦截器,调用其intercept方法,执行完了之后将结果依次返回到第一个拦截器。

3.各个拦截器的功能

重试和重定向拦截器
它在交出(交给下一个拦截器)之前,负责判断是否需要重新发起请求(重试);在获得了结果之后 ,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。 (它是最早接收Request的,也是最晚接收Response的)
OkHttp源码解析(二)五大拦截器_第9张图片
返回码是3xx的,就需要重定向了
桥接拦截器
在交出之前,负责将HTTP协议必备的请求头,加入其中(如:Host)并添加一些默认的行为(如:GZIP压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。
缓存拦截器
顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。
连接拦截器
在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后不进行额外的处理。
请求服务器拦截器
进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。

二.RetryAndFollowUpInterceptor

重试和重定向拦截器。它对Request没有做什么特殊的操作,而是把重心放在了Response上面。直接看源码
OkHttp源码解析(二)五大拦截器_第10张图片
可以看到它直接把request传进去的。
这里有几个值得注意的点
①整个重试和重定向过程是在一个while循环里面
②通过try catch来实现重试,具体是catch里面的continue
OkHttp源码解析(二)五大拦截器_第11张图片
也就是请求异常的时候,会进入catch里面,如果recover方法返回true,那就可以重试。下面就进入这个recover方法看一看

1.recover判断是否可以重试

OkHttp源码解析(二)五大拦截器_第12张图片
不允许重试的情况有四种
①通过其建造者模式,创建OkHttpClient的时候,给他配置了不可重试。
在其Builder类里面,有这么一个方法,传入false,就设置了不可重试
在这里插入图片描述
当然,如果不设置,默认情况下,是true,即可重试
OkHttp源码解析(二)五大拦截器_第13张图片
②对某一次请求设置不可重试。(用的少)
上面那种情况是全局的,这个是某一次的。具体方法是自定义一个请求体,然后让其实现标识接口UnrepeatableRequestBody
如图
OkHttp源码解析(二)五大拦截器_第14张图片
isRecoverable方法返回false,不可重试
我们看下isRecoverable方法
OkHttp源码解析(二)五大拦截器_第15张图片
如果是协议异常,就不可重试
比如在CallServerInterceptor的这里
OkHttp源码解析(二)五大拦截器_第16张图片
204和205的意思是响应体中没有内容,而后面的contentLength大于0又说响应体中有内容,所以协议冲突了,抛出异常。一般是服务器的原因
在这里插入图片描述
如果是请求时间过长,那么可以重试(比如发生网络波动了)
如果是证书不正确,或者不匹配,都不能重试
④有没有更多的路由
比如有A代理和B代理,A代理不行,可以试试B代理,两者都不行,就是没有路由了,就不能重试了

所以,总结下重试的逻辑
如果OkHttpClient允许重试,isRecoverable方法返回true,且拥有更多的路线,才能进行重试。如图
OkHttp源码解析(二)五大拦截器_第17张图片
所以重试的条件是很苛刻的。

2.重定向

假如成功获得响应,那么走出try catch,走下面,如图
OkHttp源码解析(二)五大拦截器_第18张图片
如果返回的响应码是需要重定向的响应码,如301,302,则followUpRequest方法会解析响应头里面的location字段,并将location里面的url组建成一个新的request,返回,也就是让followUp接收。如果followUpnull,就是没有location,那就是不需要重定向。当然重定向的次数有限制,最多为20次,即MAX_FOLLOW_UPS

除了30x的响应码,需要重定向之外,其他的有一些响应码需要特殊处理。如图
OkHttp源码解析(二)五大拦截器_第19张图片

三.BridgeInterceptor

桥接拦截器,
它对Request的操作,就是帮我们添加各种请求头。如图
OkHttp源码解析(二)五大拦截器_第20张图片
补全之后就将请求交给下一拦截器进行处理。得到响应后,主要干两个事情
1、保存cookie
2、如果使用gzip返回的数据,则使用 GzipSource 包装便于解析。

四.CacheInterceptor

缓存拦截器,只有GET请求可以使用,POST那些不行。且如果要使用它的缓存,就要在创建OkHttpClient的时候进行手动配置
在这里插入图片描述
当然,也可以指定某一次请求不使用缓存
在这里插入图片描述

他有一个核心类,就是缓存策略类即CacheStrategy。它会经过很复杂的逻辑判断,得到两个值,分别是networkRequestcacheResponse。根据两者是否为null的组合,来判断到底是进行网络请求还是使用缓存。具体规则为
OkHttp源码解析(二)五大拦截器_第21张图片

下面,具体看一下是如何得到最终的CacheStrategy的,也就是这一行代码干了什么事
在这里插入图片描述
进入get方法
OkHttp源码解析(二)五大拦截器_第22张图片
进入getCandidate方法
里面就是具体怎么得到的CacheStrategy。逻辑很复杂,有些我也没有学习,这里找到了一些带注释的代码,供大家参考

private CacheStrategy getCandidate() {
     
    // No cached response.
     //todo 1、没有缓存,进行网络请求
     if (cacheResponse == null) {
     
         return new CacheStrategy(request, null);
     }

     //todo okhttp会保存ssl握手信息  Handshake ,如果这次发起了https请求,但是缓存的响应中没有握手信息,发起网络请求

     //todo 2、https请求,但是没有握手信息,进行网络请求
     //Drop the cached response if it's missing a required handshake.
     if (request.isHttps() && cacheResponse.handshake() == null) {
     
         return new CacheStrategy(request, null);
     }

     //todo 3、主要是通过响应码以及头部缓存控制字段判断响应能不能缓存,不能缓存那就进行网络请求
     //If this response shouldn't have been stored, it should never be used
     //as a response source. This check should be redundant as long as the
     //persistence store is well-behaved and the rules are constant.
     if (!isCacheable(cacheResponse, request)) {
     
         return new CacheStrategy(request, null);
     }

     CacheControl requestCaching = request.cacheControl();
     //todo 4、如果 请求包含:CacheControl:no-cache 需要与服务器验证缓存有效性
     // 或者请求头包含 If-Modified-Since:时间 值为lastModified或者data 如果服务器没有在该头部指定的时间之后修改了请求的数据,服务器返回304(无修改)
     // 或者请求头包含 If-None-Match:值就是Etag(资源标记)服务器将其与存在服务端的Etag值进行比较;如果匹配,返回304
     // 请求头中只要存在三者中任意一个,进行网络请求
     if (requestCaching.noCache() || hasConditions(request)) {
     
         return new CacheStrategy(request, null);
     }

     //todo 5、如果缓存响应中存在 Cache-Control:immutable 响应内容将一直不会改变,可以使用缓存
     CacheControl responseCaching = cacheResponse.cacheControl();
     if (responseCaching.immutable()) {
     
         return new CacheStrategy(null, cacheResponse);
     }

     //todo 6、根据 缓存响应的 控制缓存的响应头 判断是否允许使用缓存
     // 6.1、获得缓存的响应从创建到现在的时间
     long ageMillis = cacheResponseAge();
     //todo
     // 6.2、获取这个响应有效缓存的时长
     long freshMillis = computeFreshnessLifetime();
     if (requestCaching.maxAgeSeconds() != -1) {
     
         //todo 如果请求中指定了 max-age 表示指定了能拿的缓存有效时长,就需要综合响应有效缓存时长与请求能拿缓存的时长,获得最小的能够使用响应缓存的时长
         freshMillis = Math.min(freshMillis,
                 SECONDS.toMillis(requestCaching.maxAgeSeconds()));
     }
     //todo
     // 6.3 请求包含  Cache-Control:min-fresh=[秒]  能够使用还未过指定时间的缓存 (请求认为的缓存有效时间)
     long minFreshMillis = 0;
     if (requestCaching.minFreshSeconds() != -1) {
     
         minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
     }

     //todo
     // 6.4
     //  6.4.1、Cache-Control:must-revalidate 可缓存但必须再向源服务器进行确认
     //  6.4.2、Cache-Control:max-stale=[秒] 缓存过期后还能使用指定的时长  如果未指定多少秒,则表示无论过期多长时间都可以;如果指定了,则只要是指定时间内就能使用缓存
     // 前者会忽略后者,所以判断了不必须向服务器确认,再获得请求头中的max-stale
     long maxStaleMillis = 0;
     if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
     
         maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
     }

     //todo
     // 6.5 不需要与服务器验证有效性 && 响应存在的时间+请求认为的缓存有效时间 小于 缓存有效时长+过期后还可以使用的时间
     // 允许使用缓存
     if (!responseCaching.noCache() && ageMillis + minFreshMillis < freshMillis + maxStaleMillis) {
     
         Response.Builder builder = cacheResponse.newBuilder();
         //todo 如果已过期,但未超过 过期后继续使用时长,那还可以继续使用,只用添加相应的头部字段
         if (ageMillis + minFreshMillis >= freshMillis) {
     
             builder.addHeader("Warning", "110 HttpURLConnection \"Response is stale\"");
         }
         //todo 如果缓存已超过一天并且响应中没有设置过期时间也需要添加警告
         long oneDayMillis = 24 * 60 * 60 * 1000L;
         if (ageMillis > oneDayMillis && isFreshnessLifetimeHeuristic()) {
     
             builder.addHeader("Warning", "113 HttpURLConnection \"Heuristic expiration\"");
         }
         return new CacheStrategy(null, builder.build());
     }

     // Find a condition to add to the request. If the condition is satisfied, the
     // response body
     // will not be transmitted.
     //todo 7、缓存过期了
     String conditionName;
     String conditionValue;
     if (etag != null) {
     
         conditionName = "If-None-Match";
         conditionValue = etag;
     } else if (lastModified != null) {
     
         conditionName = "If-Modified-Since";
         conditionValue = lastModifiedString;
     } else if (servedDate != null) {
     
         conditionName = "If-Modified-Since";
         conditionValue = servedDateString;
     } else {
     
         return new CacheStrategy(request, null); // No condition! Make a regular request.
     }
     //todo 如果设置了 If-None-Match/If-Modified-Since 服务器是可能返回304(无修改)的,则使用缓存的响应体
     Headers.Builder conditionalRequestHeaders = request.headers().newBuilder();
     Internal.instance.addLenient(conditionalRequestHeaders, conditionName, conditionValue);

     Request conditionalRequest = request.newBuilder()
             .headers(conditionalRequestHeaders.build())
             .build();
     return new CacheStrategy(conditionalRequest, cacheResponse);
}

有一个比较重要的请求头/响应头,就是Cache-Control
Cache-Control 可以在请求头存在,也能在响应头存在,对应的value可以设置多种组合:

1.max-age=[秒] :资源最大有效时间;
2.public :表明该资源可以被任何用户缓存,比如客户端,代理服务器等都可以缓存资源; 3.
3.private :表明该资源只能被单个用户缓存,默认是private。
4.no-store :资源不允许被缓存
3.no-cache :(请求)不使用缓存,就是这一次不用,并不代表没有缓存
4.immutable :(响应)资源不会改变
5.min-fresh=[秒] :(请求)缓存最小新鲜度(用户认为这个缓存有效的时长) 8. must-revalidate :(响应)不允许使用过期缓存
6.max-stale=[秒] :(请求)缓存过期后多久内仍然有效

举例:假设存在max-age=100,min-fresh=20。这代表了用户认为这个缓存的响应,从服务器创建响应 到 能够缓
存使用的时间为100-20=80s。但是如果max-stale=100。这代表了缓存有效时间80s过后,仍然允许使用
100s,可以看成缓存有效时长为180s。

具体流程是这样的
OkHttp源码解析(二)五大拦截器_第23张图片
总结一下
如果想使用缓存,首先用户要配置缓存,然后查看是否存在缓存,且要保持有握手信息。除此之外,看是否允许使用缓存,若允许,则看这次请求是否需要使用缓存,如果需要,则看返回体中是否有Cache-Control:immutable,若有,则说明缓存的资源没变,则再看看资源是否有效,也就是是否过期了。如果没过期,才能用缓存。使用缓存的话,就不需要使用下面的拦截器了,直接返回response

五.ConnectInterceptor

连接拦截器。说白了就是获取socket连接。该连接有可能是从socket连接池里面获取的

1.连接池ConnectionPool

首先看它的构造方法
OkHttp源码解析(二)五大拦截器_第24张图片
第一个是最大闲置连接数,也就是说它最多维护多少个闲置连接,类似于线程池的核心线程数。后面就是其存活时间和单位了。

往连接池中添加连接,使用的是put方法
OkHttp源码解析(二)五大拦截器_第25张图片
如果是第一次调用put方法,就会启动cleanupRunnable,即专门清理闲置连接的一个线程
OkHttp源码解析(二)五大拦截器_第26张图片
追踪里面的cleanUp方法
逻辑大概是这样:首先遍历所有的连接,得到闲置连接数和闲置了最长时间的连接。遍历完成后,如果闲置了最长时间的连接超过了5分钟(或者你自定义的其他时长),或者闲置连接数超过了5(或者你自定义的其他数目),则把这个闲置了最长时间的连接从连接池里面清除掉。具体逻辑在下面

long cleanup(long now) {
     
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    // Find either a connection to evict, or the time that the next eviction is due.
    synchronized (this) {
     
        for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
     
            RealConnection connection = i.next();

            //todo 检查连接是否正在被使用
            //If the connection is in use, keep searching.
            if (pruneAndGetAllocationCount(connection, now) > 0) {
     
                inUseConnectionCount++;
                continue;
            }
            //todo 否则记录闲置连接数
            idleConnectionCount++;

            // If the connection is ready to be evicted, we're done.
            //TODO 获得这个连接已经闲置多久
            // 执行完遍历,获得闲置了最久的连接
            long idleDurationNs = now - connection.idleAtNanos;
            if (idleDurationNs > longestIdleDurationNs) {
     
                longestIdleDurationNs = idleDurationNs;
                longestIdleConnection = connection;
            }
        }
        //todo 超过了保活时间(5分钟) 或者池内闲置连接数量超过了(5个) 马上移除闲置了最久的连接,然后返回0,表示不等待,马上再次检查清理
        if (longestIdleDurationNs >= this.keepAliveDurationNs
                || idleConnectionCount > this.maxIdleConnections) {
     
            // We've found a connection to evict. Remove it from the list, then close it
            // below (outside
            // of the synchronized block).
            connections.remove(longestIdleConnection);
        } else if (idleConnectionCount > 0) {
     
            // A connection will be ready to evict soon.
            //todo 池内存在闲置连接,就等待 保活时间(5分钟)-最长闲置时间 =还能闲置多久 再检查
            return keepAliveDurationNs - longestIdleDurationNs;
        } else if (inUseConnectionCount > 0) {
     
            // All connections are in use. It'll be at least the keep alive duration 'til we
            // run again.
            //todo 有使用中的连接,就等 5分钟 再检查
            return keepAliveDurationNs;
        } else {
     
            // No connections, idle or in use.
            //todo 都不满足,可能池内没任何连接,直接停止清理(put后会再次启动)
            cleanupRunning = false;
            return -1;
        }
    }

    closeQuietly(longestIdleConnection.socket());

    // Cleanup again immediately.
    return 0;
}

get方法
首先会遍历连接池里面的各个连接,判断ip端口等是否与要请求的相同,而且该连接是否空闲。都满足的时候,就可以复用。
比如进入isEligible看一看
OkHttp源码解析(二)五大拦截器_第27张图片

public boolean isEligible(Address address, @Nullable Route route) {
     
        // If this connection is not accepting new streams, we're done.
        //todo 实际上就是在使用(对于http1.1)就不能复用
        if (allocations.size() >= allocationLimit || noNewStreams) return false;


        // If the non-host fields of the address don't overlap, we're done.
        //todo 如果地址不同,不能复用。地址包括了配置的dns、代理、证书以及端口等等 (域名还没判断,所以下面马上判断域名)
        if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;

        // If the host exactly matches, we're done: this connection can carry the address.
        //todo 都相同,那就可以复用了
        if (address.url().host().equals(this.route().address().url().host())) {
     
            return true; // This connection is a perfect match.
        }

      	。。。。。
        return true; // The caller's address can be carried by this connection.
}

六.CallServerInterceptor

请求服务拦截器,这个拦截器就是完成HTTP协议报文的封装与解析。

有一个需要注意的点,就是需要上传大容量请求体的时候。可以用一个
在这里插入图片描述
请求头,代表了先询问服务器是否愿意接收发送请求体数据。
OkHttp是这样处理的
如果服务器允许则返回100,客户端继续发送请求体; 如果服务器不允许则直接返回给用户。 同时服务器也可能会忽略此请求头,一直无法读取应答,此时抛出超时异常。

你可能感兴趣的:(框架学习,实习篇,android,java,apache)