热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

OkHttp基本概念以及源码解析

OkHttp源码解析一、简介Okhttp是一个高性能的处理网络请求的框架,由Square公司开发。其初始流程为下图所示:Okhttp的子系统层级结

OkHttp源码解析


一、简介

Okhttp是一个高性能的处理网络请求的框架,由Square公司开发。其初始流程为下图所示:
在这里插入图片描述

Okhttp的子系统层级结构如下图:


  • 网络配置层:利用Builder模式配置各种参数,例如:超时时间、拦截器等,这些参数都会由Okhttp分发给各个需要的子系统。
    重定向层:负责重定向。

  • **Header拼接层:**负责把用户构造的请求转换为发送给服务器的请求,把服务器返回的响应转换为对用户友好的响应。

  • HTTP缓存层:负责读取缓存以及更新缓存。

  • 连接层:连接层是一个比较复杂的层级,它实现了网络协议、内部的拦截器、安全性认证,连接与连接池等功能,但这一层还没有发起真正的连接,它只是做了连接器一些参数的处理。

  • 数据响应层:负责从服务器读取响应的数据。


二、基本使用


2.1 创建 OkHttpClient 对象

/**创建OkHttpClient的使用代码*/
OkHttpClient client = new OkHttpClient();/**创建的源码如下*/
public OkHttpClient() {this(new Builder()); //调用内部的Builder方法创建相关数据
}
public Builder() {dispatcher = new Dispatcher();protocols = DEFAULT_PROTOCOLS;connectionSpecs = DEFAULT_CONNECTION_SPECS;proxySelector = ProxySelector.getDefault();COOKIEJar = COOKIEJar.NO_COOKIES;socketFactory = SocketFactory.getDefault();hostnameVerifier = OkHostnameVerifier.INSTANCE;certificatePinner = CertificatePinner.DEFAULT;proxyAuthenticator = Authenticator.NONE;authenticator = Authenticator.NONE;connectionPool = new ConnectionPool();dns = Dns.SYSTEM;followSslRedirects = true;followRedirects = true;retryOnConnectionFailure = true;connectTimeout = 10_000;readTimeout = 10_000;writeTimeout = 10_000;
}

2.2 发起 HTTP 请求

String run(String url) throws IOException {Request request = new Request.Builder().url(url).build();Response response = client.newCall(request).execute();return response.body().string();
}

OkHttpClient实现了Call.Factory,负责根据请求创建新的Call。下面我们来看看它是如何创建 Call 的:

/*** Prepares the {@code request} to be executed at some point in the future.*/
@Override public Call newCall(Request request) {return new RealCall(this, request); //调用new RealCall(this, request)创建的Call
}

  • 同步网络请求

    上面介绍到Call的创建是由RealCall完成的,下面介绍RealCall#execute:

    @Override public Response execute() throws IOException {synchronized (this) {if (executed) throw new IllegalStateException("Already Executed"); // (1)executed = true;}try {client.dispatcher().executed(this); // (2)Response result = getResponseWithInterceptorChain(); // (3)if (result == null) throw new IOException("Canceled");return result;} finally {client.dispatcher().finished(this); // (4)}
    }

    (1) 检查这个Call是否被执行,每个Call只能被执行一次,如果想要完全一样的Call,可以利用Call#clone方法来进行克隆。

    (2) 利用client.dispatcher().executed(this)来进行实际执行dispatcher是刚才看到的OkHttpClient.Builder的成员之一,它的文档说自己是异步 HTTP 请求的执行策略,现在看来,同步请求它也有掺和。

    (3) 调用getResponseWithInterceptorChain()函数获取 HTTP 返回结果,从函数名可以看出,这一步还会进行一系列“拦截”操作。

    (4) 最后还要通知dispatcher自己已经执行完毕。

    dispatcher 这里我们不过度关注,在同步执行的流程中,涉及到 dispatcher 的内容只不过是告知它我们的执行状态,比如开始执行了(调用executed),比如执行完毕了(调用finished),在异步执行流程中它会有更多的参与。

    真正发出网络请求,解析返回结果的,还是getResponseWithInterceptorChain:

    private Response getResponseWithInterceptorChain() throws IOException {// Build a full stack of interceptors.List<Interceptor> interceptors &#61; 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 (!retryAndFollowUpInterceptor.isForWebSocket()) {interceptors.addAll(client.networkInterceptors());}interceptors.add(new CallServerInterceptor(retryAndFollowUpInterceptor.isForWebSocket()));Interceptor.Chain chain &#61; new RealInterceptorChain(interceptors, null, null, null, 0, originalRequest);return chain.proceed(originalRequest);
    }

    在上述方法中&#xff0c;我们应该可以看出Interceptor这个东西很重要&#xff0c;不要误以为它只负责拦截请求进行一些额外的处理&#xff08;例如 COOKIE&#xff09;&#xff0c;实际上它把实际的网络请求、缓存、透明压缩等功能都统一了起来&#xff0c;每一个功能都只是一个Interceptor&#xff0c;它们再连接成一个Interceptor.Chain&#xff0c;环环相扣&#xff0c;最终圆满完成一次网络请求。

    getResponseWithInterceptorChain函数我们可以看到Interceptor.Chain的分布依次是&#xff1a;

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mjyEHZQ6-1597375077966)(https://blog.piasy.com/img/201607/okhttp_interceptors.png “okhttp_interceptors”)]

    1. 在配置OkHttpClient时设置的interceptors&#xff1b;
    2. 负责失败重试以及重定向的RetryAndFollowUpInterceptor&#xff1b;
    3. 负责把用户构造的请求转换为发送到服务器的请求、把服务器返回的响应转换为用户友好的响应的BridgeInterceptor&#xff1b;
    4. 负责读取缓存直接返回、更新缓存的CacheInterceptor&#xff1b;
    5. 负责和服务器建立连接的ConnectInterceptor&#xff1b;
    6. 配置OkHttpClient时设置的networkInterceptors&#xff1b;
    7. 负责向服务器发送请求数据、从服务器读取响应数据CallServerInterceptor

    在这里&#xff0c;位置决定了功能&#xff0c;最后一个 Interceptor 一定是负责和服务器实际通讯的&#xff0c;重定向、缓存等一定是在实际通讯之前的。

    责任链模式在这个Interceptor链条中得到了很好的实践。

    它包含了一些命令对象和一系列的处理对象&#xff0c;每一个处理对象决定它能处理哪些命令对象&#xff0c;它也知道如何将它不能处理的命令对象传递给该链中的下一个处理对象。该模式还描述了往该处理链的末尾添加新的处理对象的方法。

    对于把Request变成Response这件事来说&#xff0c;每个Interceptor都可能完成这件事&#xff0c;所以我们循着链条让每个Interceptor自行决定能否完成任务以及怎么完成任务&#xff08;自力更生或者交给下一个Interceptor&#xff09;。这样一来&#xff0c;完成网络请求这件事就彻底从RealCall类中剥离了出来&#xff0c;简化了各自的责任和逻辑。两个字&#xff1a;优雅&#xff01;

    责任链模式在安卓系统中也有比较典型的实践&#xff0c;例如 view 系统对点击事件&#xff08;TouchEvent&#xff09;的处理。

    回到 OkHttp&#xff0c;在这里我们先简单分析一下ConnectInterceptorCallServerInterceptor&#xff0c;看看 OkHttp 是怎么进行和服务器的实际通信的。

    建立连接&#xff1a;ConnectInterceptor

    &#64;Override public Response intercept(Chain chain) throws IOException {RealInterceptorChain realChain &#61; (RealInterceptorChain) chain;Request request &#61; realChain.request();StreamAllocation streamAllocation &#61; realChain.streamAllocation();// We need the network to satisfy this request. Possibly for validating a conditional GET.boolean doExtensiveHealthChecks &#61; !request.method().equals("GET");HttpCodec httpCodec &#61; streamAllocation.newStream(client, doExtensiveHealthChecks);RealConnection connection &#61; streamAllocation.connection();return realChain.proceed(request, streamAllocation, httpCodec, connection);
    }

    实际上建立连接就是创建了一个HttpCodec对象&#xff0c;它将在后面的步骤中被使用&#xff0c;那它又是何方神圣呢&#xff1f;它是对 HTTP 协议操作的抽象&#xff0c;有两个实现&#xff1a;Http1CodecHttp2Codec&#xff0c;顾名思义&#xff0c;它们分别对应 HTTP/1.1 和 HTTP/2 版本的实现。

    Http1Codec中&#xff0c;它利用Okio对Socket的读写操作进行封装&#xff0c;Okio 以后有机会再进行分析&#xff0c;现在让我们对它们保持一个简单地认识&#xff1a;它对java.iojava.nio进行了封装&#xff0c;让我们更便捷高效的进行 IO 操作。

    而创建HttpCodec对象的过程涉及到StreamAllocationRealConnection&#xff0c;代码较长&#xff0c;这里就不展开&#xff0c;这个过程概括来说&#xff0c;就是找到一个可用的RealConnection&#xff0c;再利用RealConnection的输入输出&#xff08;BufferedSourceBufferedSink&#xff09;创建HttpCodec对象&#xff0c;供后续步骤使用。

    发送和接收数据&#xff1a;CallServerInterceptor

    &#64;Override public Response intercept(Chain chain) throws IOException {HttpCodec httpCodec &#61; ((RealInterceptorChain) chain).httpStream();StreamAllocation streamAllocation &#61; ((RealInterceptorChain) chain).streamAllocation();Request request &#61; chain.request();long sentRequestMillis &#61; System.currentTimeMillis();httpCodec.writeRequestHeaders(request);if (HttpMethod.permitsRequestBody(request.method()) && request.body() !&#61; null) {Sink requestBodyOut &#61; httpCodec.createRequestBody(request, request.body().contentLength());BufferedSink bufferedRequestBody &#61; Okio.buffer(requestBodyOut);request.body().writeTo(bufferedRequestBody);bufferedRequestBody.close();}httpCodec.finishRequest();Response response &#61; httpCodec.readResponseHeaders().request(request).handshake(streamAllocation.connection().handshake()).sentRequestAtMillis(sentRequestMillis).receivedResponseAtMillis(System.currentTimeMillis()).build();if (!forWebSocket || response.code() !&#61; 101) {response &#61; response.newBuilder().body(httpCodec.openResponseBody(response)).build();}if ("close".equalsIgnoreCase(response.request().header("Connection"))|| "close".equalsIgnoreCase(response.header("Connection"))) {streamAllocation.noNewStreams();}// 省略部分检查代码return response;
    }

    我们抓住主干部分&#xff1a;

    1. 向服务器发送 request header&#xff1b;
    2. 如果有 request body&#xff0c;就向服务器发送&#xff1b;
    3. 读取 response header&#xff0c;先构造一个Response对象&#xff1b;
    4. 如果有 response body&#xff0c;就在 3 的基础上加上 body 构造一个新的Response对象&#xff1b;

    这里我们可以看到&#xff0c;核心工作都由HttpCodec对象完成&#xff0c;而HttpCodec实际上利用的是 Okio&#xff0c;而 Okio 实际上还是用的Socket&#xff0c;所以没什么神秘的&#xff0c;只不过一层套一层&#xff0c;层数有点多。

    其实Interceptor的设计也是一种分层的思想&#xff0c;每个Interceptor就是一层。为什么要套这么多层呢&#xff1f;分层的思想在 TCP/IP 协议中就体现得淋漓尽致&#xff0c;分层简化了每一层的逻辑&#xff0c;每层只需要关注自己的责任&#xff08;单一原则思想也在此体现&#xff09;&#xff0c;而各层之间通过约定的接口/协议进行合作&#xff08;面向接口编程思想&#xff09;&#xff0c;共同完成复杂的任务。

    简单应该是我们的终极追求之一&#xff0c;尽管有时为了达成目标不得不复杂&#xff0c;但如果有另一种更简单的方式&#xff0c;我想应该没有人不愿意替换。

  • 异步网络请求

    client.newCall(request).enqueue(new Callback() {&#64;Overridepublic void onFailure(Call call, IOException e) {}&#64;Overridepublic void onResponse(Call call, Response response) throws IOException {System.out.println(response.body().string());}
    });
    // RealCall#enqueue
    &#64;Override public void enqueue(Callback responseCallback) {synchronized (this) {if (executed) throw new IllegalStateException("Already Executed");executed &#61; true;}client.dispatcher().enqueue(new AsyncCall(responseCallback));
    }
    // Dispatcher#enqueue
    synchronized void enqueue(AsyncCall call) {if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {runningAsyncCalls.add(call);executorService().execute(call);} else {readyAsyncCalls.add(call);}
    }

    这里我们就能看到 dispatcher 在异步执行时发挥的作用了&#xff0c;如果当前还能执行一个并发请求&#xff0c;那就立即执行&#xff0c;否则加入readyAsyncCalls队列&#xff0c;而正在执行的请求执行完毕之后&#xff0c;会调用promoteCalls()函数&#xff0c;来把readyAsyncCalls队列中的AsyncCall“提升”为runningAsyncCalls&#xff0c;并开始执行。

    这里的AsyncCallRealCall的一个内部类&#xff0c;它实现了Runnable&#xff0c;所以可以被提交到ExecutorService上执行&#xff0c;而它在执行时会调用getResponseWithInterceptorChain()函数&#xff0c;并把结果通过responseCallback传递给上层使用者。

    这样看来&#xff0c;同步请求和异步请求的原理是一样的&#xff0c;都是在getResponseWithInterceptorChain()函数中通过Interceptor链条来实现的网络请求逻辑&#xff0c;而异步则是通过ExecutorService实现。


2.3 返回数据的获取

在上述同步&#xff08;Call#execute()执行之后&#xff09;或者异步&#xff08;Callback#onResponse()回调中&#xff09;请求完成之后&#xff0c;我们就可以从Response对象中获取到响应数据了&#xff0c;包括 HTTP status code&#xff0c;status message&#xff0c;response header&#xff0c;response body 等。这里 body 部分最为特殊&#xff0c;因为服务器返回的数据可能非常大&#xff0c;所以必须通过数据流的方式来进行访问&#xff08;当然也提供了诸如string()bytes()这样的方法将流内的数据一次性读取完毕&#xff09;&#xff0c;而响应中其他部分则可以随意获取。

响应 body 被封装到ResponseBody类中&#xff0c;该类主要有两点需要注意&#xff1a;


  1. 每个 body 只能被消费一次&#xff0c;多次消费会抛出异常&#xff1b;
  2. body 必须被关闭&#xff0c;否则会发生资源泄漏&#xff1b;

在2.2.1.2.发送和接收数据&#xff1a;CallServerInterceptor小节中&#xff0c;我们就看过了 body 相关的代码&#xff1a;

if (!forWebSocket || response.code() !&#61; 101) {response &#61; response.newBuilder().body(httpCodec.openResponseBody(response)).build();
}

HttpCodec#openResponseBody提供具体 HTTP 协议版本的响应 body&#xff0c;而HttpCodec则是利用 Okio 实现具体的数据 IO 操作。

这里有一点值得一提&#xff0c;OkHttp 对响应的校验非常严格&#xff0c;HTTP status line 不能有任何杂乱的数据&#xff0c;否则就会抛出异常&#xff0c;在我们公司项目的实践中&#xff0c;由于服务器的问题&#xff0c;偶尔 status line 会有额外数据&#xff0c;而服务端的问题也毫无头绪&#xff0c;导致我们不得不忍痛继续使用 HttpUrlConnection&#xff0c;而后者在一些系统上又存在各种其他的问题&#xff0c;例如魅族系统发送 multi-part form 的时候就会出现没有响应的问题。


2.4 HTTP 缓存

在2.2.1.同步网络请求小节中&#xff0c;我们已经看到了Interceptor的布局&#xff0c;在建立连接、和服务器通讯之前&#xff0c;就是CacheInterceptor&#xff0c;在建立连接之前&#xff0c;我们检查响应是否已经被缓存、缓存是否可用&#xff0c;如果是则直接返回缓存的数据&#xff0c;否则就进行后面的流程&#xff0c;并在返回之前&#xff0c;把网络的数据写入缓存。

这块代码比较多&#xff0c;但也很直观&#xff0c;主要涉及 HTTP 协议缓存细节的实现&#xff0c;而具体的缓存逻辑 OkHttp 内置封装了一个Cache类&#xff0c;它利用DiskLruCache&#xff0c;用磁盘上的有限大小空间进行缓存&#xff0c;按照 LRU 算法进行缓存淘汰&#xff0c;这里也不再展开。

我们可以在构造OkHttpClient时设置Cache对象&#xff0c;在其构造函数中我们可以指定目录和缓存大小&#xff1a;

public Cache(File directory, long maxSize);

而如果我们对 OkHttp 内置的Cache类不满意&#xff0c;我们可以自行实现InternalCache接口&#xff0c;在构造OkHttpClient时进行设置&#xff0c;这样就可以使用我们自定义的缓存策略了。


三、总结

OkHttp 还有很多细节部分没有在本文展开&#xff0c;例如 HTTP2/HTTPS 的支持等&#xff0c;但建立一个清晰的概览非常重要。对整体有了清晰认识之后&#xff0c;细节部分如有需要&#xff0c;再单独深入将更加容易。


  • OkHttpClient实现Call.Factory&#xff0c;负责为Request创建Call&#xff1b;
  • RealCall为具体的Call实现&#xff0c;其enqueue()异步接口通过Dispatcher利用ExecutorService实现&#xff0c;而最终进行网络请求时和同步execute()接口一致&#xff0c;都是通过getResponseWithInterceptorChain()函数实现&#xff1b;
  • getResponseWithInterceptorChain()中利用Interceptor链条&#xff0c;分层实现缓存、透明压缩、网络 IO 等功能&#xff1b;

推荐阅读
  • 在CentOS/RHEL 7/6,Fedora 27/26/25上安装JAVA 9的步骤和方法
    本文介绍了在CentOS/RHEL 7/6,Fedora 27/26/25上安装JAVA 9的详细步骤和方法。首先需要下载最新的Java SE Development Kit 9发行版,然后按照给出的Shell命令行方式进行安装。详细的步骤和方法请参考正文内容。 ... [详细]
  • 找到JDK下载URL当然去官网找了。目前最新的1.8的下载URL(RPM)如下:http:download.oracle.comotn-pubjavajdk8u161-b122f3 ... [详细]
  • 注意:以下分析都是基于Retrofit2转载请注明出处:http:blog.csdn.netevan_manarticledetails51320637本节是《Retrofit的使 ... [详细]
  • 跨站的艺术XSS Fuzzing 的技巧
    作者|张祖优(Fooying)腾讯云云鼎实验室对于XSS的漏洞挖掘过程,其实就是一个使用Payload不断测试和调整再测试的过程,这个过程我们把它叫做F ... [详细]
  • 有些网站设置了权限,只有在登录了之后才能爬取网站的内容,如何模拟登录,目前的方法主要是利用浏览器cookie模拟登录。浏览器访问服务器的过程在用户访问网页时,不论是通过URL输入域名或I ... [详细]
  • \\w+([-+.&amp;#39;]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*if(string.IsNullOrEmpty(Tex ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 本文介绍了作者在开发过程中遇到的问题,即播放框架内容安全策略设置不起作用的错误。作者通过使用编译时依赖注入的方式解决了这个问题,并分享了解决方案。文章详细描述了问题的出现情况、错误输出内容以及解决方案的具体步骤。如果你也遇到了类似的问题,本文可能对你有一定的参考价值。 ... [详细]
  • 本文介绍了OkHttp3的基本使用和特性,包括支持HTTP/2、连接池、GZIP压缩、缓存等功能。同时还提到了OkHttp3的适用平台和源码阅读计划。文章还介绍了OkHttp3的请求/响应API的设计和使用方式,包括阻塞式的同步请求和带回调的异步请求。 ... [详细]
  • 单页面应用 VS 多页面应用的区别和适用场景
    本文主要介绍了单页面应用(SPA)和多页面应用(MPA)的区别和适用场景。单页面应用只有一个主页面,所有内容都包含在主页面中,页面切换快但需要做相关的调优;多页面应用有多个独立的页面,每个页面都要加载相关资源,页面切换慢但适用于对SEO要求较高的应用。文章还提到了两者在资源加载、过渡动画、路由模式和数据传递方面的差异。 ... [详细]
  • Apache Shiro 身份验证绕过漏洞 (CVE202011989) 详细解析及防范措施
    本文详细解析了Apache Shiro 身份验证绕过漏洞 (CVE202011989) 的原理和影响,并提供了相应的防范措施。Apache Shiro 是一个强大且易用的Java安全框架,常用于执行身份验证、授权、密码和会话管理。在Apache Shiro 1.5.3之前的版本中,与Spring控制器一起使用时,存在特制请求可能导致身份验证绕过的漏洞。本文还介绍了该漏洞的具体细节,并给出了防范该漏洞的建议措施。 ... [详细]
  • 渗透测试基础bypass绕过阻挡我们的WAF(下)
    渗透测试基础-bypass ... [详细]
  • 一.常见基于身份识别进行反爬1通过headers字段来反爬headers中有很多字段,这些字段都有可能会被对方服务器拿过来进行判断是否为爬虫1.1通过headers中的User-A ... [详细]
  • Java大文件HTTP断点续传到服务器该怎么做?
    最近由于笔者所在的研发集团产品需要,需要支持高性能的大文件http上传,并且要求支持http断点续传。这里在简要归纳一下,方便记忆 ... [详细]
  • 这篇文章主要介绍PHP如何使用在全部作用域中始终可用的内置变量,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要 ... [详细]
author-avatar
茫茫人海啊啊啊_574
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有