OkHttp3
一 概述
特点——高效的HTTP Client
- 支持HTTP2/SPDY
- socket自动选择最好的路线,并支持自动重连
- 拥有自动维护的socket连接池,减少握手的次数
- 拥有队列线程池,轻松写并发
- 拥有Interceptors处理请求与响应(比如托名GZIP压缩,LOGGING)
- 基于Header的缓存策略
2 主要对象
Connections
对JDK中的物理socket进行了引用计数封装,用来控制socket连接Streams
维护HTTP流,用来对Request/Response进行IO操作Call
HTTP请求任务封装StreamAllocation
用来控制Connections/Streams
的资源分配与释放
3 工作流程
-
创建Request。
Request request = new Request.Builder() .cacheControl(new CacheControl.Builder().onIfCache().build()) .url("") .build();
-
执行请求。这里实际是将请求
Call
放到了Dispatcher
中,使用Dispatcher
进行线程分发。OkHttpClient.newCall(request).excute()/enqueue();
Dispatcher
有两种方法:同步单线程;使用队列进行并发任务的分发与回调。- 设置相关Interceptors。
二 Dispatcher
1 线程池
1.1 特性 线程复用减少非核心任务的损耗
- 多线程解决CPU多个线程执行的问题。减少CPU的闲置时间,增加CPU的吞吐量。
- 线程池通过对线程缓存,减少 线程创建+销毁的时间。
- 线程池通过控制线程数量阈值,减少 线程过少CPU的闲置,线程过多对JVM内存、以及线程间切换系统调用的压力。
1.2 OkHttp Executor
-
构造单例线程池
public synchronized ExecutorService executorService() { if (executorService == null) { executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false)); } return executorService; }
- 4种线程池
- 参考ThreadPoolExecutor
2 Dispatcher 异步执行机制
2.1 Field
maxRequests = 64
: 最大并发请求数为64maxRequestsPerHost = 5
: 每个主机最大请求数为5Dispatcher
: 分发者,也就是生产者(默认在主线程)AsyncCall
: 队列中需要处理的Runnable(包装了异步回调接口)ExecutorService
:消费者池(也就是线程池)Deque<readyAsyncCalls>
:缓存(用数组实现,可自动扩容,无大小限制)Deque<runningAsyncCalls>
:正在运行的任务,仅仅是用来引用正在运行的任务以判断并发量,注意它并不是消费者缓存
2.2 流程
-
根据生产者消费者模型理论,当入队
enqueue
请求时,如果满足runningRequest<64 && runningRequestPerHost<5
,就可以直接把AsyncCall
加入到runningCalls
队列中,并在线程池中执行。如果消费者缓存满了,就放到readAsyncCalls
进行缓存等待。synchronized void enqueue(AsyncCall call) { if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { runningAsyncCalls.add(call);//添加正在运行的请求 executorService().execute(call); //线程池执行请求 } else { readyAsyncCalls.add(call); //添加到缓存队列排队等待 } }
-
当任务执行完成后,调用
finished
执行promoteCall()
, 手动将缓存区清理。private void promoteCalls() { //如果目前是最大负荷运转,接着等 if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity. //如果缓存等待区是空的,接着等 if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote. for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) { AsyncCall call = i.next(); if (runningCallsForHost(call) < maxRequestsPerHost) { //将缓存等待区最后一个移动到运行区中,并执行 i.remove(); runningAsyncCalls.add(call); executorService().execute(call); } if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity. } }
3 Dispatcher 原理 —— 反向代理与分发技术(单生产者——多消费者)
- Nginx/SLB中,用户通过HTTP(Socket)访问前置的服务器,服务器会添加Header并自动转发请求给后端集群,接着返回数据结果给用户。
- 将工作分配给多个Server并共享Redis的Session,可以提高服务的负载均衡能力,实现非阻塞,高可用,高并发连接,避免资源全部放到一台服务器而来的负载,速度,在线率等影响。
Dispatcher
——任务派发器,线程池
——多台服务器,AsyncCall
——Socket请求,Deque<readyAsyncCalls>
——Nginx内部缓存Call->Dispatcher->readAsyncCalls (<-promoteCall<-) -> (Thread1,2...)
4 相关知识点
4.1. Proxy(you - Proxy - server)
- OKHttp使用jdk自带的代理
- HTTP代理的本质是改Header信息,当访问url时,发送request,远程Server会完成DNS与请求操作。
4.2. DNS (域名 -> IP)
- OkHttp中默认使用DNS接口,
Dns.SYSTEM
,包装了java原生的socket包中的InetAddress.getAllByName(hostName)
. - 使用HTTP DNS。
4.3. Platform
-
OkHttp底层是Socket,不是URLConnection(Android4.4+,也使用了okhttp),通过
Platform.Class.forName()
反射获得当前Runtime使用的Socket库。okhttp//实现HTTP协议 framwork//JRE,实现JDK中Socket封装 jvm//JDK的实现,本质对libc标准库的native封装 bionic//android下的libc标准库 systemcall//用户态切换入内核 kernel//实现下协议栈(L4,L3)与网络驱动(一般是L2,L1)
-
如果想用蓝牙硬件中的Socket的进行HTTP协议开发,尝试overrid 这个类。
三 HTTP 连接
1 HTTP keepalive
优缺点
-
HTTP中的
keepalive
在网络性能优化中,对于延迟降低与速度提升有着非常重要的作用。通常在HTTP连接时,首先进行TCP握手,然后传输数据,最后释放连接。 对于复杂的网络中,重复的连接与释放消耗时间很长。每次连接大概是一次TTL的时间,在TLS环境下消耗的时间会更多。所以延时成为了非常重要的因素。
- 解决办法就是
keepalive connection
机制——可以在数据传输后仍然保持连接,当Client再次获取数据时,直接使用闲置下来的连接,而不是再次握手。 keepalive
缺点:- 概述:虽然提高了单个Client网络性能,但复用却阻碍了其他Client的链路速度。
- 根据TCP的拥塞机制,当总水管大小固定时,如果存在大量空闲的
keepalive connections
(称作僵尸连接,泄漏连接),其他Client的正常连接速度会受到影响,也是运营商为何限制P2P连接数的道理。 - server/Firewall有并发限制,如果
apache
Server对每个请求都开线程,导致只支持150个并发连接(数据源于nginx官网),不过这个瓶颈随着高并发Server软硬件的发展(golang/分布式/IO多路复用)将会越来越少。 - 大量的DDOS产生的僵尸连接可能被用于恶意攻击Server,耗尽资源。
2. 连接池ConnectionPool
2.1 源码中连接池关键对象
Call:
对Http请求封装接口。Connection:
对JDK的Socket物理连接都的包装,内部有List<WeakPreference<StreamAllocation>>
的引用,通过引用计数实现内存回收StreamCollection:
表示Connection被上层Call引用次数,通过aquire/release
改变List<WeakPreference>的大小。 ConnectionPool:
Socket连接池,对连接缓存进行回收与管理,与CommonPool类似Deque:
双端队列,同时具有队列和栈的特性,经常在缓存中被使用
2.2 连接池概述
- okHttp自动创建连接池,自动进行内存回收,管理线程池,提供了put/get/clear的接口。
- 上层代码调用中,使用StreamAllocation引用计数(使用aquire/release)方式跟踪Socket流的调用。
2.3 ConnectionPool
关键接口
- 在
OkHttpClient
的static块实现了Internal.instance
作为ConnectionPool的包装。 ConnectionPool
中维护了ThreadPool
,用来release 末位的Socket,条件有- 并发socket空闲连接>5个
- 某个Socket的keepalive时间>5min
Deque<Connection>
get
从ConnectionPool获取put
放入ConnectionPoolConnectionBecameIdle
线程空闲,清理线程池evictAll
关闭所有连接
RouteDatabase
,记录连接是被的Route的黑名单,当连接失败后就会把Route加进去。
2.4 Connection自动回收
- 对Socket的回收
- 根据对象的引用树实现。RealConnection的虚引用StreamAllocation的引用计数为0然后GC。
-
socket连接成功,在网络线程中向ConnectionPool中put新的Socket,主动调用清理函数,线程池执行clearupRunnable
while(true) { long waitNanos = cleanup(System.nanoTime());//执行清理需要的时间 if (waitNanos == -1) return; if (waitNanos > 0) { synchronized (ConnectionPool.this) { try { //在timeout内释放锁和时间片 ConnectionPool.this.wait(TimeUnit.NANOSECONDS.toMillis(waitNanos)); } catch (InterruptedException e) {} } } }
- cleanup ——
标记-清除GC算法
,标记出最不活跃的连接(idle),然后清除。-
遍历 deque
中所有的RealConnection
,标记idle连接
(keepalive时间 > 5min空闲连接>5),将连接移除,并return 0,执行wait(0),立即执行下次清除。 0 < idle连接数 < 5,
返回此连接即将到期的时间,供下次清理。inUse连接数 > 0,
全部都是活跃连接,返回Keepalive时间,5min后再次执行清理。没有任何连接,
return -1,跳出循环。
long cleanUp(long now) { int inUseConnectionCount = 0;//活跃连接数 int idleConnectionCount = 0; //空闲连接数 RealConnection longestIdleConnection = null;//空闲连接 long longestIdleDurationsNs = Long.MIN_VALUE; //遍历deque中所有的RealConnection,标记泄漏的连接 synchronized (this) { for (RealConnection connection : connections) { //查询此连接内部StreamAllocation的引用计数。判断活跃的连接数 if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; } idleConnectionCount++; //选择排序算法,标记处空闲连接 long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationsNs) { longestIdleDurationsNs = idleDurationNs; longestIdleConnection = connection; } }//for end if (longestIdleDurationsNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { //keepalive时间> 5min(longestIdleDurationsNs) || 空闲连接> 5, 找到一个ilde Connection,将其remove connections.remove(longestIdleConnection); } else if (idleConnectionCount > 0) { //返回此连接即将到期的时间,供下次清理 return keepAliveDurationNs - longestIdleDurationsNs; } else if (inUseConnectionCount > 0) { //全部是活跃的连接,5分钟之后再清理 return keepAliveDurationNs; } else { //没有任何连接,跳出循环 cleanupRunning = false; return -1; } }//synchronized end //关闭连接,返回0,表示立即清理 closeQuietly(longestIdleConnection.socket()); return 0; }
-
5 Socket管理
封装好Request后进行HTTP连接,需要Socket握手,根据域名或代理确定Socket的ip与端口。
5.1 选择路线与自动重连(RouteSelector)—— 获取Socket的ip与port
- Proxy == null
- 构造函数中设置代理为Proxy.NO_PROXY
- 如果缓存中的lastInetSocketAddress = null,就通过DNS查询,并保存结果(array: 域名-N ip url)。
- 如果还没有查询到,就递归调用next查询,直到查到为止。否则就抛出NoSuchElementException. 退出
- Proxy != null
- 设置Socket的ip为代理地址的ip
- 设置Socket的port为代理地址的port
- next没有找到,则抛出NoSuchElementsException. 退出
5.2 连接Socket链路(RealConnection)
- ip,port准备好,就进行TCP连接,即三次握手
- 如果连接池中存在连接,就
get
RealConnection,如果没有命中就进入next - 根据选择的路线route,调用
Platform.get().connectSocket
选择当前平台Runtime下最好的Socket库进行握手 - 将简历成功的RealConnection
put
到连接池缓存 - 如果存在TLS,就根据
TLSVersion与证书
进行安全握手 - 构造
HttpStream
并维护本次的Socket连接,管道建立完成
5.3 释放Socket链路(release)
- 如果通信完成,或连接失败,就释放release connection。
- 尝试从缓存的连接池中remove
- 如果没有命中缓存,直接调用JDK的Socket关闭
6 HTTP请求序列化/反序列化(HttpStream)
分析从拼装HTTP套接字到读取的步骤,即实现Parser.
6.1 获得HTTP流
- 前提:无缓存,无多次302跳转,网络良好。
- 将从
RealConnection.connectSocket()
构造出来的httpStream = connect()
建立套接字连接,完成三次握手。 -
通过
okio + remote socket
建立IO连接。source = Okio.buffer(Okio.source(rawSocket));//source用于获取Response sink = Okio.buffer(Okio.sink(rawSocket));//sink用于write Buffer到Server
- 关于
Buffer, Source, Sink
的解释。Buffer
可变字节,类似于byte[],相当于传输介质。Source
是okio库输入组件,类似于InputStream,read(buffer, byteCount)
从流中读取数据。Sink
是okio库输出组件,类似于outputStream,用于写到file/socket,write(buffer, byteCount)
写数据到Buffer中。- 类似于管道。
Sink -> Socket/file -> Source
6.2 拼装Raw请求与Headers(writeRequestHeaders)—— Interceptor
- 拦截器是okhttp中强大的流程装置,用来监控log,修改/消费请求,修改结果,甚至对用户透明的GZIP压缩。
- 使用
Interceptor.Chain
进行多次拦截修改操作。在RealInterceptorChain.proceed
6.3 获得响应(readResponseHeaders/Body)
四 缓存策略
1 Cache
1.1 特性
- 缓存——快速获取数据的区域。指可以进行高速数据交换的存储器,先于
内存
与CPU
交换数据,速率很快。
1.2 原理
- 读取顺序
- 缓存分类
- 读取命中率
1.3 okHttp的文件系统
- 缓存载体为FileSystem(通过Okio库中的Source/Sink对File封装)
- 使用的LRU作为页面置换算法——封装了LinkeHashMap
- FileSystem: 使用Okio对File封装,简化了IO操作
- DiskLruCache:维护文件的创建,清理,读取。内部有清理线程池。
- Editor: 添加同步锁,并对FileSystem进一步封装
- Entry:维护着Key对应的多个文件
- Cache:被上层代码调用,提供put/get操作,封装了缓存检查条件,自动清理
- Entry:Response对象与Okio的序列化/反序列化
- Response/Request
1.4 OkHttp.Cache
- Response get(Request) ` Request的url——>key 传给DiskLruCache ——> snapshot ——> Cache.Entry(snapshot.getSource) ——> entry.response `
- CacheRequest put(Response) ` response ——> requestMethod(只缓存GET) ——> Cache.Entry(response) ——> DiskLruCache.Editor ——> CacheRequest`
- Cahce.Entry: Source数据流,Response数据的序列化类。
Response(java对象) <- Cache.Entry -> source/sink(文件io)
CacheRequest:DiskLruCache.Editor --> Sink
1.5 DiskLruCache
- FileSystem <- DiskLruCache.Entry/Editor -> source/sink
- Entry: 传入 key(Request url) 通过加.tmp, FileSystem –> source –> Snapshot
- Editor:
- Snapshot: Entry&values 映射,
--> Editor edit(), Source
1.6 缓存的自动清理
- DiskLruCache ThreadPoolExecutor
- cleanupRunnable
2 HTTP 缓存
2.1 Header字段
noCache :
不使用缓存,全部走网络noStore :
不使用缓存,也不存储缓存onlyIfCached :
只使用缓存maxAge :
设置最大失效时间,失效则不使用maxStale :
设置最大失效时间,失效则不使用minFresh :
设置最小有效时间,失效则不使用FORCE_NETWORK :
强制走网络FORCE_CACHE :
强制走缓存Date :
消息发送的时间Age :
CDN反代Server到原Server获取数据延时的缓存时间
2.2 一些标签的含义
Expires
到期时间,超过这个时间后需要网络:Expires: Thu, 1 Fre 2018 11:01:33 GMT
Cache-Control
某个文件被续多少秒,避免额外网络请求。- 比Expires优先级高
- 不需要C/S时间同步
Cache-Control: max-age=31536000, public
Reving Filenames
修订文件名。- 如果通过设置Header保证了Client是可以缓存的,但Server更新了文件,该怎么解决?
- 通过url中文件名版本后缀进行缓存。
bd.com/libs/jquery/jquery-2.0.3.min.js
提供多个jquery版本 - 这个方法简单,实践性高
-
(Conditional GET Requests)
与304
——缓存策略全部交给Server判断(场景 Client缓存过期或放弃缓存)。 *Last-Modified-Date
``` Client第一次网络请求时,Server返回了 Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT Client再次请求时,发送 If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT 交给Server进行判断,如果仍然可以缓存使用,服务器就返回304 ``` * `ETag`——对资源文件的一种摘要,Client并不需要了解细节。 ``` Client第一次请求,Server返回 ETag: "5694c7ef-24dc" Client再次请求,发送 If-None-Match:"5694c7ef-24dc" 交给Server进行判断,如果仍然可以缓存使用,服务器就返回304 ``` * 如果 ETag 和 Last-Modified 都有,则必须一次性都发给服务器,它们没有优先级之分
2 CacheStrategy
—— 实现的缓存策略
- 根据之前的缓存结果response 与当前将要发送的Request的Header进行策略分析。
Input(request, cacheCandidate) --> CacheStrategy(处理,判断Header信息) --> Output(networkRequest, cacheResponse)