前言
本章我们介绍一下ConnectInterceptor,这个类主要是与服务端建立连接操作,例如HTTP1,HTTP2,HTTPS以及HTTP代理与Socktet代理。在 OKHTTP 底层是通过 SOCKET 的方式于服务端进行连接的,并且在连接建立之后会通过 OKIO 获取通向 server 端的输入流 Source 和输出流 Sink。
这里我们仅仅分析HTTP1 的相关部分,同时本篇不会像之前几篇写的那么细致入微,这里仅仅大致分析一下其实现方式。
一、ConnectInterceptor
1.1 获取链接
@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");
//创建新的stream
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
streamAllocation 返回的StreamAllocation 对象实际创建与RetryAndFollowUpInterceptor 中
streamAllocation.newStream 通过这个方法得到一个 HttpStream 这个接口有两个实现类分别是 Http1xStream 和 Http2xStream 现在只分析 Http1xStream ,这个 Http1xStream 流是通过 SOCKET 与服务端建立连接之后,通向服务端的输入和输出流的封装。
public HttpCodec newStream(
OkHttpClient client, Interceptor.Chain chain, boolean doExtensiveHealthChecks) {
// 这些时间设置都是从OKHttpClinet里面读取过来的
int connectTimeout = chain.connectTimeoutMillis();
int readTimeout = chain.readTimeoutMillis();
int writeTimeout = chain.writeTimeoutMillis();
int pingIntervalMillis = client.pingIntervalMillis();
boolean connectionRetryEnabled = client.retryOnConnectionFailure();
try {
//创建连接
RealConnection resultConnection = findHealthyConnection(connectTimeout, readTimeout,
writeTimeout, pingIntervalMillis, connectionRetryEnabled, doExtensiveHealthChecks);
//创建HttpCodec
HttpCodec resultCodec = resultConnection.newCodec(client, chain, this);
synchronized (connectionPool) {
codec = resultCodec;
return resultCodec;
}
} catch (IOException e) {
throw new RouteException(e);
}
}
findHealthyConnection 在连接池中寻找或者在新创建一个连接 ,findHealthyConnection 放到后面再说,我们这里先看一下newCodec
public HttpCodec newCodec(OkHttpClient client, Interceptor.Chain chain,
StreamAllocation streamAllocation) throws SocketException {
if (http2Connection != null) {
// 这是HTT2部分,这里不做分析
return new Http2Codec(client, chain, streamAllocation, http2Connection);
} else {
//配置超时时间
socket.setSoTimeout(chain.readTimeoutMillis());
source.timeout().timeout(chain.readTimeoutMillis(), MILLISECONDS);
sink.timeout().timeout(chain.writeTimeoutMillis(), MILLISECONDS);
return new Http1Codec(client, streamAllocation, source, sink);
}
}
HttpCodec 可以看成是一个sokcet连接HttpCodec 通过这个链接发送HTTP请求并且读取返返回数据
现在我们看看上面遗漏的findHealthyConnection 方法
private RealConnection findHealthyConnection(int connectTimeout, int readTimeout,
int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled,
boolean doExtensiveHealthChecks) throws IOException {
while (true) {
RealConnection candidate = findConnection(connectTimeout, readTimeout,
writeTimeout,
pingIntervalMillis, connectionRetryEnabled);
//新创建的RealConnection,此时直接返回就可以了,省略后面的检查
synchronized (connectionPool) {
if (candidate.successCount == 0) {
return candidate;
}
}
//检查是否健康,若是健康就可以复用。
// 内部主要是实际是检查socket 是否已经关闭,
if (!candidate.isHealthy(doExtensiveHealthChecks)) {
noNewStreams();
continue;
}
return candidate;
}
}
这里还没有接触到核心代码,我们继续进入findConnection 看看
//代码有删减
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
boolean foundPooledConnection = false;
RealConnection result = null;
Route selectedRoute = null;
Connection releasedConnection;
Socket toClose;
synchronized (connectionPool) {
releasedConnection = this.connection;
toClose = releaseIfNoNewStreams();
// 例如重定向的时候,地址又没有改变,这是与第一次访问使用同一个StreatAloocation
//,其内部已经创建的connection ,这个时候直接复用。
//或者是重试的时候使用的也是同一个StreatAloocation。
//具体的参见RetryAndFollpwUpInterceptor,
if (this.connection != null) {
// We had an already-allocated connection and it's good.
result = this.connection;
releasedConnection = null;
}
if (!reportedAcquired) {
// If the connection was never reported acquired, don't report it as released!
releasedConnection = null;
}
//还没有分配连接,一般是初始访问,没有经历重定向,与重试
if (result == null) {
// 在连接池寻找可用的链接,注意最后一个参数是null
//关于如何复用的在后面介绍
Internal.instance.get(connectionPool, address, this, null);
//connection != null 表示在连接池找到了可用的链接
if (connection != null) {
foundPooledConnection = true;
result = connection;
} else {
//没有找到可用的
selectedRoute = route;
}
}
}
closeQuietly(toClose);
if (result != null) {
// If we found an already-allocated or pooled connection, we're done.
return result;
}
// If we need a route selection, make one. This is a blocking operation.
boolean newRouteSelection = false;
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
//这里一般是设置了代理之后会走到这里,前面根据要访问的url没有找到可以复用的
//链接,此时根据用户配置的代理查找可用的链接
routeSelection = routeSelector.next();
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {
//遍历当前url 对应的所有ip地址
//一个代理可能对用多个ip地址。
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
//查询是否存在可以使用的链接,注意最后的参数非空
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
//
//没有在Connection里面找到可用的链接
if (!foundPooledConnection) {
if (selectedRoute == null) {
//更换路径
selectedRoute = routeSelection.next();
}
route = selectedRoute;
refusedStreamCount = 0;
// 还是没有找到可以复用的链接,
//此时只可以建立一个新的连接
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
}
}
// If we found a pooled connection on the 2nd time around, we're done.
if (foundPooledConnection) {
eventListener.connectionAcquired(call, result);
return result;
}
//开始三次握手,与TLS握手
// Do TCP + TLS handshakes. This is a blocking operation.
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
Socket socket = null;
synchronized (connectionPool) {
reportedAcquired = true;
//保存新建立的链接
Internal.instance.put(connectionPool, result);
if (result.isMultiplexed()) {
socket = Internal.instance.deduplicate(connectionPool, address, this);
result = connection;
}
}
closeQuietly(socket);
eventListener.connectionAcquired(call, result);
return result;
}
findConnection() 返回一个用于流执行底层IO的连接。这个方法优先复用已经创建的连接;在没有可复用连接的情况下新建一个。
1.1 查询连接
前面我们看到查询复用链接的方法为Internal.instance.get(connectionPool, address, this, route);其中Internal.instance 的实现类是OKHttpClient 的一个内部类
public RealConnection get(ConnectionPool pool, Address address, StreamAllocation streamAllocation, Route route) {
return pool.get(address, streamAllocation, route);
}
Internal 的get 方法调了ConnectionPool 的get 方法
@Nullable RealConnection get(Address address, StreamAllocation streamAllocation, Route route) {
assert (Thread.holdsLock(this));
for (RealConnection connection : connections) {
if (connection.isEligible(address, route)) {
streamAllocation.acquire(connection, true);
return connection;
}
}
return null;
}
这里就是遍历连接池里面的链接然后调用connection 的isEligible 判断这个链接是不是可以复用在address 上。
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;
// this.route.address() 返回的是创建这个Connection 的时候使用的address
//实际最终调用的额Address 的equalsNonHost方法判断是否相同。
//
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
//这里比较的是主机名是不是一致,
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
if (http2Connection == null) return false;
if (route == null) return false;
// 当router参数不为空的的时候,下面就是根据route来寻找可服用的链接
//到了这里一般是使用了HTTP代理
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;
//这里面比较的是X509的SAN扩展
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) {
//这个错误会抛到RetryAndFollowUp那里去处理,重试
return false;
}
return true; // The caller's address can be carried by this connection.
}
如果不考虑代理的情况下一般查询复用就只会走前面两步
// If the non-host fields of the address don't overlap, we're done.
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
Address.java
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();
}
proxyAuthenticator ,connectionSpecs,proxySelector ,dns 等等都是从从client里面获取到的变量,因此正常情况下两个相同的主机地址这里比较都会返回true,个人觉得这里到更像是比较这两个Address 是不是由同一个OKHttpcliet 对象创建。
equalsNonHost 比较成功之后就比较主机名,如果主机一样就代表这个链接可以复用,例如
https://blog.csdn.net/guolin_blog/article/details/28863651 与https://blog.csdn.net/freak_csh/article/details/95009057
这两个url 的主机是一致的,因此链接也就可以复用。
创建连接’
在创建完RealConnection 之后会调用connect 方法建立网络连接,这里就不在分析这一块了,感兴趣的朋友可以自己研究一下。
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled, Call call,
EventListener eventListener) {
if (protocol != null) throw new IllegalStateException("already connected");
RouteException routeException = null;
List<ConnectionSpec> connectionSpecs = route.address().connectionSpecs();
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
//代码有删减,
while (true) {
try {
if (route.requiresTunnel()) {
//建立http隧道,也就是https 的代理
connectTunnel(connectTimeout, readTimeout, writeTimeout, call, eventListener);
if (rawSocket == null) {
// We were unable to connect the tunnel but properly closed down our resources.
break;
}
} else {
//打开socket
connectSocket(connectTimeout, readTimeout, call, eventListener);
}
/如果是HTTPS或者是HTTP2 的话会进行对应的协议的处理,例如tsl握手
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
} catch (IOException e) {
//代码有删减
if (routeException == null) {
routeException = new RouteException(e);
} else {
routeException.addConnectException(e);
}
throw routeException;
}
}
if (route.requiresTunnel() && rawSocket == null) {
// 建立失败
ProtocolException exception = new ProtocolException("Too many tunnel connections attempted: "
+ MAX_TUNNEL_ATTEMPTS);
throw new RouteException(exception);
}
if (http2Connection != null) {
synchronized (connectionPool) {
allocationLimit = http2Connection.maxConcurrentStreams();
}
}
}
这里主要是调用connectSocket 或connectTunnel方法建立socket 连,如果是普通的http 协议,此时就可以通过这个sokcet 发送HTTP 数据。如果使用的是HTTPS 或者是HTTP2 协议会接着调用establishProtocol 建相应的协议。这里还想说一下异常的处理,
可以看到所有的有的io异常都封装成了RouteException,关于RouteException 我们在RetryAndFollowUpInterceptor 介绍过,RetryAndFollowUpInterceptor 会捕获所有的RouteException,然后判断如果该地址还对应这其他的Ip地址的话,会自动尝试与连一个ip建立连接。
下面我们先看一下connectSocket。
private void connectSocket(int connectTimeout, int readTimeout, Call call,
EventListener eventListener) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
//根据代理类型创建不同的socket
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
eventListener.connectStart(call, route.socketAddress(), proxy);
rawSocket.setSoTimeout(readTimeout);
try {
//根据平台建立连接,放到andorid 平台就是一句代码
//socket.connect(address, connectTimeout);
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
ConnectException ce = new ConnectException("Failed to connect to " + route.socketAddress());
ce.initCause(e);
throw ce;
}
try {
//okio 是OKHTTP团队封装的一套io工具类。
//获取输入流
source = Okio.buffer(Okio.source(rawSocket));
//获取输出流
sink = Okio.buffer(Okio.sink(rawSocket));
} catch (NullPointerException npe) {
if (NPE_THROW_WITH_NULL.equals(npe.getMessage())) {
throw new IOException(npe);
}
}
}
不考虑代理的话,这里就是直接new 一个Socket 然后调用socket的connect 方法,进而与服务器建立了连接,建立连接之后获取到对应的输入输出流。
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
//代码有删减
eventListener.secureConnectStart(call);
//tsl握手
connectTls(connectionSpecSelector);
eventListener.secureConnectEnd(call, handshake);
if (protocol == Protocol.HTTP_2) {
socket.setSoTimeout(0); // HTTP/2 connection timeouts are set per-stream.
//http2 协议处理
http2Connection = new Http2Connection.Builder(true)
.socket(socket, route.address().url().host(), source, sink)
.listener(this)
.pingIntervalMillis(pingIntervalMillis)
.build();
http2Connection.start();
}
}
关于tsl 握手与http2 的内部,这里也不做多余的介绍了,干兴趣的朋友可以自己查点资料看看。至此ConnectInterceptor 也就是介绍完成了。