内容简介:一般的,在 Flutter APP 里请求 HTTP 使用的是官方提供的但是,有一个问题,在 Android 或者 iOS 上运行 Flutter APP,系统里配置的 HTTP 代理并不生效?
一般的,在 Flutter APP 里请求 HTTP 使用的是官方提供的 http 包。
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 封装的。
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(); } }
abstract class Client { /// Creates a new platform appropriate client. /// /// Creates an `IOClient` if `dart:io` is available and a `BrowserClient` if /// `dart:html` is available, otherwise it will throw an unsupported error. factory Client() => createClient(); ... }
在 Android 或 iOS 平台上,我们用的实现是 IOClient
:
BaseClient createClient() => IOClient(); /// A `dart:io`-based HTTP client. class IOClient extends BaseClient { /// The underlying `dart:io` HTTP client. HttpClient _inner; IOClient([HttpClient inner]) : _inner = inner ?? HttpClient(); ... }
可以看到, IOClient
用的是 dart:io
中的 HttpClient
。
而 HttpClient
中获取 HTTP 代理的关键源码如下:
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 代理:
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) { // Process the response. ... }); }
环境变量(environment)里有三个 HTTP Proxy 配置相关的key:
{ "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
的创建?
再看一下源码:
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
覆写的。
abstract class HttpOverrides { static HttpOverrides _global; static HttpOverrides get current { return Zone.current[_httpOverridesToken] ?? _global; } static set global(HttpOverrides overrides) { _global = overrides; } ... }
顾名思义, HttpOverrides
是用来覆写 HttpClient
的实现的,一个很简单的例子:
class MyHttpClient implements HttpClient { ... } void request() { HttpOverrides.runZoned(() { ... }, createHttpClient: (SecurityContext c) => new MyHttpClient(c)); }
但完全实现 HttpClient
的 API 又太复杂了,我们只是想设置 HTTP Proxy 而已,也就是给默认的 HttpClient
设一个自定义的 findProxy
实现就够了。
换个思路,自定义一个 MyHttpOverrides
,让 HttpOverrides.current
返回的是 MyHttpOverrides
不就好了?!
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 HttpOverrides.global = MyHttpOverrides(); runApp(...); }
如上代码,通过设置 HttpOverrides.global
,最终覆盖了默认 HttpClient
的 findProxy
实现。
同步原生的代理配置
现在新的问题来了,怎么让这个 MyHttpOverrides
能获取到原生的 HTTP Proxy 配置呢?
Flutter 和原生通信,你想到了什么?是的, MethodChannel !
Flutter 实现:
定义一个全局变量 proxySettings
,在 MyHttpOverrides
里当作 findProxyFromEnvironment
的环境变量:
class MyHttpOverrides extends HttpOverrides { @override HttpClient createHttpClient(SecurityContext context) { return super.createHttpClient(context) ..findProxy = _findProxy; } static String _findProxy(url) { // proxySettings 当作 findProxyFromEnvironment 的 environment return HttpClient.findProxyFromEnvironment(url, environment: proxySettings); } } // 定义一个全局变量,当作环境变量 Map<String, String> proxySettings = {}; void main() { HttpOverrides.global = MyHttpOverrides(); runApp(...); // 加载proxy 设置,注意需要在 runApp 之后执行 loadProxySettings(); }
定义一个 MethodChannel, 名为 “yrom.net/http_proxy”,提供一个 getProxySettings
方法。
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。
import java.net.ProxySelector class 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 获取配置。
#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]; // https://developer.apple.com/documentation/cfnetwork/global_proxy_settings_constants 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:
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 请求的代码:
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
是不是可以解决问题?
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
?
怎么这么多问题,头秃了…
该如何进一步优化,就由你来思考了 ╮( ̄▽ ̄)╭
欢迎留言,等你的好方案。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- Visual Studio Code的设置及插件同步
- 同步你 VSCode 设置及扩展插件,换机不用愁
- 三分钟教你同步 Visual Studio Code 设置
- 三分钟教你同步 Visual Studio Code 设置
- ELK(Elasticsearch,Logstash,Kibana) 搭建 同步 MySQL 及 用户权限安全设置
- [CentOS7]redis设置开机启动,设置密码
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Head First HTML5 Programming
Eric Freeman、Elisabeth Robson / O'Reilly Media / 2011-10-18 / USD 49.99
What can HTML5 do for you? If you're a web developer looking to use this new version of HTML, you might be wondering how much has really changed. Head First HTML5 Programming introduces the key featur......一起来看看 《Head First HTML5 Programming》 这本书的介绍吧!