okhttp源码解析-框架主要实现原理

1,031 阅读6分钟

1.使用场景

通过使用框架提供的接口,我们可以轻松地构造一个http请求去请求度娘的网页内容。

OkHttpClient client = new OkHttpClient();
Request request = new Request.Builder().url("http://www.baidu.com").build();
Response response = client.newCall(request).execute();
if (response.isSuccessful()) {
  log.info(response.body().string());
} 

       在这次的HTTP请求中主要有做了以下的一些操作。OKhttp框架针对每个流程节点都做了专门的优化,比如在DNS解析阶段可以加入自定义的解析器,创建连接使用的是连接池,发送和接收数据时使用了内存池封装了IO处理操作。所以 OKhttp框架不但止简单,性能和效率、扩展性能都是不错的。

2.主要流程

        一步一步debug下去,我们可以发现这整个流程如下图所示,首先OKhttp客户端会构建一个Call请求,然后压入线程池中,执行过程中会逐一调用拦截器链进行操作,最后在CallServerInterceptor拦截器中进行等待网络IO处理,并返回响应内容信息供上层应用。

3.总体架构设计

       看了上面的调用链路,估计还是一脸懵x的,其实这里用架构思想去理解整个框架会更加简单明了,整个框架可以分为客户层、执行层、连接层三个层,其中客户层主要是Client类,在执行层以为Dispatcher、线程池、拦截器链为主要部分,在连接层负责具体网络通信IO,为了资源利用率最大化,okhttp框架用了连接池、内存池两大组件。

4.主要实现原理

4.1  IO模型

        所有的系统I/O都分为两个阶段:等待就绪和操作。举例来说,读函数,分为等待系统可读和真正的读;同理,写函数分为等待网卡可以写和真正的写。需要说明的是等待就绪的阻塞是不使用CPU的,是在“空等”;而真正的读写操作的阻塞是使用CPU的,真正在”干活”,而且这个过程非常快,属于memory copy,带宽通常在1GB/s级别以上,可以理解为基本不耗时。

下图是几种常见I/O模型的对比:

以socket.read()为例子:传统的BIO里面socket.read(),如果TCP RecvBuffer里没有数据,函数会一直阻塞,直到收到数据,返回读到的数据。对于NIO,如果TCP RecvBuffer有数据,就把数据从网卡读到内存,并且返回给用户;反之则直接返回0,永远不会阻塞。最新的AIO(Async I/O)里面会更进一步:不但等待就绪是非阻塞的,就连数据从网卡到内存的过程也是异步的。OKhttp框架由于定位是给客户端使用, 所以不需要使用复杂的NIO设计,而是使用了最简单的BIO模型,一个请求对应一个线程,使用线程池管理,非常简单明了。

void enqueue(AsyncCall call) {
  ....
  promoteAndExecute();
  ...
}

private boolean promoteAndExecute() {
  List executableCalls = new ArrayList<>();
  boolean isRunning;
  synchronized (this) {
    for (Iterator i = readyAsyncCalls.iterator(); i.hasNext(); ) {
      AsyncCall asyncCall = i.next();
       //正在运行的线程数超出最大请求数则不处理
      if (runningAsyncCalls.size() >= maxRequests) break; 
      if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue;
      i.remove();
      asyncCall.callsPerHost().incrementAndGet();
      executableCalls.add(asyncCall);
      runningAsyncCalls.add(asyncCall);
    }
    isRunning = runningCallsCount() > 0;
  }

   //使用线程池批量处理请求
  for (int i = 0, size = executableCalls.size(); i < size; i++) {
    AsyncCall asyncCall = executableCalls.get(i);
    asyncCall.executeOn(executorService());
  }
  return isRunning;
}

void executeOn(ExecutorService executorService) {
  boolean success = false;
  try {
    executorService.execute(this);
    success = true;
  } catch (RejectedExecutionException e) {
    .....
    responseCallback.onFailure(RealCall.this, ioException);
  } finally {
    if (!success) {
      client.dispatcher().finished(this); 
    }
  }
}

4.2  拦截器链

         拦截器链(作为解耦神器)非常好地增强了框架的扩展性,只要通过继承Interceptor接口,就可以方便地把自定义的拦截处理器放入链条中,顺序处理请求前后的内容。

Response getResponseWithInterceptorChain() throws IOException {

  List interceptors = new ArrayList<>();
  interceptors.addAll(client.interceptors());
  //重试
  interceptors.add(new RetryAndFollowUpInterceptor(client));
  //初始化http-header内容
  interceptors.add(new BridgeInterceptor(client.cookieJar()));
  //本地缓存
  interceptors.add(new CacheInterceptor(client.internalCache()));
  //网络连接
  interceptors.add(new ConnectInterceptor(client));
  
  if (!forWebSocket) {
    interceptors.addAll(client.networkInterceptors());
  }

  //网络读写
  interceptors.add(new CallServerInterceptor(forWebSocket));

  Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0,
      originalRequest, this, client.connectTimeoutMillis(),
      client.readTimeoutMillis(), client.writeTimeoutMillis());

  boolean calledNoMoreExchanges = false;
  try {
    //开始执行拦截链
    Response response = chain.proceed(originalRequest);
    if (transmitter.isCanceled()) {
      closeQuietly(response);
      throw new IOException("Canceled");
    }
    return response;
  } catch (IOException e) {
    calledNoMoreExchanges = true;
    throw transmitter.noMoreExchanges(e);
  } finally {
    if (!calledNoMoreExchanges) {
      transmitter.noMoreExchanges(null);
    }
  }
}

4.3  连接池

         按照我们的理解都是像mysql的连接池一样,一个TCP连接保持n久时间的,但是HTTP协议怎么保持长连接呢?那么我们需要了解HTTP协议内容一些概念, 在协议头部有个keep-alive 就是浏览器和服务端之间保持长连接,说明这个连接是可以复用的。在HTTP1.1中是默认开启的。通常我们在发起HTTP请求的时候首先要完成tcp的三次握手,然后传输数据,最后再释放连接。(HTTP1.1默认将Keep-Alive首部开启,用于客户端和服务器通信连接的保存时间,TCP中有Keep-Alive报文,来定时探测通信双方是否存活,但是这一部分内容用于长连接时会存在问题)。

        OKhttp的连接的管理交由ConnectionPool,内部含有一个保存了RealConnection对象的队列。http2.0一个连接对应多个流,在RealConnection内保存了一个代表流的StreamAllocation对象的list。在ConnectIntercepter这一层调用StreamAllocation的newStrem,尝试在连接池里找到一个RealConnection,没找到则创建一个,并调用acquire添加一个自身的弱引用到RealConnection的流引用List中。newStrem最终返回一个httpcodec接口的实现,代表了具体的http协议内容创建规则,有两种实现,对应了okhttp适配的两个http版本,然后传递给下一级。当然流在读写完成后也是需要被清理的,清理函数deallocate,一个连接的流都被清理掉之后,通知ConnectionPool判断连接的kepp-alive时间,以及空闲连接数量,移除超时或者超出数量限制后空闲时间最久的连接。如下是调用流和连接的绑定过程,新创建的连接会执行socket的connect,connect的时候会判断http协议是哪个版本,然后新创建的RealConnection会添加到连接池里。

public final class ConnectionPool {
    /**
     * 后台线程用于清理过期的连接。 
     * 每个连接池最多只能运行一个线程。 线程池执行器允许池本身被垃圾收集。
     */
    private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
            Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
            new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));
    // 在执行execute会立马交由复用的线程或新创建线程执行任务
    //清理连接,在线程池executor里调用。
    private final Runnable cleanupRunnable = new Runnable() {
        @Override
        public void run() {
            while (true) {
                //执行清理,并返回下次需要清理的时间。
                // 没有任何连接时,cleanupRunning = false;
                long waitNanos = cleanup(System.nanoTime());
                if (waitNanos == -1) return;
                if (waitNanos > 0) {
                    long waitMillis = waitNanos / 1000000L;
                    waitNanos -= (waitMillis * 1000000L);
                    synchronized (ConnectionPool.this) {
                        //在timeout时间内释放锁
                        try {
                            ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                        } catch (InterruptedException ignored) {
                        }
                    }
                }
            }
        }
    };  

ConnectionPool提供对Deque进行操作的方法分别为put、get、connectionBecameIdle、evictAll几个操作。分别对应放入连接、获取连接、移除连接、移除所有连接操作。

void put(RealConnection connection) {
    assert (Thread.holdsLock(this));
    if (!cleanupRunning) {
      cleanupRunning = true;
      executor.execute(cleanupRunnable);
    }
    connections.add(connection);
  }
  
  //遍历connections缓存列表,当某个连接计数的次数小于限制的大小以及
  //request的地址和缓存列表中此连接的地址完全匹配。则直接复用缓存列表中的connection作为request的连接。
RealConnection get(Address address, StreamAllocation streamAllocation) {
    assert (Thread.holdsLock(this));
    for (RealConnection connection : connections) {
      if (connection.allocations.size() < connection.allocationLimit
          && address.equals(connection.route().address)
          && !connection.noNewStreams) {
        streamAllocation.acquire(connection);
        return connection;
      }
    }
    return null;
 }

       线程中不停调用Cleanup 清理的动作并立即返回下次清理的间隔时间。继而进入wait 等待之后释放锁,继续执行下一次的清理。所以可能理解成他是个监测时间并释放连接的后台线程。了解cleanup动作的过程。这里就是如何清理所谓闲置连接的和行了。怎么找到闲置的连接是主要解决的问题。在遍历缓存列表的过程中,使用连接数目inUseConnectionCount 和闲置连接数目idleConnectionCount 的计数累加值都是通过pruneAndGetAllocationCount() 是否大于0来控制的。那么很显然pruneAndGetAllocationCount() 方法就是用来识别对应连接是否闲置的。>0则不闲置。否则就是闲置的连接。

private final Runnable cleanupRunnable = new Runnable() {
    @Override public void run() {
      while (true) {
        long waitNanos = cleanup(System.nanoTime());
        if (waitNanos == -1)eturn;
        if (waitNanos > 0) {
          long waitMillis = waitNanos / 1000000L;
          waitNanos -= (waitMillis * 1000000L);
          synchronized (ConnectionPool.this) {
            try {
              ConnectionPool.this.wait(waitMillis, (int) waitNanos);
            } catch (InterruptedException ignored) {
            }
          }
        }
      }
    }
  };
  
  
  long cleanup(long now) {
    int inUseConnectionCount = 0;
    int idleConnectionCount = 0;
    RealConnection longestIdleConnection = null;
    long longestIdleDurationNs = Long.MIN_VALUE;

    synchronized (this) {
      //遍历所有的连接,标记处不活跃的连接。
      for (Iterator i = connections.iterator(); i.hasNext(); ) {
        RealConnection connection = i.next();
        //查询此连接内部的streamAllocation的引用数量
        if (pruneAndGetAllocationCount(connection, now) > 0) {
          inUseConnectionCount++;
          continue;
        }
        idleConnectionCount++;
        long idleDurationNs = now - connection.idleAtNanos;
        if (idleDurationNs > longestIdleDurationNs) {
          longestIdleDurationNs = idleDurationNs;
          longestIdleConnection = connection;
        }
      }
      
      if (longestIdleDurationNs >= this.keepAliveDurationNs
          || idleConnectionCount > this.maxIdleConnections) {
        connections.remove(longestIdleConnection);
      } else if (idleConnectionCount > 0) {
        return keepAliveDurationNs - longestIdleDurationNs;
      } else if (inUseConnectionCount > 0) {
        return keepAliveDurationNs;
      } else {
        cleanupRunning = false;
        return -1;
      }
    }
    
    closeQuietly(longestIdleConnection.socket());
    return 0;
  }
  
  private int pruneAndGetAllocationCount(RealConnection connection, long now) {
      List> references = connection.allocations;
      for (int i = 0; i < references.size(); ) {
        Reference reference = references.get(i);
        //如果虚饮用StreamAllocation正在被使用,则跳过进行下一次循环
        if (reference.get() != null) {
          i++;
          continue;
        }
        StreamAllocation.StreamAllocationReference streamAllocRef =
            (StreamAllocation.StreamAllocationReference) reference;
        //如果所有的StreamAllocation没被引用了,则移除    
        references.remove(i);
        connection.noNewStreams = true;
        if (references.isEmpty()) {
          connection.idleAtNanos = now - keepAliveDurationNs;
          return 0;
        }
  }
  return references.size();
}

4.4 内存池

这个东东比较有意思,而且设计比较精巧,我们单独讲。