一般的,在 Flutter APP 里请求 HTTP 使用的是官方提供的 http 包。
1 2 3 4 5 6 7 8 import 'package:http/http.dart' as http;var url = 'https://jsonplaceholder.typicode.com/posts' ;var response = await http.get (url);print ('Response status: ${response.statusCode} ' );print ('Response body: ${response.body} ' );print (await http.read('https://jsonplaceholder.typicode.com/posts/1' ));
但是,有一个问题,在 Android 或者 iOS 上运行 Flutter APP,系统里配置的 HTTP 代理并不生效?
比如在使用 Charles 这种工具通过 HTTP 代理调试 API 请求时候,会发现 Flutter 的 http 请求没有按预期走代理,无论是 Http 还是 Https。
探察真相 阅读 http 包的源码 ,可以发现其是基于 Dart HttpClient API 封装的。
http.dart 1 2 3 4 5 6 7 8 9 10 11 Future<Response> get (url, {Map <String , String > headers}) => _withClient((client) => client.get (url, headers: headers)); Future<T> _withClient<T>(Future<T> Function (Client) fn) async { var client = Client(); try { return await fn(client); } finally { client.close(); } }
client.dart 1 2 3 4 5 6 7 8 abstract class Client { factory Client() => createClient(); ... }
在 Android 或 iOS 平台上,我们用的实现是 IOClient
:
io_client.dart 1 2 3 4 5 6 7 8 9 10 11 BaseClient createClient() => IOClient(); class IOClient extends BaseClient { HttpClient _inner; IOClient([HttpClient inner]) : _inner = inner ?? HttpClient(); ... }
可以看到,IOClient
用的是 dart:io
中的 HttpClient
。
而 HttpClient
中获取 HTTP 代理的关键源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 abstract class HttpClient { ... static String findProxyFromEnvironment(Uri url, {Map <String , String > environment}) { HttpOverrides overrides = HttpOverrides.current; if (overrides == null ) { return _HttpClient._findProxyFromEnvironment(url, environment); } return overrides.findProxyFromEnvironment(url, environment); } ... } class _HttpClient implements HttpClient { ... Function _findProxy = HttpClient.findProxyFromEnvironment; set findProxy(String f(Uri uri)) => _findProxy = f; ... }
通过阅读HttpClient
源码,可以知道默认的 HttpClient
实现类 _HttpClient
是通过环境变量来获取http代理(findProxyFromEnvironment
)的。
那么,只需要在它创建后,重新设置 findProxy
属性即可实现自定义 HTTP 代理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void request() { HttpClient client = new HttpClient(); client.findProxy = (url) { return HttpClient.findProxyFromEnvironment( url, environment: {"http_proxy" : ..., "no_proxy" : ...}); } client.getUrl(Uri .parse('https://jsonplaceholder.typicode.com/posts' )) .then((HttpClientRequest request) { return request.close(); }) .then((HttpClientResponse response) { ... }); }
环境变量(environment)里有三个 HTTP Proxy 配置相关的key:
1 2 3 4 5 { "http_proxy" : "192.168.2.1:1080" , "https_proxy" : "192.168.2.1:1080" , "no_proxy" : "example.com,www.example.com,192.168.2.3" }
问题来了,该怎么介入 HttpClient
的创建?
再看一下源码:
1 2 3 4 5 6 7 8 9 10 11 abstract class HttpClient { ... factory HttpClient({SecurityContext context}) { HttpOverrides overrides = HttpOverrides.current; if (overrides == null ) { return new _HttpClient(context); } return overrides.createHttpClient(context); } ... }
答案就是 HttpOverrides
。HttpClient
是可以通过 HttpOverrides.current
覆写的。
1 2 3 4 5 6 7 8 9 10 11 12 abstract class HttpOverrides { static HttpOverrides _global; static HttpOverrides get current { return Zone.current[_httpOverridesToken] ?? _global; } static set global(HttpOverrides overrides) { _global = overrides; } ... }
顾名思义,HttpOverrides
是用来覆写 HttpClient
的实现的,一个很简单的例子:
1 2 3 4 5 6 7 8 9 class MyHttpClient implements HttpClient { ... } void request() { HttpOverrides.runZoned(() { ... }, createHttpClient: (SecurityContext c) => new MyHttpClient(c)); }
但完全实现 HttpClient
的 API 又太复杂了,我们只是想设置 HTTP Proxy 而已,也就是给默认的 HttpClient
设一个自定义的findProxy
实现就够了。
换个思路,自定义一个 MyHttpOverrides
,让 HttpOverrides.current
返回的是MyHttpOverrides
不就好了?!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class MyHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext context) { return super .createHttpClient(context) ..findProxy = _findProxy; String _findProxy(url) { return HttpClient.findProxyFromEnvironment( url, environment: {"http_proxy" : ..., "no_proxy" : ...}); } } void main() { HttpOverrides.global = MyHttpOverrides(); runApp(...); }
如上代码,通过设置 HttpOverrides.global
,最终覆盖了默认 HttpClient
的 findProxy
实现。
同步原生的代理配置 现在新的问题来了,怎么让这个 MyHttpOverrides
能获取到原生的 HTTP Proxy 配置呢?
Flutter 和原生通信,你想到了什么?是的,MethodChannel !
Flutter 实现: 定义一个全局变量proxySettings
,在 MyHttpOverrides
里当作findProxyFromEnvironment
的环境变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class MyHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext context) { return super .createHttpClient(context) ..findProxy = _findProxy; } static String _findProxy(url) { return HttpClient.findProxyFromEnvironment(url, environment: proxySettings); } } Map <String , String > proxySettings = {};void main() { HttpOverrides.global = MyHttpOverrides(); runApp(...); loadProxySettings(); }
定义一个 MethodChannel, 名为 “yrom.net/http_proxy”,提供一个 getProxySettings
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 import 'package:flutter/services.dart' ;Future<void > loadProxySettings() async { final channel = const MethodChannel('yrom.net/http_proxy' ); try { var settings = await channel.invokeMapMethod<String , String >('getProxySettings' ); if (settings != null ) { proxySettings = Map <String , String >.unmodifiable(settings); } } on PlatformException { } }
通过调用 getProxySettings
方法,获取到的原生的HTTP Proxy 配置。
从而实现同步。
Android MethodChannel 实现 Android 里通过 ProxySelector API 获取 HTTP Proxy。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 import java.net.ProxySelectorclass MainActivity : FlutterActivity () { private val CHANNEL = "yrom.net/http_proxy" override fun configureFlutterEngine (@NonNull flutterEngine: FlutterEngine ) { MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result -> if (call.method == "getProxySettings" ) { result.success(getProxySettings()) } else { result.notImplemented() } } } private fun getProxySettings () : Map<String, String> { val settings = HashMap<>(2 ); try { val https = ProxySelector.getDefault().select(URI.create("https://yrom.net" )) if (https != null && !https.isEmpty) { val proxy = https[0 ] if (proxy.type() != Proxy.Type.DIRECT) { settings["https_proxy" ] = proxy.address().toString() } } val http = ProxySelector.getDefault().select(URI.create("http://yrom.net" )) if (http != null && !http.isEmpty) { val proxy = http[0 ] if (proxy.type() != Proxy.Type.DIRECT) { settings["http_proxy" ] = proxy.address().toString() } } } catch (ignored: Exception) { } return settings; } }
iOS MethodChannel 实现 iOS 则通过 CFNetworkCopySystemProxySettings
API 获取配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 #import <Foundation/Foundation.h> #import <Flutter/Flutter.h> #import "GeneratedPluginRegistrant.h" @implementation AppDelegate - (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions { FlutterViewController* controller = (FlutterViewController*)self.window.rootViewController; FlutterMethodChannel* proxyChannel = [FlutterMethodChannel methodChannelWithName:@"yrom.net/http_proxy" binaryMessenger:controller.binaryMessenger]; [proxyChannel setMethodCallHandler:^(FlutterMethodCall* call, FlutterResult result) { if ([@"getProxySettings" isEqualToString:call.method]) { NSDictionary * proxySetting = (__bridge_transfer NSDictionary *)CFNetworkCopySystemProxySettings(); NSMutableDictionary * proxys = [NSMutableDictionary dictionary]; NSNumber * httpEnable = [proxySetting objectForKey:(NSString *) kCFNetworkProxiesHTTPEnable]; if (httpEnable != nil && httpEnable.integerValue != 0 ) { NSString * httpProxy = [NSString stringWithFormat:@"%@:%@" ,[proxySetting objectForKey:(NSString *)kCFNetworkProxiesHTTPProxy],[proxySetting objectForKey:(NSString *)kCFNetworkProxiesHTTPPort]]; proxys[@"http_proxy" ] = httpProxy; } NSNumber * httpsEnable = [proxySetting objectForKey:@"HTTPSEnable" ]; if (httpsEnable != nil && httpsEnable.integerValue != 0 ) { NSString * httpsProxy = [NSString stringWithFormat:@"%@:%@" ,[proxySetting objectForKey:@"HTTPSProxy" ],[proxySetting objectForKey:@"HTTPSPort" ]]; proxys[@"https_proxy" ] = httpsProxy; } result(proxys); } }]; [GeneratedPluginRegistrant registerWithRegistry:self]; return [super application:application didFinishLaunchingWithOptions:launchOptions]; }
还有更多问题 聪明的你看了上面的代码之后,应该会发现一些新的问题:HttpClient
的 findProxy(url)
的参数 url
似乎没用到?而且原生的 getProxySettings
实现返回的配置和具体的 url 无关?网络切换后,没有更新 proxySettings
?( ̄ε(# ̄)
理论上,getProxySettings
应该和findProxy(url)
一样,需要定义一个额外参数 url
,然后每次 findProxy
的时候,就invoke
一次,实时获取原生当前网络环境的 HTTP Proxy:
1 2 3 4 5 6 7 8 9 10 11 12 13 class MyHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext context) { return super .createHttpClient(context) ..findProxy = _findProxy; } static String _findProxy(url) { String getProxySettings() { return channel.invokeMapMethod<String , String >('getProxySettings' ); } return HttpClient.findProxyFromEnvironment(url, environment: getProxySettings()); } }
然而现实是,MethodChannel
的 invokeMapMethod
返回的是个 Future
,但 findProxy
却是一个同步方法。。。
改进一下 暂时,先把视线从 HttpClient
和 HttpOverrides
中抽离出来,回头看看发送 http 请求的代码:
1 2 3 4 import 'package:http/http.dart' as http;var url = 'https://jsonplaceholder.typicode.com/todos/1' ;var response = await http.get (url);
http 包里的的 get
的方法就是个异步的,返回的是个 Future
!如果每次请求之前,同步一下proxySettings
是不是可以解决问题?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import 'dart:io' ;import 'package:flutter/services.dart' ;import 'package:http/http.dart' as http;Future<Map <String , String >> getProxySettings(String url) async { final channel = const MethodChannel('yrom.net/http_proxy' ); try { var settings = await channel.invokeMapMethod<String , String >('getProxySettings' , url); if (settings != null ) { return Map <String , String >.unmodifiable(settings); } } on PlatformException {} return {}; } class MyHttpOverrides extends HttpOverrides { final Map <String , String > environment; MyHttpOverrides({this .environment}); @override HttpClient createHttpClient(SecurityContext context) { return super .createHttpClient(context) ..findProxy = _findProxy; } String _findProxy(url) { return HttpClient.findProxyFromEnvironment(url, environment: environment); } } Future<void > request() async { var url = 'https://jsonplaceholder.typicode.com/todos/1' ; var overrides = MyHttpOverrides(environment: await getProxySettings(url)); var response = await HttpOverrides.runWithHttpOverrides<Future<http.Response>>( () => http.get (url), overrides, ); }
但是这样每次 http 请求都有一次 MethodChannel
通信,会不会太频繁影响性能?每次都要等待 MethodChannel
的回调会不会导致 http 请求延迟变高?对于同一个域名的不同URL来说,代理配置应该是一致的,能不能合并到一起 getProxySettings
?
怎么这么多问题,头秃了…
该如何进一步优化,就由你来思考了 ╮( ̄▽ ̄)╭
欢迎留言,等你的好方案。