前言
之前我们分析了okhttp的重试机制,发现在获取可用地址的时候,都需要遍历一个路由选择器,里面保存了可用的地址,那么这些地址是从哪来的呢?这就是本篇分析的重点。
首先我们简单理解一下代理和DNS的概念:
代理:通过另一台服务器或ip,帮助我们进行网络请求的转发,例如创建的抓包工具。
DNS:万维网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。常用的有阿里云的DNS服务。
从他们的概念之间,我们可以知道,使用代理的话网速是变慢的风险,而DNS不仅不会增加网络请求的成本,还会节省访问网络的时间,所以DNS服务已经十分普遍。
(突然回忆起刚上班时,公司内网封了QQ地址,每隔一段时间就需要更换代理的日子……)
正文
首先看看怎么设置代理和DNS:
OkHttpClient okHttpClient = new OkHttpClient.Builder()
// 多个代理
.proxySelector(new ProxySelector() {
@Override
public List select(URI uri) {
return null;
}
@Override
public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
}
})
// 单独的代理
.proxy(new Proxy(Proxy.Type.HTTP, new InetSocketAddress("www.baidu.com", 8888)))
// dns
.dns(new Dns() {
@Override
public List lookup(String hostname) {
return null;
}
})
.build();
代理和DNS信息都被保存到OkhttpClient对象中:
proxySelector可以为一个URI设置多个代理,如果地址连接失败还回调connectFailed;
proxy设置单独的全局代理,他的优先级高于proxySelecttor;
dns用法和proxySelecttor类似,可以返回多个地址。
接下来我们看看okhttp到底是怎么使用代理和DNS的,回忆之前的分析,我们发现处理网络连接,释放等操作都是在StreamAllocation中:
StreamAllocation streamAllocation = new StreamAllocation(client.connectionPool(),
createAddress(request.url()), call, eventListener, callStackTrace);
其中把代理和dns信息设置到网络请求中是在createAddress()方法中:
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());
}
在Address的构造方法中,我们看到了熟悉的DNS,ProxySelector和Proxy,Address只是封装了所有的可以访问的地址信息,功能还是在StreamAllocation中,之前我们看到了findConnection方法是负责找到可用的连接,现在我们开始一步步的分析他的代码:
private RealConnection findConnection(int connectTimeout, int readTimeout, int writeTimeout,
int pingIntervalMillis, boolean connectionRetryEnabled) throws IOException {
......
// Attempt to get a connection from the pool.
// 从访问池中找到可以访问的地址
// 请注意,这里的router参数是null,对之后的connection.isEligible判断由直接影响
Internal.instance.get(connectionPool, address, this, null);
// 如果找到了可用的地址
if (connection != null) {
foundPooledConnection = true;
result = connection;
}
// 否则使用路由
else {
selectedRoute = route;
}
}
}
// 关闭之前的socket
closeQuietly(toClose);
synchronized (connectionPool) {
if (canceled) throw new IOException("Canceled");
// 遍历路由
if (newRouteSelection) {
// 这是第二次调用Internal.instance.get方法
// 请注意,这里的参数router不为空
List 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;
}
}
}
//如果没有路由可用,开始建立连接
// 会把通过url解析到的host等信息保存起来,方便下次复用
result.connect(connectTimeout, readTimeout, writeTimeout, pingIntervalMillis,
connectionRetryEnabled, call, eventListener);
routeDatabase().connected(result.route());
...
return result;
}
建立连接的过程主要分为以下三部:
1、首先判断Connection的router是否正好是我们需要的路由,是则复用;
2、如果不是,再从设置的代理和dns中寻找可用的地址
(这两个判断都是在RealConnection.isEligible方法中实现的)
3、都不可用,尝试建立连接,并把解析的host等信息保存到Connection中,方便下次复用。
这就是主要的三个过程,接下来我们找几个重点看一下源码,首先是isEligible方法,当参数router等于null或者不等于null,都会进行哪些判断呢?
public boolean isEligible(Address address, @Nullable Route route) {
if (allocations.size() >= allocationLimit || noNewStreams) return false;
// 判断当前的路由是否可用
if (!Internal.instance.equalsNonHost(this.route.address(), address)) return false;
if (address.url().host().equals(this.route().address().url().host())) {
return true; // This connection is a perfect match.
}
if (http2Connection == null) return false;
// 当router参数为空的,执行到这里就结束了
if (route == null) return false;
// 当router参数不为空的的时候,下次是针对router的判断
if (route.proxy().type() != Proxy.Type.DIRECT) return false;
if (this.route.proxy().type() != Proxy.Type.DIRECT) return false;
if (!this.route.socketAddress().equals(route.socketAddress())) return false;
// 3. This connection's server certificate's must cover the new host.
if (route.address().hostnameVerifier() != OkHostnameVerifier.INSTANCE) return false;
if (!supportsUrl(address.url())) return false;
// 4. Certificate pinning must match the host.
try {
address.certificatePinner().check(address.url().host(), handshake().peerCertificates());
} catch (SSLPeerUnverifiedException e) {
return false;
}
return true; // The caller's address can be carried by this connection.
}
那么我们设置的代理和DNS是在哪里被添加到路由里的呢?
public StreamAllocation(ConnectionPool connectionPool, Address address, Call call,
EventListener eventListener, Object callStackTrace) {
......
this.routeSelector = new RouteSelector(address, routeDatabase(), call, eventListener);
......
}
public RouteSelector(Address address, RouteDatabase routeDatabase, Call call,
EventListener eventListener) {
......
// 准备访问代理
resetNextProxy(address.url(), address.proxy());
}
private void resetNextProxy(HttpUrl url, Proxy proxy) {
// 如果指定了一个代理,那代理保存到proxies中
if (proxy != null) {
// If the user specifies a proxy, try that and only that.
proxies = Collections.singletonList(proxy);
} else {
// 从Address中选择这个url的代理
// Try each of the ProxySelector choices until one connection succeeds.
List proxiesOrNull = address.proxySelector().select(url.uri());
proxies = proxiesOrNull != null && !proxiesOrNull.isEmpty()
? Util.immutableList(proxiesOrNull)
: Util.immutableList(Proxy.NO_PROXY);
}
// 代理的索引值变为第一个
nextProxyIndex = 0;
}
真正把dns放到路由里,是在resetNextInetSocketAddress方法中
private void resetNextInetSocketAddress(Proxy proxy) throws IOException {
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;
// 使用代理的host和端口
socketHost = getHostString(proxySocketAddress);
socketPort = proxySocketAddress.getPort();
}
// 判断端口号是否合法
if (socketPort < 1 || socketPort > 65535) {
throw new SocketException("No route to " + socketHost + ":" + socketPort
+ "; port is out of range");
}
// 这里是关键,如果代理的类型是Socks,不适用DNS
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.
// dns解析代理地址
List 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));
}
}
}
首先通过上一个代理,解析出他的host和端口,如果代理是SOCKS类型,不使用DNS,如果不是SOCKS类型,就去获得这个代理的DNS地址列表,继续尝试连接。
那如果我不设置任何的代理信息,DNS还会执行吗?会的,因为okhttp默认设置的DefaultProxySelector,我们仍然可以在连接失败后,尝试访问url的DNS域名。
总结
最后我们来总结一下:
- Proxy和ProxySelector不可同时使用,同时存在优先使用Proxy。
- 如果代理的类型是SOCKS,那么他的DNS不会被使用。
- 尝试建立的过程分为三步:尝试复用 -> 使用代理和dns -> 建立连接,保存host等待复用。
- 通过代理完成网络操作,不会保持连接,因为我们无法通过代理得到访问的真正地址。
到这里okhttp源码解析系列暂时告一段落了,我们整体的分析了okhttp的工作过程,然后分别重点分析okhttp的网络读写,缓存机制,重试机制,代理和DNS,之后继续使用okhttp会更加得心应手。
如果之后还有新发现再继续补充,希望这一系列对大家对okhttp的理解有所帮助。