(发现自己已经一年多没写东西了,那就随便写点什么吧,至少保持一年更一篇)
背景
最近QA在测试需求的时候,发现有部分需求没有实现,但是我在本地测试怎么都不能复现这个问题,通过排查发现这部分需求都是通过配置下发,开关打开时,功能才能正常使用,但是QA的手机就是收不到配置,调试以后发现是报了证书校验不过的错误,我一直没想通为什么会有这个错误,后来也是经过同事的提醒,才发现是设置了系统代理才导致的问题。不过这里有个点让我感到好奇,为啥Flutter中的网络请求并没有收到系统代理的影响?(我们主要的业务逻辑请求都是在flutter层处理,拉取配置中心的数据则是在native侧处理,android端用的是okhttp请求)
正好在前司时也做过一些关于代理设置的需求,对此也有些经验,趁此把dio库的代理设置也给搞明白。然后整理整理,写一篇代理的文章
本文主要就是介绍
- Flutter的Dio库、Android的OKhttp、HttpUrlConnection的代理是怎么设置的
- 这三个库对于系统代理会有哪些不同的表现
Flutter Dio
Dio请求的流程
创建 Dio 实例: 设置基本选项。
发送请求: 调用
get()
、post()
等方法构建请求。请求构建: Dio 构建请求对象,包含所有请求信息。
拦截器处理: 通过拦截器处理请求和响应。
发送请求: Dio 使用
HttpClient
发送请求。处理响应: 封装响应数据到
Response
对象。资源管理****: 关闭响应体以释放连接资源。
大部分流程都比较熟悉,这里不做过多说明,主要对「发送请求」展开。
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
创建配置HttpClient
创建一个请求的Future
发起请求
@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
创建HttpClient
调用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类。
首先调用findProxy(uri)方法,这个方法在pink中已经被重写了,也就是返回了
"PROXY $
_proxyString
"
的字符串然后通过_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;
});
}
小结
Dio库本身并不会主动去处理系统代理,默认是直连,所以这也就是为什么系统设置了代理,Pink也需要提供一个设置代理的入口,才能真正让代理生效。
Dio库有设置代理的入口,可以通过自定义onHttpClientCreate来设置, 同时也支持有用户名和密码的代理。格式:
"PROXY username:password@host:port"
Android OkHttp
OKHttp的请求流程
创建 OkHttpClient 实例: 配置 HTTP 客户端。
构建 Request 对象: 设置请求参数。
发送请求: 通过
newCall(request).execute()
或enqueue()
方法发送请求。拦截器处理: 通过拦截器处理请求和响应。
处理响应: 解析并处理服务器返回的响应。
资源管理: 关闭响应体以释放连接资源。
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的作用:
负责查找和验证可用的连接。它检查连接的状态和健康性,确保其能够满足当前请求的需求。
找不到缓存,则创建路由,获取代理信息,创建新的连接。
这里只讲下怎么创建路由,设置代理信息
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
-
初始化时创建proxy数组
当传入的proxy不为空时(也就是调用方主动设置了Proxy),则用传入的Proxy
如果uri的host为空,则认为没有设置代理
否则使用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)
}
- 循环遍历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
建立Socket连接
处理代理
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();
小结
OKhttp能够设置代理,也能够主动去寻找系统代理。
本文开头提到因为设置了系统代理,导致了OKHttp的请求出现了证书校验的错误,所以很明显OKHttp会去主动获取当前系统的代理信息,这一点与Dio的设计有点不太一样。所以我们在使用的时候需要注意。
Android HttpURLConnection
HttpURLConnection简介
我们都知道Google从Android4.4开始就将HttpURLConnection的具体实现都是基于OKHttp来处理,请求的流程最终都会走到OKHttp内部,不做过多介绍。不过在RouteSelector里面去获取代理的时候,还是发生了一些变化
RouterSelector
当传入的代理不为空时,就会直接使用传入的proxy
如果传入的代理为空时,去寻找系统的代理(这一步与OKHttp保持一致),将系统代理加到数组中
在将直连也加入到数组中(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();
小结
内部就是基于OKHttp请求的,
设置鉴权代理的方式与OKHttp不一致
HttpURLConnection在代码中未设置代理的情况下,会去将系统代理和直连全部加到数组中,使得请求的成功率会高于OKHttp
总结
总的来说这三个网络库都支持设置代理,设置代理的方式也都比较简单。
Flutter的Dio库没法主动去获取系统代理,所以需要开发者注意。
在请求时,如果请求不成功,OKHttp内部会切换代理发起请求,所以理论上HttpURLConnection的成功率会高于OKHttp,因为OKHttp在获取到系统代理以后就不会再去直连请求了(即便是系统代理请求不成功)。