文章目录
【1】五大拦截器总体概述一、责任链设计模式①定义:②为什么要使用责任链模式 【2】拦截器的工作流程一、默认的5大拦截器有哪些? 【3】RetryAndFollowUpInterceptor拦截器一、重试二、重定向 【4】BridgeInterceptor 桥接拦截器【5】CacheInterceptor缓存拦截器【6】ConnectInterceptor连接拦截器【7】CallServerInterceptor请求服务连接器【8】总结【1】五大拦截器总体概述
OkHttp最核心的工作是在 getResponseWithInterceptorChain() 中进行,在进入这个方法分析之前,我们先来了 解什么是责任链模式,因为此方法就是利用的责任链模式完成一步步的请求。责任链顾名思义就是由一系列的负责者构成的一个链条,类似于工厂流水线
一、责任链设计模式
①定义:
它为请求创建了一个接收者对象的链。为了避免请求发送者与多个请求处理者耦合在一起,于是将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链,当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。(责任链模式也叫职责链模式)
在责任链模式中,每一个对象对其下家的引用而接起来形成一条链。请求在这个链上传递,直到链上的某一个对象 决定处理此请求。客户并不知道链上的哪一个对象最终处理这个请求,系统可以在不影响客户端的 情况下动态的重 新组织链和分配责任。处理者有两个选择:承担责任或者把责任推给下家。一个请求可以最终不被任何接收端对象 所接受。
②为什么要使用责任链模式
我觉得原因如下:
解耦实现单依职责原则
【2】拦截器的工作流程
(借助别人的图了,我太懒了)
一、默认的5大拦截器有哪些?
RetryAndFollowUpInterceptor
(重试和重定向拦截器)第一个接触到请求,最后接触到响应;负责判断是否需要重新发起整个请求BridgeInterceptor
(桥接拦截器)
补全请求,并对响应进行额外处理CacheInterceptor
(缓存拦截器)
请求前查询缓存,获得响应并判断是否需要缓存ConnectInterceptor
(链接拦截器)
与服务器完成TCP连接 (Socket)CallServerInterceptor
(请求服务拦截器)
与服务器通信;封装请求数据与解析响应数据(如:HTTP报文)
【3】RetryAndFollowUpInterceptor拦截器
第一个拦截器: RetryAndFollowUpInterceptor ,主要就是完成两件事情:重试与重定向。
一、重试
请求阶段发生了 RouteException 或者 IOException会进行判断是否重新发起请求。
RouteException
catch (RouteException e) {//todo 路由异常,连接未成功,请求还没发出去if (!recover(e.getLastConnectException(), streamAllocation, false, request)) {throw e.getLastConnectException();}releaseConnection = false;continue; }
IOException
catch (IOException e) {//todo 请求发出去了,但是和服务器通信失败了。(socket流正在读写数据的时候断开连接)// HTTP2才会抛出ConnectionShutdownException。所以对于HTTP1 requestSendStarted一定是true boolean requestSendStarted = !(e instanceof ConnectionShutdownException);if (!recover(e, streamAllocation, requestSendStarted, request)) throw e;releaseConnection = false;continue; }
两个异常都是根据recover
方法判断是否能够进行重试,如果返回true
,则表示允许重试。
private boolean recover(IOException e, StreamAllocation streamAllocation,boolean requestSendStarted, Request userRequest) {streamAllocation.streamFailed(e);//todo 1、在配置OkhttpClient是设置了不允许重试(默认允许),则一旦发生请求失败就不再重试if (!client.retryOnConnectionFailure()) return false;//todo 2、如果是RouteException,不用管这个条件,// 如果是IOException,由于requestSendStarted只在http2的io异常中可能为false,所以主要是第二个条件 if (requestSendStarted && userRequest.body() instanceof UnrepeatableRequestBody)return false;//todo 3、判断是不是属于重试的异常if (!isRecoverable(e, requestSendStarted)) return false; //todo 4、有没有可以用来连接的路由路线if (!streamAllocation.hasMoreRoutes()) return false;// For failure recovery, use the same route selector with a new connection.return true;}
所以首先使用者在不禁止重试的前提下,如果出现了某些异常,并且存在更多的路由线路,则会尝试换条线路进行 请求的重试。其中某些异常是在isRecoverable
中进行判断:
private boolean isRecoverable(IOException e, boolean requestSendStarted) {// 出现协议异常,不能重试if (e instanceof ProtocolException) {return false;}// 如果不是超时异常,不能重试if (e instanceof InterruptedIOException) {return e instanceof SocketTimeoutException && !requestSendStarted;}// SSL握手异常中,证书出现问题,不能重试if (e instanceof SSLHandshakeException) {if (e.getCause() instanceof CertificateException) {return false;} }// SSL握手未授权异常 不能重试if (e instanceof SSLPeerUnverifiedException) {return false;}return true;}
1、协议异常
,如果是那么直接判定不能重试;(你的请求或者服务器的响应本身就存在问题,没有按照http协议来 定义数据,再重试也没用)
2、超时异常
,可能由于网络波动造成了Socket连接的超时,可以使用不同路线重试。
3、SSL证书异常/SSL验证失败异常
,前者是证书验证失败,后者可能就是压根就没证书,或者证书数据不正确, 那还怎么重试?
经过了异常的判定之后,如果仍然允许进行重试,就会再检查当前有没有可用路由路线来进行连接。简单来说,比 如 DNS 对域名解析后可能会返回多个 IP,在一个IP失败后,尝试另一个IP进行重试。
二、重定向
如果请求结束后没有发生异常并不代表当前获得的响应就是最终需要交给用户的,还需要进一步来判断是否需要重 定向的判断。重定向的判断位于followUpRequest
方法
private Request followUpRequest(Response userResponse) throws IOException {if (userResponse == null) throw new IllegalStateException();Connection connection = streamAllocation.connection();Route route = connection != null? connection.route(): null;int responseCode = userResponse.code();final String method = userResponse.request().method();switch (responseCode) {// 407 客户端使用了HTTP代理服务器,在请求头中添加 “Proxy-Authorization”,让代理服务器授权 case HTTP_PROXY_AUTH: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 usingproxy");}return client.proxyAuthenticator().authenticate(route, userResponse);// 401 需要身份验证 有些服务器接口需要验证使用者身份 在请求头中添加 “Authorization” case HTTP_UNAUTHORIZED:case HTTP_UNAUTHORIZED:return client.authenticator().authenticate(route, userResponse); // 308 永久重定向// 307 临时重定向case HTTP_PERM_REDIRECT:case HTTP_TEMP_REDIRECT:// 如果请求方式不是GET或者HEAD,框架不会自动重定向请求if (!method.equals("GET") && !method.equals("HEAD")) {return null;}// 300 301 302 303case HTTP_MULT_CHOICE:case HTTP_MOVED_PERM:case HTTP_MOVED_TEMP:case HTTP_SEE_OTHER:// 如果用户不允许重定向,那就返回nullif (!client.followRedirects()) return null;// 从响应头取出locationString location = userResponse.header("Location");if (location == null) return null;// 根据location 配置新的请求 urlHttpUrl url = userResponse.request().url().resolve(location);// 如果为null,说明协议有问题,取不出来HttpUrl,那就返回null,不进行重定向if (url == null) return null;// 如果重定向在http到https之间切换,需要检查用户是不是允许(默认允许)boolean sameScheme = url.scheme().equals(userResponse.request().url().scheme()); if (!sameScheme && !client.followSslRedirects()) return null;Request.Builder requestBuilder = userResponse.request().newBuilder();/*** 重定向请求中 只要不是 PROPFIND 请求,无论是POST还是其他的方法都要改为GET请求方式, * 即只有 PROPFIND 请求才能有请求体*///请求不是get与headif (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");} }// 在跨主机重定向时,删除身份验证请求头if (!sameConnection(userResponse, url)) {requestBuilder.removeHeader("Authorization");}return requestBuilder.url(url).build();// 408 客户端请求超时case HTTP_CLIENT_TIMEOUT:// 408 算是连接失败了,所以判断用户是不是允许重试 if (!client.retryOnConnectionFailure()) {return null;}// UnrepeatableRequestBody实际并没发现有其他地方用到if (userResponse.request().body() instanceof UnrepeatableRequestBody) {return null;}// 如果是本身这次的响应就是重新请求的产物同时上一次之所以重请求还是因为408,那我们这次不再重请求 了if (userResponse.priorResponse() != null&& userResponse.priorResponse().code() == HTTP_CLIENT_TIMEOUT) {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) {return null;}if (retryAfter(userResponse, Integer.MAX_VALUE) == 0) {return userResponse.request();}return null;default:return null;}}
整个是否需要重定向的判断内容很多,记不住,这很正常,关键在于理解他们的意思。如果此方法返回空,那就表 示不需要再重定向了,直接返回响应;但是如果返回非空,那就要重新请求返回的Request
,但是需要注意的是, 我们的followup
在拦截器中定义的最大次数为20
次。
总结
本拦截器是整个责任链中的第一个,这意味着它会是首次接触到Request
与最后接收到Response
的角色,在这个 拦截器中主要功能就是判断是否需要重试与重定向。
重试的前提是出现了RouteException
或者IOException
。一但在后续的拦截器执行过程中出现这两个异常,就会 通过 recover 方法进行判断是否进行连接重试。
重定向发生在重试的判定之后,如果不满足重试的条件,还需要进一步调用followUpRequest
根据Response
的响 应码(当然,如果直接请求失败,Response
都不存在就会抛出异常)。followup
最大发生20次。
【4】BridgeInterceptor 桥接拦截器
连接应用程序和服务器的桥梁,我们发出的请求将会经过它的处理才能发给服务器,比如设置请求内容长度,编码,gzip
压缩,cookie
等,获取响应后保存Cookie
等操作。这个拦截器相对比较简单。
补全请求头:
在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:
1、 保存cookie,在下次请求则会读取对应的数据设置进入请求头,默认的CookieJar
不提供实现
2、如果使用gzip返回的数据,则使用GzipSource
包装便于解析。
总结
桥接拦截器的执行逻辑主要就是以下几点
对用户构建的Request
进行添加或者删除相关头部信息,以转化成能够真正进行网络请求的Request
将符合网络 请求规范的Request
交给下一个拦截器处理,并获取Response
如果响应体经过了GZIP
压缩,那就需要解压,再构 建成用户可用的Response
并返回
【5】CacheInterceptor缓存拦截器
在发出请求前,判断是否命中缓存。如果命中则可以不请求,直接使用缓存的响应。 (只会存 在Get请求的缓存)
步骤为:
1、 从缓存中获得对应请求的响应缓存
2、创建 CacheStrategy ,创建时会判断是否能够使用缓存,在 CacheStrategy 中存在两个成员: networkRequest 与 cacheResponse 。他们的组合如下:
3、交给下一个责任链继续处理
4、后续工作,返回304
则用缓存的响应;否则使用网络响应并缓存本次响应(只缓存Get请求的响应)
缓存拦截器的工作说起来比较简单,但是具体的实现,需要处理的内容很多。在缓存拦截器中判断是否可以使用缓 存,或是请求服务器都是通过CacheStrategy
判断。
想讲清楚CacheInterceptor有些困难,也比较复杂,所以这里我只是简单介绍一下。
【6】ConnectInterceptor连接拦截器
打开与目标服务器的连接,并执行下一个拦截器。它简短的可以直接完整贴在这里:
public final class ConnectInterceptor implements Interceptor {public final OkHttpClient client;public ConnectInterceptor(OkHttpClient client) {this.client = client;}@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连接池。可复用 的RealConnection
需要:
public boolean isEligible(Address address, @Nullable Route route) {// If this connection is not accepting new streams, we're done.if (allocations.size() >= allocationLimit || noNewStreams) return false;// If the non-host fields of the address don't overlap, we're done.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.}// At this point we don't have a hostname match. But we still be able to carry the requestif// our connection coalescing requirements are met. See also:// https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding// https://daniel.haxx.se/blog//08/18/http2-connection-coalescing/// 1. This connection must be HTTP/2.if (http2Connection == null) return false;// 2. The routes must share an IP address. This requires us to have a DNS address for both// hosts, which only happens after route planning. We can't coalesce connections that use a// proxy, since proxies don't tell us the origin server's IP address.if (route == null) return false;if (route.proxy().type() != Proxy.Type.DIRECT) return false;if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;if (!this.route.socketAddress().equals(route.socketAddress())) return false;// 3. This connection's server certificate's must cover the new host.if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;if (!supportsUrl(address.url())) return false;// 4. Certificate pinning must match the host.try {address.certificatePinner().check(address.url().host(), handshake().peerCertificates());} catch (SSLPeerUnverifiedException e) {return false;}return true; // The caller's address can be carried by this connection.}
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数据的收发。
【7】CallServerInterceptor请求服务连接器
利用HttpCodec
发出请求到服务器并且解析生成Response
。
首先调用httpCodec.writeRequestHeaders(request);
将请求头写入到缓存中(直到调用 flushRequest() 才真正发
送给服务器)。然后马上进行第一个逻辑判断
Response.Builder responseBuilder = null;if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) {// If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100// Continue" response before transmitting the request body. If we don't get that, return// what we did get (such as a 4xx response) without ever transmitting the request body.if ("100-continue".equalsIgnoreCase(request.header("Expect"))) {httpCodec.flushRequest();realChain.eventListener().responseHeadersStart(realChain.call());responseBuilder = httpCodec.readResponseHeaders(true);}if (responseBuilder == null) {// Write the request body if the "Expect: 100-continue" expectation was met.realChain.eventListener().requestBodyStart(realChain.call());long contentLength = request.body().contentLength();CountingSink requestBodyOut =new CountingSink(httpCodec.createRequestBody(request, contentLength));BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut);request.body().writeTo(bufferedRequestBody);bufferedRequestBody.close();realChain.eventListener().requestBodyEnd(realChain.call(),requestBodyOut.successfulCount);} else if (!connection.isMultiplexed()) {//HTTP2多路复用,不需要关闭socket,不管!// If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 // connection// from being reused. Otherwise we're still obligated to transmit the request // body to// leave the connection in a consistent state. streamAllocation.noNewStreams();} }httpCodec.finishRequest();
整个if都和一个请求头有关:Expect: 100-continue
。这个请求头代表了在发送请求体之前需要和服务器确定是 否愿意接受客户端发送的请求体。所以permitsRequestBody
判断为是否会携带请求体的方式(POST),如果命中 if,则会先给服务器发起一次查询是否愿意接收请求体,这时候如果服务器愿意会响应100
(没有响应体,responseBuilder
即为nul)。这时候才能够继续发送剩余请求数据。但是如果服务器不同意接受请求体,那么我们就需要标记该连接不能再被复用,调用noNewStreams()
关闭相关的Socket
。
【8】总结
整个OkHttp功能的实现就在这五个默认的拦截器中,所以先理解拦截器模式的工作机制是先决条件。这五个拦截 器分别为: 重试拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求服务拦截器。每一个拦截器负责的工作不一 样,就好像工厂流水线,最终经过这五道工序,就完成了最终的产品。
但是与流水线不同的是,OkHttp中的拦截器每次发起请求都会在交给下一个拦截器之前干一些事情,在获得了结 果之后又干一些事情。整个过程在请求向是顺序的,而响应向则是逆序。
当用户发起一个请求后,会由任务分发起 Dispatcher 将请求包装并交给重试拦截器处理。
1、重试拦截器在交出(交给下一个拦截器)之前,负责判断用户是否取消了请求;在获得了结果之后,会根据响应码判断是否需要重定向,如果满足条件那么就会重启执行所有拦截器。
2、桥接拦截器在交出之前,负责将HTTP协议必备的请求头加入其中(如:Host)并添加一些默认的行为(如:GZIP 压缩);在获得了结果后,调用保存cookie接口并解析GZIP数据。
3、缓存拦截器顾名思义,交出之前读取并判断是否使用缓存;获得结果后判断是否缓存。
4、连接拦截器在交出之前,负责找到或者新建一个连接,并获得对应的socket流;在获得结果后不进行额外的处
理。
5、请求服务器拦截器进行真正的与服务器的通信,向服务器发送数据,解析读取的响应数据。 在经过了这一系列的流程后,就完成了一次HTTP请求!
如果觉得《OKHttp五大拦截器》对你有帮助,请点赞、收藏,并留下你的观点哦!