Okhttp解析(四)網路連線的建立
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代理伺服器只會對資訊進行轉發,而不會解析修改。
1. SOCKS代理下的HTTP請求, 與SOCKS代理伺服器建立TCP連線。
2. SOCKS代理下的HTTPS加密請求,通過SOCKS代理伺服器與HTTP伺服器建立連線,然後建立TLS加密連線。
3. 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網路連線的建立過程就講解完成了。