okhttp框架的介绍
对于http交互,android为我们提供了什么方式呢?
HttpURLConnection 和 Apache HTTP Client,为什么还要使用okhttp呢?那么应该okhttp可以让你的应用更快的运行 网络通信更节省流量
网络上的文章都介绍okhttp是一个高效的http库,而且支持SPDY。不过我真的不知道spdy是什么。那么通过一小节介绍一下SPDY。
okhttp的个主要优点如下:
![](http://upload-images.jianshu.io/upload_images/4395181-cb022433d74b7df8.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/638)
2.1:SPDY(speedy,可以理解为更快)
SPDY是什么?
备注:不探讨spdy如何修改http的细节
spdy 是application layer层的通讯协议,用以替换http,但是沿用了http的语义,SPDY 协议旨在通过压缩、多路复用和优先级来缩短网页的加载时间和提高安全性。
http协议的不足:
单路连接,请求低效
http只允许客户端发起请求
http头冗余
spdy协议的优点:
SPDY 规定在一个 SPDY 连接内可以有无限个并行请求,即允许多个并发 HTTP 请求共用一个 TCP会话。
服务器可以主动向客户端发起通信向客户端推送数据,这种预加载可以使用户一直保持一个快 速的网络。
舍弃掉了不必要的头信息,经过压缩之后可以节省多余数据传输所带来的等待时间和带宽
Google 认为 Web 未来的发展方向必定是安全的网络连接,全部请求 SSL 加密后,信息传输更加安全。
备注:SPDY 的实现需要浏览器客户端和 Web 服务器同时支持。
2.2 okhttp 的类图
原图很大,下载下来应该能看清:
![](http://upload-images.jianshu.io/upload_images/4395181-2e70cc64b89a9ade.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
那么okhttp的总体设计是怎么样的呢?想一想再怎么设计也是为了完成网络请求,okhttp到底在网络请求的过程中做了哪些封装与优化呢,先看看okhttp总体的架构设计图吧
![](http://upload-images.jianshu.io/upload_images/4395181-6e6e396fe70b0318.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/354)
现在看着个图可能对这个图不可能一下子理解的完全准确,不过可以大概描述一下上图的流程
和一般的异步处理框架类似,网络请求request都存储在一个队列中,一个dispatcher(其实是一个线程)不断的从request队列中取出队列,根据是否有缓存这个判定条件,通过缓存数据获取接口或者网络数据获取接口来获取请求的响应数据,获取数据的实质过程可以是异步也可以是同步的。更加具体的流程可看下图
![](http://upload-images.jianshu.io/upload_images/4395181-c1db2c853c2138e3.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
那就按照这个流程图看看细节吧,okhttp库代码量还是挺大的,这里化繁为简的介绍
OkhttpClient这个类有个内部类Builder,可以通过Builder配置okhttpclient获得一个配置好的OkhttpClient实例。那么这个okhttpclient实例完成了哪些操作呢
![](http://upload-images.jianshu.io/upload_images/4395181-29f61aab23841443.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
上图是okhttpclient的思维脑图,可以先看看了解下okhttpclient这个角色承担的功能。对我而言上图的重点是变量分支和发起请求分支。
下面看下发起请求分支中的 newCall()方法。其实okhttpclient中的Call类是对Request类的封装,所以新建一个Call的时候肯定要传入一个配置好的Request,而一个request从具体流程图中可以看出包括url,method,header,body四个部分(其实request还有一个tag成员变量)。
![](http://upload-images.jianshu.io/upload_images/4395181-d80fc789d8bfd810.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
当我们在使用okhttp库时,一定是先获取okhttpclient实例,然后新建一个request,调用okhttpclient的newCall()方法。其实Call类只是一个接口,newCall方法调用的是实现了Call接口的RealCall类的newRealcall方法,可以看出newRealcall方法返回的是Realcall,每个realcall有一个listener。
![](http://upload-images.jianshu.io/upload_images/4395181-524759a98df6e6f9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
Realcall创建出来肯定要被分发出来,但是要经过okhttp的拦截器机制。拦截器机制在okhttp3之前是在HttpEngine这个类中的,okhttp3中没有了HttpEngine这个类,取而代之的是5个Interceptor类。如下图:可以看出拦截器分为两大类 应用拦截器和网络拦截器,okhttp自带的拦截器有5个。
![](http://upload-images.jianshu.io/upload_images/4395181-1a9a0f05a2f34d5e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
这5个拦截器的功能分别是
RetryAndFollowUpInterceptor,重试那些失败或者redirect的请求。
BridgeInterceptor,请求之前对响应头做了一些检查,并添加一些头,然后在请求之后对响应做一些处理(gzip解压or设置cookie)。
CacheInterceptor,根据用户是否有设置cache,如果有的话,则从用户的cache中获取当前请求的缓存。
ConnectInterceptor,复用连接池中的连接,如果没有就与服务器建立新的socket连接。
CallServerInterceptor,负责发送请求和获取响应。
看下Interceptor接口的源码:
![](http://upload-images.jianshu.io/upload_images/4395181-5e68eef84f981d49.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
可以看到interceptor接口中有一个接口Chain,chain的preceed方法传入request得到响应response。得到response就可以修改response或者根据response执行其它逻辑。
chain的request方法可以得到Request,这样就可以修改request。
下图是在Interceptor Chain中的数据流:
![](http://upload-images.jianshu.io/upload_images/4395181-8a90c8a698fa9632.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
2.3 http客户端向服务器发送报文
step1.从url中解析出服务器的ip地址和端口号
step2.在客户端和服务器端建立tcp/ip连接
step3.开始传输http报文
从文章开始的第一节可以知道http协议是application layer层的协议,位于transport layer层tcp协议的上层。
如果你使用okhttp请求一个URL,具体的工作如下:
Step1.框架使用URL和配置好的OkHttpClient创建一个address。此地址指定我们将如何连接到网络服务器。
看下address源码
![](http://upload-images.jianshu.io/upload_images/4395181-dc78929fe004aff4.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
Step2.框架通过address从连接池中取回一个连接。
如果没有在池中找到连接,ok会选择一个route尝试连接。这通常意味着使用一个DNS请求, 以获取服务器的IP地址。如果需要,ok还会选择一个TLS版本和代理服务器。
如果获取到一个新的route,它会与服务器建立一个直接的socket连接、使用TLS安全通道(基于HTTP代理的HTTPS),或直接TLS连接。它的TLS握手是必要的。
Step3.开始发送HTTP请求并读取响应。
如果有连接出现问题,OkHttp将选择另一条route,然后再试一次。这样的好处是当服务器地址的一个子集不可达时,OkHttp能够自动恢复。而且当连接池过期或者TLS版本不受支持时,这种方式非常有用。
一旦响应已经被接收到,该连接将被返回到池中,以便它可以在将来的请求中被重用。连接在池中闲置一段时间后,它会被赶出。
------------------------------------------------我是分割线------------------------------------------------------
Step1和Step2的步骤都在ConnectInterceptor中执行。看下源码
![](http://upload-images.jianshu.io/upload_images/4395181-9c59aa388abd1ddb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
connectinterceptor的功能注释上写的是:开启与目标服务器的connection(连接),并执行下一个拦截器。不妨看下connection的源码,也可以看connection接口的实现类RealConnection的源码
![](http://upload-images.jianshu.io/upload_images/4395181-0c7d4e462d65dab9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
接口connection的功能注释是:
The sockets and streams of an HTTP, HTTPS, or HTTPS+HTTP/2 connection. May be used for multiple HTTP request/response exchanges. Connections may be direct to the origin server or via a proxy.
我翻译的是:http连接的套接字和流,可用于多个http请求响应的交互,连接可能直达服务器或者经过代理服务器。
![](http://upload-images.jianshu.io/upload_images/4395181-b8b7033630820455.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
可以看到realconnection中有ConnectionPool,其实是因为tcp连接断开操作耗时,为了加快响应速度,用了tcp连接池。如果pool中有与你想要连接的服务器的可用连接,则直接用,没有的话才去连接服务器。
不如继续看下connectionPool的源码
![](http://upload-images.jianshu.io/upload_images/4395181-9e90451e42d2cfea.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
connectionPool的功能注释翻译过来是:
管理http和http2连接的重用
可以看到connectionPool中有一个线程池用于清理过期的连接,但在connectionPool中最多只能运行一个线程。executor也运行被gc回收。
在图connectinterceptor中可以看到代码
RealConnection connection = streamAllocation.connection();
执行这行代码获得到了connection,至于这个connection是从pool中获取还是重新连接服务器获取,肯定是在赋值符号右侧的connection方法中执行了具体逻辑。不过我们还是先看下connection的拥有者streamAllocation类。
源码中给streamAllocation这个类的注释是:
这个类协调三个实体之间的关系,这三个实体是connections,calls,streams。
connections---physical socket connections to remote servers
calls---a logical sequence of streams, typically an initial request and its follow up requests.
streams---logical HTTP request/response pairs that are layered on connections.
我翻译过来就是:
connections--通过套接字与远程服务器的连接
calls----流的逻辑序列,通常是初始请求和该请求的后续请求
streams---基于连接的http请求响应对
其实okhttp把socket的io(发送request和接收response)封装到了httpstream中,其实这个底层的io操作又设计到支撑okhttp库的okio库,先看下httpstream的构造函数,再分析okio的应用场景。
![](http://upload-images.jianshu.io/upload_images/4395181-36417cd61be98ed7.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
/*************************************************okio**********************************************/
首先看下okio为okttp做了什么,给一张图先有个概念
![](http://upload-images.jianshu.io/upload_images/4395181-480c7d132eba404c.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
上面这张图其实表达的是网络io的流程图。
okio库的主要功能都被封装在ByteString和Buffer这两个类中,整个库也是围绕这两个类展开。
ByteString代表一个不可变的 字节序列。对于字符数据来说,String是非常基础的,但在二进制数据的处理中,则没有与之对应的存在,ByteString应运而生。它为我们提供了对串操作所需要的各种 API,例如子串、判等、查找等,也能把二进制数据编解码为十六进制(hex),base64 和 UTF-8 格式
ByteString的源码注释是:
这个类提供了不受信任的输入流和输出流,并对底层字节数组进行原始访问。看下ByteString的源码:
![](http://upload-images.jianshu.io/upload_images/4395181-291fc42a6d860edc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
对于ByteString,可以看一个png格式图片解码的例子
![](http://upload-images.jianshu.io/upload_images/4395181-c41b853dbb3b1f5d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
上图第一行代码有一个ByteString的decodeHex方法,看下方法实现
![](http://upload-images.jianshu.io/upload_images/4395181-ea5c055a43ca2b31.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
decodeHex传入的参数是字符串,字符串中的每个字符都是一个16进制字符,也就是从0-f。
decodeHex方法得到的类型是ByteString,字节串,在decodeHex方法中可以看到新建的byte[]数组的长度是hex String 长度的一半,可知,hex串的每两个字符组成用一个byte表示,比如od可以表示为13,一个byte也就是00001101。
先不看Buffer这个类,先看Sink和Source这两个类。
Okio 吸收了java.io一个非常优雅的设计:流(stream),流可以一层一层套起来,不断扩充能力,最终完成像加密和压缩这样复杂的操作。这其实也是装饰模式。
Okio 有自己的流类型,那就是Source和Sink,它们和InputStream与OutputStream类似,前者为输入流,后者为输出流。
但它们还有一些新特性:
超时机制,所有的流都有超时机制;
Source和Sink的 API 非常简洁,为了应对更复杂的需求,Okio 还提供了BufferedSource和 BufferedSink接口,便于使用(按照任意类型进行读写,BufferedSource 还能进行查找和判 等);
不再区分字节流和字符流,它们都是数据,可以按照任意类型去读写;
便于测试,Buffer同时实现了BufferedSource和BufferedSink接口,便于测试;
Source和InputStream互相操作,我们可以把它们等同对待,同理Sink和OutputStream也可以等同对待。
---------------------------------------------------------------------------------------------------------------------
刚才没有讲述的Buffer类实现了BufferedSource和BufferedSink接口,BufferedSource和BufferedSink接口分别继承于接口Source和Sink。
Buffer类是可变 字节序列,但它像ArrayList一样,封装的很好。我们只有从Buffer的头部读取数据,从尾部添加数据。
继续看上图pgn解码那张图,decodePng方法体中的第一行代码就是:
BufferedSource pngSource=Okio.buffer(Okio.source(in));
看下Okio类中的source方法源码:
![](http://upload-images.jianshu.io/upload_images/4395181-f58be2608b49f4a8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
查看上述代码可知,Okio的source方法最终返回了一个匿名的实现了Source接口的实现类。
其实source方法就是把inputStream读取到Buffer的segment中,segment是Buffer中一种提高性能的数据结构,这里暂时不分析。
BufferedSource接口继承自Source接口,RealBufferedSource是其实现类,RealBufferedSource是个装饰类,内部管理Source对象来扩展Source的功能,通顺拥有Source读取数据的Buffer对象。
![](http://upload-images.jianshu.io/upload_images/4395181-156edc52ad9514d8.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
读写的流程可以归结为如下
InputStream-->Source-->BufferedSource-->Buffer-->segment-->Buffer-->Sink-->BufferedSink-->OutputStream
/*************************************************okio**********************************************/
把图再copy一遍
![](http://upload-images.jianshu.io/upload_images/4395181-9c59aa388abd1ddb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
connectinterceptor
RealConnection connection = streamAllocation.connection()这行代码的上一行代码执行了
streamAllocation类的newStream方法,返回了一个实现了HttpCodec的匿名类,HttpCodec的实现类是Http1Codec。
在网络请求过程中,首先创建一个StreamAllocation的对象,然后调用其newStream()方法,查找一个可用连接,要么复用连接,要么新建连接,复用连接则根据address从连接池中查找,新建连接则是根据address查找一个Route对象建立连接,建立连接以后会将该连接添加到连接池中,同时连接池的清理任务停止的情况下,添加新的连接进去会触发开启清理任务。这是建立连接和管理连接的整个过程,当拥有连接以后,StreamAllocation就会在连接上建立一个流对象,该流持有connection的输入输出流,也就是socket的输入输出流,通过它们最终完成数据通信的过程,下面分析流对象Http1Codec,以及数据通信的过程。
![](http://upload-images.jianshu.io/upload_images/4395181-8ab36d418f2ebbb6.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
这里首先简单介绍一下流对象,在okhttp中,流对象对应着HttpCodec, 它有对应的两个子类, Http1Codec和Http2Codec, 分别对应Http1.1协议以及Http2.0协议,本文主要学习前者。在Http1Codec中主要包括两个重要的属性,即source和sink,它们分别封装了socket的输入和输出,CallServerInterceptor正是利用HttpCodec提供的I/O操作完成网络通信。
/***********************************************************************************/
Http1Codec的功能注释是:
可用于发送HTTP / 1.1消息的套接字连接。这个类严格执行以下生命周期:
发送请求头-->打开一个sink为了写入请求体-->写入然后关闭sink-->读响应头-->
打开一个source来读响应体-->读出然后关闭source
/***********************************************************************************/
下面来看CallServerInterceptor的源码,这个拦截器的功能注释是
这是链中的最后一个拦截器。它对服务器进行网络调用。
对于拦截器,依然是学习它的intercept方法.我们提取intercept方法中对应Http1Codec调用生命周期的代码来看。
//1. 向socket中写入请求header信息
httpCodec.writeRequestHeaders(request);
//2. 向socket中写入请求body信息
Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength());
//3. 完成网络请求的写入
httpCodec.finishRequest();
//4. 读取网络响应header信息
if (responseBuilder == null) {
responseBuilder = httpCodec.readResponseHeaders(false);
}
//5. 读取网络响应的body信息
response = response.newBuilder()
.body(httpCodec.openResponseBody(response))
.build();
因为要严格执行以上生命周期调用,所以Http1Codec使用了状态模式。Http1Codec类中维护了几个状态常量,根据类的状态执行逻辑。
![](http://upload-images.jianshu.io/upload_images/4395181-8f3645c3e09526df.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/700)
我们知道在okhttp中关于连接和流,有三个重要的类,即RealConnection, StreamAllocation和Http1Codec,StreamAllocation作为连接和流的桥梁,承担资源的回收和清理工作。
作者:铜雀春深锁不住
链接:https://www.jianshu.com/p/816223c8960c
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。