okhttp分享5:ConnectInterceptor(1)
按順序我們現(xiàn)在走到了ConnectInterceptor,該攔截器主要負(fù)責(zé)建立與服務(wù)器的鏈接。先簡單看一下代碼,代碼量不多
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中實現(xiàn)的。我們回憶一下,這個streamAllocation最早是在RetryAndFollowUpInterceptor中實例化的,我們看一下RetryAndFollowUpInterceptor中代碼
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
在看下其構(gòu)造方法
public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
EventListener eventListener, Object callStackTrace) {
this.connectionPool = connectionPool;//連接池
this.address = address;//地址相關(guān)信息
this.call = call;//用戶請求
this.eventListener = eventListener;
this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);//路由選擇策略
this.callStackTrace = callStackTrace;
}
其中我們主要關(guān)注routeSelector和connectionPool
一、路由選擇
之前我們說到,okhttp會自動選擇最優(yōu)的路由,并以此建立或復(fù)用連接。連接的建立是要基于路由選擇的,我們先看一下okhttp的路由相關(guān)類
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時作為參數(shù)實例化的。對于簡單的鏈接,這里是服務(wù)器的主機名和端口號。如果是通過代理(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方法,用于后續(xù)連接池調(diào)用
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表示通過代理服務(wù)器的信息proxy及鏈接的目標(biāo)地址Address來描述的路由,連接的目標(biāo)地址inetSocketAddress根據(jù)代理類型的不同而有著不同的含義,這主要是通過不同代理協(xié)議的差異而造成的。對于無需代理的情況,連接的目標(biāo)地址inetSocketAddress中包含HTTP服務(wù)器經(jīng)過DNS域名解析的IP地址以及協(xié)議端口號;對于SOCKET代理其中包含HTTP服務(wù)器的域名及協(xié)議端口號;對于HTTP代理,其中則包含代理服務(wù)器經(jīng)過域名解析的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客戶端與服務(wù)端網(wǎng)絡(luò)通信的過程,可以細(xì)分成很多塊,大體分的話其實就是兩步首先建立連接(tcp/udp),連接建立后基于當(dāng)前連接及請求構(gòu)建信息傳輸流(http/socket),通過流進行io操作,與服務(wù)器通信。做個簡單的比喻,連接就是橋梁,流就是橋上跑的貨車,信息就是貨車裝載的貨物。而tcp連接建立過程所需要的關(guān)鍵元素就是Route,現(xiàn)在借助于域名做負(fù)載均衡也十分常見,所以路由選擇也變得較為復(fù)雜,okhttp中通過RouteSelector對路由信息進行管理,使建立tcp連接時可以使用最優(yōu)的路由。下面我們看下源碼
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());
}
成員變量都是之前提過的內(nèi)容,直接看構(gòu)造器中調(diào)用的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根據(jù)請求uri產(chǎn)生proxy,若產(chǎn)生的proxy為null則使用Proxy.NO_PROXY。
這邊我們注意下proxySelector(),這個是在okhttpclient傳入的代理選擇策略,用戶若不傳入則使用系統(tǒng)默認(rèn)的,我們簡單看下系統(tǒng)默認(rèn)的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方法,其實就是根據(jù)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的核心邏輯,外部進行路由選擇時會調(diào)用next方法,返回一個Selection對象,該對象就是一個可選路由的list。其邏輯如下
- 1、判斷是否還有路由(包括延遲路由)
- 2、進入while循環(huán),當(dāng)還存在可嘗試的proxy時進行以下邏輯
- 2.1、通過nextProxy()方法獲取下一個proxy
- 2.2、根據(jù)proxy,address,dns解析后得到的inetSocketAddresses生成route
- 2.3、根據(jù)routeDatabase判斷生成route是否為延遲路由,并加入相應(yīng)的list
- 2.4、若可用路由列表不為空則跳出循環(huán),返回Selection;若可用路由列表為空則將延遲路由列表全部加如可用路由列表,清空延遲路由列表,執(zhí)行下一輪循環(huán)
我們繼續(xù)看next中調(diào)用的nextProxy方法,就是從proxy列表中獲取下一個proxy,作為參數(shù)傳入resetNextInetSocketAddress方法,而resetNextInetSocketAddress邏輯如下
- 1、對于沒有配置代理的情況,會對HTTP服務(wù)器的域名進行DNS域名解析,并為每個解析到的IP地址創(chuàng)建 連接的目標(biāo)地址
- 2、對于SOCKS代理,直接以HTTP的服務(wù)器的域名以及協(xié)議端口創(chuàng)建 連接目標(biāo)地址
- 3、對于HTTP代理,則會對HTTP代理服務(wù)器的域名進行DNS域名解析,并為每個解析到的IP地址創(chuàng)建 連接的目標(biāo)地址
從代碼中我們可以看出這里就是okhttp網(wǎng)絡(luò)請求解析dns的地方List<InetAddress> addresses = address.dns().lookup(socketHost);該行就是根據(jù)根據(jù)host解析出其對應(yīng)的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);
}
當(dāng)連接失敗,外部會調(diào)用這個方法,該方法會維護RouteDatabase中的失敗路由信息。到這里okhttp的路由選擇就講完了,通過RouteSelector收集、選擇路由以及維護失敗路由列表,使連接時不會優(yōu)先使用之前已出過錯的路由,節(jié)省時間,提高效率。
二、連接:Connection類
上面我們介紹了路由選擇策略,下面我們看一下okhttp具體連接相關(guān)邏輯。okhttp連接相關(guān)邏輯都由Connection負(fù)責(zé),Connection是一個接口,有四個抽象方法
Route route(); //返回一個路由
Socket socket(); //返回一個socket
Handshake handshake(); //如果是一個https,則返回一個TLS握手協(xié)議
Protocol protocol(); //返回一個協(xié)議類型 比如 http1.1 等或者自定義類型
其實現(xiàn)類為RealConnection,該對象會在需要構(gòu)建tcp連接時實例化,具體在StreamAllocation中,我們后面會講到。如果擁有了一個RealConnection就代表了我們已經(jīng)跟服務(wù)器有了一條通信鏈路,也就說明此時tcp三次握手已經(jīng)完成。先看下這個類的源碼
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; //應(yīng)用層socket
//握手
private Handshake handshake;
//協(xié)議
private Protocol protocol;
// http2的鏈接
private Http2Connection http2Connection;
//通過source和sink,大家可以猜到是與服務(wù)器交互的輸入輸出流
private BufferedSource source;
private BufferedSink sink;
// The fields below track connection state and are guarded by connectionPool.
//下面這個字段是 屬于表示鏈接狀態(tài)的字段,并且有connectPool統(tǒng)一管理
/** If true, no new streams can be created on this connection. Once true this is always true. */
//如果noNewStreams被設(shè)為true,則noNewStreams一直為true,不會被改變,并且表示這個鏈接不會再創(chuàng)新的stream流
public boolean noNewStreams;
//成功的次數(shù)
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.
*/
//此鏈接可以承載最大并發(fā)流的限制,如果不超過限制,可以隨意增加
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可以簡單理解為它表示該連接不可用。這個值一旦被設(shè)為true,則這個conncetion則不會再創(chuàng)建stream。
- 2、allocationLimit是分配流的數(shù)量上限,http2支持一個連接上并發(fā)多個流,其他的都只支持一個連接對應(yīng)一個流。
- 3、allocations是關(guān)聯(lián)StreamAllocation,它用來統(tǒng)計在一個連接上建立了哪些流,通過StreamAllocation的acquire方法和release方法可以將一個allcation對象添加到鏈表或者移除鏈表。
看下其核心方法connect,也就是在這里面構(gòu)建了連接。
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,修改每個連接上可承載的流的數(shù)量
allocationLimit = http2Connection.maxConcurrentStreams();
}
}
}
整理下邏輯
- 1、檢查連接是否已經(jīng)建立,若已經(jīng)建立,則拋出異常,否則繼續(xù)。連接是否建立由protocol標(biāo)示,它表示在整個連接建立、協(xié)商過程中選擇所有要用到的協(xié)議
- 2、根據(jù)ConnectionSpec的集合connnectionspecs構(gòu)造ConnectionSpecSelector。這邊說一下,ConnectionSpec用于描述傳輸HTTP流量的socket連接的配置。若是https連接,這些配置主要包括協(xié)商安全連接時要使用的TLS版本號和密碼套間,是否支持TLS擴展等;http連接則包含是否支持明文傳輸?shù)龋脩艨梢宰远xConnectionSpec集合,但一般使用okhttp默認(rèn)的
- 3、若不是https請求,則根據(jù)ConnectionSpec進行相關(guān)邏輯檢測
- 4、根據(jù)請求判斷是否需要建立隧道連接,如果建立隧道連接則調(diào)用
connectTunnel(connectTimeout, readTimeout, writeTimeout); - 5、如果不是隧道連接則調(diào)用connectSocket(connectTimeout, readTimeout);建立普通連接。
- 6、建立協(xié)議
- 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、創(chuàng)建Socket,設(shè)置超時。非SOCKS代理的情況下,通過SocketFactory創(chuàng)建;在SOCKS代理則傳入proxy手動new一個出來。
- 2、基于第一步創(chuàng)建的socket,調(diào)用其connectSocket方法,完成三次握手構(gòu)建tcp連接。最后會調(diào)用java.net.Socket類中的connect方法,具體細(xì)節(jié)有興趣的同學(xué)可以繼續(xù)深挖。
- 3、創(chuàng)建用于I/O的source和sink
接著我們著重看下隧道連接的建立。首先先解釋下什么是隧道連接,我們這里說的隧道連接是指https ssl隧道協(xié)議?,F(xiàn)在客戶端請求大部分都已經(jīng)是https請求,我們知道https連接需要建立在tls握手成功的基礎(chǔ)上,但是網(wǎng)絡(luò)連接過程中會存在代理,如果我們想在復(fù)用現(xiàn)有的HTTP proxy的傳輸方式來代理HTTPS流量,那么就會變成瀏覽器和代理握手跑TLS,代理拿到明文的請求報文,代理和網(wǎng)站握手跑TLS。但是代理沒有,也不可能有網(wǎng)站的私鑰證書,所以這么做會導(dǎo)致瀏覽器和代理之間的TLS無法建立,證書校驗根本通不過。
HTTP tunnel以及CONNECT報文解決了這個問題,代理服務(wù)器不再作為中間人,不再改寫瀏覽器的請求,而是把瀏覽器和遠端服務(wù)器之間通信的數(shù)據(jù)原樣透傳,這樣瀏覽器就可以直接和遠端服務(wù)器進行TLS握手并傳輸加密的數(shù)據(jù)。

隧道連接建立的過程如下
- 1、由于是https請求,代理服務(wù)器無法解析header信息,所以需要額外添加明文Header信息,告訴代理使用CONNECT擴展建立與服務(wù)器的隧道連接。CONNECT 方法就是一條單行的文本命令,它提供了由冒號分隔的安全原始服務(wù)器的主機名和端口號。host:port 后面跟著一個空格和 HTTP 版本字符串,再后面是 CRLF。
CONNECT home.netscape.com:443 HTTP/1.0
User-agent: Mozilla/1.1N
- 2、服務(wù)器收到信令后首先進行身份驗證,通過后便與遠程主機建立tcp連接,連接成功后代理會返回給客戶端
HTTP/1.0 200 Connection Established(與普通 HTTP 響應(yīng)不同,這個響應(yīng)并不需要包含 Content-Type 首部。此時連接只是對原始字節(jié)進行轉(zhuǎn)接,不再是報文的承載者,所以不需要使用內(nèi)容類型了。) -
3、建立成功后,代理將不會解析客戶端報文,只會作為一個通道對報文進行盲轉(zhuǎn)發(fā)。
隧道連接建立過程.jpeg
了解了http隧道的原理,我們再來看下okhttp中的實現(xiàn)
/**
* 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、創(chuàng)建tunnelRequest,并建立與proxy的連接
- 2、創(chuàng)建一個while循環(huán),調(diào)用createTunnel方法創(chuàng)建隧道連接,該方法會返回一個Request,當(dāng)返回Request為null時,表示連接建立成功,跳出循環(huán)。
- 2.1、createTunnel方法首先生成CONNECT頭部的文本requestLine
- 2.2、生成數(shù)據(jù)讀寫的管理類HttpCodec,將requestLine寫入tunnelRequest頭部,并開始發(fā)送數(shù)據(jù)
- 2.3、當(dāng)返回的reponse滿足要求時return null,否則return tunnelRequest
tcp連接建立成功后,我們繼續(xù)看建立協(xié)議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但是需要服務(wù)器支持明文http2請求,因此不能使用https)則給相關(guān)值賦值并調(diào)用startHttp2方法建立http2連接(http2后續(xù)會專門分享,此處不展開)并return;若不是h2_prior_knowledge,則給相關(guān)值賦值后返回。
- 2、若是https請求,則調(diào)用connectTls進行tls握手,tls握手成功后判斷當(dāng)前連接是否為http2,若是則調(diào)用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握手、收發(fā)過程中的加解密等功能。并最后構(gòu)建好一個SSLSocket。
- 1、基于原始的socket構(gòu)建SSLSocket
- 2、根據(jù)用戶傳入connectionSpecSelector與生成的SSLSocket取交集獲得連接配置connectionSpec
- 3、判斷connectionSpec是否需要tls擴展,若需要則進行tls擴展
- 4、開始tls握手
- 5、TLS握手完成之后,獲取證書信息,對TLS握手過程中傳回來的證書進行驗證。
- 6、在前面選擇的ConnectionSpec支持TLS擴展參數(shù)時,獲取TLS握手過程中順便完成的協(xié)議協(xié)商過程所選擇的協(xié)議。這個過程主要用于HTTP/2的ALPN擴展。
- 7、基于之前建立的SSLSocket創(chuàng)建I/O用的source,sink。
至此連接建立分析結(jié)束。
三、HttpCodec
上面在構(gòu)建連接時出現(xiàn)了Http1Codec,Http2Codec,這里就簡單介紹下這幾個類。在okHttp中,HttpCodec是網(wǎng)絡(luò)讀寫的管理類,也可以理解為解碼器。當(dāng)tcp連接建立后,信息在連接上傳輸是以流的形式,所以服務(wù)端和客戶端都需要負(fù)責(zé)I/O操作的管理類。它有對應(yīng)的兩個子類,Http1Codec和Http2Codec,分別對應(yīng)HTTP/1.1以及HTTP/2.0協(xié)議。我們看下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() 相當(dāng)于flush,把請求刷入底層socket
finishRequest() throws IOException : 相當(dāng)于flush,把請求輸入底層socket并不在發(fā)出請求
readResponseHeaders(boolean expectContinue) //讀取響應(yīng)頭
openResponseBody(Response response) //讀取響應(yīng)體
void cancel() :取消請求
由于HTTP/2和HTTP/1.x在支持流數(shù)量以及傳輸格式上有本質(zhì)的區(qū)別,因此需要Http1Codec,Http2Codec兩個來滿足不同協(xié)議的需求,而volley之所以無法支持http2就是缺少了類似Http2Codec這樣針對HTTP/2的解碼器。關(guān)于Http1Codec具體實現(xiàn)這邊就不做展開,關(guān)于http2在okhttp中的使用及邏輯,后續(xù)我們會專門做一期分享,這邊也不展開。
