建立隧道连接的过程如下:
关于建立隧道连接更详细的过程可参考 OkHttp3中的代理与路由 的相关部分。
建立普通连接的过程比较直接:
/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
private void buildConnection(int connectTimeout, int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
connectSocket(connectTimeout, readTimeout);
establishProtocol(readTimeout, writeTimeout, connectionSpecSelector);
}
更详细的过程可参考 OkHttp3中的代理与路由 的相关部分。
不管是建立隧道连接,还是建立普通连接,都少不了 建立协议 这一步。这一步是在建立好了TCP连接之后,而在该TCP能被拿来收发数据之前执行的。它主要为数据的加密传输做一些初始化,比如TLS握手,HTTP/2的协议协商等。
private void establishProtocol(int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
if (route.address().sslSocketFactory() != null) {
connectTls(readTimeout, writeTimeout, connectionSpecSelector);
} else {
protocol = Protocol.HTTP_1_1;
socket = rawSocket;
}
if (protocol == Protocol.HTTP_2) {
socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
Http2Connection http2Connection = new Http2Connection.Builder(true)
.socket(socket, route.address().url().host(), source, sink)
.listener(this)
.build();
http2Connection.start();
// Only assign the framed connection once the preface has been sent successfully.
this.allocationLimit = http2Connection.maxConcurrentStreams();
this.http2Connection = http2Connection;
} else {
this.allocationLimit = 1;
}
}
对于加密的数据传输,创建TLS连接。对于明文传输,则设置protocol
和socket
。 socket
指向直接与应用层,如HTTP或HTTP/2,交互的Socket: 对于明文传输没有设置HTTP代理的HTTP请求,它是与HTTP服务器之间的TCP socket; 对于明文传输设置了HTTP代理或SOCKS代理的HTTP请求,它是与代理服务器之间的TCP socket; 对于加密传输没有设置HTTP代理服务器的HTTP或HTTP2请求,它是与HTTP服务器之间的SSLScoket; 对于加密传输设置了HTTP代理服务器的HTTP或HTTP2请求,它是与HTTP服务器之间经过了代理服务器的SSLSocket,一个隧道连接; 对于加密传输设置了SOCKS代理的HTTP或HTTP2请求,它是一条经过了代理服务器的SSLSocket连接。
对于HTTP/2,会建立HTTP/2连接,并进一步协商连接参数,如连接上可同时执行的并发请求数等。而对于非HTTP/2,则将连接上可同时执行的并发请求数设置为1。
进一步来看建立协议过程中,为安全请求所做的建立TLS连接的过程:
private void connectTls(int readTimeout, int writeTimeout,
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();
Handshake unverifiedHandshake = Handshake.get(sslSocket.getSession());
// Verify that the socket's certificates are acceptable for the target host.
if (!address.hostnameVerifier().verify(address.url().host(), sslSocket.getSession())) {
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连接的一个封装,以提供TLS握手,及数据收发过程中的加密解密等功能。在Java中,用SSLSocket来描述。上面建立TLS连接的过程大体为:
具体来看ConnectionSpecSelector
中配置SSLSocket的过程:
/**
* Configures the supplied {@link SSLSocket} to connect to the specified host using an appropriate
* {@link ConnectionSpec}. Returns the chosen {@link ConnectionSpec}, never {@code null}.
*
* @throws IOException if the socket does not support any of the TLS modes available
*/
public ConnectionSpec configureSecureSocket(SSLSocket sslSocket) throws IOException {
ConnectionSpec tlsConfiguration = null;
for (int i = nextModeIndex, size = connectionSpecs.size(); i < size; i++) {
ConnectionSpec connectionSpec = connectionSpecs.get(i);
if (connectionSpec.isCompatible(sslSocket)) {
tlsConfiguration = connectionSpec;
nextModeIndex = i + 1;
break;
}
}
if (tlsConfiguration == null) {
// This may be the first time a connection has been attempted and the socket does not support
// any the required protocols, or it may be a retry (but this socket supports fewer
// protocols than was suggested by a prior socket).
throw new UnknownServiceException(
"Unable to find acceptable protocols. isFallback=" + isFallback
+ ", modes=" + connectionSpecs
+ ", supported protocols=" + Arrays.toString(sslSocket.getEnabledProtocols()));
}
isFallbackPossible = isFallbackPossible(sslSocket);
Internal.instance.apply(tlsConfiguration, sslSocket, isFallback);
return tlsConfiguration;
}
这个过程分为如下的两个步骤:
从为OkHttp配置的ConnectionSpec集合中选择一个与SSLSocket兼容的一个。SSLSocket与ConnectionSpec兼容的标准如下:
public boolean isCompatible(SSLSocket socket) {
if (!tls) {
return false;
}
if (tlsVersions != null
&& !nonEmptyIntersection(tlsVersions, socket.getEnabledProtocols())) {
return false;
}
if (cipherSuites != null
&& !nonEmptyIntersection(cipherSuites, socket.getEnabledCipherSuites())) {
return false;
}
return true;
}
/**
* An N*M intersection that terminates if any intersection is found. The sizes of both arguments
* are assumed to be so small, and the likelihood of an intersection so great, that it is not
* worth the CPU cost of sorting or the memory cost of hashing.
*/
private static boolean nonEmptyIntersection(String[] a, String[] b) {
if (a == null || b == null || a.length == 0 || b.length == 0) {
return false;
}
for (String toFind : a) {
if (indexOf(b, toFind) != -1) {
return true;
}
}
return false;
}
即ConnectionSpec启用的TLS版本及密码套件,与SSLSocket启用的有交集。 2 将选择的ConnectionSpec应用在SSLSocket上。OkHttpClient中ConnectionSpec的应用:
@Override
public void apply(ConnectionSpec tlsConfiguration, SSLSocket sslSocket, boolean isFallback) {
tlsConfiguration.apply(sslSocket, isFallback);
}
而在ConnectionSpec中:
/** Applies this spec to {@code sslSocket}. */
void apply(SSLSocket sslSocket, boolean isFallback) {
ConnectionSpec specToApply = supportedSpec(sslSocket, isFallback);
if (specToApply.tlsVersions != null) {
sslSocket.setEnabledProtocols(specToApply.tlsVersions);
}
if (specToApply.cipherSuites != null) {
sslSocket.setEnabledCipherSuites(specToApply.cipherSuites);
}
}
/**
* Returns a copy of this that omits cipher suites and TLS versions not enabled by {@code
* sslSocket}.
*/
private ConnectionSpec supportedSpec(SSLSocket sslSocket, boolean isFallback) {
String[] cipherSuitesIntersection = cipherSuites != null
? intersect(String.class, cipherSuites, sslSocket.getEnabledCipherSuites())
: sslSocket.getEnabledCipherSuites();
String[] tlsVersionsIntersection = tlsVersions != null
? intersect(String.class, tlsVersions, sslSocket.getEnabledProtocols())
: sslSocket.getEnabledProtocols();
// In accordance with https://tools.ietf.org/html/draft-ietf-tls-downgrade-scsv-00
// the SCSV cipher is added to signal that a protocol fallback has taken place.
if (isFallback && indexOf(sslSocket.getSupportedCipherSuites(), "TLS_FALLBACK_SCSV") != -1) {
cipherSuitesIntersection = concat(cipherSuitesIntersection, "TLS_FALLBACK_SCSV");
}
return new Builder(this)
.cipherSuites(cipherSuitesIntersection)
.tlsVersions(tlsVersionsIntersection)
.build();
}
主要是:
我们知道HTTP/2的协议协商主要是利用了TLS的ALPN扩展来完成的。这里再来详细的看一下配置TLS扩展的过程。对于Android平台而言,这部分逻辑在AndroidPlatform:
@Override public void configureTlsExtensions(
SSLSocket sslSocket, String hostname, List<Protocol> protocols) {
// Enable SNI and session tickets.
if (hostname != null) {
setUseSessionTickets.invokeOptionalWithoutCheckedException(sslSocket, true);
setHostname.invokeOptionalWithoutCheckedException(sslSocket, hostname);
}
// Enable ALPN.
if (setAlpnProtocols != null && setAlpnProtocols.isSupported(sslSocket)) {
Object[] parameters = {concatLengthPrefixed(protocols)};
setAlpnProtocols.invokeWithoutCheckedException(sslSocket, parameters);
}
}
TLS扩展相关的方法不是SSLSocket接口的标准方法,不同的SSL/TLS实现库对这些接口的支持程度不一样,因而这里通过反射机制调用TLS扩展相关的方法。
这里主要配置了3个TLS扩展,分别是session tickets,SNI和ALPN。session tickets用于会话回复,SNI用于支持单个主机配置了多个域名的情况,ALPN则用于HTTP/2的协议协商。可以看到为SNI设置的hostname最终来源于Url,也就意味着使用HttpDns时,如果直接将IP地址替换原来Url中的域名来发起HTTPS请求的话,SNI将是IP地址,这有可能使服务器下发不恰当的证书。
TLS扩展相关方法的OptionalMethod创建过程也在AndroidPlatform中:
public AndroidPlatform(Class<?> sslParametersClass, OptionalMethod<Socket> setUseSessionTickets,
OptionalMethod<Socket> setHostname, OptionalMethod<Socket> getAlpnSelectedProtocol,
OptionalMethod<Socket> setAlpnProtocols) {
this.sslParametersClass = sslParametersClass;
this.setUseSessionTickets = setUseSessionTickets;
this.setHostname = setHostname;
this.getAlpnSelectedProtocol = getAlpnSelectedProtocol;
this.setAlpnProtocols = setAlpnProtocols;
}
......
public static Platform buildIfSupported() {
// Attempt to find Android 2.3+ APIs.
try {
Class<?> sslParametersClass;
try {
sslParametersClass = Class.forName("com.android.org.conscrypt.SSLParametersImpl");
} catch (ClassNotFoundException e) {
// Older platform before being unbundled.
sslParametersClass = Class.forName(
"org.apache.harmony.xnet.provider.jsse.SSLParametersImpl");
}
OptionalMethod<Socket> setUseSessionTickets = new OptionalMethod<>(
null, "setUseSessionTickets", boolean.class);
OptionalMethod<Socket> setHostname = new OptionalMethod<>(
null, "setHostname", String.class);
OptionalMethod<Socket> getAlpnSelectedProtocol = null;
OptionalMethod<Socket> setAlpnProtocols = null;
// Attempt to find Android 5.0+ APIs.
try {
Class.forName("android.net.Network"); // Arbitrary class added in Android 5.0.
getAlpnSelectedProtocol = new OptionalMethod<>(byte[].class, "getAlpnSelectedProtocol");
setAlpnProtocols = new OptionalMethod<>(null, "setAlpnProtocols", byte[].class);
} catch (ClassNotFoundException ignored) {
}
return new AndroidPlatform(sslParametersClass, setUseSessionTickets, setHostname,
getAlpnSelectedProtocol, setAlpnProtocols);
} catch (ClassNotFoundException ignored) {
// This isn't an Android runtime.
}
return null;
}
建立TLS连接的第7步,获取协议的过程与配置TLS的过程类似,同样利用反射调用SSLSocket的方法,在AndroidPlatform中:
@Override public String getSelectedProtocol(SSLSocket socket) {
if (getAlpnSelectedProtocol == null) return null;
if (!getAlpnSelectedProtocol.isSupported(socket)) return null;
byte[] alpnResult = (byte[]) getAlpnSelectedProtocol.invokeWithoutCheckedException(socket);
return alpnResult != null ? new String(alpnResult, Util.UTF_8) : null;
}
至此我们分析了OkHttp3中,所有HTTP请求,包括设置了代理的明文HTTP请求,设置了代理的HTTPS请求,设置了代理的HTTP/2请求,无代理的明文HTTP请求,无代理的HTTPS请求,无代理的HTTP/2请求的连接建立过程,其中包括TLS的握手,HTTP/2的协议协商等。
总结一下,OkHttp中,IO相关的组件的其关系大体如下图所示:
Done。
相关阅读:
网易云新用户大礼包:https://www.163yun.com/gift
本文来自网易实践者社区,经作者韩鹏飞授权发布。