一般的,在 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 {
/// 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:

io_client.dart
1
2
3
4
5
6
7
8
9
10
11

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 代理的关键源码如下:

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) {
// Process the 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);
}
...
}

答案就是 HttpOverridesHttpClient 是可以通过 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
HttpOverrides.global = MyHttpOverrides();
runApp(...);
}

如上代码,通过设置 HttpOverrides.global,最终覆盖了默认 HttpClientfindProxy 实现。

同步原生的代理配置

现在新的问题来了,怎么让这个 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) {
// 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方法。

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.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 获取配置。

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];
// 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];
}

还有更多问题

聪明的你看了上面的代码之后,应该会发现一些新的问题:HttpClientfindProxy(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());
}
}

然而现实是,MethodChannelinvokeMapMethod 返回的是个 Future,但 findProxy却是一个同步方法。。。

改进一下

暂时,先把视线从 HttpClientHttpOverrides 中抽离出来,回头看看发送 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

怎么这么多问题,头秃了…

该如何进一步优化,就由你来思考了 ╮( ̄▽ ̄)╭

欢迎留言,等你的好方案。