HTTP请求的整体处理过程大体可以理解为,
- 建立TCP连接。
- 如果是HTTPS的话,完成SSL/TLS的协商。
- 发送请求。
- 获取响应。
- 结束请求,关闭连接。
然而,当为系统设置了代理的时候,整个数据流都会经过代理服务器。那么代理设置究竟是如何工作的呢?它是如何影响我们上面看到的HTTP请求的处理过程的呢?是在操作系统内核的TCP实现中的策略呢,还是HTTP stack中的机制?这里我们通过OkHttp3中的实现来一探究竟。
代理分为两种类型,一种是SOCKS代理,另一种是HTTP代理。对于SOCKS代理,在HTTP的场景下,代理服务器完成TCP数据包的转发工作。而HTTP代理服务器,在转发数据之外,还会解析HTTP的请求及响应,并根据请求及响应的内容做一些处理。这里看一下OkHttp中对代理的处理。
代理服务器的描述
在Java中,通过 java.net.Proxy 类描述一个代理服务器:
只用代理的类型及代理服务器地址即可描述代理服务器的全部。对于HTTP代理,代理服务器地址可以通过域名和IP地址等方式来描述。
代理选择器ProxySelector
在Java中通过ProxySelector为一个特定的URI选择代理:
这个组件会读区系统中配置的所有代理,并根据调用者传入的URI,返回特定的代理服务器集合。由于不同系统中,配置代理服务器的方法,及相关配置的保存机制不同,该接口在不同的系统中有着不同的实现。
OkHttp3的路由
OkHttp3中抽象出Route来描述网络数据包的传输路径,最主要还是要描述直接与其建立TCP连接的目标端点。
主要通过 代理服务器的信息proxy ,及 连接的目标地址 描述路由。 连接的目标地址inetSocketAddress 根据代理类型的不同而有着不同的含义,这主要是由不同代理协议的差异而造成的。对于无需代理的情况, 连接的目标地址inetSocketAddress 中包含HTTP服务器经过了DNS域名解析的IP地址及协议端口号;对于SOCKS代理,其中包含HTTP服务器的域名及协议端口号;对于HTTP代理,其中则包含代理服务器经过域名解析的IP地址及端口号。
路由选择器RouteSelector
HTTP请求处理过程中所需的TCP连接建立过程,主要是找到一个Route,然后依据代理协议的规则与特定目标建立TCP连接。对于无代理的情况,是与HTTP服务器建立TCP连接;对于SOCKS代理及HTTP代理,是与代理服务器建立TCP连接,虽然都是与代理服务器建立TCP连接,而SOCKS代理协议与HTTP代理协议做这个动作的方式又会有一定的区别。
借助于域名解析做负载均衡已经是网络中非常常见的手法了,因而,常常会有相同域名对应不同IP地址的情况。同时相同系统也可以设置多个代理,这使Route的选择变得复杂起来。
在OkHttp中,对Route连接失败有一定的错误处理机制。OkHttp会逐个尝试找到的Route建立TCP连接,直到找到可用的那一个。这同样要求,对Route信息有良好的管理。
OkHttp3借助于 RouteSelector
类管理所有的路由信息,并帮助选择路由。 RouteSelector
主要完成3件事:
- 收集所有可用的路由。1234567891011121314151617181920212223242526272829303132333435363738394041424344public final class RouteSelector {private final Address address;private final RouteDatabase routeDatabase;/* The most recently attempted route. */private Proxy lastProxy;private InetSocketAddress lastInetSocketAddress;/* 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();private int nextInetSocketAddressIndex;/* State for negotiating failed routes */private final List<Route> postponedRoutes = new ArrayList<>();public RouteSelector(Address address, RouteDatabase routeDatabase) {this.address = address;this.routeDatabase = routeDatabase;resetNextProxy(address.url(), address.proxy());}....../** 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. If none succeed// then we'll try a direct connection below.proxies = new ArrayList<>();List<Proxy> selectedProxies = address.proxySelector().select(url.uri());if (selectedProxies != null) proxies.addAll(selectedProxies);// Finally try a direct connection. We only try it once!proxies.removeAll(Collections.singleton(Proxy.NO_PROXY));proxies.add(Proxy.NO_PROXY);}nextProxyIndex = 0;}
收集路由分为两个步骤:第一步收集所有的代理;第二步则是收集特定代理服务器选择情况下的所有 连接的目标地址 。
收集代理的过程如上面的这段代码所示,有两种方式,一是外部通过address传入了代理,此时代理集合将包含这唯一的代理。address的代理最终来源于OkHttpClient,我们可以在构造OkHttpClient时设置代理,来指定由该client执行的所有请求经过特定的代理。
另一种方式是,借助于ProxySelector获取多个代理。ProxySelector最终也来源于OkHttpClient,OkHttp的用户当然也可以对此进行配置。但通常情况下,使用系统默认的ProxySelector,来获取系统中配置的代理。
收集到的所有代理保存在列表 proxies
中。
为OkHttpClient配置Proxy或ProxySelector的场景大概是,需要让连接使用代理,但不使用系统的代理配置的情况。
收集特定代理服务器选择情况下的所有路由,因代理类型的不同而有着不同的过程:
收集一个特定代理服务器选择下的 连接的目标地址 因代理类型的不同而不同,这主要分为3种情况。 对于没有配置代理的情况,会对HTTP服务器的域名进行DNS域名解析,并为每个解析到的IP地址创建 连接的目标地址;对于SOCKS代理,直接以HTTP服务器的域名及协议端口号创建 连接的目标地址;而对于HTTP代理,则会对HTTP代理服务器的域名进行DNS域名解析,并为每个解析到的IP地址创建 连接的目标地址。
这里是OkHttp中发生DNS域名解析唯一的场合。对于使用代理的场景,没有对HTTP服务器的域名做DNS域名解析,也就意味着HTTP服务器的域名解析要由代理服务器完成。
代理服务器的收集是在创建 RouteSelector
完成的;而一个特定代理服务器选择下的 连接的目标地址 收集则是在选择Route时根据需要完成的。
RouteSelector
做的第二件事情是选择可用的路由。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960/*** Returns true if there's another route to attempt. Every address has at least one route.*/public boolean hasNext() {return hasNextInetSocketAddress()|| hasNextProxy()|| hasNextPostponed();}public Route next() throws IOException {// Compute the next route to attempt.if (!hasNextInetSocketAddress()) {if (!hasNextProxy()) {if (!hasNextPostponed()) {throw new NoSuchElementException();}return nextPostponed();}lastProxy = nextProxy();}lastInetSocketAddress = nextInetSocketAddress();Route route = new Route(address, lastProxy, lastInetSocketAddress);if (routeDatabase.shouldPostpone(route)) {postponedRoutes.add(route);// We will only recurse in order to skip previously failed routes. They will be tried last.return next();}return route;}/** Returns true if there's another proxy to try. */private boolean hasNextProxy() {return nextProxyIndex < proxies.size();}/** Returns true if there's another socket address to try. */private boolean hasNextInetSocketAddress() {return nextInetSocketAddressIndex < inetSocketAddresses.size();}/** Returns the next socket address to try. */private InetSocketAddress nextInetSocketAddress() throws IOException {if (!hasNextInetSocketAddress()) {throw new SocketException("No route to " + address.url().host()+ "; exhausted inet socket addresses: " + inetSocketAddresses);}return inetSocketAddresses.get(nextInetSocketAddressIndex++);}/** Returns true if there is another postponed route to try. */private boolean hasNextPostponed() {return !postponedRoutes.isEmpty();}/** Returns the next postponed route to try. */private Route nextPostponed() {return postponedRoutes.remove(0);}RouteSelector
实现了两级迭代器来提供选择路由的服务。- 维护接失败的路由的信息,以避免浪费时间去连接一些不可用的路由。
RouteSelector
借助于RouteDatabase
维护失败的路由的信息。12345678910111213/*** 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);}
RouteDatabase
是一个简单的容器:
代理选择器ProxySelector的实现
在OkHttp3中,ProxySelector
对象由OkHttpClient维护。
|
|
在创建OkHttpClient时,可以通过为OkHttpClient.Builder设置ProxySelector
来定制ProxySelector
。若没有指定,则使用系统默认的ProxySelector
。OpenJDK 1.8版默认的ProxySelector
为sun.net.spi.DefaultProxySelector
:
在Android平台上,默认ProxySelector
所用的则是另外的实现:
Android平台下,默认的ProxySelector
ProxySelectorImpl,其实现 (不同Android版本实现不同,这里以android-6.0.1_r61为例) 如下:
在Android平台上,主要是从系统属性System properties中获取代理服务器的配置信息,这里会过滤掉不能进行代理的主机的访问。
前面我们看到 RouteSelector
通过 Address
提供的Proxy和ProxySelector来收集Proxy信息及连接的目标地址信息。OkHttp3中用 Address
描述建立连接所需的配置信息,包括HTTP服务器的地址,DNS,SocketFactory,Proxy,ProxySelector及TLS所需的一些设施等等:
OkHttp3中通过职责链执行HTTP请求。在其中的RetryAndFollowUpInterceptor里创建Address对象时,从OkHttpClient对象获取ProxySelector。Address对象会被用于创建StreamAllocation对象。StreamAllocation在建立连接时,从Address对象中获取ProxySelector以选择路由。
在StreamAllocation中,Address对象会被用于创建 RouteSelector
对象:
代理协议
如我们在 OkHttp3 HTTP请求执行流程分析 中看到的,OkHttp3对HTTP请求是通过Interceptor链来处理的。RetryAndFollowUpInterceptor
创建StreamAllocation
对象,处理http的重定向及出错重试。对后续Interceptor的执行的影响为修改Request并创建StreamAllocation对象。BridgeInterceptor
补全缺失的一些http header。对后续Interceptor的执行的影响主要为修改了Request。CacheInterceptor
处理http缓存。对后续Interceptor的执行的影响为,若缓存中有所需请求的响应,则后续Interceptor不再执行。ConnectInterceptor
借助于前面分配的StreamAllocation
对象建立与服务器之间的连接,并选定交互所用的协议是HTTP 1.1还是HTTP 2。对后续Interceptor的执行的影响为,创建了HttpStream和connection。CallServerInterceptor
作为Interceptor链中的最后一个Interceptor,用于处理IO,与服务器进行数据交换。
在OkHttp3中,收集的路由信息,是在ConnectInterceptor
中建立连接时用到的。ConnectInterceptor
借助于 StreamAllocation
完成整个连接的建立,包括TCP连接建立,代理协议所要求的协商,以及SSL/TLS协议的协商,如ALPN等。我们暂时略过整个连接建立的完整过程,主要关注TCP连接建立及代理协议的协商过程的部分。
StreamAllocation
的findConnection()用来为某次特定的网络请求寻找一个可用的连接。
OkHttp3中有一套连接池的机制,这里先尝试从连接池中寻找可用的连接,找不到时才会新建连接。新建连接的过程是:
- 选择一个Route;
- 创建
RealConnection
连接对象。 - 将连接对象保存进连接池中。
- 建立连接。
RealConnection
中建立连接的过程是这样的:
在这个方法中,SSLSocketFactory为空,也就是要求请求/响应明文传输时,先做安全性检查,以确认系统允许明文传输,允许以请求的域名做明文传输。
然后根据路由的具体情况,执行不同的连接建立过程。对于需要创建隧道连接的路由,执行buildTunneledConnection(),对于其它情况,则执行buildConnection()。
判断是否要建立隧道连接的依据是代理的类型,以及连接的类型:
如果是HTTP代理,且请求建立SSL/TLS加密通道 (http/1.1的https和http2) ,则需要建立隧道连接。其它情形不需要建立隧道连接。
非隧道连接的建立
非隧道连接的建立过程为:
有 3 种情况需要建立非隧道连接:
- 无代理。
- 明文的HTTP代理。
- SOCKS代理。
非隧道连接的建立过程为建立TCP连接,然后在需要时完成SSL/TLS的握手及HTTP/2的握手建立Protocol。建立TCP连接的过程为:
- 创建Socket。非SOCKS代理的情况下,通过SocketFactory创建;在SOCKS代理则传入proxy手动new一个出来。
- 为Socket设置读超时。
- 完成特定于平台的连接建立。
- 创建用语IO的source和sink。
AndroidPlatform
的 connectSocket()
是这样的:
设置了SOCKS代理的情况下,仅有的特别之处在于,是通过传入proxy手动创建的Socket。route的socketAddress包含着目标HTTP服务器的域名。由此可见SOCKS协议的处理,主要是在Java标准库的 java.net.Socket
中处理的。对于外界而言,就好像是于HTTP服务器直接建立连接一样,因为连接时传入的地址都是HTTP服务器的域名。
而对于明文HTTP代理的情况下,这里没有任何特殊的处理。route的socketAddress包含着代理服务器的IP地址。HTTP代理自身会根据请求及响应的实际内容,建立与HTTP服务器的TCP连接,并转发数据。猜测HTTP代理服务器是根据HTTP请求中的”Host”等header内容来确认HTTP服务器地址的。
暂时先略过对建立协议过程的分析。
HTTP代理的隧道连接
buildTunneledConnection()用于建立隧道连接:
这里主要是两个过程:
- 建立隧道连接。
- 建立Protocol。
建立隧道连接的过程又分为几个步骤:
- 创建隧道请求
- 建立Socket连接
- 发送请求建立隧道
隧道请求是一个常规的HTTP请求,只是请求的内容有点特殊。最初创建的隧道请求如:
一个隧道请求的例子如下:
请求的”Host” header中包含了目标HTTP服务器的域名。建立socket连接的过程这里不再赘述。
创建隧道的过程是这样子的:
在前面创建的TCP连接之上,完成与代理服务器的HTTP请求/响应交互。请求的内容类似下面这样:
这里可能会根据HTTP代理是否需要认证而有多次HTTP请求/响应交互。
总结一下OkHttp3中代理相关的处理:
- 没有设置代理的情况下,直接与HTTP服务器建立TCP连接,然后进行HTTP请求/响应的交互。
- 设置了SOCKS代理的情况下,创建Socket时,为其传入proxy,连接时还是以HTTP服务器为目标地址。在标准库的Socket中完成SOCKS协议相关的处理。此时基本上感知不到代理的存在。
- 设置了HTTP代理时的HTTP请求,与HTTP代理服务器建立TCP连接。HTTP代理服务器解析HTTP请求/响应的内容,并根据其中的信息来完成数据的转发。也就是说,如果HTTP请求中不包含”Host” header,则有可能在设置了HTTP代理的情况下无法与HTTP服务器建立连接。
- 设置了HTTP代理时的HTTPS/HTTP2请求,与HTTP服务器建立通过HTTP代理的隧道连接。HTTP代理不再解析传输的数据,仅仅完成数据转发的功能。此时HTTP代理的功能退化为如同SOCKS代理类似。
- 设置了代理时,HTTP服务器的域名解析会被交给代理服务器执行。其中设置了HTTP代理时,会对HTTP代理的域名做域名解析。
关于HTTP代理的更多内容,可以参考HTTP 代理原理及实现(一)。
OkHttp3中代理相关的处理大体如此。