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 内存池
这个东东比较有意思,而且设计比较精巧,我们单独讲。