Android OkHttp源码分析--拦截器

news2024/12/23 22:16:14

拦截器责任链:

OkHttp最核心的工作是在 getResponseWithInterceptorChain() 中进行,在进入这个方法分析之前,我们先来了 解什么是责任链模式,因为此方法就是利用的责任链模式完成一步步的请求。 

拦截器流程:

OkHttp中的 getResponseWithInterceptorChain() 中经历的流程为:

请求会被交给责任链中的一个个拦截器。默认情况下有五大拦截器:

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

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

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

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

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

拦截器详情:

一、重试及重定向拦截器

第一个拦截器: 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 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 {
    //...
}

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

总结:

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

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

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

二、桥接拦截器

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

在补全了请求头后交给下一个拦截器处理,得到响应后,主要干两件事情:

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

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

总结:

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

三、缓存拦截器

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

1、从缓存中获得对应请求的响应缓存;

2、创建 CacheStrategy ,创建时会判断是否能够使用缓存,在 CacheStrategy 中存在两个成员: networkRequest 与 cacheResponse 。

他们的组合如下:

3、交给下一个责任链继续处理;

4、后续工作,返回304则用缓存的响应;否则使用网络响应并缓存本次响应(只缓存Get请求的响应)。

缓存拦截器的工作说起来比较简单,但是具体的实现,需要处理的内容很多。在缓存拦截器中判断是否可以使用缓 存,或是请求服务器都是通过 CacheStrategy 判断。

总结:

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 ,打开与目标服务器的连接,并执行下一个拦截器。它简短的可以直接完整贴在这里:

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 需要:

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

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

总结:

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

五、请求服务器拦截器

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。

后续代码为:

if (responseBuilder == null) {
    realChain.eventListener().responseHeadersStart(realChain.call());
    responseBuilder = httpCodec.readResponseHeaders(false);
}
Response response = responseBuilder
        .request(request)
        .handshake(streamAllocation.connection().handshake())
        .sentRequestAtMillis(sentRequestMillis)
        .receivedResponseAtMillis(System.currentTimeMillis())
        .build();

这时 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 ?那此时的响应头是第一次查询 服务器是否支持接受请求体的,而不是真正的请求对应的结果响应。

所以紧接着:

int code = response.code();
if (code == 100) {
    // server sent a 100-continue even though we did not request one.
    // try again to read the actual response
    responseBuilder = httpCodec.readResponseHeaders(false);
    response = responseBuilder
            .request(request)
            .handshake(streamAllocation.connection().handshake())
            .sentRequestAtMillis(sentRequestMillis)
            .receivedResponseAtMillis(System.currentTimeMillis())
            .build();
    code = response.code();
}

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

最后:

if (forWebSocket && code == 101) {
    // Connection is upgrading, but we need to ensure interceptors see a non-null
    // response body.
    response = response.newBuilder()
            .body(Util.EMPTY_RESPONSE)
            .build();
} else {
    response = response.newBuilder()
            .body(httpCodec.openResponseBody(response))
            .build();
}
if ("close".equalsIgnoreCase(response.request().header("Connection"))
        || "close".equalsIgnoreCase(response.header("Connection"))) {
    streamAllocation.noNewStreams();
}
if ((code == 204 || code == 205) && response.body().contentLength() > 0) {
    throw new ProtocolException(
            "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength());
}
return response;

forWebSocket 代表websocket的请求,我们直接进入else,这里就是读取响应体数据。然后判断请求和服务器是 不是都希望长连接,一旦有一方指明 close ,那么就需要关闭 socket 。而如果服务器返回204/205,一般情况而 言不会存在这些返回码,但是一旦出现这意味着没有响应体,但是解析到的响应头中包含 Content-Lenght 且不为 0,这表响应体的数据字节长度。此时出现了冲突,直接抛出协议异常!

总结:

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

拦截器总结:

整个OkHttp功能的实现就在这五个默认的拦截器中,所以先理解拦截器模式的工作机制是先决条件。这五个拦截 器分别为: 重试拦截器、桥接拦截器、缓存拦截器、连接拦截器、请求服务拦截器。每一个拦截器负责的工作不一 样,就好像工厂流水线,最终经过这五道工序,就完成了最终的产品。

但是与流水线不同的是,OkHttp中的拦截器每次发起请求都会在交给下一个拦截器之前干一些事情,在获得了结 果之后又干一些事情。整个过程在请求向是顺序的,而响应向则是逆序。

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

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

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

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/850388.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

支付整体架构

5.4 支付的技术架构 架构即未来,只有建立在技术架构设计良好的体系上,支付机构才能有美好的未来。如果支付的技术体系在架构上存在问题,那么就没有办法实现高可用性、高安全性、高效率和水平可扩展性。 总结多年来在海内外支付机构主持和参与…

C#在自动化领域的应用前景与潜力

人机界面(HMI)开发:使用C#开发人机界面软件,实现与自动化设备的交互和监控。C#的图形界面设计能力和丰富的控件库使得开发人员能够创建直观、易用的界面。 数据采集与处理:C#可以与各种传感器、设备进行数据通信和采集…

机加工行业如何做好生产管理?

导 读 ( 文/ 2715 ) 机加工行业是制造业中的一个重要领域,它涉及将原材料通过机械设备进行切削、加工和加工成形的过程。 机械加工通常从原料开始,通过不断的切削或去除材料的过程,逐步将工件加工成所需的形状和尺寸。这个过程中&#xff0…

SpringBoot 2.1.7.RELEASE + Activiti 5.18.0 喂饭级练习手册

环境准备 win10 eclipse 2023-03 eclipse Activiti插件 Mysql 5.x Activiti的作用等不再赘叙&#xff0c;直接上代码和细节 POM <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId>…

JVM基础篇-直接内存

JVM基础篇-直接内存 什么是直接内存? 直接内存( 堆外内存 ) 指的是 Java 应用程序通过直接方式从操作系统中申请的内存,这块内存不属于jvm 传统方式读取文件 首先会从用户态切换到内核态&#xff0c;调用操作系统函数从磁盘读取文件&#xff0c;读取一部分到操作系统缓冲区…

我的Python教程:使用Pyecharts画柱状图

Pyecharts是一个用于生成 Echarts 图表的 Python 库。Echarts 是一个基于 JavaScript 的数据可视化库&#xff0c;提供了丰富的图表类型和交互功能。通过 Pyecharts&#xff0c;你可以使用 Python 代码生成各种类型的 Echarts 图表&#xff0c;例如折线图、柱状图、饼图、散点图…

Linux学习之sed替换命令加强版

参考文章&#xff1a;《Shell 编程–Sed》 cat /etc/redhat-release看到操作系统是CentOS Linux release 7.6.1810&#xff0c;uname -r看到内核版本是3.10.0-957.el7.x86_64&#xff0c;bash --version可以看到bash版本是4.2.46(2)。 echo a : 1 : good :::: >> sedpl…

docker部署

docker部署 1.Docker2.mysql5.73.Redis4.ES&Kibana&IK分词器 1.Docker Docker 安装官方文档&#xff1a;https://docs.docker.com/install/linux/docker-ce/centos/ 1.卸载之前的docker sudo yum remove docker \ docker-client \ docker-client-latest \ docker-com…

springboot工程使用阿里云OSS传输文件

在application.yml文件中引入对应的配置&#xff0c;一个是对应的节点&#xff0c;两个是密钥和账号&#xff0c;还有一个是对应文件的名称&#xff1b; 采用这样方式进行解耦&#xff0c;便于后期修改。 然后需要设置一个properties类&#xff0c;去读对应的配置信息 用到了…

Vue2源码分析-day2

实现数组的响应式原理 首先我们在index.html中定义一个数组&#xff0c;并且打印实例 const vm new MVue({data() {return {name: "zhangsan",age: "16",hobby:[zhangsan,lisi]}} }) console.log(vm);我们会发现定义的数组每一项都有get和set方法虽然数…

14.3.4 【Linux】使用 LVM thin Volume 让 LVM 动态自动调整磁盘使用率

想像一个情况&#xff0c;你有个目录未来会使用到大约 5T 的容量&#xff0c;但是目前你的磁盘仅有 3T&#xff0c;问题是&#xff0c;接下来的两个月你的系统都还不会超过 3T 的容量&#xff0c; 不过你想要让用户知道&#xff0c;就是他最多有 5T 可以使用就是了&#xff01;…

并发多线程篇

线程的基础知识 面试题1&#xff1a;线程与进程的区别&#xff1f; 面试题2&#xff1a;并行和并发有什么区别&#xff1f; 面试题3&#xff1a;创建线程的方式有哪些&#xff1f; 面试题 4&#xff1a;runnable 和 callable 有什么区别&#xff1f; 面试题5&#xff1a;线程…

基于Centos7的Nginx源码安装

目录 1、准备安装环境 2、获取tar包&#xff1a; 3、解压创建软链接 4、创建用户和组 5、执行安装 6、创建服务脚本 7、开启nginx&#xff1a;​编辑​编辑 1、准备安装环境 yum insatall -y make gcc gcc-c pcre-devel #pcre-devel -- pcre库 #安装openssl-devel yum …

基于 CentOS 7 构建 LVS-DR 群集以及配置nginx负载均衡

目录 一、基于 CentOS 7 构建 LVS-DR 群集 1、前期准备 1、关闭防火墙 2、安装ifconfig 3、准备四台虚拟机 2、在DS上 2.1、配置LVS虚拟IP 2.2、手工执行配置添加LVS服务并增加两台RS 2.3、查看配置 3、在RS端&#xff08;第三台、第四台&#xff09; 上 3.1、配置W…

H7-TOOL的高速DAPLINK用于新版STM32CubeIDE V1.13及其以上版本的超简单实现方法(2023-08-08)

之前分享了一个方法&#xff0c;太繁琐了&#xff0c;H7-TOOL群的群友提供了一个方法&#xff0c;实现非常简单。1、使用STM32CubeMX或者自己创建一个STM32CubeIDE工程后&#xff0c;设置这两个地方即可&#xff1a; 配置调试器&#xff0c;设置完毕记得点击右下角的Apply 2、然…

【个人记录】CentOS7 编译安装最新版本Git

说明 使用yum install git安装的git版本是1.8&#xff0c;并不是最新版本&#xff0c;使用gitlab-runner托管时候会拉项目失败&#xff0c;这里使用编译源码方式安装最新版本的git。 基础环境安装 echo "nameserver 8.8.8.8" >> /etc/resolv.conf curl -o /…

Excel表格(一)

1.单一栏的宽度和高度设置 2.大标题的跨栏居中 3.让单元格内的文字------自动适应 4.序号递增 5.货币符号 6.日期格式的选择 选到单元格&#xff0c;选中对应的日期格式 7.自动求和的计算 然后在按住回车键即可求出当前行的金额 点击自动求和 8.冻结表格栏 9.排序 1.单栏排序 …

【性能类】—页面性能类

一、提升页面性能的方法有哪些&#xff1f; 1. 资源压缩合并&#xff0c;减少HTTP请求 图片、视频、js、css等资源压缩合并&#xff0c;开启HTTP压缩&#xff0c;把资源文件变小 2. 非核心代码异步加载 →异步加载的方式 → 异步加载的区别 异步加载的方式 ① 动态脚本加载…

【JavaWeb】 JavaScript 开发利器之 jQuery

&#x1f384;欢迎来到边境矢梦的csdn博文&#xff0c;本文主要讲解Java 中JavaScript 开发利器之 jQuery的相关知识&#x1f384; &#x1f308;我是边境矢梦&#xff0c;一个正在为秋招和算法竞赛做准备的学生&#x1f308; &#x1f386;喜欢的朋友可以关注一下&#x1faf0…

QT信号与槽的理解

文章目录 信号与槽的理解 信号与槽的理解 信号就是事件&#xff0c;比如button被点击的事件&#xff0c;ComboBox选项改变的事件&#xff0c;都是信号槽就是对信号进行响应的函数&#xff0c;可以是任意自定义函数一个信号可以对应多个槽多个信号可以对应一个槽信号的参数不能…