转自:http://blog.csdn.net/evan_man/article/details/51173810
Android 4.4之后,HttpURLConnection底层实现已被OkHttp替换。可以见得OkHttp的性能已经被Google所认同。对于为何会想深入了解该库的原因:因为它的最底层走到了java的Socket;利用向Socket写入特定的Http协议数据包,实现网络通信。学习该开源项目,对于网络的学历大有益处,除此之外OkHttp使用了缓存和线程池概念。总之个人觉得OkHttp开源项目可以作为学习网络通信一篇很好的教科书。
OkHttp的特点:
- 支持HTTP2/SPDY黑科技
- socket自动选择最好路线,并支持自动重连
- 拥有自动维护的socket连接池,减少握手次数
- 拥有队列线程池,轻松写并发
- 拥有Interceptors轻松处理请求与响应(比如透明GZIP压缩,LOGGING)
- 实现基于Headers的缓存策略
OkHttp的使用:
- 创建OkHttpClient对象:OkHttpClient client = new OkHttpClient();
- 创建网络请求:Request request = new Request.Builder() .url("http://sethfeng.github.io/index.html") .build();
- 得到Call对象:Call call = client.newCall(request); //实际创建的是一个RealCall对象,RealCall中有一个对client对象的引用
- 发送同步请求:Response response = call.excute();
- 发送异步请求:
-
OkHttpClient工作原理初步分析
RealCall.class
先来看一下通过 client.newCall(request)得到的Call对象
newCall(request)@OkHttpClient.class
方法很简单就是利用调用newCall方法的OkHttpClient对象和newCall的输入参数构造一个RealCall对象。接下来看一下RealCall的构造器。
RealCall()@RealCall.class
接下来看一下RealCall类中的execute和enqueue方法
构造器也很简单,就是对RealCall中的OkHttpClient和Request域进行赋值。
execute@RealCall.class
enqueue@RealCall.class
Dispatcher负责对客户请求进行处理,因此接下来分析一下Dispatcher的execute和enqueue方法。将在下一节分析getResponseWithInterceptorChain方法的底层实现。
Dispatcher.class
OkHttpClient中有一个Dispatcher类型的域,在构造OkHttpClient的时候会调用new Dispatcher()方法获得一个Dispatcher对象,因此我们直接看Dispatcher的源码。
Dispatcher()@Dispatcher.class
public Dispatcher() {
}
看到这个方法一脸的懵逼。什么也没做?不要方,既然这里没有做任何工作,那么有两种可能,Dispatcher的相关域要么在类加载时或者声明时就进行了初始化,要么就会在使用的时候再临时进行初始化,即延迟初始化,这也是一种优化。那我们直接看execute方法和enqueue方法
executed@Dispatcher.class
enqueue()@Dispatcher.class
executorService()@Dispatcher.class
那么到此为止对于execute方法和enqueue方法的介绍就结束了。如果好奇点的童鞋,可能会问,如果异步请求被添加到readyAsyncCalls集合中,那么它何时会被执行呢?注意到,在RealCall的execute和enqueue方法执行完后都会执行this.client.getDispatcher().finished(this); 这样一条语句。那么我们来看下Dispatcher的finished方法。
finished()@Dispatcher.class
finished()@Dispatcher.class
从等待执行异步请求集合中获取到请求,判断该请求的hostname对应的的请求个数是否小于maxRequestsPerHost,为真则将该任务从等待执行异步请求集合中移出,存入runningAsyncCalls集合中,最后调用线程池执行器执行该异步请求的execute方法。到此为止我们对于Dispatcher的介绍就到此为止了。
在当前运行任务数大于maxRequests和等待执行异步任务数为空的两种情况下直接返回不进行任何操作。否则
对Dispatcher的总结如下:
- 该类中有两个集合分别为:runningAsyncCalls、readyAsyncCalls前者存放正在执行的请求,后者存放等待执行的请求
- 该类中有一个newCachedThreadPool线程执行器,利用该执行器来执行异步请求的execute方法。也就是说异步请求发送在非当前工作线程,即创建异步请求的线程,而是从线程池中获取一条线程执行网络请求。同步请求则直接是在当前工作线程中执行。
- 该类对异步请求的管理是通过maxRequests、maxRequestsPerHost进行控制的,前者控制线程池中同时运行的最大请求数,防止同时运行线程过多,造成OOM。后者限制了同一hostname下的请求数,防止一个应用占用的网络资源过多,优化用户体验。
文章的最后我们对okhttp中使用过程中遇到的Request、Response、OkHttpClient这几个类进行一下介绍。
Request.class
该类中有如下域
只能通过Builder方法构建Request对象。
Builder()@Builder.class@Request.class
默认创建的是Get方法
Build()@Builder.class@Request.class
调用Request构造器,创建Request对象。
Response.class
类中有如下域
Okhttp中有client.internalCache()和client.connectionPool()两个重要的概念,前者管理网络访问的缓存信息,后者用于存储已链接的RealConnection(该RealConnection已经跟对应的hostname完成了三次握手)。下面我们看一下创建Cache和ConnectionPool这两个对象的OkHttpClient对象。
OkHttpClient.class
Internal.instance@OkHttpClient.class
这一段代码用static修饰,表明在加载OkHttpClient类时就会对Internal.instance进行初始化操作。
internalCache() @OkHttpClient.class
cache域在我们构造OkHttpClient的时候是没有被初始化的,因此如果我们没有通过调用Builder的cache方法设置cache值的话,该方法返回的对象实际上是一个不支持任何缓存操作的对象,说着说该对象的所有方法为空。因此如果需要OkHttpClient支持缓存,需要我们写一个Cache对象并在构造OkHttpClient的时候将其传给OkHttpClient。
cache()@Builder.class@OkHttpClient.class
完成cache的初始化,如果不调用该方法那么OkHttpClient默认不提供Cache功能。对于Cahce更为详细的介绍在后面的章节我们会进行详细介绍。《OkHttp深入学习(三)——Cache》
connectionPool()@OkHttpClient.class
connectionPool的初始化是在构建OkHttpClient时创建的,调用的构造器为new ConnectionPool()。
==
RealCall.class
首先直接看getResponseWithInterceptorChain()的源码。
getResponseWithInterceptorChain()@RealCall.class
ApplicationInterceptorChain.class@RealCall.class
ApplicationInterceptorChain内部类还算简单,里面有一个index和一个request对象。proceed方法使用了递归,将用户的请求通过拦截器一层一层的包装最后得到一个全新的请求,在递归的最底层即所有的拦截器已经执行完毕后,则调用getResponse方法获取response,通过该请求得到的请求结果最后又利用拦截器一层层的包装最终得到一个全新的网络请求结果。
直接这样说可能不太好理解,下面给出一个普通的拦截器使用例子:
ApplicationInterceptorChain拦截器的效果图如下。
chain.proceed方法的最底层是通过调用getResponse方法获得对应的response,接着看看该部分的源码。
getResponse()@RealCall.class
HttpEngine.class
首先看一下该类的构造器
HttpEngine()@HttpEngine.class
此外还注意到构造器中对this.streamAllocation = streamAllocation != null
? streamAllocation
: new StreamAllocation(client.connectionPool(), createAddress(client, request));的初始化。默认都是通过调用new StreamAllocation(client.connectionPool(), createAddress(client, request))获得StreamAllocation对象的。对StreamAllocation它主要负责对RealConnection的管理,我们先不看其源码,后面再讲。先看sendRequest()方法。
这里我们主要关注的是传入的OkHttpClient client, Request request两个参数,这两个参数在HttpEngine中的命名分别为client和userRequest。
sendRequest()@HttpEngine.class
2、对于InternalCache responseCache = Internal.instance.internalCache(client),一脸的懵逼,之前从来没有见过,不过不要方,它在我们构建OkHttpClient的时候被初始化的!让我们回头撸一把OkHttpClient,果然发现有下面的一段内容, Internal.instance = new Internal() { .....}。这下我们就知道Internal.instance对象在构造OkHttpClient的时候就创建好了。实际上Internal.instance.internalCache(client)等价于 client.internalCache();即得到一个缓存操作符。随后调用缓存操作符的get方法得到对应的缓存结果。
3、cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();利用一定的缓存策略对request和cacheCandidate进行处理,并对HttpEngine中的networkRequest和cacheResponse对象进行赋值。如果得到的networkRequest==null,表明缓存策略认为该请求不需要访问网络,如果cacheResponse==null表明该请求在缓存中没有找到想要的结果。可能有童鞋不太懂为何要这样做,其实举个栗子就懂了,就是比如我们本地有缓存内容,但是可能该内容存放时间太长已经过期了,系统不能再使用该缓存,需要重新去网上下载。如果对该部分比较感兴趣参考下一篇博客
《OkHttp深入学习(三)——Cache》,本节不对其进行详细说明。
4、经过上面的分析如果没有从缓存中得到预期的结果,那么需要通过网络得到预期的response。首先执行的是
httpStream = connect();
该方法
内部调用streamAllocation.newStream()方法;骚年你还记得吗?streamAllocation是在构造HttpEngine时创建的对象。调用的构造器为new StreamAllocation(client.connectionPool(), createAddress(client, request)); 对于OkHttpClient的链接池connectionPool部分参考后面的OkHttpClient.class部分的分析。构造完StreamAllocation之后调用其newStream方法。得到一个HttpStream对象,该对象是一个已经跟服务器端进行了三次握手的链接,通过该对象就能向服务器发送接收Http报文数据。对于connect()后面的内容,一般情况我们不太会遇到,下面直接开始分析readResponse()方法
readResponse()@HttpEngine.class
2、将得到的networkResponse中的cookies存入OkHttpClient中的cookieJar对象中,该对象默认是空的,即不能存入任何url的cookie,如果我们在构造OkHttpClient的时候给予它一个CookieJar那么OkHttpClient就会将每次获得的cookie都存入我们定义的CookieJar中。一般是把url作为key,cookies作为value。
3、如果该Request在OkHttpC中的缓存中存在对应的Response,则更新该缓存
4、利用获得的networkResponse给userResponse赋值
5、根据得到的Response判断是否存入缓存
因为上面得到的Response来自于方法readNetworkResponse(),那么接下来我们来分析一下该方法是如何工作的。
readNetworkResponse()@HttpEngine.class
getResponse()@HttpEngine.class
StreamAllocation.class
StreamAllocation@StreamAllocation.class
routeDatabase()@StreamAllocation.class
得到了RealConnection对象意味着我们已经跟服务端有了一条通信链路了,而且通过RealConnection的source 和 sink分别实现向管道中读写数据。
1、首先利用findHealthyConnection()方法得到一个RealConnection对象。
2、根据请求的协议不同创建不同的HttpStream,一般情况下我们创建的都是Http1xStream对象,即Http1.X协议。
该对象功能就是对request中的数据按照对应的http协议中的格式暴力的通过sink写入到管道流中,对response中的数据通过source读取并进行解析。
该部分的重点转移到了方法findHealthyConnection方法,往下看。
findHealthyConnection()@StreamAllocation.class
findConnection()@StreamAllocation.class
2、从线程池中获取一个RealConnection对象,等价于直接调用connectionPool.get(address, streamAllocation),如果获取到了RealConnection则直接返回该RealConnection。step1和step2得到的RealConnection都是已经跟服务器完成了三次握手链接的连接。
3、利用routeSelector.next()方法得到一个route对象;routeSelector在构造StreamAllocation时创建的跟我们的request是绑定的;每个链接都需要选择一个代理,IP地址,TLS。
4、通过得到的route对象构建一个RealConnection对象,该route包含了客户请求链接的ip地址和端口号等信息。
5、connection.allocations.add(new WeakReference<>(this)); 为当前StreamAllocation创建一个弱引用,添加到RealConnection的allocation中,即增加其引用计数。
6、将上面创建的RealConnection添加到OkHttpClient的ConnectionPool链接池中。
7、调用RealConnection的connect方法,
创建链接,
完成跟目标地址的的三次握手。
8、将newConnection对应的route从routeDatabase中移除出去,routeDatabase实际上维护一个集合,里面存入的route都是failed
最后给大家科普StreamAllocation最后一个方法,该方法用于回收StreamAllocation对应的Connection。调用StreamAllocation的noNewStreams和release方法都会调用到该方法。
一般在任务执行结束后都会通过HttpEngine的releaseStreamAllocation()方法间接调用StreamAllocation的deallocate方法将RealConnection和StreamAllocation进行解绑。
deallocate@S
treamAllocation.class
2、Connection的Allocation队列如果为空,表明该RealConnection没有被任何StreamAllocation使用。
3、Internal.instance.connectionBecameIdle(connectionPool, connection)等价于pool.connectionBecameIdle(connection); 该方法内部判断connection.noNewStreams为真,即该Connection不能给新的stream提供服务,则将该Connection从Connectionpool中移除出去,同时返回true。否则让Connectionpool的清理线程去处理,返回false。
4、第三步中返回结果为真,即RealConnection已经从ConnectionPool中移除,则在此处强制调用socket的close方法,关闭套接字,回收网络资源。
继续往下走,那就是RealConnection对象的创建和其connected方法的实现。RealConnection这也是我们所能走到的最底层的类了。胜利的曙光要来了~
RealConnection.class
private Socket rawSocket; //最底层socket
public Socket socket; //应用层socket
public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
RealConnection@RealConnection.class
connect@RealConnection.class
2、调用 connectSocket方法。
connectSocket()@RealConnection.class
1、Platform.get()等价于Platform.findPlatform();结果是根据平台的不同得到不同的Platform对象,使用Class.forName方法获取对应的类对象;下面假设平台是Android平台。Platform. get(). connectSocket() 方法等价于调用socket.connect(address, connectTimeout); 完成了TCP三次握手
2、source = Okio.buffer(Okio.source(rawSocket));就是从rawSocket获得一个InputStream再用buffer包装一下。
sink = Okio.buffer(Okio.sink(rawSocket));
就是从rawSocket获得一个OutputStream再用buffer包装一下。
到此为止我们已经获得了跟服务器通信的链路。
在本节结束之前我们最后对ConnectionPool和RouteSelector进行介绍
ConnectionPool.class
ConnectionPool()@ConnectionPool.class
创建的连接池默认维护5条自由链接,且自由链接空闲时间最多为5分钟。RealConnection是否是自由态由RealConnection中存储的Allocations集合的大小决定。如果RealConnection.noNewStream==true则表明该RealConnection拒绝为新StreamAllocation服务,往往意味着等待被回收。
get()@ConnectionPool.class
该方法的作用就是尝试从集合connections中获取到一个可重复利用的RealConnection。
1、connection.allocations.size() < connection.allocationLimit;判断该RealConnection所服务的StreamAllocation数量是否小于门限值。
2、address.equals(connection.route().address);该Connection的hostname地址等于方法参数的address值。
3、connection.noNewStreams能否被其它StreamAllocation。
4、streamAllocation.acquire(connection);等价于 connection.allocations.add(streamAllocation) ,将StreamAllocation添加到RealConnection的allocations集合中。增加RealConnection的引用计数。当该引用计数为0时考虑回收该RealConnection。
1、connection.allocations.size() < connection.allocationLimit;判断该RealConnection所服务的StreamAllocation数量是否小于门限值。
2、address.equals(connection.route().address);该Connection的hostname地址等于方法参数的address值。
3、connection.noNewStreams能否被其它StreamAllocation。
4、streamAllocation.acquire(connection);等价于 connection.allocations.add(streamAllocation) ,将StreamAllocation添加到RealConnection的allocations集合中。增加RealConnection的引用计数。当该引用计数为0时考虑回收该RealConnection。
put()@ConnectionPool.class
1、对connections集合进行维护,cleanupRunnable的run方法内部会执行cleanup方法,下面我们将对其进行介绍
2、将RealConnection加入到ConnectionPool的connects集合
cleanup()@ConnectionPool.class
Connectionpool维护其链接池中链接;该方法是在cleanupRunnable中的run方法中被调用。
1、缩减和获得与该Connection绑定的StreamAllocation数量,如果数量不为0,证明该RealConnection正在被某个StreamAllocation使用,否则进行下面的步骤。
2、获取该Connection的自由时间,如果该链接自由时间超过当前系统所记录的Connection最长自由时间,则刷新当前记录最大值。这是标记过程
3、执行到这里,已经得到了当前系统空闲线程等待的最长时间,如果该时间大于系统设定的最大自由时间或自由链接数大于系统所能维护的最大自由链接数,则将该RealConnection从链接池中移除出去。
4、执行到这里,表明刚刚有一个链接从连接池中被移出,此处将关闭该RealConnection对应的socket,即执行socket.close().
connectionBecameIdle@ConnectionPool.class
该方法在StreamAllocation的deallocate中被调用。用于将connection回收,或者将RealConnection变成自由态。
我们对ConnectionPool做个小结:
Connection pool 创建了一个线程池,用于维护池中的自由链接数,RealConnection采用引用计数的方法判断一个Connection是否是自由态,如果RealConnection的Allocations集合为空则判断为自由态。最后采用标记清除的算法实现对废弃RealConnection的垃圾回收。当自由态链接数大于门限或者链接空闲时间超过门限值时则对该RealConnection资源进行回收,具体工作就是将RealConnection从ConnectionPool的connections集合中移出,底层调用socket.close()关闭网络连接。
RouteSelector.class
在StreamAllocation的findConnection方法中在构造RealConnection之前是需要获得一个route对象的,而route对象的获取是通过调用routeSelector的next方法来获取的。该route对象包含了我的url请求对应的ip地址和对应端口号。
RouteSelector()@ RouteSelector.class
该构造其中的Address参数是在HttpEngine构建StreamAllocation时创建的,创建方法是通过调用HttpEngine的createAddress(client, request)方法来获得的,方法参数分别为OkHttpClient和Request。在RouteSelector的构造器中会调用resetNextProxy方法,参数为客户请求的url和对应的代理,一般情况刚开始时代理是空。
resetNextProxy()@ RouteSelector.class
1、如果代理不为空,则直接对proxies赋值
2、address.proxySelector()等价于client.proxySelector(),后者默认等价于ProxySelector.getDefault()等价于new java,net.ProxySelectorImpl();经历这么一堆等价于其实简单讲就是调用address.proxySelector()等于创建一个java,net.ProxySelectorImpl()对象。随后调用该对象的select方法获取与该url对应的Proxy
下面我们就来看看next方法具体完成的操作。
next()@ RouteSelector.class
1、首先运行到这里,获取下一个代理,同时刷新集合inetSocketAddresses的数据
2、获取到InetSocketAddress
3、利用前面得到的代理和SocketAddress构造一个Route
4、查找route是否存在于routeDatabase中,即检验生成的route是不是可用
5、step4返回真,该route加入到postponedRoutes集合中,如果最后所有的代理都试过了还是不行,则还会将该route重新再尝试一次
6、递归调用
nextProxy()@ RouteSelector.class
从Proxies对象中获取一个Proxy对象。proxies的值是在resetNextProxy()方法中获得的
resetNextInetSocketAddress()@ RouteSelector.class
至此我们对于okhttp的网络通信功能的实现进行了了解,下面对本节进行一下总结:
1、该部分的层次关系有这样的形式:httpEngine->httpStream->RealConnection。
2、无论是同步还是异步的请求最终都是通过getResponseWithInterceptorChain()方法来获得对应的Response。在真实的网络请求之前会使用ApplicationInterceptorChain拦截器对请求进行拦截。之后利用request和OkHttpClient创建一个HttpEngine对象,该对象持有有一个对StreamAllocation对象的引用,通过调用该对象的engine.sendRequest()方法发送请求,通过调用engine.readResponse();方法读取响应数据,同时给HttpEngine中的userResponse域赋值,最后通过engine.getResponse()返回HttpEngine中的userResponse域。
3、
engine.sendRequest()方法,
在通过HttpEngine的sendRequest方法发送请求的到网络上时,会
先去OkHttpClient的Cache中
查找是否有对应的Response数据,如果有则直接对userResponse进行赋值并返回,如果没有则通过StreamAllocation的newStream方法获得的HttpStream对象,随后利用该对象访问网络的响应。StreamAllocation的newstream方法首先
从OKHttpClient中的链接池connection
s中尝试获取到RealConnection对象,如果没有获得则利用Route对象创建一个RealConnection,该route是由OkHttpClient和request共同构建得到的,里面有请求主机的对应IP和端口等信息,随后调用该对象的connect方法,操作一个socket完成跟指定主机三次握手。随后将该connection加入到OkHttpClient的链接池中。将上面得到的RealConnection根据不同的Http协议创建不同的HttpStream对象,该对象的作用是根据不同的Http协议,将用户的请求转换成特定的格式后通过RealConnection写入到对应ip的服务端中,然后对通过RealConnection读取到的数据进行解析。
4、engine.readResponse()方法,该方法首先检测usrResponse是否为空,即是否通过上面的sendRequest方法已经获得了缓存Response,有则直接返回usrResponse,如果没有获得则
一般情况下会通过networkResponse = new NetworkInterceptorChain(0, networkRequest).proceed(networkRequest)获得Response。该部分与ApplicationInterceptorChain的功能类似,只是拦截的位置出现变化,这些拦截器更加靠近网络发送时的位置,越底层。所有拦截器执行结束后,通过调用 readNetworkResponse()方法得到网络返回数据,该方法内部通过HttpStream向服务端写入和读取数据。随后对返回数据的cookies进行存储,并判断是否加入缓存中,最后将结果赋值给userResponse。
===