zoukankan      html  css  js  c++  java
  • 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 封装的。

    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 却是一个同步方法。。。

    资源搜索网站大全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 ?

  • 相关阅读:
    纯html的table打印注意事项
    Silverlight:针式打印机文字模糊的改善办法
    C#执行XSL转换
    tomcat 新手上路
    跨浏览器的剪贴板访问解决方案
    打印机设置(PrintDialog)、页面设置(PageSetupDialog) 及 RDLC报表如何选择指定打印机
    利用ActiveX实现web页面设置本地默认打印机、纸张大小
    前端工程化的理解
    算法注意---3、分治的本质
    算法与数据结构---4.9、最大子段和-dp空间优化
  • 原文地址:https://www.cnblogs.com/qianxiaox/p/13751171.html
Copyright © 2011-2022 走看看