OkHttp源码阅读(一)连接池的复用

本文深入探讨OkHttp的连接池复用机制,通过findConnection()函数解析连接建立与关闭的过程,揭示http短连接的特性。
摘要由CSDN通过智能技术生成
public final class ConnectionPool {
       /**
    	* Background threads are used to cleanup expired connections. There will be at most a single
	* thread running per connection pool. The thread pool executor permits the pool itself to be
	* garbage collected.
	* 后台线程用于清理过期的连接。 每个连接池最多只能运行一个线程。 线程池执行器允许池本身被垃圾收集。
	*/
	private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,	//线程池,用来检测闲置socket并对其进行清理
		  Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
		  new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
	  /**
	   * The maximum number of idle connections for each address. 
	   * 每个地址的最大空闲连接数。
	   */
	private final int maxIdleConnections;//最大闲置连接数
	private final long keepAliveDurationNs;//连接超时时间
	private final Runnable cleanupRunnable = new Runnable() {
		@Override
		public void run() {
			while (true) {
				//线程中不停调用Cleanup 清理的动作并立即返回下次清理的间隔时间。继而进入wait 等待之后释放锁,继续执行下一次的清理
				long waitNanos = cleanup(System.nanoTime());
				if (waitNanos == -1) {
					return;
				}
				if (waitNanos > 0) {
					long waitMillis = waitNanos / 1000000L;
					waitNanos -= (waitMillis * 1000000L);//计算下次清理时间
					synchronized (ConnectionPool.this) {
						try {
							ConnectionPool.this.wait(waitMillis, (int) waitNanos);//等待一定时间间隔
						} catch (InterruptedException ignored) {
						}
					}
				}
			}
		}
	};
	//connection缓存池。Deque是一个双端列表,支持在头尾插入元素,这里用作LIFO(后进先出)堆栈,多用于缓存数据。
	private final Deque<RealConnection> connections = new ArrayDeque<>();
	final RouteDatabase routeDatabase = new RouteDatabase();//用来记录连接失败router
	boolean cleanupRunning;
	/*
	 * 在遍历缓存列表的过程中,使用连接数目inUseConnectionCount 和闲置连接数目idleConnectionCount 的计数累加值都是
	 * 通过pruneAndGetAllocationCount() 是否大于0来控制的。那么很显然pruneAndGetAllocationCount() 方法就是用来识别对应连接是否闲置的。
	 * >0则不闲置。否则就是闲置的连接。
	 */
	long cleanup(long now) {
		int inUseConnectionCount = 0;//使用中的连接数
		int idleConnectionCount = 0;//闲置连接数目
		RealConnection longestIdleConnection = null;
		long longestIdleDurationNs = Long.MIN_VALUE;//用来记录连接池闲置连接中最久闲置时间
		// Find either a connection to evict, or the time that the next eviction is due.
		synchronized (this) {//遍历连接池
			for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) {
				RealConnection connection = i.next();
				// If the connection is in use, keep searching.
				if (pruneAndGetAllocationCount(connection, now) > 0) {
					inUseConnectionCount++;//连接还在使用中,使用中连接数+1
					continue;
				}
				idleConnectionCount++;//连接未使用了,闲置连接数+1
				// If the connection is ready to be evicted, we're done.
				long idleDurationNs = now - connection.idleAtNanos;
				if (idleDurationNs > longestIdleDurationNs) {
					longestIdleDurationNs = idleDurationNs;
					longestIdleConnection = connection;
				}
			}
			//连接存在时间超过限制、或者限制连接数超过最大闲置连接数,则从连接池中remove掉多余连接
			if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) {
				connections.remove(longestIdleConnection);//移除闲置时间超过限制的连接
			} else if (idleConnectionCount > 0) {
				//返回闲置时间最久的连接,多久后要超过时间限制,方便在这段时间后再次扫描连接池
				return keepAliveDurationNs - longestIdleDurationNs;
			} else if (inUseConnectionCount > 0) {
				//所有连接都在使用中,返回连接保活的最大时间,在这段时间后再次扫描连接池
				return keepAliveDurationNs;
			} else {
				//没有任何连接正在使用或者闲置,返回-1,停止扫描连接池的线程
				cleanupRunning = false;
				return -1;
			}
		}
		closeQuietly(longestIdleConnection.socket());//关闭从连接池中移除的连接的socket
		// Cleanup again immediately.
		return 0;
	}
	/**
	 * 因为每次使用连接,都会在连接的allocations里增加一条弱引用数据
	 * 连接使用完之后弱引用中的对象自然就释放了,如果还有任何一个引用未释放,则说明连接还在使用中
	 */
	private int pruneAndGetAllocationCount(RealConnection connection, long now) {
		List<Reference<StreamAllocation>> references = connection.allocations;
		for (int i = 0; i < references.size(); ) {//size()就是该连接对应socket被引用的次数
			Reference<StreamAllocation> reference = references.get(i);
			if (reference.get() != null) {
				i++;
				continue;
			}
			// We've discovered a leaked allocation. This is an application bug.
			StreamAllocation.StreamAllocationReference streamAllocRef = (StreamAllocation.StreamAllocationReference) reference;
			String message = "A connection to " + connection.route().address().url() + " was leaked. Did you forget to close a response body?";
			Platform.get().logCloseableLeak(message, streamAllocRef.callStackTrace);
			references.remove(i);
			connection.noNewStreams = true;
			// If this was the last allocation, the connection is eligible for immediate eviction.
			if (references.isEmpty()) {
				connection.idleAtNanos = now - keepAliveDurationNs;
				return 0;//连接的allocations计数器中存的弱引用对象都已经释放了
			}
		}
		return references.size();
	}
	
	//往连接池中放入新的连接
	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, Route route) {
		assert (Thread.holdsLock(this));
		for (RealConnection connection : connections) {
			if (connection.isEligible(address, route)) {//校验连接是否可以复用
			/*
			 * streamAllocation.allocations是个对象计数器,其本质是一个 List<Reference<StreamAllocation>> 
			 * 存放在RealConnection连接对象中用于记录Connection的活跃情况。
			 */
				streamAllocation.acquire(connection);//会在connection.allocations中添加一个弱引用保存的对象
				return connection;
			}
		}
		return null;
	}
    }
//RealConnection中,校验连接是否可以复用
    public boolean isEligible(Address address, @Nullable Route route) {
        // If this connection is not accepting new streams, we're done.
        if (allocations.size() >= allocationLimit || noNewStreams) return false;//连接调用次数没有超过限制
        // If the non-host fields of the address don't overlap, we're done.
        if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;//比较dns、代理证书、协议、端口等信息
        // If the host exactly matches, we're done: this connection can carry the address.
        if (address.url().host().equals(this.route().address().url().host())) {//比较host
            return true; // This connection is a perfect match.
        }
        // At this point we don't have a hostname match. But we still be able to carry the request if
        // our connection coalescing requirements are met. See also:
        // https://hpbn.co/optimizing-application-delivery/#eliminate-domain-sharding
        // https://daniel.haxx.se/blog/2016/08/18/http2-connection-coalescing/
        // 1. This connection must be HTTP/2.
        if (http2Connection == null) return false;
        // 2. The routes must share an IP address. This requires us to have a DNS address for both
        // hosts, which only happens after route planning. We can't coalesce connections that use a
        // proxy, since proxies don't tell us the origin server's IP address.
        if (route == null) return false;
        if (route.proxy().type() != Proxy.Type.DIRECT) return false;
        if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
        if (!this.route.socketAddress().equals(route.socketAddress())) return false;
        // 3. This connection's server certificate's must cover the new host.
        if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
        if (!supportsUrl(address.url())) return false;
        // 4. Certificate pinning must match the host.
        try {//证书校验
            address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
        } catch (SSLPeerUnverifiedException e) {
            return false;
        }
        return true; // The caller's address can be carried by this connection.
    }
	OkHttpClient->Internal.instance->equalsNonHost(Address a, Address b)->Address.equalsNonHost()
	boolean equalsNonHost(Address that) {
		return this.dns.equals(that.dns)
			&& this.proxyAuthenticator.equals(that.proxyAuthenticator)
			&& this.protocols.equals(that.protocols)
			&& this.connectionSpecs.equals(that.connectionSpecs)
			&& this.proxySelector.equals(that.proxySelector)
			&& equal(this.proxy, that.proxy)
			&& equal(this.sslSocketFactory, that.sslSocketFactory)
			&& equal(this.hostnameVerifier, that.hostnameVerifier)
			&& equal(this.certificatePinner, that.certificatePinner)
			&& this.url().port() == that.url().port();
	}
	
	3、获取连接池中闲置连接  RealCall.java
	final class RealCall implements Call {
		...
		public Response execute() throws IOException {
			synchronized (this) {
				if (executed) throw new IllegalStateException("Already Executed");
				executed = true;
			}
			captureCallStackTrace();
			try {
				client.dispatcher().executed(this);//请求塞到队列
				Response result = getResponseWithInterceptorChain();//这里面有添加一些拦截器
				if (result == null) throw new IOException("Canceled");
				return result;
			} finally {
				client.dispatcher().finished(this);
			}
		}
		
		Response getResponseWithInterceptorChain() throws IOException {
			// Build a full stack of interceptors.
			List<Interceptor> interceptors = new ArrayList<>();
			interceptors.addAll(client.interceptors());//这些是调用处传入的拦截器
			interceptors.add(retryAndFollowUpInterceptor);
			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, null, null, null, 0, originalRequest);
			return chain.proceed(originalRequest);
		}
		...
	}
	
	public final class ConnectInterceptor implements Interceptor {
        public final OkHttpClient client;

        public ConnectInterceptor(OkHttpClient client) {
            this.client = client;
        }

        @Override 
        public Response intercept(Chain chain) throws IOException {
            RealInterceptorChain realChain = (RealInterceptorChain) chain;
            Request request = realChain.request();
            StreamAllocation streamAllocation = realChain.streamAllocation();
            // We need the network to satisfy this request. Possibly for validating a conditional GET.
            boolean doExtensiveHealthChecks = !request.method().equals("GET");
            HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);//这里去连接池
            RealConnection connection = streamAllocation.connection();
            return realChain.proceed(request, streamAllocation, httpCodec, connection);
        }
    }
	
	public final class StreamAllocation {
		//去连接池拿连接
		public HttpCodec newStream(OkHttpClient client, boolean doExtensiveHealthChecks) {
			int connectTimeout = client.connectTimeoutMillis();
			int readTimeout = client.readTimeoutMillis();
			int writeTimeout = client.writeTimeoutMillis();
			boolean connectionRetryEnabled = client.retryOnConnectionFailure();
			try {//findHealthyConnection会从连接池中拿到可用连接
				RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
						writeTimeout, connectionRetryEnabled, doExtensiveHealthChecks);
				HttpCodec resultCodec = resultConnection.newCodec(client, this);
				synchronized (connectionPool) {
					codec = resultCodec;
					return resultCodec;
				}
			} catch (IOException e) {
				throw new RouteException(e);
			}
		}
	}

总结:连接池复用流程

ConnectInterceptor.intercept(..) -> StreamAllocation.newStream(..) -> StreamAllocation.findHealthyConnection(..)
->findConnection(..) -> Internal.instance.get(connectionPool, address, this, null) -> OkHttpClient.Internal.instance.get(..)
最终调用的方法:
@Override 
public RealConnection get(ConnectionPool pool, Address address, StreamAllocation streamAllocation, Route route) {
    return pool.get(address, streamAllocation, route);//遍历连接池,取出合适的(一系列校验)连接

}

补充:在findConnection()中有执行connect(),内部创建了socket,在findConnection()末尾关闭socket,
http请求本来就是短连接,用完后就是要关闭的!

参考资料

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值