内容简介:一般的,在 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设置开机启动,设置密码
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Node.js in Action
Mike Cantelon、Marc Harter、TJ Holowaychuk、Nathan Rajlich / Manning Publications / 2013-11-25 / USD 44.99
* Simplifies web application development * Outlines valuable online resources * Teaches Node.js from the ground up Node.js is an elegant server-side JavaScript development environment perfect for scal......一起来看看 《Node.js in Action》 这本书的介绍吧!