Flutter 同步系统的 HTTP 代理设置

更新日期: 2020-02-10阅读: 2k标签: 同步

一般的,在 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 封装的。

http.dart
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
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
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 ?

原文 https://yrom.net/blog/2020/04/09/load-http-proxy-from-platform-for-flutter-app/

链接: https://fly63.com/article/detial/8601

内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!