如何自定义HttpClient的DNS解析过程
一、DNS解析过程
我们知道常见的DNS解析流程大致如下图所示,当我们尝试获取某个网站的内容时,需要先通过DNS服务获取域名对应的ip,在没有DNS缓存的情况下,以www.example.com为例,解析过程大致如下:
- 请求HTTP根服务器,获取存放
.com域名解析的DNS服务器地址 - 请求
.com解析服务器,获取存放www.example.com的DNS服务器地址 - 最后请求
www.example.com的DNS服务器,获取其服务器所在的ip地址
二、Java中的DNS解析
JDK中提供了一种DNS解析的接口:InetAddress,我们可以通过InetAddress来获取某个域名的解析结果,其基本使用方式如下所示
InetAddress[] allByName = InetAddress.getAllByName("cloudflare.com");
//输出结果如下
104.16.133.229
104.16.132.229
2606:4700:0:0:0:0:6810:84e5
2606:4700:0:0:0:0:6810:85e5
在返回结果中会包含这个域名所有的dns解析,需要注意的是,如果解析结果不存在,那么这个方法会抛出UnknownHostException异常。
三、HttpClient如何进行DNS
Apache HttpClient通过DnsResolver接口来进行DNS解析,Apache HttpClient提供了两个实现:
- SystemDefaultDnsResolver:这是默认的选择,它会直接使用InetAddress获取解析结果并返回
- InMemoryDnsResolver:这是一个完全基于内存的DNS解析,需要手动添加dns解析,如果获取结果为空,会产生报错
HttpClient的链接创建过程如下图所示:
这部分代码位于
org.apache.http.impl.conn.DefaultHttpClientConnectionOperator#connect中,如下所示
public void connect(
final ManagedHttpClientConnection conn,
final HttpHost host,
final InetSocketAddress localAddress,
final int connectTimeout,
final SocketConfig socketConfig,
final HttpContext context) throws IOException {
final Lookup<ConnectionSocketFactory> registry = getSocketFactoryRegistry(context);
final ConnectionSocketFactory sf = registry.lookup(host.getSchemeName());
if (sf == null) {
throw new UnsupportedSchemeException(host.getSchemeName() +
" protocol is not supported");
}
final InetAddress[] addresses = host.getAddress() != null ?
new InetAddress[] { host.getAddress() } : this.dnsResolver.resolve(host.getHostName());
final int port = this.schemePortResolver.resolve(host);
for (int i = 0; i < addresses.length; i++) {
final InetAddress address = addresses[i];
final boolean last = i == addresses.length - 1;
Socket sock = sf.createSocket(context);
sock.setSoTimeout(socketConfig.getSoTimeout());
sock.setReuseAddress(socketConfig.isSoReuseAddress());
sock.setTcpNoDelay(socketConfig.isTcpNoDelay());
sock.setKeepAlive(socketConfig.isSoKeepAlive());
if (socketConfig.getRcvBufSize() > 0) {
sock.setReceiveBufferSize(socketConfig.getRcvBufSize());
}
if (socketConfig.getSndBufSize() > 0) {
sock.setSendBufferSize(socketConfig.getSndBufSize());
}
final int linger = socketConfig.getSoLinger();
if (linger >= 0) {
sock.setSoLinger(true, linger);
}
conn.bind(sock);
final InetSocketAddress remoteAddress = new InetSocketAddress(address, port);
if (this.log.isDebugEnabled()) {
this.log.debug("Connecting to " + remoteAddress);
}
try {
sock = sf.connectSocket(
connectTimeout, sock, host, remoteAddress, localAddress, context);
conn.bind(sock);
if (this.log.isDebugEnabled()) {
this.log.debug("Connection established " + conn);
}
return;
} catch (final SocketTimeoutException ex) {
if (last) {
throw new ConnectTimeoutException(ex, host, addresses);
}
} catch (final ConnectException ex) {
if (last) {
final String msg = ex.getMessage();
throw "Connection timed out".equals(msg)
? new ConnectTimeoutException(ex, host, addresses)
: new HttpHostConnectException(ex, host, addresses);
}
} catch (final NoRouteToHostException ex) {
if (last) {
throw ex;
}
}
if (this.log.isDebugEnabled()) {
this.log.debug("Connect to " + remoteAddress + " timed out. " +
"Connection will be retried using another IP address");
}
}
}
可以看到,HttpClient的Dns解析操作是通过DnsResolver来进行的,在不做配置的情况下,DnsResolver默认会使用SystemDefaultDnsResolver也就是InetAdress来做dns解析。
四、如何自定义HttpClient的DNS
在创建HttpClient时,我们可以选择自定义一个DNSResolver来进行一些定制化的需求,例如我们我们需要将含有特定规则的域名解析到特定ip上面,那么就可以通过在创建HttpClient的时候指定DnsResolver来执行。
指定DnsResolver的代码如下所示:
CloseableHttpClient client = HttpClientBuilder.create().setDnsResolver(dnsResolver).build();
DnsResolver是一个接口,我们可以通过实现这个接口的方式来制定自己的dns规则。
public interface DnsResolver {
InetAddress[] resolve(String host) throws UnknownHostException;
}
五、什么是HttpDns
HttpDns则是在自定义解析方式的基础上更进一步,HttpDns即通过一个HTTP请求向某个特定的服务器获取DNS的解析结果,不再使用传统的DNS解析方式,它有几个优势:
- 完全定制化的服务,扩展性更强
- 解析结果不易被劫持,减少来自运营商的干扰
- 实时性更强,调度生效会更快
我们先来看下HttpDns的的请求过程
可以看到,对比普通Dns,HttpDns本身的步骤要更少,天然的就会比普通的Dns要更快一些,同时HttpDns由于绕过了运营商,不会使用运营商的Dns服务器,也就避免了由于运营商错误缓存或者不当操作导致的Dns异常的情况的发生,又或者错误的Dns服务器导致的Dns劫持。
更重要的是HttpDns如果只使用了一家的服务,那么就不会有不同地区TTL不一致甚至最基础的实现都不一致的情况,同时通过HTTP请求下发的Dns解析实时性会更强一些。
HttpClient如何集成HttpDns
与自定义Dns的过程一样,我们只需要重新实现DnsResolver接口,然后将HttpDns的过程加入到其中就可以了