OkHttp原理解析(2)

16 篇文章 0 订阅

5、资源是否不变

CacheControl responseCaching = cacheResponse.cacheControl();
if (responseCaching.immutable()) {
          return new CacheStrategy(null, cacheResponse);
}

如果缓存的响应中包含 Cache-Control: immutable ,这意味着对应请求的响应内容将一直不会改变。此时就可以直接使用缓存。否则继续判断缓存是否可用
6、响应的缓存有效期
这一步为进一步根据缓存响应中的一些信息判定缓存是否处于有效期内。如果满足:缓存存活时间 < 缓存新鲜度 - 缓存最小新鲜度 + 过期后继续使用时长代表可以使用缓存。其中新鲜度可以理解为有效时间,而这里的 "缓存新鲜度-缓存最小新鲜度" 就代表了缓存真正有效的时间。

6.1、缓存到现在存活的时间:ageMillis
首先 cacheResponseAge() 方法获得了响应大概存在了多久:

1、 apparentReceivedAge 代表了客户端收到响应到服务器发出响应的一个时间差seredData 是从缓存中获得的 Date 响应头对应的时间(服务器发出本响应的时间);
receivedResponseMillis 为本次响应对应的客户端发出请求的时间
2、 receivedAge 是代表了客户端的缓存,在收到时就已经存在多久了
ageSeconds 是从缓存中获得的 Age 响应头对应的秒数 (本地缓存的响应是由服务器的缓存返回,这个缓存在服务器存在的时间)ageSeconds 与上一步计算结果 apparentReceivedAge 的最大值为收到响应时,这个响应数据已经存在多久。
假设我们发出请求时,服务器存在一个缓存,其中  Data: 0点 。 此时,客户端在1小时候发起请求,此时由服务器在缓存中插入 Age: 1小时 并返回给客户端,此时客户端计算的 receivedAge 就是1小时,这就代表了客户端的缓存在收到时就已经存在多久了。(不代表到本次请求时存在多久了)
3、 responseDuration 是缓存对应的请求,在发送请求与接收请求之间的时间差
4、 residentDuration 是这个缓存接收到的时间到现在的一个时间差
receivedAge + responseDuration + residentDuration 所代表的意义就是:缓存在客户端收到时就已经存在的时间 + 请求过程中花费的时间 + 本次请求距离缓存获得的时间,就是缓存真正存在了多久。

6.2、缓存新鲜度(有效时间):freshMillis

缓存新鲜度(有效时长)的判定会有几种情况,按优先级排列如下:
1、缓存响应包含 Cache-Control: max-age=[秒] 资源最大有效时间
2、缓存响应包含 Expires: 时间 ,则通过 Data 或接收该响应时间计算资源有效时间
3、缓存响应包含 Last-Modified: 时间 ,则通过 Data 或发送该响应对应请求的时间计算资源有效时间;并且根据
建议以及在Firefox浏览器的实现,使用得到结果的10%来作为资源的有效时间。

6.3、缓存最小新鲜度:minFreshMillis

long minFreshMillis = 0;
if (requestCaching.minFreshSeconds() != -1) {
         minFreshMillis = SECONDS.toMillis(requestCaching.minFreshSeconds());
}

如果用户的请求头中包含 Cache-Control: min-fresh=[秒] ,代表用户认为这个缓存有效的时长。假设本身缓存新
鲜度为: 100毫秒,而缓存最小新鲜度为:10毫秒,那么缓存真正有效时间为90毫秒。

6.4、缓存过期后仍有效时长:maxStaleMillis

long maxStaleMillis = 0;
if (!responseCaching.mustRevalidate() && requestCaching.maxStaleSeconds() != -1) {
        maxStaleMillis = SECONDS.toMillis(requestCaching.maxStaleSeconds());
}

这个判断中第一个条件为缓存的响应中没有包含 Cache-Control: must-revalidate (不可用过期资源),获得用户请
求头中包含 Cache-Control: max-stale=[秒] 缓存过期后仍有效的时长。
6.5、判定缓存是否有效

最后利用上4步产生的值,只要缓存的响应未指定 no-cache 忽略缓存,如果:
缓存存活时间+缓存最小新鲜度 < 缓存新鲜度+过期后继续使用时长,代表可以使用缓存。
假设 缓存到现在存活了:100 毫秒; 用户认为缓存有效时间(缓存最小新鲜度)为:10 毫秒; 缓存新鲜度为: 100毫秒; 缓存过期后仍能使用: 0 毫秒; 这些条件下,首先缓存的真实有效时间为: 90毫秒,而缓存已经过了这个
时间,所以无法使用缓存。

不等式可以转换为: 缓存存活时间 < 缓存新鲜度 - 缓存最小新鲜度 + 过期后继续使用时长,即 存活时间 < 缓存有效时间 + 过期后继续使用时间总体来说,只要不忽略缓存并且缓存未过期,则使用缓存。

7、缓存过期处理

如果继续执行,表示缓存已经过期无法使用。此时我们判定缓存的响应中如果存在 Etag ,则使用 If-None-Match交给服务器进行验证;如果存在 Last-Modified 或者 Data ,则使用 If-Modified-Since 交给服务器验证。服务器如果无修改则会返回304,这时候注意:由于是缓存过期而发起的请求(与第4个判断用户的主动设置不同),如果服务器返回304,那框架会自动更新缓存,所以此时 CacheStrategy 既包含 networkRequest 也包含 cacheResponse
8、收尾
至此,缓存的判定结束,拦截器中只需要判断 CacheStrategy 中 networkRequest 与 cacheResponse 的不同组合就能够判断是否允许使用缓存。但是需要注意的是,如果用户在创建请求时,配置了 onlyIfCached 这意味着用户这次希望这个请求只从缓存获得,不需要发起请求。那如果生成的 CacheStrategy 存在 networkRequest 这意味着肯定会发起请求,此时出现冲突!那会直接给到拦截器一个既没有 networkRequest 又没有 cacheResponse 的对象。拦截器直接返回用户 504 !

9、总结
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!

缓存拦截器本身主要逻辑其实都在缓存策略中,拦截器本身逻辑非常简单,如果确定需要发起网络请求,则下一个拦截器为 ConnectInterceptor

四、连接拦截器
ConnectInterceptor ,打开与目标服务器的连接,并执行下一个拦截器。它简短的可以直接完整贴在这里:

虽然代码量很少,实际上大部分功能都封装到其它类去了,这里只是调用而已。
首先我们看到的 StreamAllocation 这个对象是在第一个拦截器:重定向拦截器创建的,但是真正使用的地方却在这里。
"当一个请求发出,需要建立连接,连接建立后需要使用流用来读写数据";而这个StreamAllocation就是协调请求、连接与数据流三者之间的关系,它负责为一次请求寻找连接,然后获得流来实现网络通信。这里使用的 newStream 方法实际上就是去查找或者建立一个与请求主机有效的连接,返回的 HttpCodec 中包含了输入输出流,并且封装了对HTTP请求报文的编码与解码,直接使用它就能够与请求主机完成HTTP通信。StreamAllocation 中简单来说就是维护连接: RealConnection ——封装了Socket与一个Socket连接池。可复用的 RealConnection 需要:

1、 if (allocations.size() >= allocationLimit || noNewStreams) return false;
连接到达最大并发流或者连接不允许建立新的流;如http1.x正在使用的连接不能给其他人用(最大并发流为:1)或者
连接被关闭;那就不允许复用;
2、if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
if (address.url().host().equals(this.route().address().url().host())) {
          return true; // This connection is a perfect match.
}

DNS、代理、SSL证书、服务器域名、端口完全相同则可复用;
如果上述条件都不满足,在HTTP/2的某些场景下可能仍可以复用(http2先不管)。
所以综上,如果在连接池中找到个连接参数一致并且未被关闭没被占用的连接,则可以复用。

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

五、请求服务器拦截器
CallServerInterceptor ,利用 HttpCodec 发出请求到服务器并且解析生成 Response 。
首先调用 httpCodec.writeRequestHeaders(request); 将请求头写入到缓存中(直到调用 flushRequest() 才真正发送给服务器)。然后马上进行第一个逻辑判断

整个if都和一个请求头有关:  Expect: 100-continue 。这个请求头代表了在发送请求体之前需要和服务器确定是否愿意接受客户端发送的请求体。所以 permitsRequestBody 判断为是否会携带请求体的方式(POST),如果命中if,则会先给服务器发起一次查询是否愿意接收请求体,这时候如果服务器愿意会响应100(没有响应体,responseBuilder 即为nul)。这时候才能够继续发送剩余请求数据。

但是如果服务器不同意接受请求体,那么我们就需要标记该连接不能再被复用,调用 noNewStreams() 关闭相关的Socket。
后续代码为:

这时 responseBuilder 的情况即为:
1、POST方式请求,请求头中包含 Expect ,服务器允许接受请求体,并且已经发出了请求体, responseBuilder
为null;
2、POST方式请求,请求头中包含 Expect ,服务器不允许接受请求体, responseBuilder 不为null
3、POST方式请求,未包含 Expect ,直接发出请求体, responseBuilder 为null;
4、POST方式请求,没有请求体, responseBuilder 为null;
5、GET方式请求, responseBuilder 为null;
对应上面的5种情况,读取响应头并且组成响应 Response ,注意:此 Response 没有响应体。同时需要注意的是,如果服务器接受  Expect: 100-continue 这是不是意味着我们发起了两次 Request ?那此时的响应头是第一次查询服务器是否支持接受请求体的,而不是真正的请求对应的结果响应。所以紧接着:

如果响应是100,这代表了是请求 Expect: 100-continue 成功的响应,需要马上再次读取一份响应头,这才是真正的请求对应结果响应头。

然后收尾

forWebSocket 代表websocket的请求,我们直接进入else,这里就是读取响应体数据。然后判断请求和服务器是不是都希望长连接,一旦有一方指明 close ,那么就需要关闭 socket 。而如果服务器返回204/205,一般情况而言不会存在这些返回码,但是一旦出现这意味着没有响应体,但是解析到的响应头中包含 Content-Lenght 且不为0,这表响应体的数据字节长度。此时出现了冲突,直接抛出协议异常!
总结
在这个拦截器中就是完成HTTP协议报文的封装与解析。
OkHttp总结
整个OkHttp功能的实现就在这五个默认的拦截器中,所以先理解拦截器模式的工作机制是先决条件。这五个拦截器分别为: 重试拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求服务拦截器。每一个拦截器负责的工作不一样,就好像工厂流水线,最终经过这五道工序,就完成了最终的产品。但是与流水线不同的是,OkHttp中的拦截器每次发起请求都会在交给下一个拦截器之前干一些事情,在获得了结果之后又干一些事情。整个过程在请求向是顺序的,而响应向则是逆序。当用户发起一个请求后,会由任务分发起 Dispatcher 将请求包装并交给重试拦截器处理。
1、重试拦截器在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。
2、桥接拦截器在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的行为(如:GZIP压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。

3、缓存拦截器顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。
4、连接拦截器在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后不进行额外的处理。
5、请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。在经过了这一系列的流程后,就完成了一次HTTP请求!

补充: 代理
在使用OkHttp时,如果用户在创建 OkHttpClient 时,配置了 proxy 或者 proxySelector ,则会使用配置的代理,并且 proxy 优先级高于 proxySelector 。而如果未配置,则会获取机器配置的代理并使用。

因此,如果我们不需要自己的App中的请求走代理,则可以配置一个 proxy(Proxy.NO_PROXY) ,这样也可以避免被抓包。 NO_PROXY 的定义如下:

 

public static final Proxy NO_PROXY = new Proxy();
private Proxy() {
       this.type = Proxy.Type.DIRECT;
       this.sa = null;
}

代理在Java中对应的抽象类有三种类型:

public static enum Type {
    DIRECT,
    HTTP,
    SOCKS;
  private Type() {
  }
}

DIRECT :无代理, HTTP :http代理, SOCKS :socks代理。第一种自然不用多说,而Http代理与Socks代理有什么区别?

对于Socks代理,在HTTP的场景下,代理服务器完成TCP数据包的转发工作; 而Http代理服务器,在转发数据之外,还会解析HTTP的请求及响应,并根据请求及响应的内容做一些处理。
RealConnection 的 connectSocket 方法:

//如果是Socks代理则 new Socket(proxy); 否则无代理或http代理就
address.socketFactory().createSocket(),相当于直接:new Socket()
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP? address.socketFactory().createSocket(): new Socket(proxy);
//connect方法
socket.connect(address);

设置了SOCKS代理的情况下,创建Socket时,为其传入proxy,写代码时连接时还是以HTTP服务器为目标地址(实际上Socket肯定是与SOCKS代理服务器连);但是如果设置的是Http代理,创建的Socket是与Http代理服务器建立连接。
在 connect 方法时传递的 address 来自于下面的集合 inetSocketAddresses RouteSelector 的resetNextInetSocketAddress 方法:

设置代理时,Http服务器的域名解析会被交给代理服务器执行。但是如果是设置了Http代理,会对Http代理服务器的域名使用 OkhttpClient 配置的dns解析代理服务器,Http服务器的域名解析被交给代理服务器解析。
上述代码就是代理与DNS在OkHttp中的使用,但是还有一点需要注意,Http代理也分成两种类型:普通代理与隧道代理。

其中普通代理不需要额外的操作,扮演「中间人」的角色,在两端之间来回传递报文。这个“中间人”在收到客户端发送的请求报文时,需要正确的处理请求和连接状态,同时向服务器发送新的请求,在收到响应后,将响应结果包装成一个响应体返回给客户端。在普通代理的流程中,代理两端都是有可能察觉不到"中间人“的存在。但是隧道代理不再作为中间人,无法改写客户端的请求,而仅仅是在建立连接后,将客户端的请求,通过建立好的隧道,无脑的转发给终端服务器。

隧道代理需要发起Http CONNECT请求,这种请求方式没有请求体,仅供代理服务器使用,并不会传递给终端服务器。请求头 部分一旦结束,后面的所有数据,都被视为应该转发给终端服务器的数据,代理需要把他们无脑的直接转发,直到从客户端的 TCP 读通道关闭。CONNECT 的响应报文,在代理服务器和终端服务器建立连接后,可以向客户端返回一个  200 Connect established  的状态码,以此表示和终端服务器的连接,建立成功。
RealConnection的connect方法

requiresTunnel 方法的判定为:当前请求为https并且存在http代理,这时候 connectTunnel 中会发起:

CONNECT xxxx HTTP/1.1
Host: xxxx
Proxy-Connection: Keep-Alive
User-Agent: okhttp/${version}

的请求,连接成功代理服务器会返回200;如果返回407表示代理服务器需要鉴权(如:付费代理),这时需要在请求
头中加入 Proxy-Authorization :

Authenticator authenticator = new Authenticator() {
@Nullable
@Override
public Request authenticate(Route route, Response response) throws IOException {

            if(response.code == 407){
                 //代理鉴权
                   String credential = Credentials.basic("代理服务用户名", "代理服务密码");
                   return response.request().newBuilder()
                  .header("Proxy-Authorization", credential)
                 .build();
               }
         return null;
        }
};
new OkHttpClient.Builder().proxyAuthenticator(authenticator);

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值