okhttp源码解析(五):代理和DNS

前言

之前我们分析了okhttp的重试机制,发现在获取可用地址的时候,都需要遍历一个路由选择器,里面保存了可用的地址,那么这些地址是从哪来的呢?这就是本篇分析的重点。

首先我们简单理解一下代理和DNS的概念:

代理:通过另一台服务器或ip,帮助我们进行网络请求的转发,例如创建的抓包工具。

DNS:万维网上作为域名和IP地址相互映射的一个分布式数据库,能够使用户更方便的访问互联网,而不用去记住能够被机器直接读取的IP数串。常用的有阿里云的DNS服务。

从他们的概念之间,我们可以知道,使用代理的话网速是变慢的风险,而DNS不仅不会增加网络请求的成本,还会节省访问网络的时间,所以DNS服务已经十分普遍。

(突然回忆起刚上班时,公司内网封了QQ地址,每隔一段时间就需要更换代理的日子……)

正文

首先看看怎么设置代理和DNS:

OkHttpClient okHttpClient = new OkHttpClient.Builder()
                // 多个代理
                .proxySelector(new ProxySelector() {
                    @Override
                    public List<Proxy> 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<InetAddress> 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<Route> 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<Proxy> 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<InetAddress> 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域名。

总结

最后我们来总结一下:

  1. Proxy和ProxySelector不可同时使用,同时存在优先使用Proxy。
  2. 如果代理的类型是SOCKS,那么他的DNS不会被使用。
  3. 尝试建立的过程分为三步:尝试复用 -> 使用代理和dns -> 建立连接,保存host等待复用。
  4. 通过代理完成网络操作,不会保持连接,因为我们无法通过代理得到访问的真正地址。

到这里okhttp源码解析系列暂时告一段落了,我们整体的分析了okhttp的工作过程,然后分别重点分析okhttp的网络读写,缓存机制,重试机制,代理和DNS,之后继续使用okhttp会更加得心应手。

如果之后还有新发现再继续补充,希望这一系列对大家对okhttp的理解有所帮助。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容