okhttp分享5:ConnectInterceptor(1)
按顺序我们现在走到了ConnectInterceptor,该拦截器主要负责建立与服务器的链接。先简单看一下代码,代码量不多
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, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
return realChain.proceed(request, streamAllocation, httpCodec, connection);
}
}
可以看到总共没几行代码,核心代码就两句
HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
虽然只有两句,但是却涉及okhttp连接池,路由选择策略等多种逻辑,我们一步步看,这两步核心逻辑都是在streamAllocation中实现的。我们回忆一下,这个streamAllocation最早是在RetryAndFollowUpInterceptor中实例化的,我们看一下RetryAndFollowUpInterceptor中代码
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
在看下其构造方法
public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
EventListener eventListener, Object callStackTrace) {
this.connectionPool = connectionPool;//连接池
this.address = address;//地址相关信息
this.call = call;//用户请求
this.eventListener = eventListener;
this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);//路由选择策略
this.callStackTrace = callStackTrace;
}
其中我们主要关注routeSelector和connectionPool
一、路由选择
之前我们说到,okhttp会自动选择最优的路由,并以此建立或复用连接。连接的建立是要基于路由选择的,我们先看一下okhttp的路由相关类
1.1、Address
final HttpUrl url;
final Dns dns;
final SocketFactory socketFactory;
final Authenticator proxyAuthenticator;
final List<Protocol> protocols;
final List<ConnectionSpec> connectionSpecs;
final ProxySelector proxySelector;
final @Nullable
Proxy proxy;
final @Nullable SSLSocketFactory sslSocketFactory;
final @Nullable HostnameVerifier hostnameVerifier;
final @Nullable
CertificatePinner certificatePinner;
public Address(String uriHost, int uriPort, Dns dns, SocketFactory socketFactory,
@Nullable SSLSocketFactory sslSocketFactory, @Nullable HostnameVerifier hostnameVerifier,
@Nullable CertificatePinner certificatePinner, Authenticator proxyAuthenticator,
@Nullable Proxy proxy, List<Protocol> protocols, List<ConnectionSpec> connectionSpecs,
ProxySelector proxySelector) {
this.url = new HttpUrl.Builder()
.scheme(sslSocketFactory != null ? "https" : "http")
.host(uriHost)
.port(uriPort)
.build();
if (dns == null) throw new NullPointerException("dns == null");
this.dns = dns;
if (socketFactory == null) throw new NullPointerException("socketFactory == null");
this.socketFactory = socketFactory;
if (proxyAuthenticator == null) {
throw new NullPointerException("proxyAuthenticator == null");
}
this.proxyAuthenticator = proxyAuthenticator;
if (protocols == null) throw new NullPointerException("protocols == null");
this.protocols = Util.immutableList(protocols);
if (connectionSpecs == null) throw new NullPointerException("connectionSpecs == null");
this.connectionSpecs = Util.immutableList(connectionSpecs);
if (proxySelector == null) throw new NullPointerException("proxySelector == null");
this.proxySelector = proxySelector;
this.proxy = proxy;
this.sslSocketFactory = sslSocketFactory;
this.hostnameVerifier = hostnameVerifier;
this.certificatePinner = certificatePinner;
}
可以看出这是一个地址封装类,是在RetryAndFollowUpInterceptor实例化StreamAllocation时作为参数实例化的。对于简单的链接,这里是服务器的主机名和端口号。如果是通过代理(Proxy)的链接,则包含代理信息(Proxy)。如果是安全链接,则还包括SSL socket Factory、hostname验证器,证书等。
注意下这个类注释的最后一句<p>HTTP requests that share the same {@code Address} may also share the same {@link Connection}.
,拥有相同address的http请求可以共用同一个连接。配合在看下这个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();
}
1.2、Route
final Address address;
final Proxy proxy;
final InetSocketAddress inetSocketAddress;
public Route(Address address, Proxy proxy, InetSocketAddress inetSocketAddress) {
if (address == null) {
throw new NullPointerException("address == null");
}
if (proxy == null) {
throw new NullPointerException("proxy == null");
}
if (inetSocketAddress == null) {
throw new NullPointerException("inetSocketAddress == null");
}
this.address = address;
this.proxy = proxy;
this.inetSocketAddress = inetSocketAddress;
Route表示通过代理服务器的信息proxy及链接的目标地址Address来描述的路由,连接的目标地址inetSocketAddress根据代理类型的不同而有着不同的含义,这主要是通过不同代理协议的差异而造成的。对于无需代理的情况,连接的目标地址inetSocketAddress中包含HTTP服务器经过DNS域名解析的IP地址以及协议端口号;对于SOCKET代理其中包含HTTP服务器的域名及协议端口号;对于HTTP代理,其中则包含代理服务器经过域名解析的IP地址及端口号。
1.3、RouteDatabase
这个是用于记录路由历史情况的类,看下源码
/**
* A blacklist of failed routes to avoid when creating a new connection to a target address. This is
* used so that OkHttp can learn from its mistakes: if there was a failure attempting to connect to
* a specific IP address or proxy server, that failure is remembered and alternate routes are
* preferred.
*/
public final class RouteDatabase {
private final Set<Route> failedRoutes = new LinkedHashSet<>();
/** Records a failure connecting to {@code failedRoute}. */
public synchronized void failed(Route failedRoute) {
failedRoutes.add(failedRoute);
}
/** Records success connecting to {@code route}. */
public synchronized void connected(Route route) {
failedRoutes.remove(route);
}
/** Returns true if {@code route} has failed recently and should be avoided. */
public synchronized boolean shouldPostpone(Route route) {
return failedRoutes.contains(route);
}
}
这个类很简单,维护了一个用于记录失败路由的LinkHashSet,并提供一个方法(shouldPostpone)用于判断传入路由是否在失败列表中。
1.4、RouteSelector
下面我们就要看下okhttp路由选择的核心类RouteSelector,大家可以把它理解为路由选择器。android客户端与服务端网络通信的过程,可以细分成很多块,大体分的话其实就是两步首先建立连接(tcp/udp),连接建立后基于当前连接及请求构建信息传输流(http/socket),通过流进行io操作,与服务器通信。做个简单的比喻,连接就是桥梁,流就是桥上跑的货车,信息就是货车装载的货物。而tcp连接建立过程所需要的关键元素就是Route,现在借助于域名做负载均衡也十分常见,所以路由选择也变得较为复杂,okhttp中通过RouteSelector对路由信息进行管理,使建立tcp连接时可以使用最优的路由。下面我们看下源码
private final Address address;
private final RouteDatabase routeDatabase;
private final Call call;
private final EventListener eventListener;
/* State for negotiating the next proxy to use. */
private List<Proxy> proxies = Collections.emptyList();
private int nextProxyIndex;
/* State for negotiating the next socket address to use. */
private List<InetSocketAddress> inetSocketAddresses = Collections.emptyList();
/* State for negotiating failed routes */
private final List<Route> postponedRoutes = new ArrayList<>();
public RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
EventListener eventListener) {
this.address = address;
this.routeDatabase = routeDatabase;
this.call = call;
this.eventListener = eventListener;
resetNextProxy(address.url(), address.proxy());
}
成员变量都是之前提过的内容,直接看构造器中调用的resetNextProxy方法
/** Prepares the proxy servers to try. */
private void resetNextProxy(HttpUrl url, Proxy proxy) {
if (proxy != null) {
// If the user specifies a proxy, try that and only that.
proxies = Collections.singletonList(proxy);
} else {
// Try each of the ProxySelector choices until one connection succeeds.
List<Proxy> proxiesOrNull = address.proxySelector().select(url.uri());
proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
? Util.immutableList(proxiesOrNull)
: Util.immutableList(Proxy.NO_PROXY);
}
nextProxyIndex = 0;
}
这个方法主要是选择可用代理,如果用户传如proxy则就指定使用该proxy;若用户没有指定proxy则通过proxySelector根据请求uri产生proxy,若产生的proxy为null则使用Proxy.NO_PROXY。
这边我们注意下proxySelector(),这个是在okhttpclient传入的代理选择策略,用户若不传入则使用系统默认的,我们简单看下系统默认的ProxySelectorImpl,在java.net包中
final class ProxySelectorImpl extends ProxySelector {
@Override public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
if (uri == null || sa == null || ioe == null) {
throw new IllegalArgumentException();
}
}
@Override public List<Proxy> select(URI uri) {
return Collections.singletonList(selectOneProxy(uri));
}
private Proxy selectOneProxy(URI uri) {
if (uri == null) {
throw new IllegalArgumentException("uri == null");
}
String scheme = uri.getScheme();
if (scheme == null) {
throw new IllegalArgumentException("scheme == null");
}
int port = -1;
Proxy proxy = null;
String nonProxyHostsKey = null;
boolean httpProxyOkay = true;
if ("http".equalsIgnoreCase(scheme)) {
port = 80;
nonProxyHostsKey = "http.nonProxyHosts";
proxy = lookupProxy("http.proxyHost", "http.proxyPort", Proxy.Type.HTTP, port);
} else if ("https".equalsIgnoreCase(scheme)) {
port = 443;
nonProxyHostsKey = "https.nonProxyHosts"; // RI doesn't support this
proxy = lookupProxy("https.proxyHost", "https.proxyPort", Proxy.Type.HTTP, port);
} else if ("ftp".equalsIgnoreCase(scheme)) {
port = 80; // not 21 as you might guess
nonProxyHostsKey = "ftp.nonProxyHosts";
proxy = lookupProxy("ftp.proxyHost", "ftp.proxyPort", Proxy.Type.HTTP, port);
} else if ("socket".equalsIgnoreCase(scheme)) {
httpProxyOkay = false;
} else {
return Proxy.NO_PROXY;
}
if (nonProxyHostsKey != null
&& isNonProxyHost(uri.getHost(), System.getProperty(nonProxyHostsKey))) {
return Proxy.NO_PROXY;
}
if (proxy != null) {
return proxy;
}
if (httpProxyOkay) {
proxy = lookupProxy("proxyHost", "proxyPort", Proxy.Type.HTTP, port);
if (proxy != null) {
return proxy;
}
}
proxy = lookupProxy("socksProxyHost", "socksProxyPort", Proxy.Type.SOCKS, 1080);
if (proxy != null) {
return proxy;
}
return Proxy.NO_PROXY;
}
····
}
主要看selectOneProxy方法,其实就是根据url的scheme不同生成不同的proxy并返回。我们接着看RouteSelector
/**
* Returns true if there's another set of routes to attempt. Every address has at least one route.
*/
public boolean hasNext() {
return hasNextProxy() || !postponedRoutes.isEmpty();
}
/** Returns true if there's another proxy to try. */
//是否还有代理
private boolean hasNextProxy() {
return nextProxyIndex < proxies.size();
}
public Selection next() throws IOException {
if (!hasNext()) {
throw new NoSuchElementException();
}
// Compute the next set of routes to attempt.
List<Route> routes = new ArrayList<>();
while (hasNextProxy()) {
// Postponed routes are always tried last. For example, if we have 2 proxies and all the
// routes for proxy1 should be postponed, we'll move to proxy2. Only after we've exhausted
// all the good routes will we attempt the postponed routes.
Proxy proxy = nextProxy();
for (int i = 0, size = inetSocketAddresses.size(); i < size; i++) {
Route route = new Route(address, proxy, inetSocketAddresses.get(i));
if (routeDatabase.shouldPostpone(route)) {
postponedRoutes.add(route);
} else {
routes.add(route);
}
}
if (!routes.isEmpty()) {
break;
}
}
if (routes.isEmpty()) {
// We've exhausted all Proxies so fallback to the postponed routes.
routes.addAll(postponedRoutes);
postponedRoutes.clear();
}
return new Selection(routes);
}
/** Returns the next proxy to try. May be PROXY.NO_PROXY but never null. */
private Proxy nextProxy() throws IOException {
if (!hasNextProxy()) {
throw new SocketException("No route to " + address.url().host()
+ "; exhausted proxy configurations: " + proxies);
}
Proxy result = proxies.get(nextProxyIndex++);
resetNextInetSocketAddress(result);
return result;
}
/** Prepares the socket addresses to attempt for the current proxy or host. */
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
// Clear the addresses. Necessary if getAllByName() below throws!
inetSocketAddresses = new ArrayList<>();
String socketHost;
int socketPort;
if (proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.SOCKS) {
socketHost = address.url().host();
socketPort = address.url().port();
} else {
SocketAddress proxyAddress = proxy.address();
if (!(proxyAddress instanceof InetSocketAddress)) {
throw new IllegalArgumentException(
"Proxy.address() is not an " + "InetSocketAddress: " + proxyAddress.getClass());
}
InetSocketAddress proxySocketAddress = (InetSocketAddress) proxyAddress;
socketHost = getHostString(proxySocketAddress);
socketPort = proxySocketAddress.getPort();
}
if (socketPort < 1 || socketPort > 65535) {
throw new SocketException("No route to " + socketHost + ":" + socketPort
+ "; port is out of range");
}
if (proxy.type() == Proxy.Type.SOCKS) {
inetSocketAddresses.add(InetSocketAddress.createUnresolved(socketHost, socketPort));
} else {
eventListener.dnsStart(call, socketHost);
// Try each address for best behavior in mixed IPv4/IPv6 environments.
List<InetAddress> addresses = address.dns().lookup(socketHost);
if (addresses.isEmpty()) {
throw new UnknownHostException(address.dns() + " returned no addresses for " + socketHost);
}
eventListener.dnsEnd(call, socketHost, addresses);
for (int i = 0, size = addresses.size(); i < size; i++) {
InetAddress inetAddress = addresses.get(i);
inetSocketAddresses.add(new InetSocketAddress(inetAddress, socketPort));
}
}
}
这块是RouteSelector的核心逻辑,外部进行路由选择时会调用next方法,返回一个Selection对象,该对象就是一个可选路由的list。其逻辑如下
- 1、判断是否还有路由(包括延迟路由)
- 2、进入while循环,当还存在可尝试的proxy时进行以下逻辑
- 2.1、通过nextProxy()方法获取下一个proxy
- 2.2、根据proxy,address,dns解析后得到的inetSocketAddresses生成route
- 2.3、根据routeDatabase判断生成route是否为延迟路由,并加入相应的list
- 2.4、若可用路由列表不为空则跳出循环,返回Selection;若可用路由列表为空则将延迟路由列表全部加如可用路由列表,清空延迟路由列表,执行下一轮循环
我们继续看next中调用的nextProxy方法,就是从proxy列表中获取下一个proxy,作为参数传入resetNextInetSocketAddress方法,而resetNextInetSocketAddress逻辑如下
- 1、对于没有配置代理的情况,会对HTTP服务器的域名进行DNS域名解析,并为每个解析到的IP地址创建 连接的目标地址
- 2、对于SOCKS代理,直接以HTTP的服务器的域名以及协议端口创建 连接目标地址
- 3、对于HTTP代理,则会对HTTP代理服务器的域名进行DNS域名解析,并为每个解析到的IP地址创建 连接的目标地址
从代码中我们可以看出这里就是okhttp网络请求解析dns的地方List<InetAddress> addresses = address.dns().lookup(socketHost);
该行就是根据根据host解析出其对应的ip地址。
最后再看一下连接失败的情况
/**
* Clients should invoke this method when they encounter a connectivity failure on a connection
* returned by this route selector.
*/
public void connectFailed(Route failedRoute, IOException failure) {
if (failedRoute.proxy().type() != Proxy.Type.DIRECT && address.proxySelector() != null) {
// Tell the proxy selector when we fail to connect on a fresh connection.
address.proxySelector().connectFailed(
address.url().uri(), failedRoute.proxy().address(), failure);
}
routeDatabase.failed(failedRoute);
}
当连接失败,外部会调用这个方法,该方法会维护RouteDatabase中的失败路由信息。到这里okhttp的路由选择就讲完了,通过RouteSelector收集、选择路由以及维护失败路由列表,使连接时不会优先使用之前已出过错的路由,节省时间,提高效率。
二、连接:Connection类
上面我们介绍了路由选择策略,下面我们看一下okhttp具体连接相关逻辑。okhttp连接相关逻辑都由Connection负责,Connection是一个接口,有四个抽象方法
Route route(); //返回一个路由
Socket socket(); //返回一个socket
Handshake handshake(); //如果是一个https,则返回一个TLS握手协议
Protocol protocol(); //返回一个协议类型 比如 http1.1 等或者自定义类型
其实现类为RealConnection,该对象会在需要构建tcp连接时实例化,具体在StreamAllocation中,我们后面会讲到。如果拥有了一个RealConnection就代表了我们已经跟服务器有了一条通信链路,也就说明此时tcp三次握手已经完成。先看下这个类的源码
private final ConnectionPool connectionPool;
private final Route route;
// The fields below are initialized by connect() and never reassigned.
//下面这些字段,通过connect()方法开始初始化,并且绝对不会再次赋值
/** The low-level TCP socket. */
private Socket rawSocket; //底层socket
/**
* The application layer socket. Either an {@link SSLSocket} layered over {@link #rawSocket}, or
* {@link #rawSocket} itself if this connection does not use SSL.
*/
private Socket socket; //应用层socket
//握手
private Handshake handshake;
//协议
private Protocol protocol;
// http2的链接
private Http2Connection http2Connection;
//通过source和sink,大家可以猜到是与服务器交互的输入输出流
private BufferedSource source;
private BufferedSink sink;
// The fields below track connection state and are guarded by connectionPool.
//下面这个字段是 属于表示链接状态的字段,并且有connectPool统一管理
/** If true, no new streams can be created on this connection. Once true this is always true. */
//如果noNewStreams被设为true,则noNewStreams一直为true,不会被改变,并且表示这个链接不会再创新的stream流
public boolean noNewStreams;
//成功的次数
public int successCount;
/**
* The maximum number of concurrent streams that can be carried by this connection. If {@code
* allocations.size() < allocationLimit} then new streams can be created on this connection.
*/
//此链接可以承载最大并发流的限制,如果不超过限制,可以随意增加
public int allocationLimit = 1;
/** Current streams carried by this connection. */
public final List<Reference<StreamAllocation>> allocations = new ArrayList<>();
/** Nanotime timestamp when {@code allocations.size()} reached zero. */
public long idleAtNanos = Long.MAX_VALUE;
简单解释几个变量:
- 1、noNewStream可以简单理解为它表示该连接不可用。这个值一旦被设为true,则这个conncetion则不会再创建stream。
- 2、allocationLimit是分配流的数量上限,http2支持一个连接上并发多个流,其他的都只支持一个连接对应一个流。
- 3、allocations是关联StreamAllocation,它用来统计在一个连接上建立了哪些流,通过StreamAllocation的acquire方法和release方法可以将一个allcation对象添加到链表或者移除链表。
看下其核心方法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);
if (route.address().sslSocketFactory() == null) {
if (!connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not enabled for client"));
}
String host = route.address().url().host();
if (!Platform.get().isCleartextTrafficPermitted(host)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication to " + host + " not permitted by network security policy"));
}
} else {
if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
throw new RouteException(new UnknownServiceException(
"H2_PRIOR_KNOWLEDGE cannot be used with HTTPS"));
}
}
while (true) {
try {
if (route.requiresTunnel()) {
// 如果要求隧道模式,建立通道连接
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的建立
establishProtocol(connectionSpecSelector, pingIntervalMillis, call, eventListener);
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), protocol);
break;
} catch (IOException e) {
closeQuietly(socket);
closeQuietly(rawSocket);
socket = null;
rawSocket = null;
source = null;
sink = null;
handshake = null;
protocol = null;
http2Connection = null;
eventListener.connectFailed(call, route.socketAddress(), route.proxy(), null, e);
if (routeException == null) {
routeException = new RouteException(e);
} else {
routeException.addConnectException(e);
}
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(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) {
// http2,修改每个连接上可承载的流的数量
allocationLimit = http2Connection.maxConcurrentStreams();
}
}
}
整理下逻辑
- 1、检查连接是否已经建立,若已经建立,则抛出异常,否则继续。连接是否建立由protocol标示,它表示在整个连接建立、协商过程中选择所有要用到的协议
- 2、根据ConnectionSpec的集合connnectionspecs构造ConnectionSpecSelector。这边说一下,ConnectionSpec用于描述传输HTTP流量的socket连接的配置。若是https连接,这些配置主要包括协商安全连接时要使用的TLS版本号和密码套间,是否支持TLS扩展等;http连接则包含是否支持明文传输等,用户可以自定义ConnectionSpec集合,但一般使用okhttp默认的
- 3、若不是https请求,则根据ConnectionSpec进行相关逻辑检测
- 4、根据请求判断是否需要建立隧道连接,如果建立隧道连接则调用
connectTunnel(connectTimeout, readTimeout, writeTimeout); - 5、如果不是隧道连接则调用connectSocket(connectTimeout, readTimeout);建立普通连接。
- 6、建立协议
- 7、若是http2,则修改allocationLimit值
先看普通的非隧道连接的建立connectSocket(connectTimeout, readTimeout),这个方法后tcp连接就会建立,三次握手也会完成。
/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
private void connectSocket(int connectTimeout, int readTimeout, Call call,
EventListener eventListener) throws IOException {
Proxy proxy = route.proxy();
Address address = route.address();
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 {
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;
}
// The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
// More details:
// https://github.com/square/okhttp/issues/3245
// https://android-review.googlesource.com/#/c/271775/
try {
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);
}
}
}
有3种情况需要建立普通连接:无代理(直连),http代理,socks代理。方法逻辑如下
- 1、创建Socket,设置超时。非SOCKS代理的情况下,通过SocketFactory创建;在SOCKS代理则传入proxy手动new一个出来。
- 2、基于第一步创建的socket,调用其connectSocket方法,完成三次握手构建tcp连接。最后会调用java.net.Socket类中的connect方法,具体细节有兴趣的同学可以继续深挖。
- 3、创建用于I/O的source和sink
接着我们着重看下隧道连接的建立。首先先解释下什么是隧道连接,我们这里说的隧道连接是指https ssl隧道协议。现在客户端请求大部分都已经是https请求,我们知道https连接需要建立在tls握手成功的基础上,但是网络连接过程中会存在代理,如果我们想在复用现有的HTTP proxy的传输方式来代理HTTPS流量,那么就会变成浏览器和代理握手跑TLS,代理拿到明文的请求报文,代理和网站握手跑TLS。但是代理没有,也不可能有网站的私钥证书,所以这么做会导致浏览器和代理之间的TLS无法建立,证书校验根本通不过。
HTTP tunnel以及CONNECT报文解决了这个问题,代理服务器不再作为中间人,不再改写浏览器的请求,而是把浏览器和远端服务器之间通信的数据原样透传,这样浏览器就可以直接和远端服务器进行TLS握手并传输加密的数据。
http隧道.png
隧道连接建立的过程如下
- 1、由于是https请求,代理服务器无法解析header信息,所以需要额外添加明文Header信息,告诉代理使用CONNECT扩展建立与服务器的隧道连接。CONNECT 方法就是一条单行的文本命令,它提供了由冒号分隔的安全原始服务器的主机名和端口号。host:port 后面跟着一个空格和 HTTP 版本字符串,再后面是 CRLF。
CONNECT home.netscape.com:443 HTTP/1.0
User-agent: Mozilla/1.1N
- 2、服务器收到信令后首先进行身份验证,通过后便与远程主机建立tcp连接,连接成功后代理会返回给客户端
HTTP/1.0 200 Connection Established
(与普通 HTTP 响应不同,这个响应并不需要包含 Content-Type 首部。此时连接只是对原始字节进行转接,不再是报文的承载者,所以不需要使用内容类型了。) -
3、建立成功后,代理将不会解析客户端报文,只会作为一个通道对报文进行盲转发。
隧道连接建立过程.jpeg
了解了http隧道的原理,我们再来看下okhttp中的实现
/**
* Does all the work to build an HTTPS connection over a proxy tunnel. The catch here is that a
* proxy server can issue an auth challenge and then close the connection.
*/
private void connectTunnel(int connectTimeout, int readTimeout, int writeTimeout, Call call,
EventListener eventListener) throws IOException {
Request tunnelRequest = createTunnelRequest();
HttpUrl url = tunnelRequest.url();
for (int i = 0; i < MAX_TUNNEL_ATTEMPTS; i++) {
connectSocket(connectTimeout, readTimeout, call, eventListener);
tunnelRequest = createTunnel(readTimeout, writeTimeout, tunnelRequest, url);
if (tunnelRequest == null) break; // Tunnel successfully created.
// The proxy decided to close the connection after an auth challenge. We need to create a new
// connection, but this time with the auth credentials.
closeQuietly(rawSocket);
rawSocket = null;
sink = null;
source = null;
eventListener.connectEnd(call, route.socketAddress(), route.proxy(), null);
}
}
/**
* To make an HTTPS connection over an HTTP proxy, send an unencrypted CONNECT request to create
* the proxy connection. This may need to be retried if the proxy requires authorization.
*/
private Request createTunnel(int readTimeout, int writeTimeout, Request tunnelRequest,
HttpUrl url) throws IOException {
// Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
while (true) {
Http1Codec tunnelConnection = new Http1Codec(null, null, source, sink);
source.timeout().timeout(readTimeout, MILLISECONDS);
sink.timeout().timeout(writeTimeout, MILLISECONDS);
tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
tunnelConnection.finishRequest();
Response response = tunnelConnection.readResponseHeaders(false)
.request(tunnelRequest)
.build();
// The response body from a CONNECT should be empty, but if it is not then we should consume
// it before proceeding.
long contentLength = HttpHeaders.contentLength(response);
if (contentLength == -1L) {
contentLength = 0L;
}
Source body = tunnelConnection.newFixedLengthSource(contentLength);
Util.skipAll(body, Integer.MAX_VALUE, TimeUnit.MILLISECONDS);
body.close();
switch (response.code()) {
case HTTP_OK:
// Assume the server won't send a TLS ServerHello until we send a TLS ClientHello. If
// that happens, then we will have buffered bytes that are needed by the SSLSocket!
// This check is imperfect: it doesn't tell us whether a handshake will succeed, just
// that it will almost certainly fail because the proxy has sent unexpected data.
if (!source.buffer().exhausted() || !sink.buffer().exhausted()) {
throw new IOException("TLS tunnel buffered too many bytes!");
}
return null;
case HTTP_PROXY_AUTH:
tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
if (tunnelRequest == null) throw new IOException("Failed to authenticate with proxy");
if ("close".equalsIgnoreCase(response.header("Connection"))) {
return tunnelRequest;
}
break;
default:
throw new IOException(
"Unexpected response code for CONNECT: " + response.code());
}
}
}
/**
* Returns a request that creates a TLS tunnel via an HTTP proxy. Everything in the tunnel request
* is sent unencrypted to the proxy server, so tunnels include only the minimum set of headers.
* This avoids sending potentially sensitive data like HTTP cookies to the proxy unencrypted.
*/
private Request createTunnelRequest() {
return new Request.Builder()
.url(route.address().url())
.header("Host", Util.hostHeader(route.address().url(), true))
.header("Proxy-Connection", "Keep-Alive") // For HTTP/1.0 proxies like Squid.
.header("User-Agent", Version.userAgent())
.build();
}
- 1、创建tunnelRequest,并建立与proxy的连接
- 2、创建一个while循环,调用createTunnel方法创建隧道连接,该方法会返回一个Request,当返回Request为null时,表示连接建立成功,跳出循环。
- 2.1、createTunnel方法首先生成CONNECT头部的文本requestLine
- 2.2、生成数据读写的管理类HttpCodec,将requestLine写入tunnelRequest头部,并开始发送数据
- 2.3、当返回的reponse满足要求时return null,否则return tunnelRequest
tcp连接建立成功后,我们继续看建立协议establishProtocol
private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,
int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {
if (route.address().sslSocketFactory() == null) {
if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {
socket = rawSocket;
protocol = Protocol.H2_PRIOR_KNOWLEDGE;
startHttp2(pingIntervalMillis);
return;
}
socket = rawSocket;
protocol = Protocol.HTTP_1_1;
return;
}
eventListener.secureConnectStart(call);
connectTls(connectionSpecSelector);
eventListener.secureConnectEnd(call, handshake);
if (protocol == Protocol.HTTP_2) {
startHttp2(pingIntervalMillis);
}
}
整理下逻辑
- 1、若不是https请求,首先判断是否为h2_prior_knowledge类型的请求(这种类型的请求属于http2但是需要服务器支持明文http2请求,因此不能使用https)则给相关值赋值并调用startHttp2方法建立http2连接(http2后续会专门分享,此处不展开)并return;若不是h2_prior_knowledge,则给相关值赋值后返回。
- 2、若是https请求,则调用connectTls进行tls握手,tls握手成功后判断当前连接是否为http2,若是则调用startHttp2方法建立http2连接。
看下connectTls方法
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException {
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
// Create the wrapper over the connected socket.
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
// Configure the socket's ciphers, TLS versions, and extensions.
ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket);
if (connectionSpec.supportsTlsExtensions()) {
Platform.get().configureTlsExtensions(
sslSocket, address.url().host(), address.protocols());
}
// Force handshake. This can throw!
sslSocket.startHandshake();
// block for session establishment
SSLSession sslSocketSession = sslSocket.getSession();
Handshake unverifiedHandshake = Handshake.get(sslSocketSession);
// Verify that the socket's certificates are acceptable for the target host.
if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) {
X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0);
throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:"
+ "\n certificate: " + CertificatePinner.pin(cert)
+ "\n DN: " + cert.getSubjectDN().getName()
+ "\n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert));
}
// Check that the certificate pinner is satisfied by the certificates presented.
address.certificatePinner().check(address.url().host(),
unverifiedHandshake.peerCertificates());
// Success! Save the handshake and the ALPN protocol.
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
socket = sslSocket;
source = Okio.buffer(Okio.source(socket));
sink = Okio.buffer(Okio.sink(socket));
handshake = unverifiedHandshake;
protocol = maybeProtocol != null
? Protocol.get(maybeProtocol)
: Protocol.HTTP_1_1;
success = true;
} catch (AssertionError e) {
if (Util.isAndroidGetsocknameError(e)) throw new IOException(e);
throw e;
} finally {
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket);
}
if (!success) {
closeQuietly(sslSocket);
}
}
}
TLS连接是对原始TCP连接(Socket)的一个封装,完成TLS握手、收发过程中的加解密等功能。并最后构建好一个SSLSocket。
- 1、基于原始的socket构建SSLSocket
- 2、根据用户传入connectionSpecSelector与生成的SSLSocket取交集获得连接配置connectionSpec
- 3、判断connectionSpec是否需要tls扩展,若需要则进行tls扩展
- 4、开始tls握手
- 5、TLS握手完成之后,获取证书信息,对TLS握手过程中传回来的证书进行验证。
- 6、在前面选择的ConnectionSpec支持TLS扩展参数时,获取TLS握手过程中顺便完成的协议协商过程所选择的协议。这个过程主要用于HTTP/2的ALPN扩展。
- 7、基于之前建立的SSLSocket创建I/O用的source,sink。
至此连接建立分析结束。
三、HttpCodec
上面在构建连接时出现了Http1Codec,Http2Codec,这里就简单介绍下这几个类。在okHttp中,HttpCodec是网络读写的管理类,也可以理解为解码器。当tcp连接建立后,信息在连接上传输是以流的形式,所以服务端和客户端都需要负责I/O操作的管理类。它有对应的两个子类,Http1Codec和Http2Codec,分别对应HTTP/1.1以及HTTP/2.0协议。我们看下HttpCodec接口
/** Encodes HTTP requests and decodes HTTP responses. */
public interface HttpCodec {
/**
* The timeout to use while discarding a stream of input data. Since this is used for connection
* reuse, this timeout should be significantly less than the time it takes to establish a new
* connection.
*/
int DISCARD_STREAM_TIMEOUT_MILLIS = 100;
/** Returns an output stream where the request body can be streamed. */
Sink createRequestBody(Request request, long contentLength);
/** This should update the HTTP engine's sentRequestMillis field. */
void writeRequestHeaders(Request request) throws IOException;
/** Flush the request to the underlying socket. */
void flushRequest() throws IOException;
/** Flush the request to the underlying socket and signal no more bytes will be transmitted. */
void finishRequest() throws IOException;
/**
* Parses bytes of a response header from an HTTP transport.
*
* @param expectContinue true to return null if this is an intermediate response with a "100"
* response code. Otherwise this method never returns null.
*/
Response.Builder readResponseHeaders(boolean expectContinue) throws IOException;
/** Returns a stream that reads the response body. */
ResponseBody openResponseBody(Response response) throws IOException;
/**
* Cancel this stream. Resources held by this stream will be cleaned up, though not synchronously.
* That may happen later by the connection pool thread.
*/
void cancel();
}
writeRequestHeaders(Request request) :写入请求头
createRequestBody(Request request, long contentLength) :写入请求体
flushRequest() 相当于flush,把请求刷入底层socket
finishRequest() throws IOException : 相当于flush,把请求输入底层socket并不在发出请求
readResponseHeaders(boolean expectContinue) //读取响应头
openResponseBody(Response response) //读取响应体
void cancel() :取消请求
由于HTTP/2和HTTP/1.x在支持流数量以及传输格式上有本质的区别,因此需要Http1Codec,Http2Codec两个来满足不同协议的需求,而volley之所以无法支持http2就是缺少了类似Http2Codec这样针对HTTP/2的解码器。关于Http1Codec具体实现这边就不做展开,关于http2在okhttp中的使用及逻辑,后续我们会专门做一期分享,这边也不展开。
网友评论