Okhttp3

2018/04/20

OkHttp3

一 概述

特点——高效的HTTP Client

  1. 支持HTTP2/SPDY
  2. socket自动选择最好的路线,并支持自动重连
  3. 拥有自动维护的socket连接池,减少握手的次数
  4. 拥有队列线程池,轻松写并发
  5. 拥有Interceptors处理请求与响应(比如托名GZIP压缩,LOGGING)
  6. 基于Header的缓存策略

2 主要对象

  1. Connections 对JDK中的物理socket进行了引用计数封装,用来控制socket连接
  2. Streams维护HTTP流,用来对Request/Response进行IO操作
  3. CallHTTP请求任务封装
  4. StreamAllocation用来控制Connections/Streams的资源分配与释放

3 工作流程

  1. 创建Request。

     Request request = new Request.Builder()
                     .cacheControl(new CacheControl.Builder().onIfCache().build())
                     .url("")
                     .build();
    
  2. 执行请求。这里实际是将请求Call放到了Dispatcher中,使用Dispatcher进行线程分发。

     OkHttpClient.newCall(request).excute()/enqueue();
    
  3. Dispatcher有两种方法:同步单线程;使用队列进行并发任务的分发与回调。
  4. 设置相关Interceptors。

二 Dispatcher

1 线程池

1.1 特性 线程复用减少非核心任务的损耗

  1. 多线程解决CPU多个线程执行的问题。减少CPU的闲置时间,增加CPU的吞吐量。
  2. 线程池通过对线程缓存,减少 线程创建+销毁的时间。
  3. 线程池通过控制线程数量阈值,减少 线程过少CPU的闲置,线程过多对JVM内存、以及线程间切换系统调用的压力。

1.2 OkHttp Executor

  1. 构造单例线程池

     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;
     }
    	
    
  2. 4种线程池
  3. 参考ThreadPoolExecutor

2 Dispatcher 异步执行机制

2.1 Field

  1. maxRequests = 64: 最大并发请求数为64
  2. maxRequestsPerHost = 5: 每个主机最大请求数为5
  3. Dispatcher: 分发者,也就是生产者(默认在主线程)
  4. AsyncCall: 队列中需要处理的Runnable(包装了异步回调接口)
  5. ExecutorService:消费者池(也就是线程池)
  6. Deque<readyAsyncCalls>:缓存(用数组实现,可自动扩容,无大小限制)
  7. Deque<runningAsyncCalls>:正在运行的任务,仅仅是用来引用正在运行的任务以判断并发量,注意它并不是消费者缓存

2.2 流程

  1. 根据生产者消费者模型理论,当入队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); //添加到缓存队列排队等待
       }
     }
    
  2. 当任务执行完成后,调用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 原理 —— 反向代理与分发技术(单生产者——多消费者)

  1. Nginx/SLB中,用户通过HTTP(Socket)访问前置的服务器,服务器会添加Header并自动转发请求给后端集群,接着返回数据结果给用户。
  2. 将工作分配给多个Server并共享Redis的Session,可以提高服务的负载均衡能力,实现非阻塞,高可用,高并发连接,避免资源全部放到一台服务器而来的负载,速度,在线率等影响。
  3. Dispatcher——任务派发器,线程池——多台服务器,AsyncCall——Socket请求,Deque<readyAsyncCalls>——Nginx内部缓存
  4. Call->Dispatcher->readAsyncCalls (<-promoteCall<-) -> (Thread1,2...)

4 相关知识点

4.1. Proxy(you - Proxy - server)

  1. OKHttp使用jdk自带的代理
  2. HTTP代理的本质是改Header信息,当访问url时,发送request,远程Server会完成DNS与请求操作。

4.2. DNS (域名 -> IP)

  1. OkHttp中默认使用DNS接口,Dns.SYSTEM,包装了java原生的socket包中的InetAddress.getAllByName(hostName).
  2. 使用HTTP DNS。

4.3. Platform

  1. 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)
    
  2. 如果想用蓝牙硬件中的Socket的进行HTTP协议开发,尝试overrid 这个类。

三 HTTP 连接

1 HTTP keepalive优缺点

  1. HTTP中的keepalive在网络性能优化中,对于延迟降低与速度提升有着非常重要的作用。

    通常在HTTP连接时,首先进行TCP握手,然后传输数据,最后释放连接。如图 对于复杂的网络中,重复的连接与释放消耗时间很长。每次连接大概是一次TTL的时间,在TLS环境下消耗的时间会更多。所以延时成为了非常重要的因素。

  2. 解决办法就是keepalive connection机制——可以在数据传输后仍然保持连接,当Client再次获取数据时,直接使用闲置下来的连接,而不是再次握手。如图
  3. keepalive缺点:
    • 概述:虽然提高了单个Client网络性能,但复用却阻碍了其他Client的链路速度。
    • 根据TCP的拥塞机制,当总水管大小固定时,如果存在大量空闲的keepalive connections(称作僵尸连接,泄漏连接),其他Client的正常连接速度会受到影响,也是运营商为何限制P2P连接数的道理。
    • server/Firewall有并发限制,如果apache Server对每个请求都开线程,导致只支持150个并发连接(数据源于nginx官网),不过这个瓶颈随着高并发Server软硬件的发展(golang/分布式/IO多路复用)将会越来越少。
    • 大量的DDOS产生的僵尸连接可能被用于恶意攻击Server,耗尽资源。

2. 连接池ConnectionPool

2.1 源码中连接池关键对象

  1. Call: 对Http请求封装接口。
  2. Connection: 对JDK的Socket物理连接都的包装,内部有List<WeakPreference<StreamAllocation>>的引用,通过引用计数实现内存回收
  3. StreamCollection: 表示Connection被上层Call引用次数,通过aquire/release改变List<WeakPreference>的大小。
  4. ConnectionPool: Socket连接池,对连接缓存进行回收与管理,与CommonPool类似
  5. Deque: 双端队列,同时具有队列和栈的特性,经常在缓存中被使用

2.2 连接池概述

  1. okHttp自动创建连接池,自动进行内存回收,管理线程池,提供了put/get/clear的接口。
  2. 上层代码调用中,使用StreamAllocation引用计数(使用aquire/release)方式跟踪Socket流的调用。

2.3 ConnectionPool 关键接口

  1. OkHttpClientstatic块实现了Internal.instance作为ConnectionPool的包装。
  2. ConnectionPool中维护了ThreadPool,用来release 末位的Socket,条件有
    • 并发socket空闲连接>5个
    • 某个Socket的keepalive时间>5min
  3. Deque<Connection>
    • get 从ConnectionPool获取
    • put 放入ConnectionPool
    • ConnectionBecameIdle 线程空闲,清理线程池
    • evictAll 关闭所有连接
  4. RouteDatabase,记录连接是被的Route的黑名单,当连接失败后就会把Route加进去。

2.4 Connection自动回收

  1. 对Socket的回收
  2. 根据对象的引用树实现。RealConnection的虚引用StreamAllocation的引用计数为0然后GC。
  3. 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) {}	
             }
         }
     }
    	
    
  4. 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

  1. Proxy == null
    1. 构造函数中设置代理为Proxy.NO_PROXY
    2. 如果缓存中的lastInetSocketAddress = null,就通过DNS查询,并保存结果(array: 域名-N ip url)。
    3. 如果还没有查询到,就递归调用next查询,直到查到为止。否则就抛出NoSuchElementException. 退出
  2. Proxy != null
    1. 设置Socket的ip为代理地址的ip
    2. 设置Socket的port为代理地址的port
    3. next没有找到,则抛出NoSuchElementsException. 退出

5.2 连接Socket链路(RealConnection)

  1. ip,port准备好,就进行TCP连接,即三次握手
  2. 如果连接池中存在连接,就get RealConnection,如果没有命中就进入next
  3. 根据选择的路线route,调用Platform.get().connectSocket选择当前平台Runtime下最好的Socket库进行握手
  4. 将简历成功的RealConnection put到连接池缓存
  5. 如果存在TLS,就根据TLSVersion与证书进行安全握手
  6. 构造HttpStream并维护本次的Socket连接,管道建立完成

5.3 释放Socket链路(release)

  1. 如果通信完成,或连接失败,就释放release connection。
  2. 尝试从缓存的连接池中remove
  3. 如果没有命中缓存,直接调用JDK的Socket关闭

6 HTTP请求序列化/反序列化(HttpStream)

分析从拼装HTTP套接字到读取的步骤,即实现Parser.

6.1 获得HTTP流

  1. 前提:无缓存,无多次302跳转,网络良好。
  2. 将从RealConnection.connectSocket()构造出来的httpStream = connect()建立套接字连接,完成三次握手。
  3. 通过okio + remote socket建立IO连接。

     source = Okio.buffer(Okio.source(rawSocket));//source用于获取Response
     sink = Okio.buffer(Okio.sink(rawSocket));//sink用于write Buffer到Server
    
  4. 关于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

  1. 拦截器是okhttp中强大的流程装置,用来监控log,修改/消费请求,修改结果,甚至对用户透明的GZIP压缩。
  2. 使用Interceptor.Chain进行多次拦截修改操作。在RealInterceptorChain.proceed

6.3 获得响应(readResponseHeaders/Body)

四 缓存策略

1 Cache

1.1 特性

  1. 缓存——快速获取数据的区域。指可以进行高速数据交换的存储器,先于内存CPU交换数据,速率很快。

1.2 原理

  1. 读取顺序
  2. 缓存分类
  3. 读取命中率

1.3 okHttp的文件系统

  1. 缓存载体为FileSystem(通过Okio库中的Source/Sink对File封装)
  2. 使用的LRU作为页面置换算法——封装了LinkeHashMap
  3. FileSystem: 使用Okio对File封装,简化了IO操作
  4. DiskLruCache:维护文件的创建,清理,读取。内部有清理线程池。
    • Editor: 添加同步锁,并对FileSystem进一步封装
    • Entry:维护着Key对应的多个文件
  5. Cache:被上层代码调用,提供put/get操作,封装了缓存检查条件,自动清理
    • Entry:Response对象与Okio的序列化/反序列化
  6. Response/Request

1.4 OkHttp.Cache

  1. Response get(Request) ` Request的url——>key 传给DiskLruCache ——> snapshot ——> Cache.Entry(snapshot.getSource) ——> entry.response `
  2. CacheRequest put(Response) ` response ——> requestMethod(只缓存GET) ——> Cache.Entry(response) ——> DiskLruCache.Editor ——> CacheRequest`
  3. Cahce.Entry: Source数据流,Response数据的序列化类。 Response(java对象) <- Cache.Entry -> source/sink(文件io)
  4. CacheRequest:DiskLruCache.Editor --> Sink

1.5 DiskLruCache

  1. FileSystem <- DiskLruCache.Entry/Editor -> source/sink
  2. Entry: 传入 key(Request url) 通过加.tmp, FileSystem –> source –> Snapshot
  3. Editor:
  4. Snapshot: Entry&values 映射,--> Editor edit(), Source

1.6 缓存的自动清理

  1. DiskLruCache ThreadPoolExecutor
  2. cleanupRunnable

2 HTTP 缓存

2.1 Header字段

  1. noCache :不使用缓存,全部走网络
  2. noStore :不使用缓存,也不存储缓存
  3. onlyIfCached : 只使用缓存
  4. maxAge : 设置最大失效时间,失效则不使用
  5. maxStale : 设置最大失效时间,失效则不使用
  6. minFresh : 设置最小有效时间,失效则不使用
  7. FORCE_NETWORK : 强制走网络
  8. FORCE_CACHE : 强制走缓存
  9. Date : 消息发送的时间
  10. Age : CDN反代Server到原Server获取数据延时的缓存时间

2.2 一些标签的含义

  1. Expires 到期时间,超过这个时间后需要网络: Expires: Thu, 1 Fre 2018 11:01:33 GMT
  2. Cache-Control 某个文件被续多少秒,避免额外网络请求。
    • 比Expires优先级高
    • 不需要C/S时间同步
    • Cache-Control: max-age=31536000, public
  3. Reving Filenames 修订文件名。
    • 如果通过设置Header保证了Client是可以缓存的,但Server更新了文件,该怎么解决?
    • 通过url中文件名版本后缀进行缓存。bd.com/libs/jquery/jquery-2.0.3.min.js提供多个jquery版本
    • 这个方法简单,实践性高
  4. (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—— 实现的缓存策略

  1. 流程图
  2. 根据之前的缓存结果response 与当前将要发送的Request的Header进行策略分析。 Input(request, cacheCandidate) --> CacheStrategy(处理,判断Header信息) --> Output(networkRequest, cacheResponse)

4. 参考文章

  1. Retrofit 缓存
  2. Retrofit 会用
  3. okHttp3 源码分析

文内导航