浅谈Android中的网络库代理

背景

最近QA在测试需求的时候,发现有部分需求没有实现,但是我在本地测试怎么都不能复现这个问题,通过排查发现这部分需求都是通过配置下发,开关打开时,功能才能正常使用,但是QA的手机就是收不到配置,调试以后发现是报了证书校验不过的错误,我一直没想通为什么会有这个错误,后来也是经过同事的提醒,才发现是设置了系统代理才导致的问题。不过这里有个点让我感到好奇,为啥Flutter中的网络请求并没有收到系统代理的影响?(我们主要的业务逻辑请求都是在flutter层处理,拉取配置中心的数据则是在native侧处理,android端用的是okhttp请求)
正好在前司时也做过一些关于代理设置的需求,对此也有些经验,趁此把dio库的代理设置也给搞明白。然后整理整理,写一篇代理的文章
本文主要就是介绍

  1. Flutter的Dio库、Android的OKhttp、HttpUrlConnection的代理是怎么设置的

  2. 这三个库对于系统代理会有哪些不同的表现

Flutter Dio

Dio请求的流程

  1. 创建 Dio 实例: 设置基本选项。

  2. 发送请求: 调用 get()post() 等方法构建请求。

  3. 请求构建: Dio 构建请求对象,包含所有请求信息。

  4. 拦截器处理: 通过拦截器处理请求和响应。

  5. 发送请求: Dio 使用 HttpClient 发送请求。

  6. 处理响应: 封装响应数据到 Response 对象。

  7. 资源管理****: 关闭响应体以释放连接资源。

大部分流程都比较熟悉,这里不做过多说明,主要对「发送请求」展开。

DioMixin

Future<Response<T>> _dispatchRequest<T>(RequestOptions reqOpt) async {
  var cancelToken = reqOpt.cancelToken;
  ResponseBody responseBody;
  try {
    var stream = await _transformData(reqOpt);
    responseBody = await httpClientAdapter.fetch(
      reqOpt,
      stream,
      cancelToken?.whenCancel,
    );
    ...代码省略...
  } catch (e) {
    throw assureDioError(e, reqOpt);
  }
}

发起请求就是从httpClientAdapter.fetch方法开始的。 httpClientAdapter是一个抽象类,Dio在创建的时候默认使用的是DioForNative, 而DioForNative默认初始化的是DefaultHttpClientAdapter类。所以主要就是看下DefaultHttpClientAdapter的fetch方法

DefaultHttpClientAdapter

  1. 创建配置HttpClient

  2. 创建一个请求的Future

  3. 发起请求

@override
Future<ResponseBody> fetch(
  RequestOptions options,
  Stream<Uint8List>? requestStream,
  Future? cancelFuture,
) async {
  if (_closed) {
    throw Exception(
        "Can't establish connection after [HttpClientAdapter] closed!");
  }
  //创建配置HttpClient
  var _httpClient = _configHttpClient(cancelFuture, options.connectTimeout);
 //创建一个请求的Future
  var reqFuture = _httpClient.openUrl(options.method, options.uri);
  ...代码省略...
 }
首先创建并配置HttpClient
  1. 创建HttpClient

  2. 调用onHttpClientCreate方法,而onHttpClientCreate方法是可以从外部传入的。

HttpClient _configHttpClient(Future? cancelFuture, int connectionTimeout) {
  ...代码省略...
  if (_defaultHttpClient == null) {
    //创建HttpClient
    _defaultHttpClient = HttpClient();
    _defaultHttpClient!.idleTimeout = Duration(seconds: 3);
    if (onHttpClientCreate != null) {
      //user can return a HttpClient instance
      //调用onHttpClientCreate方法
      _defaultHttpClient =
          onHttpClientCreate!(_defaultHttpClient!) ?? _defaultHttpClient;
    }
    _defaultHttpClient!.connectionTimeout = _connectionTimeout;
  }
  return _defaultHttpClient!;
}

在Pink工程中就是通过这个方法设置Proxy,代理为"PROXY $_proxyString", _proxyString则是从native传回来的系统代理信息,包含ip, 端口号。

//这里并没有返回HttpClient,所以依然是使用dio库内部创建的HttpClient,只不过我们将Proxy设置进去了
final proxy = (HttpClient client) {
  _addProxy(client);
  onClientCreate?.call(client);
  // client.idleTimeout = const Duration(seconds: 60);
  // client.maxConnectionsPerHost = 5;
};


(_dio?.httpClientAdapter as DefaultHttpClientAdapter)
      .onHttpClientCreate = proxy;


//添加代理
static void _addProxy(HttpClient client) {
  if (onProxy) {
    client.badCertificateCallback =
        (X509Certificate cert, String host, int port) => true;
  }
  if (_proxyString != null && _proxyString?.isNotEmpty == true) {
    client.findProxy = (_) {
      return "PROXY $_proxyString";
    };
  }
}
然后创建请求的Future

HttpClient也是个抽象类,这里使用的是三方库创建的HttpClient项目,所以此处就是_HttpClient类。

  1. 首先调用findProxy(uri)方法,这个方法在pink中已经被重写了,也就是返回了"PROXY $_proxyString"的字符串

  2. 然后通过_getConnection创建Dio内部的Proxy对象,发起请求

Future<_HttpClientRequest> _openUrl(String method, Uri uri) {
  // Check to see if a proxy server should be used for this connection.
  var proxyConf = const _ProxyConfiguration.direct();
  var findProxy = _findProxy;
  if (findProxy != null) {
    // TODO(sgjesse): Keep a map of these as normally only a few
    // configuration strings will be used.
    try {
      proxyConf = _ProxyConfiguration(findProxy(uri));
    } catch (error, stackTrace) {
      return Future.error(error, stackTrace);
    }
  }
  ...代码省略...
  return _getConnection(uri, uri.host, port, proxyConf, isSecure, profileData)
      .then((_ConnectionInfo info) {
    ...代码省略...
    return send(info);
  }, onError: (error) {
    profileData?.finishRequestWithError(error.toString());
    throw error;
  });
}

小结

  1. Dio库本身并不会主动去处理系统代理,默认是直连,所以这也就是为什么系统设置了代理,Pink也需要提供一个设置代理的入口,才能真正让代理生效。

  2. Dio库有设置代理的入口,可以通过自定义onHttpClientCreate来设置, 同时也支持有用户名和密码的代理。格式: "PROXY username:password@host:port"

Android OkHttp

OKHttp的请求流程

  1. 创建 OkHttpClient 实例: 配置 HTTP 客户端。

  2. 构建 Request 对象: 设置请求参数。

  3. 发送请求: 通过 newCall(request).execute()enqueue() 方法发送请求。

  4. 拦截器处理: 通过拦截器处理请求和响应。

  5. 处理响应: 解析并处理服务器返回的响应。

  6. 资源管理: 关闭响应体以释放连接资源。

OKHttp的请求流程与Dio比较相似,同样这里也只对「拦截器处理」展开说明,OKHttp默认的拦截器有6个,其中ConnectIntercerptor拦截器主要的作用如下,因为建立连接需要连接信息,所以这个拦截器自然也就是设置代理的地方。

Opens a connection to the target server and proceeds to the next interceptor. 
The network might be used for the returned response, or to validate a cached response with a conditional GET.
而ConnectInterceptor拦截器主要是依赖ExchangeFinder来查找或者创建新的连接.

ExChangeFinder

ExchangeFinder的作用:

  1. 负责查找和验证可用的连接。它检查连接的状态和健康性,确保其能够满足当前请求的需求。

  2. 找不到缓存,则创建路由,获取代理信息,创建新的连接。

这里只讲下怎么创建路由,设置代理信息

private fun findConnection(
  connectTimeout: Int,
  readTimeout: Int,
  writeTimeout: Int,
  pingIntervalMillis: Int,
  connectionRetryEnabled: Boolean
): RealConnection {
  ...代码省略...
  else {
    ...代码省略...
    if (localRouteSelector == null) {
      localRouteSelector = RouteSelector(address, call.client.routeDatabase, call, eventListener)
      this.routeSelector = localRouteSelector
    }    
    val localRouteSelection = localRouteSelector.next()
    ...代码省略...
  }
  // Connect. Tell the call about the connecting call so async cancels work.
  val newConnection = RealConnection(connectionPool, route)
  call.connectionToCancel = newConnection
  try {
    newConnection.connect(
        connectTimeout,
        readTimeout,
        writeTimeout,
        pingIntervalMillis,
        connectionRetryEnabled,
        call,
        eventListener
    )
  } finally {
    call.connectionToCancel = null
  }
  ...代码省略...
  return newConnection
}

RouteSelector

  1. 初始化时创建proxy数组

    1. 当传入的proxy不为空时(也就是调用方主动设置了Proxy),则用传入的Proxy

    2. 如果uri的host为空,则认为没有设置代理

    3. 否则使用OKHttp默认的ProxySelector去获取proxy, OkHttp默认使用的是sun.net``.spi.DefaultProxySelector

private fun resetNextProxy(url: HttpUrl, proxy: Proxy?) {
  fun selectProxies(): List<Proxy> {
    // If the user specifies a proxy, try that and only that.
    if (proxy != null) return listOf(proxy)


    // If the URI lacks a host (as in "http://</"), don't call the ProxySelector.
    val uri = url.toUri()
    if (uri.host == null) return immutableListOf(Proxy.NO_PROXY)


    // Try each of the ProxySelector choices until one connection succeeds.
    val proxiesOrNull = address.proxySelector.select(uri)
    if (proxiesOrNull.isNullOrEmpty()) return immutableListOf(Proxy.NO_PROXY)


    return proxiesOrNull.toImmutableList()
  }


  eventListener.proxySelectStart(call, url)
  proxies = selectProxies()
  nextProxyIndex = 0
  eventListener.proxySelectEnd(call, url, proxies)
}
  1. 循环遍历Proxy数组,根据Proxy对象,创建可用的路由。

operator fun next(): Selection {
  ...代码省略....
  while (hasNextProxy()) {
    // Postponed routes are always tried last. For example, if we have 2 proxies and all the
    // routes for proxy1 should be postponed, we'll move to proxy2\. Only after we've exhausted
    // all the good routes will we attempt the postponed routes.
    val proxy = nextProxy()
    ...代码省略...


    if (routes.isNotEmpty()) {
      break
    }
  }
  ...代码省略...
  return Selection(routes)
}

DefaultProxySelector

直接从注释就可以看出来,这个方法最终会调用Properties.getProperty方法来获取系统设置的代理。

/**
 * select() method. Where all the hard work is done.
 * Build a list of proxies depending on URI.
 * Since we're only providing compatibility with the system properties
 * from previous releases (see list above), that list will typically
 * contain one single proxy, default being NO_PROXY.
 * If we can get a system proxy it might contain more entries.
 */
public java.util.List<Proxy> select(URI uri) {
    ...代码省略。。


}

RealConnection

  1. 建立Socket连接

  2. 处理代理

  3. SSL握手

private fun connectSocket(
  connectTimeout: Int,
  readTimeout: Int,
  call: Call,
  eventListener: EventListener
) {
  val proxy = route.proxy
  val address = route.address


  val rawSocket = when (proxy.type()) {
    Proxy.Type.DIRECT, Proxy.Type.HTTP -> address.socketFactory.createSocket()!!
    else -> Socket(proxy)
  }
  this.rawSocket = rawSocket


  eventListener.connectStart(call, route.socketAddress, proxy)
  rawSocket.soTimeout = readTimeout
  try {
    Platform.get().connectSocket(rawSocket, route.socketAddress, connectTimeout)
  } catch (e: ConnectException) {
    throw ConnectException("Failed to connect to ${route.socketAddress}").apply {
      initCause(e)
    }
  }


  // The following try/catch block is a pseudo hacky way to get around a crash on Android 7.0
  // More details:
  // https://github.com/square/okhttp/issues/3245
  // https://android-review.googlesource.com/#/c/271775/
  try {
    source = rawSocket.source().buffer()
    sink = rawSocket.sink().buffer()
  } catch (npe: NullPointerException) {
    if (npe.message == NPE_THROW_WITH_NULL) {
      throw IOException(npe)
    }
  }
}

设置代理

//设置http代理服务器ip端口
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 1081));


//设置鉴权信息
Authenticator authenticator = new Authenticator() {
    @Override
    public Request authenticate(Route route, Response response) throws IOException {
            String credential = Credentials.basic(username, password);
            return response.request().newBuilder()
                            .header("Proxy-Authorization", credential)
                            .build();
    }
};


//创建OkHttpClient,并且设置超时时间和代理
OkHttpClient client = new OkHttpClient.Builder().proxy(proxy).proxyAuthenticator(authenticator).build();
小结
  1. OKhttp能够设置代理,也能够主动去寻找系统代理。

  2. 本文开头提到因为设置了系统代理,导致了OKHttp的请求出现了证书校验的错误,所以很明显OKHttp会去主动获取当前系统的代理信息,这一点与Dio的设计有点不太一样。所以我们在使用的时候需要注意。

Android HttpURLConnection

HttpURLConnection简介

我们都知道Google从Android4.4开始就将HttpURLConnection的具体实现都是基于OKHttp来处理,请求的流程最终都会走到OKHttp内部,不做过多介绍。不过在RouteSelector里面去获取代理的时候,还是发生了一些变化

RouterSelector

  1. 当传入的代理不为空时,就会直接使用传入的proxy

  2. 如果传入的代理为空时,去寻找系统的代理(这一步与OKHttp保持一致),将系统代理加到数组中

  3. 在将直连也加入到数组中(OKHttp如果发现存在系统代理时,则不会将直连加入到数组中)。

直连就是NO_PROXY,意思就是不使用代理

/** Prepares the proxy servers to try. */
  private void resetNextProxy(HttpUrl url, Proxy proxy) {
    if (proxy != null) {
      // If the user specifies a proxy, try that and only that.
      proxies = Collections.singletonList(proxy);
    } else {
      // Try each of the ProxySelector choices until one connection succeeds. If none succeed
      // then we'll try a direct connection below.
      proxies = new ArrayList<>();
      List<Proxy> selectedProxies = address.getProxySelector().select(url.uri());
      if (selectedProxies != null) proxies.addAll(selectedProxies);
      // Finally try a direct connection. We only try it once!
      proxies.removeAll(Collections.singleton(Proxy.NO_PROXY));
      proxies.add(Proxy.NO_PROXY);
    }
    nextProxyIndex = 0;
  }

设置代理

// 创建 URL 对象
URL url = new URL(urlStr);
// 创建代理对象
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort));
// 打开连接并指定代理
connection = (HttpURLConnection) url.openConnection(proxy);
// 设置全局代理
java.net.Authenticator.setDefault(new java.net.Authenticator()
{
        private PasswordAuthentication authentication = new PasswordAuthentication("username", "password".toCharArray());


        @Override
        protected PasswordAuthentication getPasswordAuthentication()
{
                return authentication;
        }
});
// 检查响应代码
int responseCode = connection.getResponseCode();

小结

  1. 内部就是基于OKHttp请求的,

  2. 设置鉴权代理的方式与OKHttp不一致

  3. HttpURLConnection在代码中未设置代理的情况下,会去将系统代理和直连全部加到数组中,使得请求的成功率会高于OKHttp

总结

总的来说这三个网络库都支持设置代理,设置代理的方式也都比较简单。

Flutter的Dio库没法主动去获取系统代理,所以需要开发者注意。

在请求时,如果请求不成功,OKHttp内部会切换代理发起请求,所以理论上HttpURLConnection的成功率会高于OKHttp,因为OKHttp在获取到系统代理以后就不会再去直连请求了(即便是系统代理请求不成功)。

关注我获取更多知识或者投稿

ff297c9d80285f05a5b6218349c6681c.jpeg

8c585bccc7b81e425a23824133040ed1.jpeg

作者:CDF_cc7d
链接:https://www.jianshu.com/p/2e9a8fde5e5a
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值