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 ?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM