12

Flutter 同步系统的 HTTP 代理设置

 4 years ago
source link: https://yrom.net/blog/2020/04/09/load-http-proxy-from-platform-for-flutter-app/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

一般的,在 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 代理并不生效?

iYBZrmZ.png!web

比如在使用 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);
  }
  ...
}

答案就是 HttpOverridesHttpClient 是可以通过 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 ,最终覆盖了默认 HttpClientfindProxy 实现。

同步原生的代理配置

现在新的问题来了,怎么让这个 MyHttpOverrides 能获取到原生的 HTTP Proxy 配置呢?

Flutter 和原生通信,你想到了什么?是的, MethodChannel

iM77r2r.png!web

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

还有更多问题

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

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

改进一下

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

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

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK