热门标签 | 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;

推荐阅读
  • IOS Run loop详解
    为什么80%的码农都做不了架构师?转自http:blog.csdn.netztp800201articledetails9240913感谢作者分享Objecti ... [详细]
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • 本文总结了一些开发中常见的问题及其解决方案,包括特性过滤器的使用、NuGet程序集版本冲突、线程存储、溢出检查、ThreadPool的最大线程数设置、Redis使用中的问题以及Task.Result和Task.GetAwaiter().GetResult()的区别。 ... [详细]
  • 优化后的标题:深入探讨网关安全:将微服务升级为OAuth2资源服务器的最佳实践
    本文深入探讨了如何将微服务升级为OAuth2资源服务器,以订单服务为例,详细介绍了在POM文件中添加 `spring-cloud-starter-oauth2` 依赖,并配置Spring Security以实现对微服务的保护。通过这一过程,不仅增强了系统的安全性,还提高了资源访问的可控性和灵活性。文章还讨论了最佳实践,包括如何配置OAuth2客户端和资源服务器,以及如何处理常见的安全问题和错误。 ... [详细]
  • HTTP协议作为互联网通信的基础,其重要性不言而喻。相比JDK自带的URLConnection,HttpClient不仅提升了易用性和灵活性,还在性能、稳定性和安全性方面进行了显著优化。本文将深入解析HttpClient的使用方法与技巧,帮助开发者更好地掌握这一强大的工具。 ... [详细]
  • SQLmap自动化注入工具命令详解(第28-29天 实战演练)
    SQL注入工具如SQLMap等在网络安全测试中广泛应用。SQLMap是一款开源的自动化SQL注入工具,支持12种不同的数据库,具体支持的数据库类型可在其插件目录中查看。作为当前最强大的注入工具之一,SQLMap在实际应用中具有极高的效率和准确性。 ... [详细]
  • HTML5 Web存储技术是许多开发者青睐本地应用程序的重要原因之一,因为它能够实现在客户端本地存储数据。HTML5通过引入Web Storage API,使得Web应用程序能够在浏览器中高效地存储数据,从而提升了应用的性能和用户体验。相较于传统的Cookie机制,Web Storage不仅提供了更大的存储容量,还简化了数据管理和访问的方式。本文将从基础概念、关键技术到实际应用,全面解析HTML5 Web存储技术,帮助读者深入了解其工作原理和应用场景。 ... [详细]
  • 深入浅出解析HTTP协议的核心功能与应用
    前言——协议是指预先设定的通信规则,确保双方能够按照既定标准进行有效沟通,从而实现准确的信息交换。例如,驯兽师通过拍手使动物坐下,这实际上是一种预设的协议。本文将详细探讨HTTP协议的核心功能及其广泛应用,解析其在现代网络通信中的重要作用。 ... [详细]
  • 在《OWASP TOP 10 注入漏洞》中,详细探讨了注入攻击的发生机制:当应用程序未能有效识别和拦截恶意输入时,攻击者可以通过 SQL 注入等手段利用这一漏洞。本文将重点介绍 SQL 注入的基本原理及其防范措施,帮助读者全面了解并有效应对这一常见安全威胁。 ... [详细]
  • 开发笔记:深度探索!Android之OkHttp网络架构源码解析
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了深度探索!Android之OkHttp网络架构源码解析相关的知识,希望对你有一定的参考价值。 ... [详细]
  • 本文分享了一位Android开发者多年来对于Android开发所需掌握的技能的笔记,包括架构师基础、高级UI开源框架、Android Framework开发、性能优化、音视频精编源码解析、Flutter学习进阶、微信小程序开发以及百大框架源码解读等方面的知识。文章强调了技术栈和布局的重要性,鼓励开发者做好学习规划和技术布局,以提升自己的竞争力和市场价值。 ... [详细]
  • 在 Ubuntu 中遇到 Samba 服务器故障时,尝试卸载并重新安装 Samba 发现配置文件未重新生成。本文介绍了解决该问题的方法。 ... [详细]
  • Java Socket 关键参数详解与优化建议
    Java Socket 的 API 虽然被广泛使用,但其关键参数的用途却鲜为人知。本文详细解析了 Java Socket 中的重要参数,如 backlog 参数,它用于控制服务器等待连接请求的队列长度。此外,还探讨了其他参数如 SO_TIMEOUT、SO_REUSEADDR 等的配置方法及其对性能的影响,并提供了优化建议,帮助开发者提升网络通信的稳定性和效率。 ... [详细]
  • 本文作为探讨PHP依赖注入容器系列文章的开篇,将首先通过具体示例详细阐述依赖注入的基本概念及其重要性,为后续深入解析容器的实现奠定基础。 ... [详细]
  • Sanic 是一个类似于 Flask 的 Python 3.5 Web 服务器,以其出色的写入速度而著称。与 Flask 不同,Sanic 支持异步请求处理,这使得它在处理高并发请求时表现更加出色。通过利用 Python 的异步特性,Sanic 能够显著提高应用程序的性能和响应能力,适用于构建高性能的异步 Web 应用。 ... [详细]
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社区 版权所有