基于OkHttp3 加入HttpDns功能

什么是HttpDns

HttpDns是通过网络请求的方式,获取即将发送的业务请求所需要的ip地址。

为什么要用HttpDns

在使用HttpDns时,android发送网络请求时会请求本地dns或本地运营商 的dns服务获取目标ip,但是一旦你使用的这个默认的dns不靠谱,不受信任,则请求稳定性将会降低,甚至可能被劫持。
因此,如果能够使用自己信任的dns服务器做dns域名解析,将大大降低这种风险。

基于OkHttp3如何定制?

image.png

OkHttp3一大亮点在其强大的Interceptor机制。因此HttpDns在整个request发射过程中就有了两个结合点:

  1. 使用Interceptor,直接将域名替换为ip地址
  2. 使用OkHttp提供的dns接口,新建Dns子类,实现lookup()方法。

使用Interceptor做ip直连,则会存在以下优点:

  • 对Dns的控制偏上层,可更加细化,控制灵活。
  • 容灾处理更容易
    但也会存在比较致命的缺点,一切跟域名有关的处理全部失效,具体有:
  • 在Https下处理SSL证书会出现校验问题
  • ip访问时出现Cookie校验问题。

若使用OkHttp自带的dns(),优点在于:

  • Https下不会存在证书校验问题,保证流程正常执行
  • 种Cookie时不会存在问题
    缺点在于:
  • 时机过于底层,容灾控制都不方便。
  • okhttp自身存在缓存,一旦dns自身ttl过期,okhttp缓存有可能还在使用,会存在一定的风险。

综上来看,使用OkHttp原生Dns接口更加科学。除非不要求Cookie,不使用Https,使用Interceptor做简单的场景才比较合适。

实现

image.png
setDNS(new Dns() {
     @Override
      public List<InetAddress> lookup(@NonNull String hostname) throws UnknownHostException {
List<InetAddress> result = DnsManager.getInstance().getIps(hostname);
            if (result == null)
            result = new ArrayList<>();
            return result;
          }
     })

OkHttp内使用RouteDatabase进行每次使用ip的监控和反馈:

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);
  }
}

为了能够复用shouldPostpone()获取okhttp自身对ip可用性的判断结果,使得自定义manager对ip的可用性的判断与okhttp一致,从而更新Manager自身的ip名单,可以通过
Internal.instance.routeDatabase(getConnectionPool());得到RouteDatabase对象。

Internal.instance在OkHttpClient实例化之后就被赋值,事实上,Internal.instance就是OkHttpClient。因此可以使用NetworkInterceptor获取RouteDatabase。即:

addNetworkInterceptor(new Interceptor() {
                    @Override
                    public Response intercept(Chain chain) throws IOException {
                        Request request = chain.request();
                        try {

                            Connection connection = chain.connection();
                            RouteDatabase routeDatabase = Internal.instance.routeDatabase(getConnectionPool());
                            Route route = connection.route();
                            Log.d("GI", "Route:" + route + "=======Can Trust?:" + !routeDatabase.shouldPostpone(route));
                            if (routeDatabase.shouldPostpone(route)) {
                                DnsManager.getInstance().putToBlackList(route.socketAddress().getAddress().getHostName(), route.socketAddress().getAddress().getHostAddress());
                                DnsManager.getInstance().removeFromWhiteList(route.socketAddress().getAddress().getHostName(), route.socketAddress().getAddress().getHostAddress());
                            }
                        } catch (Exception e) {
                            e.printStackTrace();
                        }
                        return chain.proceed(request);
                    }
                })

Manager内维护两个名单Map,

  • 白名单:存储Dns下发的ip及经过检验可靠的ip
  • 黑名单:存储使用过程中不可靠的ip
    根据每次的dns请求和RouteDatabase进行反馈更新。

附:OkHttp 链式调用原理

Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }

interceptors.addAll(client.interceptors())是将addInterceptor时的所有Interceptor加入列表,然后再加入OkHttpCore核心处理Interceptor。
如果本次请求是一个需要走网络的请求,还会继续添加addNetInterceptor时所有的Interceptor加入列表。最后才加入CallServerInterceptor用来处理真正的网络请求。
这个顺序能够保证在递归调用过程中,自定义拦截器只会影响到OkHttpCore处理流程之前或者之后,而Core内的核心流程不会受到影响。

image.png

推荐阅读更多精彩内容