一、前言
初看OkHttp源碼,由于對Address、Route、Proxy、ProxySelector、RouteSelector等理解不夠,讀源碼非常吃力,看了幾遍依然對于尋找復用連接、創建連接、連接服務器、連接代理服務器、創建隧道連接等邏輯似懂非懂,本篇決定梳理一遍相關的概念及基本原理。
二、基礎知識
(1)HTTP網絡協議:
● HTTP/1.1(HTTPS)
● HTTP/2
● SPDY
(2)HTTP請求流程
一個http請求的流程(直連):
1、輸入url及參數;
2、如果是url是域名則解析ip地址,可能對應多個ip,如果沒有指定端口,則用默認端口,http請求用80;
3、創建socket,根據ip和端口連接服務器(socket內部會完成3次TCP握手);
4、socket成功連接后,發送http報文數據。
一個https請求的流程(直連):
1、輸入url及參數;
2、如果是url是域名則解析ip地址,可能對應多個ip,如果沒有指定端口,則用默認端口,https請求用443;
3、創建socket,根據ip和端口連接服務器(socket內部會完成3次TCP握手);
4、socket成功連接后進行TLS握手,可通過java標準款提供的SSLSocket完成;
5、握手成功后,發送https報文數據。
(3)代理相關說明
1、分類
● HTTP代理:普通代理、隧道代理
● SOCKS代理:SOCKS4、SOCKS5
2、HTTP代理分類及說明
普通代理
HTTP/1.1 協議的第一部分。其代理過程為:
● client 請求 proxy
● proxy 解析請求獲取 origin server 地址
● proxy 向 origin server 轉發請求
● proxy 接收 origin server 的響應
● proxy 向 client 轉發響應
其中proxy獲取目的服務器地址的標準方法是解析 request line 里的 request-URL。因為proxy需要解析報文,因此普通代理無法適用于https,因為報文都是加密的。
隧道代理
通過 Web 代理服務器用隧道方式傳輸基于 TCP 的協議。
請求包括兩個階段,一是連接(隧道)建立階段,二是數據通信(請求響應)階段,數據通信是基于 TCP packet ,代理服務器不會對請求及響應的報文作任何的處理,都是原封不動的轉發,因此可以代理 HTTPS請求和響應。
代理過程為:
● client 向 proxy 發送 CONNET 請求(包含了 origin server 的地址)
● proxy 與 origin server 建立 TCP 連接
● proxy 向 client 發送響應
● client 向 proxy 發送請求,proxy 原封不動向 origin server 轉發請求,請求數據不做任何封裝,為原生 TCP packet.
3、SOCKS代理分類及說明
● SOCKS4:只支持TCP協議(即傳輸控制協議)
● SOCKS5: 既支持TCP協議又支持UDP協議(即用戶數據包協議),還支持各種身份驗證機制、服務器端域名解析等。
SOCK4能做到的SOCKS5都可得到,但反過來卻不行,比如我們常用的聊天工具QQ在使用代理時就要求用SOCKS5代理,因為它需要使用UDP協議來傳輸數據。
三、OkHttp路由
有了上面的基礎知識,下面分析結合源碼分析OkHttp路由相關的邏輯。OkHttp用Address來描述與目標服務器建立連接的配置信息,但請求輸入的可能是域名,一個域名可能對于多個ip,真正建立連接是其中一個ip,另外,如果設置了代理,客戶端是與代理服務器建立直接連接,而不是目標服務器,代理又可能是域名,可能對應多個ip。因此,這里用Route來描述最終選擇的路由,即客戶端與哪個ip建立連接,是代理還是直連。下面對比下Address及Route的屬性,及路由選擇器RouteSelector。
(1)Address解析
描述與目標服務器建立連接所需要的配置信息,包括目標主機名、端口、dns,SocketFactory,如果是https請求,包括TLS相關的SSLSocketFactory 、HostnameVerifier 、CertificatePinner,代理服務器信息Proxy 、ProxySelector 。
public final class Address {
final HttpUrl url; //與目標服務器連接的配置,包括協議、主機名、端口等
final Dns dns; //dns解析器
final SocketFactory socketFactory; //原始Socket創建工廠
final Authenticator proxyAuthenticator;
final List<Protocol> protocols;
final List<ConnectionSpec> connectionSpecs;
final ProxySelector proxySelector; //代理選擇器,默認用系統的
final @Nullable Proxy proxy;//設置的代理,可為空
final @Nullable SSLSocketFactory sslSocketFactory; //SSLSocket創建工廠
final @Nullable HostnameVerifier hostnameVerifier; //用于https證書檢驗
final @Nullable CertificatePinner certificatePinner;//用于https證書檢驗
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) {
}
(2)Route解析
Route提供了真正連接服務器所需要的動態信息,明確需要連接的服務器IP地址及代理服務器,一個Address可能會有很多個路由Route供選擇(一個DNS對應對個IP)。
public final class 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;
}
(3)RouteSelector解析
Address和Route都是數據對象,沒有提供操作方法,OkHttp另外定義了RouteSelector來完成選擇的路由的操作。
public final class RouteSelector {
private final Address address;
//用于記錄連接失敗的路由的黑名單,如果在黑名單,不用嘗試
private final RouteDatabase routeDatabase;
private final Call call;
private final EventListener eventListener;
private List<Proxy> proxies = Collections.emptyList(); //代理列表
private int nextProxyIndex;//下一個嘗試的代理地址
private List<InetSocketAddress> inetSocketAddresses = Collections.emptyList(); //sockect地址列表
private final List<Route> postponedRoutes = new ArrayList<>();//失敗的路由列表
public RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
EventListener eventListener) {
...
resetNextProxy(address.url(), address.proxy());
}
/*
* 獲取下個需要嘗試的Route集
*/
public Selection next() throws IOException {}
/*
* 記錄失敗路由
*/
public void connectFailed(Route failedRoute, IOException failure) {}
/*
* 讀取代理服務器信息
*/
private void resetNextProxy(HttpUrl url, Proxy proxy) {}
/*
* 準備需要嘗試的socket地址(目標服務器或者代理服務器)
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {}
}
1、讀取代理配置信息:resetNextProxy()
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;
}
}
讀取代理配置:
● 如果有指定代理(不讀取系統配置,在OkHttpClient實例中指定),則只用1個該指定代理;
● 如果沒有指定,則讀取系統配置的,可能有多個。
2、獲取需要嘗試的socket地址(目標服務器或者代理服務器):resetNextInetSocketAddress()
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));
}
}
}
結合Address的host和代理,解析要嘗試的套接字地址(ip+端口)列表:
● 直連或者SOCK代理, 則用目標服務器的主機名和端口,如果是HTTP代理,則用代理服務器的主機名和端口;
● 如果是SOCK代理,根據目標服務器主機名和端口號創建未解析的套接字地址,列表只有1個地址;
● 如果是直連或HTTP代理,先DNS解析,得到InetAddress列表(沒有端口),再創建InetSocketAddress列表(帶上端口),InetSocketAddress與InetAddress的區別是前者帶端口信息。
3、獲取路由列表:next()
/**
* 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();
}
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 true if there's another proxy to try. */
private boolean hasNextProxy() {
return nextProxyIndex < proxies.size();
}
/** 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;
}
選擇路由的流程解析:
● 遍歷每個代理對象,可能多個,直連的代理對象為Proxy.DIRECT(實際是沒有中間代理的);
● 對每個代理獲取套接字地址列表;
● 遍歷地址列表,創建Route,判斷Route如果在路由黑名單中,則添加到失敗路由列表,不在黑名單中則添加到待返回的Route列表;
● 如果最后待返回的Route列表為空,即可能所有路由都在黑名單中,實在沒有新路由了,則將失敗的路由集合返回;
● 傳入Route列表創建Selection對象,對象比較簡單,就是一個目標路由集合,及讀取方法。
(4)RouteDatabase 解析
為了避免不必要的嘗試,OkHttp會把連接失敗的路由加入到黑名單中,由RouteDatabase管理,該類比較簡單,就是一個失敗路由集合。
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);
}
}
(5)選擇路由的流程分析
1、創建Address
Address的創建在RetryAndFollowUpInteceptor里,每次請求會聲明一個新的Address及StreamAllocation對象,而StreamAllocation使用Address創建RouteSelector對象,在連接時RouteSelector確定請求的路由。
public final class RetryAndFollowUpInterceptor implements Interceptor {
@Override public Response intercept(Chain chain) throws IOException {
...
//原始請求
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
...
while (true) {
...
//原始請求返回
response = realChain.proceed(request, streamAllocation, null, null);
...
//創建重定向請求
Request followUp = followUpRequest(response, streamAllocation.route());
...
//重定向StreamAllocation
streamAllocation = new StreamAllocation(client.connectionPool(),createAddress(followUp.url()), call,
eventListener, callStackTrace);
}
}
private Address createAddress(HttpUrl url) {
SSLSocketFactory sslSocketFactory = null;
HostnameVerifier hostnameVerifier = null;
CertificatePinner certificatePinner = null;
if (url.isHttps()) {
sslSocketFactory = client.sslSocketFactory();
hostnameVerifier = client.hostnameVerifier();
certificatePinner = client.certificatePinner();
}
return new Address(url.host(), url.port(), client.dns(), client.socketFactory(),
sslSocketFactory, hostnameVerifier, certificatePinner, client.proxyAuthenticator(),
client.proxy(), client.protocols(), client.connectionSpecs(), client.proxySelector());
}
每個Requst都會構造一個Address對象,構造好了Address對象只是有了與服務器連接的配置信息,但沒有確定最終服務器的ip,也沒有確定連接的路由。
2、創建RouteSelector
在StreamAllocation聲明的同時會聲明路由選擇器RouteSelector,為一次請求尋找路由。
public final class StreamAllocation {
public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
EventListener eventListener, Object callStackTrace) {
...
//每個StreamAllocation會聲明一個RouteSelector
this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);
}
}
3、選擇可用的路由Route
public final class StreamAllocation {
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
...
//根據address判斷ConnectionPool中是否有可重用的連接
Internal.instance.get(connectionPool, address, this, null);
...
//ConnectionPool沒有可用的,根據Address和proxy獲取路由集合
if (selectedRoute == null && (routeSelection == null || !routeSelection.hasNext())) {
newRouteSelection = true;
routeSelection = routeSelector.next();
}
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
if (newRouteSelection) {
// Now that we have a set of IP addresses, make another attempt at getting a connection from
// the pool. This could match due to connection coalescing.
//遍歷每個路由(IP地址),根據Route判斷線程池是否有符合重用的連接
List<Route> routes = routeSelection.getAll();
for (int i = 0, size = routes.size(); i < size; i++) {
Route route = routes.get(i);
Internal.instance.get(connectionPool, address, this, route);
if (connection != null) {
foundPooledConnection = true;
result = connection;
this.route = route;
break;
}
}
}
//線程池沒有匹配的,從集合中選擇一個路由
if (!foundPooledConnection) {
if (selectedRoute == null) {
selectedRoute = routeSelection.next();
}
// Create a connection and assign it to this allocation immediately. This makes it possible
// for an asynchronous cancel() to interrupt the handshake we're about to do.
// 根據剛選擇的路由創建新連接RealConnection,并分配給當前流
route = selectedRoute;
refusedStreamCount = 0;
result = new RealConnection(connectionPool, selectedRoute);
acquire(result, false);
}
}
}
}
四、實例分析
下面在測試過程跟蹤實例對象來理解,分別測試直連和HTTP代理HTTP2請求路由的選擇過程:
● 直連請求流程
● HTTP代理HTTPS流程
請求url:http://www.lxweimin.com/p/63ba15d8877a
(1)直連請求
1、構造address對象
scheme:https
host:www.lxweimin.com
端口:默認443
proxy:null
2、讀取代理配置:resetNextProxy
沒有設置代理服務器,返回的代理對象為DIRECT。
3、解析目標服務器套接字地址:resetNextInetSocketAddress
域名:www.lxweimin.com
IP地址:8個
端口:默認443
4、選擇Route創建RealConnection
選中了路由列表中第一個,IP為183.240.216.228
5、確定協議
通過SSLSockect完成ssl握手后,判斷協議版本,獲得的協議是h2,可以看出簡書用了HTTP/2.0.
(2)HTTP代理
測試方法:
● 在PC端打開Charles,設置端口,如何設置代理,網上有教程,比較簡單;
● 手機打開WIFI,選擇連接的WIFI修改網絡,在高級選項中設置中指定了代理服務器,ip為PC的ip,端口是Charles剛設置的端口;
● OkHttpClient不指定代理,發起請求。
1、構造address對象
address對象與直連一樣。
scheme:https
host:www.lxweimin.com
端口:默認443
proxy:null
2、讀取代理配置:resetNextProxy
可以看出通過默認的代理選擇器讀取到一個代理地址。
3、解析目標服務器套接字地址:resetNextInetSocketAddress
可以看到目標地址就是代理服務器地址,只有1個,可以HTTP代理時客戶端與代理服務器建立socket連接。
4、選擇Route創建RealConnection
創建與代理服務器的連接對象。
5、創建隧道
由于是代理https請求,需要用到隧道代理。
從圖可以看出,建立隧道其實是發送CONNECT請求,header包括字段Proxy-Connection,目標主機名,請求內容類似:
CONNECT www.lxweimin.com:443 HTTP/1.1
6、確定協議,SSL握手
隧道代理,客戶端需要與代理服務器建立安全連接。
五、總結
1、代理可分為HTTP代理和SOCK代理;
2、HTTP代理又分為普通代理和隧道代理;普通代理適合明文傳輸,即http請求;隧道代理僅轉發TCP包,適合加密傳輸,即https/http2;
3、SOCK代理又分為SOCK4和SOCK5,區別是后者支持UDP傳輸,適合代理聊天工具如QQ;
4、沒有設置代理(OkHttpClient沒有指定同時系統也沒有設置),客戶端直接與目標服務器建立TCP連接;
5、設置了代理,代理http請求時,客戶端與代理服務器建立TCP連接,如果代理服務器是域名,則解釋代理服務器域名,而目標服務器的域名由代理服務器解析;
6、設置了代理,代理https/http2請求時,客戶端與代理服務器建立TCP連接,發送CONNECT請求與代理服務器建立隧道,并進行SSL握手,代理服務器不解析數據,僅轉發TCP數據包。