Okhttp作为一款底层网络访问框架,它和Volley等上层网络框架不一样的地方在于,Okhttp自己实现了与服务端的TCP连接,并在此连接上根据HTTP协议的规范与服务端进行HTTP协议及内容的请求和响应。Okhttp将请求内容通过修正,填充等方式封装成符合HTTP规范的HTTP请求内容,通过TCP连接,将内容以流的方式输出给服务端,并从服务端返回的响应流中读取出响应内容,根据HTTP协议解析并作出相应的响应。
Okhttp连接建立的情况区分
Okhttp支持HTTP 1.0/1.1, HTTP2,SPDY 三种HTTP协议,同时支持加密传输HTTPS,以及SOCKS代理和HTTP代理。那么在连接建立时就需要处理它们组合时的处理操作了。这里的连接根据代理类型做主要区分。
无代理
- 无代理的HTTP请求, 与服务器建立TCP连接。
- 无代理的HTTPS加密请求, 与服务器建立TCP连接,然后建立TLS加密连接。
- 无代理的HTTP2/SPDY请求, 与服务器建立TCP连接,然后建立TLS加密连接,然后创建帧连接。
SOCKS代理
SOCKS代理服务器会将请求信息转发给目标HTTP服务器,目标HTTP服务器返回信息之后,SOCKS代理服务器再将响应信息返回给客户端。SOCKS代理服务器只会对信息进行转发,而不会解析修改。
- SOCKS代理下的HTTP请求, 与SOCKS代理服务器建立TCP连接。
- SOCKS代理下的HTTPS加密请求, 通过SOCKS代理服务器与HTTP服务器建立连接,然后建立TLS加密连接。
- SOCKS代理下的HTTP2/SPDY请求, 通过SOCKS代理服务器与HTTP服务器建立连接,然后建立TLS加密连接,然后创建帧连接。
HTTP代理
- HTTP代理下的HTTP请求, 与HTTP代理服务器建立TCP连接。HTTP代理服务器会解析请求的内容信息,并根据这些信息去请求目标HTTP服务器,目标HTTP2服务器返回信息之后,再解析响应内容,然后再将这些响应信息返回给客户端。HTTP服务器会解析和修改请求和响应的内容。
- HTTP代理下的HTTPS加密请求, 与目标服务器建立通过HTTP代理的隧道连接,然后建立TLS加密连接。因为是加密连接,HTTP代理服务器不再解析请求和响应内容,而只是转发数据。
- HTTP代理下的HTTP2/SPDY请求, 与目标服务器建立通过HTTP代理的隧道连接,然后建立TLS加密连接,然后创建帧连接。因为是加密连接,HTTP代理服务器不再解析请求和响应内容,而只是转发数据。
虽然情况分为很多种,但是重要的部分只是以下几个阶段。
- 根据是否需要代理,创建Socket连接。
- 进入Socket连接阶段,判断是否需要TLS连接,进入TLS连接处理。
- 进入TLS连接处理阶段,判断是否需要建立隧道连接。
- 完成TLS握手,判断是否需要帧连接。
关于SOCKS代理和HTTP代理区别
Socks代理
是全能代理,就像有很多跳线的转接板,它只是简单地将一端的系统连接到另外一端。支持多种协议,包括http、ftp请求及其它类型的请求。它分socks 4 和socks 5两种类型,socks 4只支持TCP协议而socks 5支持TCP/UDP协议,还支持各种身份验证机制等协议。其标准端口为1080。socks代理相应的采用socks协议的代理服务器就是SOCKS服务器,是一种通用的代理服务器。Socks是个电路级的底层网关,是DavidKoblas在1990年开发的,此后就一直作为Internet RFC标准的开放标准。Socks不要求应用程序遵循特定的操作系统平台,Socks 代理与应用层代理、 HTTP 层代理不同,Socks代理只是简单地传递数据包,而不必关心是何种应用协议(比如FTP、HTTP和NNTP请求)。所以,Socks代理比其他应用层代理要快得多。它通常绑定在代理服务器的1080端口上。如果您在企业网或校园网上,需要透过防火墙或通过代理服务器访问Internet就可能需要使用SOCKS。一般情况下,对于拨号上网用户都不需要使用它。注意,浏览网页时常用的代理服务器通常是专门的http代理,它和SOCKS是不同的。因此,您能浏览网页不等于您一定可以通过SOCKS访问Internet。 常用的防火墙,或代理软件都支持SOCKS,但需要其管理员打开这一功能。如果您不确信您是否需要SOCKS或是否有SOCKS可用,请与您的网络管理员联系。
HTTP代理
www对于每一个上网的人都再熟悉不过了,www连接请求就是采用的http协议,所以我们在浏览网页,下载数据(也可采用ftp协议)是就是用http代理。它通常绑定在代理服务器的80、3128、8080等端口上。
详见
http://blog.csdn.net/clh604/article/details/9235597
http://blog.csdn.net/extraordinarylife/article/details/52512860
Okhttp连接建立的流程
有了以上对HTTP连接建立的大概了解后,我们接下来分析代码就会更容易理解了。我们从RealConnection的connect方法开始
public void connect(int connectTimeout, int readTimeout, int writeTimeout,
List<ConnectionSpec> connectionSpecs, boolean connectionRetryEnabled) throws RouteException {
if (protocol != null) throw new IllegalStateException("already connected");
RouteException routeException = null;
//根据连接配置信息创建连接配置选择器,用于后面自动重试路由地址建立可用TCP连接
ConnectionSpecSelector connectionSpecSelector = new ConnectionSpecSelector(connectionSpecs);
Proxy proxy = route.proxy();
Address address = route.address();
//对于普通HTTP连接,连接配置信息中必须包含CLEARTEXT,也就是明文传输
if (route.address().sslSocketFactory() == null
&& !connectionSpecs.contains(ConnectionSpec.CLEARTEXT)) {
throw new RouteException(new UnknownServiceException(
"CLEARTEXT communication not supported: " + connectionSpecs));
}
//进入while循环,创建连接,直到连接成功
while (protocol == null) {
try {
//创建Socket,如果是无代理或HTTP代理,交给SocketFactory创建Socket,如果是SOCKS代理,则手动创建一个代理Socket
rawSocket = proxy.type() == Proxy.Type.DIRECT || proxy.type() == Proxy.Type.HTTP
? address.socketFactory().createSocket()
: new Socket(proxy);
//连接Socket
connectSocket(connectTimeout, readTimeout, writeTimeout, connectionSpecSelector);
} catch (IOException e) {
closeQuietly(socket);
closeQuietly(rawSocket);
socket = null;
rawSocket = null;
source = null;
sink = null;
handshake = null;
protocol = null;
if (routeException == null) {
routeException = new RouteException(e);
} else {
routeException.addConnectException(e);
}
if (!connectionRetryEnabled || !connectionSpecSelector.connectionFailed(e)) {
throw routeException;
}
}
}
}
可以看到上面很简单,首先创建一个底层Socket,然后进入Socket连接阶段。我们继续看connectSocket
/** Does all the work necessary to build a full HTTP or HTTPS connection on a raw socket. */
private void connectSocket(int connectTimeout, int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
//设置读取超时时间
rawSocket.setSoTimeout(readTimeout);
分android和java两种平台进行Socket的连接,其实也就是调用socket的connect
try {
Platform.get().connectSocket(rawSocket, route.socketAddress(), connectTimeout);
} catch (ConnectException e) {
throw new ConnectException("Failed to connect to " + route.socketAddress());
}
//根据socket取得封装后的输入和输出流,类似InputStream和OutputStream
source = Okio.buffer(Okio.source(rawSocket));
sink = Okio.buffer(Okio.sink(rawSocket));
if (route.address().sslSocketFactory() != null) {
//建立TLS连接,看看哪些情况需要建立TLS连接
connectTls(readTimeout, writeTimeout, connectionSpecSelector);
} else {
//不需要建立TLS连接的,统一当做HTTP 1.1协议
protocol = Protocol.HTTP_1_1;
socket = rawSocket;
}
if (protocol == Protocol.SPDY_3 || protocol == Protocol.HTTP_2) {
socket.setSoTimeout(0); // Framed connection timeouts are set per-stream.
//如果是SPDY或者HTTP2协议连接,则需要创建帧连接
FramedConnection framedConnection = new FramedConnection.Builder(true)
.socket(socket, route.address().url().host(), source, sink)
.protocol(protocol)
.listener(this)
.build();
//发送帧连接相关的信息
framedConnection.sendConnectionPreface();
//获取一个帧连接支持的最大并发流的数量
// Only assign the framed connection once the preface has been sent successfully.
this.allocationLimit = framedConnection.maxConcurrentStreams();
this.framedConnection = framedConnection;
} else {
//不支持帧连接的,最大并发流为1,也是不支持流并发
this.allocationLimit = 1;
}
}
可以看到这里分为3个步骤。
- 建立了底层Socket的连接,并获得输入和输出流。
- 判断是否进行TLS握手连接。HTTPS,HTTP2,SPDY三种情况下需要建立TLS加密连接。
- 判断是否需要创建帧连接,并进行帧连接握手。HTTP2,SPDY情况下需要建立帧连接。
接着我们进入建立TLS连接的过程,
private void connectTls(int readTimeout, int writeTimeout,
ConnectionSpecSelector connectionSpecSelector) throws IOException {
//在HTTP代理下,HTTPS,HTTP2,SPDY情况下,要建立隧道连接
if (route.requiresTunnel()) {
createTunnel(readTimeout, writeTimeout);
}
Address address = route.address();
SSLSocketFactory sslSocketFactory = address.sslSocketFactory();
boolean success = false;
SSLSocket sslSocket = null;
try {
//基于之前的TCP连接,创建加密的SSLSocket连接
// Create the wrapper over the connected socket.
sslSocket = (SSLSocket) sslSocketFactory.createSocket(
rawSocket, address.url().host(), address.url().port(), true /* autoClose */);
//配置加密连接需要的秘钥,TLS版本等信息
// 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());
}
//开始TLS握手
// Force handshake. This can throw!
sslSocket.startHandshake();
//获取TLS握手后的结果
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());
//建立了TLS连接,获取协商之后的HTTP协议
// Success! Save the handshake and the ALPN protocol.
String maybeProtocol = connectionSpec.supportsTlsExtensions()
? Platform.get().getSelectedProtocol(sslSocket)
: null;
//指定默认的socket连接,输入和输出流
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 {
//进行TLS握手结束阶段的资源清理
if (sslSocket != null) {
Platform.get().afterHandshake(sslSocket);
}
//TLS连接不成功的话,关闭TLS连接
if (!success) {
closeQuietly(sslSocket);
}
}
}
可以看到TLS连接这个阶段主要做了以下操作。
- 判断是否需要建立隧道连接。在HTTP代理下,HTTPS,HTTP2,SPDY情况下,要建立隧道连接
- 创建SSLSocket这个TLS连接,并开始握手。
- 获取握手后的返回信息,例如连接协议的商定,版本信息,加密算法,证书的验证等。
- TLS成功建立后,更新socket,HTTP协议,输入输出流等信息。
- 释放资源,TLS连接成功后的资源清理和失败后的关闭操作。
接下来我们看看是如何建立隧道连接的。
/**
* 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 void createTunnel(int readTimeout, int writeTimeout) throws IOException {
//创建隧道连接的请求
// Make an SSL Tunnel on the first message pair of each SSL + proxy connection.
Request tunnelRequest = createTunnelRequest();
HttpUrl url = tunnelRequest.url();
//隧道连接的请求行
String requestLine = "CONNECT " + Util.hostHeader(url, true) + " HTTP/1.1";
//while循环中,不断重试建立隧道连接,直到建立成功
while (true) {
//创建隧道连接
Http1xStream tunnelConnection = new Http1xStream(null, source, sink);
source.timeout().timeout(readTimeout, MILLISECONDS);
sink.timeout().timeout(writeTimeout, MILLISECONDS);
//输出隧道连接的请求头和请求行数据,请求建立隧道连接
tunnelConnection.writeRequest(tunnelRequest.headers(), requestLine);
tunnelConnection.finishRequest();
//读取响应信息
Response response = tunnelConnection.readResponse().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 = OkHeaders.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:
//响应码返回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;
case HTTP_PROXY_AUTH:
//响应码返回HTTP_PROXY_AUTH,说明还需要进行证书验证,while循环再次请求
tunnelRequest = route.address().proxyAuthenticator().authenticate(route, response);
if (tunnelRequest != null) continue;
throw new IOException("Failed to authenticate with proxy");
default:
throw new IOException(
"Unexpected response code for CONNECT: " + response.code());
}
}
}
/**
* Returns a request that creates a TLS tunnel via an HTTP proxy, or null if no tunnel is
* necessary. 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() throws IOException {
//创建隧道连接的请求,就是指定请求头的一些字段信息
return new Request.Builder()
.url(route.address().url())
.header("Host", Util.hostHeader(route.address().url(), true))
.header("Proxy-Connection", "Keep-Alive")
.header("User-Agent", Version.userAgent()) // For HTTP/1.0 proxies like Squid.
.build();
}
可见隧道连接的建立过程,是通过封装的Http1xStream流对象,输出建立隧道连接所需要的请求头和请求行信息,去请求建立隧道连接,这个过程中,可能需要证书的验证,因此,在while循环中,根据返回的响应信息重新创建请求,并提交请求,知道返回隧道创建成功或抛出异常。
到这里的话,Okhttp网络连接的建立过程就讲解完成了。