Android架构师学好OKhttp网络框架Socket连接
概述
提高网络性能优化,很重要的一点就是降低延迟和提升响应速度。通常我们在浏览器中发起请求的时候header部分往往是这样的keepalive就是浏览器和服务端之间保持长连接,这个连接是可以复用的。在HTTP1。1中是默认开启的。连接的复用为什么会提高性能呢?通常我们在发起http请。。。
通常我们在浏览器中发起请求的时候header部分往往是这样的
keepalive就是浏览器和服务端之间保持长连接,这个连接是可以复用的。在HTTP1。1中是默认开启的。
连接的复用为什么会提高性能呢?通常我们在发起http请求的时候首先要完成tcp的三次握手,然后传输数据,最后再释放连接。三次握手的过程可以参考
一次响应的过程
在高并发的请求连接情况下或者同个客户端多次频繁的请求操作,无限制的创建会导致性能低下。
如果使用keepalive
在timeout空闲时间内,连接不会关闭,相同重复的request将复用原先的connection,减少握手的次数,大幅提高效率。
并非keepalive的timeout设置时间越长,就越能提升性能。长久不关闭会造成过多的僵尸连接和泄露连接出现。
那么okttp在客户端是如果类似于客户端做到的keepalive的机制。连接池原理
多少了解点OkHttp3的同学都知道,OkHttp可以降低网络延时加快网络请求响应的速度。那么它是怎样做到的呢?在说这个之前,我们先简单回顾一下Http协议。Http协议是一个无连接的协议,客户端(请求头请求体)发送请求给服务端,服务端收到(请求头请求体)后响应数据(响应头和响应体)并返回。由于Http协议的底层实现是基于TCP协议的(保证数据准确到达),所以在请求响应的过程中必然少不了Tcp的三次握手和释放资源时的四次挥手。我们假设有这么一种情况,客户端需要每隔10秒向服务端发送心跳包,如果按照无连接的状态每次客户端请求和服务端响应都需要经过Tcp的三次握手和四次挥手,这样高频率的发送重复的请求会严重影响网络的性能,就算除去头部字段在频繁三次握手和四次挥手的情况下网络性能也非常堪忧。那么有没有一种办法能够让,Http的链接保持一段时间,如果有形同请求时复用这个链接,在超时的时候把链接断掉,从而减少握手次数呢?答案是肯定的,OkHttp3已经帮我们设计好了。
OkHttp3连接池原理:OkHttp3使用ConnectionPool连接池来复用链接,其原理是:当用户发起请求是,首先在链接池中检查是否有符合要求的链接(复用就在这里发生),如果有就用该链接发起网络请求,如果没有就创建一个链接发起请求。这种复用机制可以极大的减少网络延时并加快网络的请求和响应速度。
三、源码分析
我们主要看下ConnectionPool连接池的源代码,看其是怎样实现的,我们一段一段拆分着看。privatefinalintmaxIdleConnections;每个地址最大的空闲连接数privatefinallongkeepAliveDurationNs;
privatefinalDequeRealConnectionconnectionsnewArrayDeque();连接池,其是一个双端链表结果,支持在头尾插入元素,且是一个后进先出的队列finalRouteDatabaserouteDatabasenewRouteDatabase();用来记录链接失败的路由
booleancleanupRunning;privatestaticfinalExecutorexecutornewThreadPoolExecutor(0核心线程数,Integer。MAXVALUE线程池可容纳的最大线程数量,60L线程池中的线程最大闲置时间,TimeUnit。SECONDS,闲置时间的单位newSynchronousQueueRunnable()线程池中的任务队列,通过线程池的execute方法提交的runnable会放入这个队列中,Util。threadFactory(OkHttpConnectionPool,true)
工具类用来创建线程的,其原型是ThreadFactory);
通过上面的代码可知,ConnectionPool中会创建一个线程池,这个线程池的作用就是为了清理掉闲置的链接(Socket)。ConnectionPool利用自身的put方法向连接池中添加链接(每一个RealConnection都是一个链接)voidput(RealConnectionconnection){
java1。4中新增的关键字,如果为true无异常,如果为false则抛出一个异常assert(Thread。holdsLock(this));
利用线程池清除空闲的Socketif(!cleanupRunning){cleanupRunningtrue;executor。execute(cleanupRunnable);}
向链接池中加入链接connections。add(connection);}
通过以上代码我们发现向线程池中添加一个链接(RealConnection)其实是向连接池connections添加RealConnection。并且在添加之前需要调用线程池的execute方法区清理闲置的链接。
下面我们看下清理动作是如何实现的,直接看cleanupRunnable这个匿名内部类privatefinalRunnablecleanupRunnablenewRunnable(){Overridepublicvoidrun(){
死循环,不停的执行cleanup的清理工作while(true){
返回下次清理的时间间隔longwaitNanoscleanup(System。nanoTime());
如果返回1就直接停止if(waitNanos1)return;
如果下次清理的时间几个大于0if(waitNanos0){longwaitMilliswaitNanos1000000L;waitNanos(waitMillis1000000L);synchronized(ConnectionPool。this){try{
根据下次返回的时间间隔来释放wait锁ConnectionPool。this。wait(waitMillis,(int)waitNanos);}catch(InterruptedExceptionignored){}}}}}};
在Runnable的内部会不停的执行死循环,调用cleanup来清理空闲的链接,并返回一个下次清理的时间间隔,根据这个时间间隔来释放wait锁。
接下来看下cleanup的具体执行步骤longcleanup(longnow){intinUseConnectionCount0;正在使用的链接数量intidleConnectionCount0;闲置的链接数量
长时间闲置的链接RealConnectionlongestIdleConnectionnull;longlongestIdleDurationNsLong。MINVALUE;用for循环来遍历连接池synchronized(this){for(IteratorRealConnectioniconnections。iterator();i。hasNext();){RealConnectionconnectioni。next();如果当前链接正在使用,就执行continue,进入下一次循环。if(pruneAndGetAllocationCount(connection,now)0){inUseConnectionCount;continue;}否则闲置链接1idleConnectionCount;如果闲置时间间隔大于最大的闲置时间,那就把当前的链接赋值给最大闲置时间的链接。longidleDurationNsnowconnection。idleAtNanos;if(idleDurationNslongestIdleDurationNs){longestIdleDurationNsidleDurationNs;longestIdleConnectionconnection;}}如果最大闲置时间间隔大于保持链接的最大时间间隔或者限制连接数大于连接池允许的最大闲置连接数,就把该链接从连接池中移除if(longestIdleDurationNsthis。keepAliveDurationNsidleConnectionCountthis。maxIdleConnections){connections。remove(longestIdleConnection);}elseif(idleConnectionCount0){如果闲置链接数大于0,则返回允许保持链接的最大时间间隔最长时间间隔,也就是下次返回的时间间隔returnkeepAliveDurationNslongestIdleDurationNs;}elseif(inUseConnectionCount0){如果所有的链接都在使用则直接返回保持时间间隔的最大值returnkeepAliveDurationNs;}else{如果以上条件都不满足,则清除事变,返回1cleanupRunningfalse;return1;}}关闭闲置时间最长的那个socketcloseQuietly(longestIdleConnection。socket());Cleanupagainimmediately。return0;}
cleanup(now)这个方法比较长,内容也比较多。我们把握大体逻辑就行。其核心逻辑是返回下次清理的时间间隔,其清理的核心是:链接的限制时间如果大于用户设置的最大限制时间或者闲置链接的数量已经超出了用户设置的最大数量,则就执行清除操作。其下次清理的时间间隔有四个值:
1。如果闲置的连接数大于0就返回用户设置的允许限制的时间闲置时间最长的那个连接的闲置时间。
2。如果清理失败就返回1,
3。如果清理成功就返回0,
4。如果没有闲置的链接就直接返回用户设置的最大清理时间间隔。
下面看一下系统是如何判断当前循环到的链接是正在使用的链接privateintpruneAndGetAllocationCount(RealConnectionconnection,longnow){
编译StreamAllocation弱引用链表ListReferenceStreamAllocationreferencesconnection。allocations;for(inti0;ireferences。size();){ReferenceStreamAllocationreferencereferences。get(i);如果StreamAllocation不为空则继续遍历,计数器1;if(reference。get()!null){i;continue;}Wevediscoveredaleakedallocation。Thisisanapplicationbug。StreamAllocation。StreamAllocationReferencestreamAllocRef(StreamAllocation。StreamAllocationReference)reference;移除链表中为空的引用references。remove(i);connection。noNewStreamstrue;Ifthiswasthelastallocation,theconnectioniseligibleforimmediateeviction。
如果链表为空则返回0if(references。isEmpty()){connection。idleAtNanosnowkeepAliveDurationNs;return0;}}returnreferences。size();}
通过以上的代码我们可以看出,其遍历了弱引用列表,链表中为空的引用,最后返回一个链表数量。如果返回的数量0表示RealConnection活跃,如果0则表示RealConnection空闲。也就是用这个来方法来判断当前的链接是不是空闲的链接。
我们再来看一下closeQuietly(longestIdleConnection。socket());是如何关闭空闲时间最长的链接的。publicstaticvoidcloseQuietly(Socketsocket){if(socket!null){try{socket。close();}catch(AssertionErrore){if(!isAndroidGetsocknameError(e))throwe;}catch(RuntimeExceptionrethrown){throwrethrown;}catch(Exceptionignored){}}}
其实就一行核心代码socket。close()。socket的使用不再介绍,大家可以看专门类的文章。
我们已经分析了从连接池清理空闲链接,到向连接池中加入新的链接。下面看看连接的使用以及连接的复用是如何实现的NullableRealConnectionget(Addressaddress,StreamAllocationstreamAllocation,Routeroute){assert(Thread。holdsLock(this));for(RealConnectionconnection:connections){if(connection。isEligible(address,route)){streamAllocation。acquire(connection,true);returnconnection;}}returnnull;}
获取连接池中的链接的逻辑非常的简单,利用for循环循环遍历连接池查看是否有符合要求的链接,如果有则直接返回该链接使用,如果没有就发挥null,然后会在另外的地方创建一个新的RealConnection放入连接池。这里的核心代码就是判断是否有符合条件的链接:connection。isEligible(address,route)publicbooleanisEligible(Addressaddress,NullableRouteroute){如果当前链接的技术次数大于限制的大小,或者无法在此链接上创建流,则直接返回falseif(allocations。size()allocationLimitnoNewStreams)returnfalse;如果地址主机字段不一致直接返回falseif(!Internal。instance。equalsNonHost(this。route。address(),address))returnfalse;如果主机地址完全匹配我们就重用该连接if(address。url()。host()。equals(this。route()。address()。url()。host())){returntrue;Thisconnectionisaperfectmatch。}。。。。。。returntrue;}OkHttp中的复用机制
前面提了HTTP中的复用机制,通过对TCP连接的复用,大幅提高了网络请求的效率。无论是HTTP1。1中的KeepAlive还是HTTP2中的多路复用,都需要连接池来维护TCP连接,让我们看看OkHttp中连接池的实现。
我们知道,在findConnection过程中,若无法从transimitter中获取到连接,则会尝试从连接池中获取连接。
我们可以看到RealConnectionPool。connections,它是一个Deque,保存了所有的连接:privatefinalDequeRealConnectionconnectionsnewArrayDeque();连接清理机制
同时会发现,在这个类中还存在着一个executor,它的设置与OkHttp用于异步请求的线程池的设置几乎一样,它是用来做什么的呢?Backgroundthreadsareusedtocleanupexpiredconnections。Therewillbeatmostasinglethreadrunningperconnectionpool。Thethreadpoolexecutorpermitsthepoolitselftobegarbagecollected。privatestaticfinalExecutorexecutornewThreadPoolExecutor(0corePoolSize,Integer。MAXVALUEmaximumPoolSize,60LkeepAliveTime,TimeUnit。SECONDS,newSynchronousQueue(),Util。threadFactory(OkHttpConnectionPool,true));
通过上面的注释可以看出,它是用来执行清理过期连接的任务的,并且最多每个连接池只会有一个线程在执行清理任务。这个清理的任务就是下面的cleanupRunnable:privatefinalRunnablecleanupRunnable(){while(true){longwaitNanoscleanup(System。nanoTime());if(waitNanos1)return;if(waitNanos0){longwaitMilliswaitNanos1000000L;waitNanos(waitMillis1000000L);synchronized(RealConnectionPool。this){try{RealConnectionPool。this。wait(waitMillis,(int)waitNanos);}catch(InterruptedExceptionignored){}}}}};
可以看到它是采用一个循环的方式调用cleanup方法进行清理,并从返回值中获取了需要wait的秒数,调用wait方法进入阻塞,也就是说每次清理的间隔由cleanup的返回值进行决定。
我们看到cleanup方法:Performsmaintenanceonthispool,evictingtheconnectionthathasbeenidlethelongestifeitherithasexceededthekeepalivelimitortheidleconnectionslimit。pReturnsthedurationinnanostosleepuntilthenextscheduledcalltothismethod。Returns1ifnofurthercleanupsarerequired。longcleanup(longnow){intinUseConnectionCount0;intidleConnectionCount0;RealConnectionlongestIdleConnectionnull;longlongestIdleDurationNsLong。MINVALUE;synchronized(this){for(IteratorRealConnectioniconnections。iterator();i。hasNext();){RealConnectionconnectioni。next();统计连接被引用的transimitter的个数,若大于0则说明是正在使用的连接if(pruneAndGetAllocationCount(connection,now)0){inUseConnectionCount;continue;}否则是空闲连接idleConnectionCount;找出空闲连接中空闲时间最长的连接longidleDurationNsnowconnection。idleAtNanos;if(idleDurationNslongestIdleDurationNs){longestIdleDurationNsidleDurationNs;longestIdleConnectionconnection;}}if(longestIdleDurationNsthis。keepAliveDurationNsidleConnectionCountthis。maxIdleConnections){如果发现空闲时间最久的连接所空闲时间超过了KeepAlive设定的时间,或者是空闲连接数超过了最大空闲连接数将前面的其从队列中删除,并且在之后对其socket进行关闭connections。remove(longestIdleConnection);}elseif(idleConnectionCount0){返回离达到keepalive设定的时间的距离,将在达到时执行进行清理returnkeepAliveDurationNslongestIdleDurationNs;}elseif(inUseConnectionCount0){如果当前连接都是正在使用的,返回keepalive所设定的时间returnkeepAliveDurationNs;}else{没有连接了,停止运行cleanupcleanupRunningfalse;return1;}}关闭空闲最久的连接,继续尝试清理closeQuietly(longestIdleConnection。socket());return0;}
可以看到,主要是下面几步:调用pruneAndGetAllocationCount方法统计连接被引用的数量,大于0说明连接正在被使用通过上面的方法统计空闲连接数及正在使用的连接数,并从中找出空闲最久的连接若空闲最久的连接空闲的时间超过了所设定的keepAliveDurationNs(这里不是指的KeepAlive所设定时间),或者空闲连接数超过了所设定的maxIdleConnections,清理该连接(移除并关闭socket),并返回0表示立即继续清理。若还未超过,则返回下一次超过外部设定的keepAliveDurationNs,表示等到下次超时的时候再进行清理若当前连接都正处于使用中,返回所设定的keepAliveDurationNs若当前没有连接,则将cleanupRunning置为false停止清理
在OkHttp中,将空闲连接的最长存活时间设定为了5分钟,并且将最大空闲连接数设置为了5
我们看看pruneAndGetAllocationCount是如何对连接被引用的数量进行统计的:Prunesanyleakedtransmittersandthenreturnsthenumberofremaininglivetransmitterson{codeconnection}。Transmittersareleakediftheconnectionistrackingthembuttheapplicationcodehasabandonedthem。Leakdetectionisimpreciseandreliesongarbagecollection。privateintpruneAndGetAllocationCount(RealConnectionconnection,longnow){ListReferenceTransmitterreferencesconnection。transmitters;for(inti0;ireferences。size();){ReferenceTransmitterreferencereferences。get(i);if(reference。get()!null){i;continue;}Wevediscoveredaleakedtransmitter。Thisisanapplicationbug。TransmitterReferencetransmitterRef(TransmitterReference)reference;StringmessageAconnectiontoconnection。route()。address()。url()wasleaked。Didyouforgettoclosearesponsebody?;Platform。get()。logCloseableLeak(message,transmitterRef。callStackTrace);references。remove(i);connection。noNewExchangestrue;Ifthiswasthelastallocation,theconnectioniseligibleforimmediateeviction。if(references。isEmpty()){connection。idleAtNanosnowkeepAliveDurationNs;return0;}}returnreferences。size();}
可以看到,connection中是有维护一个引用它的Transmitter的Reference队列的,通过遍历并判断该Transimitter是否为null即可进行统计。这里的Reference所存的实际是一个继承自WeakReference的TransimitterReference类:staticfinalclassTransmitterReferenceextendsWeakReferenceTransmitter{。。。}
可以发现,这种设计有点像JVM中的引用计数法标记清除,实际上就是OkHttp仿照JVM的垃圾回收设计了这样一种类似引用计数法的方式来统计一个连接是否是空闲连接,同时采用标记清除法对空闲且不满足设定的规则的连接进行清除。获取连接
我们看到connectionPool。transmitterAcquirePooledConnection方法,了解一下连接池获取连接的过程:Attemptstoacquirearecycledconnectionto{codeaddress}for{codetransmitter}。Returnstrueifaconnectionwasacquired。pIf{coderoutes}isnonnullthesearetheresolvedroutes(ie。IPaddresses)fortheconnection。ThisisusedtocoalescerelateddomainstothesameHTTP2connection,suchas{codesquare。com}and{codesquare。ca}。booleantransmitterAcquirePooledConnection(Addressaddress,Transmittertransmitter,NullableListRouteroutes,booleanrequireMultiplexed){assert(Thread。holdsLock(this));for(RealConnectionconnection:connections){if(requireMultiplexed!connection。isMultiplexed())continue;if(!connection。isEligible(address,routes))continue;transmitter。acquireConnectionNoEvents(connection);returntrue;}returnfalse;}
可以看到,首先注释中对我们传入不同的routes参数进行了解释,若routes不为null说明这是已解析过的路由,可以将其合并到同一个HTTP2连接。
而在connection。isMultiplexed的注释中说到,若该连接为HTTP2连接,则会返回true。
在connection。isEligible注释中则说到,若该连接可以给对应的address分配stream,则返回true。
在代码中,对connections进行了遍历:当需要进行多路费用且当前的连接不是HTTP2连接时,则放弃当前连接当当前连接不能用于为address分配stream,则放弃当前连接。前两者都不满足,则获取该连接,并设置到transimitter中。三次获取连接的区别
我们回顾一下findConnection中三次尝试从连接池获取连接的过程:第一次尝试:connectionPool。transmitterAcquirePooledConnection(address,transmitter,null,false)第二次尝试(需要在进行了路由选择的情况下):connectionPool。transmitterAcquirePooledConnection(address,transmitter,routes,false)第三次尝试:connectionPool。transmitterAcquirePooledConnection(address,transmitter,routes,true)
可以发现,其传入的参数是不同的。第一次由于是尝试从已经解析过的路由的连接池中获取连接,因此route设置为null。
第二次由于是在无法找到对应的连接,在进行了路由选择的条件下进行的,因此将route设置为了null。
而最后一次尝试从连接池获取连接之所以需要将requireMultiplexed设置为true,因为这次只有可能是在多个请求并行进行的情况下才有可能发生,这种情况只有HTTP2的连接才有可能发生。加入连接
通过RealConnectionPool。put方法可以向连接池中加入连接:voidput(RealConnectionconnection){assert(Thread。holdsLock(this));if(!cleanupRunning){cleanupRunningtrue;executor。execute(cleanupRunnable);}connections。add(connection);}
由于之前判断了如果连接池中没有连接,就会暂停连接清理线程,所以这里如果放入了新的连接,就会判断连接清理线程是否正在执行,若已停止执行则将其继续执行。之后将该连接放入了Deque中。通知连接空闲
每当外部调用了Transimitter。releaseConnectionNoEvents方法时,最后都会调用到RealConnection。connectionBecameIdle方法来通知连接池连接进入了空闲状态:Notifythispoolthat{codeconnection}hasbecomeidle。Returnstrueiftheconnectionhasbeenremovedfromthepoolandshouldbeclosed。booleanconnectionBecameIdle(RealConnectionconnection){assert(Thread。holdsLock(this));if(connection。noNewExchangesmaxIdleConnections0){connections。remove(connection);returntrue;}else{notifyAll();Awakethecleanupthread:wemayhaveexceededtheidleconnectionlimit。returnfalse;}}
此时如果该连接不支持用于创建新Exchange,或不允许有空闲连接,则会直接将该连接移除,否则会通过notifyAll方法唤醒阻塞的清理线程,尝试对空闲连接进行清理,这样能保证每当有空闲连接时最及时地对连接池进行清理。连接的建立
我们知道,在寻找连接的过程中,若从Transimitter及连接池中都无法获取到连接时,就会创建一个新的连接,让我们看看这个创建连接的过程是怎样的:
在寻找连接的代码中,创建连接的核心代码如下:。。。resultnewRealConnection(connectionPool,selectedRoute);result。connect(connectTimeout,readTimeout,writeTimeout,pingIntervalMillis,connectionRetryEnabled,call,eventListener);
我们先看到RealConnection的构造函数:publicRealConnection(RealConnectionPoolconnectionPool,Routeroute){this。connectionPoolconnectionPool;this。routeroute;}
只是进行了简单的赋值,我们接着看到RealConnection。connect方法:publicvoidconnect(intconnectTimeout,intreadTimeout,intwriteTimeout,intpingIntervalMillis,booleanconnectionRetryEnabled,Callcall,EventListenereventListener){if(protocol!null)thrownewIllegalStateException(alreadyconnected);RouteExceptionrouteExceptionnull;ListConnectionSpecconnectionSpecsroute。address()。connectionSpecs();ConnectionSpecSelectorconnectionSpecSelectornewConnectionSpecSelector(connectionSpecs);。。。一些错误处理while(true){try{if(route。requiresTunnel()){如果使用了隧道技术,调用connectTunnel方法connectTunnel(connectTimeout,readTimeout,writeTimeout,call,eventListener);if(rawSocketnull){Wewereunabletoconnectthetunnelbutproperlycloseddownourresources。break;}}else{未使用隧道技术,调用connectSocket方法connectSocket(connectTimeout,readTimeout,call,eventListener);}建立协议establishProtocol(connectionSpecSelector,pingIntervalMillis,call,eventListener);eventListener。connectEnd(call,route。socketAddress(),route。proxy(),protocol);break;}catch(IOExceptione){。。。异常下的资源释放}}。。。一些错误处理}
可以看到,这里是一个循环,不断尝试建立连接,其中核心步骤如下:若使用了隧道技术,调用connectTunnel方法若未使用隧道技术,调用connectSocket方法调用establishProtocol方法建立协议
让我们看看三个方法分别是如何实现的。直接连接
我们先看看直接连接是如何实现的,我们看到connectSocket方法:DoesalltheworknecessarytobuildafullHTTPorHTTPSconnectiononarawsocket。privatevoidconnectSocket(intconnectTimeout,intreadTimeout,Callcall,EventListenereventListener)throwsIOException{Proxyproxyroute。proxy();Addressaddressroute。address();初始化rawSocket,其中对SOCKS代理采用了SOCKS代理服务器rawSocketproxy。type()Proxy。Type。DIRECTproxy。type()Proxy。Type。HTTP?address。socketFactory()。createSocket():newSocket(proxy);eventListener。connectStart(call,route。socketAddress(),proxy);rawSocket。setSoTimeout(readTimeout);try{调用connectSocket方法对Socket进行连接,这里预置了不同平台的实现Platform。get()。connectSocket(rawSocket,route。socketAddress(),connectTimeout);}catch(ConnectExceptione){ConnectExceptioncenewConnectException(Failedtoconnecttoroute。socketAddress());ce。initCause(e);throwce;}获取source及sink,用于读取及写入try{sourceOkio。buffer(Okio。source(rawSocket));sinkOkio。buffer(Okio。sink(rawSocket));}catch(NullPointerExceptionnpe){if(NPETHROWWITHNULL。equals(npe。getMessage())){thrownewIOException(npe);}}}
可以看到,这里主要是进行Socket的连接,首先根据代理类型创建了Socket,之后调用了connectSocket方法进行连接(里面调用的其实仍然是socket。connect方法)。最后调用Okio的方法获取source及sink。
这个过程还是比较简单的,和正常使用Socket的流程大致相同:创建Socket连接获取stream,其中在connectSocket时根据不同平台做了不同的处理。通过隧道连接
首先我们要理解一下什么是隧道。这个其实是计网中的知识,之前在《计算机网络自顶向下方法》中看到过,不过书中没有详细介绍,这里刚好学习一下。
隧道技术的出现主要是为了适配IPv4到IPv6的转变。通过这种隧道技术,可以通过一种网络协议来传输另外一种网络协议的数据,比如A主机与B主机都是采用IPv6,而连接A与B的是IPv4的网络,为了实现A与B的通信,可以使用隧道技术,数据包经过IPv4的多协议路由时将IPv6的数据包放入IPv4的数据包中,传递给B。当到达B的路由器时,数据又被剥离之后传递给B。这样在A与B看来,它们使用的都是IPv6与对方通信。如下图所示:
那么怎么打开隧道呢?
HTTP提供了一个特殊的methodCONNECT,它是HTTP1。1协议中预留的方法,可以通过它将连接改为隧道的代理服务器。客户端发送一个CONNECT请求给隧道网关请求打开一条TCP连接,当隧道打通之后,客户端通过HTTP隧道发送的所有数据会转发给TCP连接,服务器响应的所有数据会通过隧道发给客户端。
而在OkHttp中,对隧道的支持主要是为了支持SSL隧道SSL隧道的初衷是为了通过防火墙来传输加密的SSL数据,此时隧道的作用就是将非HTTP的流量(SSL流量)传过防火墙到达指定的服务器(比如HTTPS)。
接着我们看到connectTunnel方法的实现:DoesalltheworktobuildanHTTPSconnectionoveraproxytunnel。Thecatchhereisthataproxyservercanissueanauthchallengeandthenclosetheconnection。privatevoidconnectTunnel(intconnectTimeout,intreadTimeout,intwriteTimeout,Callcall,EventListenereventListener)throwsIOException{创建隧道RequestRequesttunnelRequestcreateTunnelRequest();HttpUrlurltunnelRequest。url();for(inti0;iMAXTUNNELATTEMPTS;i){通过connectSocket建立SocketconnectSocket(connectTimeout,readTimeout,call,eventListener);创建隧道tunnelRequestcreateTunnel(readTimeout,writeTimeout,tunnelRequest,url);当创建的隧道为null时,说明隧道成功建立,breakif(tunnelRequestnull)break;回收资源closeQuietly(rawSocket);rawSocketnull;sinknull;sourcenull;eventListener。connectEnd(call,route。socketAddress(),route。proxy(),null);}}
这里首先构建了一个隧道的tunnelRequest。之后进行了循环,不断尝试建立隧道,不过OkHttp限制了其最大尝试次数为21次。
建立隧道的过程首先通过connectSocket方法建立了Socket连接,然后通过createTunnel方法建立隧道。
我们看看createTunnelRequest方法做了什么:privateRequestcreateTunnelRequest()throwsIOException{RequestproxyConnectRequestnewRequest。Builder()。url(route。address()。url())。method(CONNECT,null)。header(Host,Util。hostHeader(route。address()。url(),true))。header(ProxyConnection,KeepAlive)ForHTTP1。0proxieslikeSquid。。header(UserAgent,Version。userAgent())。build();ResponsefakeAuthChallengeResponsenewResponse。Builder()。request(proxyConnectRequest)。protocol(Protocol。HTTP11)。code(HttpURLConnection。HTTPPROXYAUTH)。message(PreemptiveAuthenticate)。body(Util。EMPTYRESPONSE)。sentRequestAtMillis(1L)。receivedResponseAtMillis(1L)。header(ProxyAuthenticate,OkHttpPreemptive)。build();RequestauthenticatedRequestroute。address()。proxyAuthenticator()。authenticate(route,fakeAuthChallengeResponse);returnauthenticatedRequest!null?authenticatedRequest:proxyConnectRequest;}
可以看到,这里构建了一个method为CONENCT的请求。
我们接着看看createTunnel方法又做了什么事情:TomakeanHTTPSconnectionoveranHTTPproxy,sendanunencryptedCONNECTrequesttocreatetheproxyconnection。Thismayneedtoberetriediftheproxyrequiresauthorization。privateRequestcreateTunnel(intreadTimeout,intwriteTimeout,RequesttunnelRequest,HttpUrlurl)throwsIOException{构造HTTP1。1请求StringrequestLineCONNECTUtil。hostHeader(url,true)HTTP1。1;while(true){Http1ExchangeCodectunnelCodecnewHttp1ExchangeCodec(null,null,source,sink);source。timeout()。timeout(readTimeout,MILLISECONDS);sink。timeout()。timeout(writeTimeout,MILLISECONDS);tunnelCodec。writeRequest(tunnelRequest。headers(),requestLine);tunnelCodec。finishRequest();发出隧道请求ResponseresponsetunnelCodec。readResponseHeaders(false)。request(tunnelRequest)。build();tunnelCodec。skipConnectBody(response);switch(response。code()){caseHTTPOK:返回200说明成功建立隧道,返回nullif(!source。getBuffer()。exhausted()!sink。buffer()。exhausted()){thrownewIOException(TLStunnelbufferedtoomanybytes!);}returnnull;caseHTTPPROXYAUTH:表示服务端要进行代理认证进行代理认证tunnelRequestroute。address()。proxyAuthenticator()。authenticate(route,response);代理认证不通过if(tunnelRequestnull)thrownewIOException(Failedtoauthenticatewithproxy);代理认证通过,但需要关闭TCP连接if(close。equalsIgnoreCase(response。header(Connection))){returntunnelRequest;}break;default:thrownewIOException(UnexpectedresponsecodeforCONNECT:response。code());}}}
可以看到,这里主要进行如下的工作:拼接HTTP1。1请求发出隧道请求,读取响应若隧道请求返回200,说明隧道建立成功,返回null若隧道返回407,说明服务器需要进行代理认证,调用对应方法进行代理认证
隧道打通之后,就可以通过隧道进行网络请求了。发布协议
经过前面的步骤,我们建立了一条与服务端的Socket通道,我们接着看到establishProtocol方法:privatevoidestablishProtocol(ConnectionSpecSelectorconnectionSpecSelector,intpingIntervalMillis,Callcall,EventListenereventListener)throwsIOException{如果不是https地址if(route。address()。sslSocketFactory()null){如果协议中包含了http2withpriorknowledgeif(route。address()。protocols()。contains(Protocol。H2PRIORKNOWLEDGE)){socketrawSocket;protocolProtocol。H2PRIORKNOWLEDGE;startHttp2(pingIntervalMillis);return;}协议为HTTP1。1socketrawSocket;protocolProtocol。HTTP11;return;}eventListener。secureConnectStart(call);TLS握手connectTls(connectionSpecSelector);eventListener。secureConnectEnd(call,handshake);if(protocolProtocol。HTTP2){如果是HTTP2协议,调用startHttp2方法startHttp2(pingIntervalMillis);}}
可以看到,这个方法主要是在建立了Socket连接的基础上,对各个协议进行支持。
首先判断了当前地址是否是HTTPS地址。
不是HTTPS的情况下,若协议中包含了H2PRIORKNOWLEDGE则采用HTTP2进行请求,调用startHttp2方法,否则采用HTTP1。1。
是HTTPS的情况下,首先调用了connectTls方法进行TLS握手,之后若是HTTP2协议,则调用startHttp2方法。启动HTTP2连接
让我们先看看startHttp2方法究竟是做了什么:privatevoidstartHttp2(intpingIntervalMillis)throwsIOException{socket。setSoTimeout(0);HTTP2connectiontimeoutsaresetperstream。http2ConnectionnewHttp2Connection。Builder(true)。socket(socket,route。address()。url()。host(),source,sink)。listener(this)。pingIntervalMillis(pingIntervalMillis)。build();http2Connection。start();}
这里主要是构建了一个HTTP2的Http2Connection,并且将listener设置为了该RealConnection,之后通过http2Connection。start方法启动了HTTP2连接。paramsendConnectionPrefacetruetosendconnectionprefaceframes。Thisshouldalwaysbetrueexceptforinteststhatdontcheckforaconnectionpreface。voidstart(booleansendConnectionPreface)throwsIOException{if(sendConnectionPreface){writer。connectionPreface();writer。settings(okHttpSettings);intwindowSizeokHttpSettings。getInitialWindowSize();if(windowSize!Settings。DEFAULTINITIALWINDOWSIZE){writer。windowUpdate(0,windowSizeSettings。DEFAULTINITIALWINDOWSIZE);}}newThread(readerRunnable)。start();Notadaemonthread。}
这里sendConnectionPreface默认为true,它首先调用了writer。connectionPreface方法,之后调用了writer。settings方法。最后,启用了一个readerRunnable的读取线程。
在HTTP2中,每个终端都需要发送一个连接preface作为在使用的协议的一个最终的确认,并为HTTP2连接建立初始的设定。客户端和服务器相互发送一个不同的连接preface。
连接preface以字符串PRIHTTP2。0rrSMrr开始,这个序列后面必须跟着一个SETTINGS帧。因此,在之后又调用了writer。settings方法,写入SETTINGS帧。
我们先看到connectionPreface方法:publicsynchronizedvoidconnectionPreface()throwsIOException{if(closed)thrownewIOException(closed);if(!client)return;Nothingtowrite;serversdontsendconnectionheaders!if(logger。isLoggable(FINE)){logger。fine(format(CONNECTIONs,CONNECTIONPREFACE。hex()));}sink。write(CONNECTIONPREFACE。toByteArray());sink。flush();}
这里实际上是向HTTP2连接的Socket中写入了PRIHTTP2。0rrSMrr这一字符串。之后我们看到writer。settings方法:Writeokhttpssettingstothepeer。publicsynchronizedvoidsettings(Settingssettings)throwsIOException{if(closed)thrownewIOException(closed);intlengthsettings。size()6;bytetypeTYPESETTINGS;byteflagsFLAGNONE;intstreamId0;frameHeader(streamId,length,type,flags);for(inti0;iSettings。COUNT;i){if(!settings。isSet(i))continue;intidi;if(id4){id3;SETTINGSMAXCONCURRENTSTREAMSrenumbered。}elseif(id7){id4;SETTINGSINITIALWINDOWSIZErenumbered。}sink。writeShort(id);sink。writeInt(settings。get(i));}sink。flush();}
这里主要是写入了一些配置的数据,其中调用了frameHeader写入了帧头。
最后我们看到readerRunnable。execute:Overrideprotectedvoidexecute(){ErrorCodeconnectionErrorCodeErrorCode。INTERNALERROR;ErrorCodestreamErrorCodeErrorCode。INTERNALERROR;IOExceptionerrorExceptionnull;try{reader。readConnectionPreface(this);while(reader。nextFrame(false,this)){}connectionErrorCodeErrorCode。NOERROR;streamErrorCodeErrorCode。CANCEL;}catch(IOExceptione){errorExceptione;connectionErrorCodeErrorCode。PROTOCOLERROR;streamErrorCodeErrorCode。PROTOCOLERROR;}finally{close(connectionErrorCode,streamErrorCode,errorException);Util。closeQuietly(reader);}}
可以看到,这里主要是调用了reader。readConnectionPreface方法读取服务端发送来的preface,并判断是否为对应字符串,从而完成HTTP2连接的启动。TLS握手
接着我们看到TLS握手的过程,让我们看看connectTls方法:privatevoidconnectTls(ConnectionSpecSelectorconnectionSpecSelector)throwsIOException{Addressaddressroute。address();SSLSocketFactorysslSocketFactoryaddress。sslSocketFactory();booleansuccessfalse;SSLSocketsslSocketnull;try{基于之前建立的Socket建立一个包装对象SSLSocketsslSocket(SSLSocket)sslSocketFactory。createSocket(rawSocket,address。url()。host(),address。url()。port(),trueautoClose);对TLS相关信息进行配置ConnectionSpecconnectionSpecconnectionSpecSelector。configureSecureSocket(sslSocket);if(connectionSpec。supportsTlsExtensions()){Platform。get()。configureTlsExtensions(sslSocket,address。url()。host(),address。protocols());}进行握手sslSocket。startHandshake();获取SSLSessionSSLSessionsslSocketSessionsslSocket。getSession();HandshakeunverifiedHandshakeHandshake。get(sslSocketSession);验证证书对该主机是否有效if(!address。hostnameVerifier()。verify(address。url()。host(),sslSocketSession)){ListCertificatepeerCertificatesunverifiedHandshake。peerCertificates();if(!peerCertificates。isEmpty()){X509Certificatecert(X509Certificate)peerCertificates。get(0);thrownewSSLPeerUnverifiedException(Hostnameaddress。url()。host()notverified:certificate:CertificatePinner。pin(cert)DN:cert。getSubjectDN()。getName()subjectAltNames:OkHostnameVerifier。allSubjectAltNames(cert));}else{thrownewSSLPeerUnverifiedException(Hostnameaddress。url()。host()notverified(nocertificates));}}address。certificatePinner()。check(address。url()。host(),unverifiedHandshake。peerCertificates());StringmaybeProtocolconnectionSpec。supportsTlsExtensions()?Platform。get()。getSelectedProtocol(sslSocket):null;socketsslSocket;获取source及sinksourceOkio。buffer(Okio。source(socket));sinkOkio。buffer(Okio。sink(socket));handshakeunverifiedHandshake;protocolmaybeProtocol!null?Protocol。get(maybeProtocol):Protocol。HTTP11;successtrue;}catch(AssertionErrore){if(Util。isAndroidGetsocknameError(e))thrownewIOException(e);throwe;}finally{if(sslSocket!null){Platform。get()。afterHandshake(sslSocket);}if(!success){closeQuietly(sslSocket);}}}
可以看到,这里的步骤主要是下列步骤:基于之前建立的Socket建立包装类SSLSocket对TLS相关信息进行配置通过SSLSocket进行握手验证一些证书相关信息获取source及sink总结
学习资料整理:Android架构师进阶之路
资料领取,可前往私信:发送核心笔记或手册,即可领取Android架构成长资料!连接池小结:1。创建一个连接池
创建连接池非常简单只需要使用new关键字创建一个对象向就行了。newConnectionPool(maxIdleConnections,keepAliveDuration,timeUnit)2。向连接池中添加一个连接
a。通过ConnectionPool的put(realConnection)方法加入链接,在加入链接之前会先调用线程池执行cleanupRunnable匿名内部类来清理空闲的链接,然后再把链接加入Deque队列中,
b。在cleanupRunnable匿名内部类中执行死循环不停的调用cleanup来清理空闲的连接,并返回一个下次清理的时间间隔,调用ConnectionPool。wait方法根据下次清理的时间间隔
c。在cleanup的内部会遍历connections连接池队列,移除空闲时间最长的连接并返回下次清理的时间。
d。判断连接是否空闲是利用RealConnection内部的ListReference的size。如果size0就说明不空闲,如果size0就说明空闲。3。获取一个链接
通过ConnectionPool的get方法来获取符合要求的RealConnection。如果有服务要求的就返回RealConnection,并用该链接发起请求,如果没有符合要求的就返回null,并在外部重新创建一个RealConnection,然后再发起链接。判断条件:1。如果当前链接的技术次数大于限制的大小,或者无法在此链接上创建流,则直接返回false2。如果地址主机字段不一致直接返回false3。如果主机地址完全匹配我们就重用该连接复用机制小结
OkHttp中采用了连接池机制实现了连接的复用,避免了每次都创建新的连接从而导致资源的浪费。获取连接的过程主要如下:尝试在transimitter中寻找已经分配的连接transimitter中获取不到,尝试从连接池中获取连接连接池中仍然获取不到,尝试进行一次路由选择,再次从连接池中获取连接连接池中仍然找不到需要的连接,则创建一个新的连接由于HTTP2下采用了连接的多路复用机制,所以连接可以并行进行,因此再次尝试从连接池中获取连接,获取到则丢弃创建的连接若连接池中仍获取不到连接,则将刚刚创建的连接放入连接池
其中,在连接池中采用了一个清理线程对超过了设定参数的空闲连接进行清理,每次清理后会计算下一次需要清理的时间并进入阻塞,每当有新连接进入或连接进入空闲时会重新唤醒该清理线程。
对于每个连接,都采用了一种类似GC中的引用计数法的形式,每个RealConnection都持有了使用它的Transimitter的弱引用,通过判断持有的弱引用个数从而判断该连接是否空闲。
OkHttp默认将最大存活空闲连接个数设置为了5,且每个连接空闲时间不能超过5分钟,否则将被清理线程所回收。
而在连接建立过程中,首先会判断该连接是否需要SSL隧道,若不需要则直接建立了Socket并获取了其source及sink,若需要则会先尝试建立SSL隧道,最后再进行Socket连接。
Socket连接建立成功后,会通过establishProtocol方法对每个协议进行不同的处理,从而对各个协议进行支持(如对HTTPS的支持)