一般的,在 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 封裝的。
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(); } }
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 :
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 ?