如何自定义HttpClient的DNS的解析过程

1,948 阅读4分钟

如何自定义HttpClient的DNS解析过程

一、DNS解析过程

我们知道常见的DNS解析流程大致如下图所示,当我们尝试获取某个网站的内容时,需要先通过DNS服务获取域名对应的ip,在没有DNS缓存的情况下,以www.example.com为例,解析过程大致如下:

  1. 请求HTTP根服务器,获取存放.com域名解析的DNS服务器地址
  2. 请求.com解析服务器,获取存放www.example.com的DNS服务器地址
  3. 最后请求www.example.com的DNS服务器,获取其服务器所在的ip地址

image-20230423203724744

二、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提供了两个实现:

  1. SystemDefaultDnsResolver:这是默认的选择,它会直接使用InetAddress获取解析结果并返回
  2. InMemoryDnsResolver:这是一个完全基于内存的DNS解析,需要手动添加dns解析,如果获取结果为空,会产生报错

HttpClient的链接创建过程如下图所示:

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解析方式,它有几个优势:

  1. 完全定制化的服务,扩展性更强
  2. 解析结果不易被劫持,减少来自运营商的干扰
  3. 实时性更强,调度生效会更快

我们先来看下HttpDns的的请求过程

HttpDns的解析流程

可以看到,对比普通Dns,HttpDns本身的步骤要更少,天然的就会比普通的Dns要更快一些,同时HttpDns由于绕过了运营商,不会使用运营商的Dns服务器,也就避免了由于运营商错误缓存或者不当操作导致的Dns异常的情况的发生,又或者错误的Dns服务器导致的Dns劫持。

更重要的是HttpDns如果只使用了一家的服务,那么就不会有不同地区TTL不一致甚至最基础的实现都不一致的情况,同时通过HTTP请求下发的Dns解析实时性会更强一些。

HttpClient如何集成HttpDns

与自定义Dns的过程一样,我们只需要重新实现DnsResolver接口,然后将HttpDns的过程加入到其中就可以了