Flutter 同步系统的 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 却是一个同步方法。。。
资源搜索网站大全https://55wd.com 广州品牌设计公司http://www.maiqicn.com
改进一下
暂时,先把视线从 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 ?