Flutter框架最佳实践

更新时间:2025-04-24 05:49:35

本文介绍如何在Flutter应用开发中集成使用HTTPDNS。

FlutterGoogle开源的应用开发框架,仅通过一套代码库,就能构建精美的、原生平台编译的多平台应用。

我们提供Flutter框架下的HTTPDNS插件:aliyun_httpdns

插件本地包下载页面:下载页面

Flutter应用可以集成插件,并参考Flutter最佳实践来使用HTTPDNS能力。

以下是插件的使用说明和最佳实践:

一、快速入门

1.1 开通服务

请参考快速入门开通HTTPDNS。

1.2 获取配置

请参考开发配置EMAS控制台开发配置中获取AccountId/SecretKey/AESSecretKey等信息,用于初始化SDK。

二、安装

pubspec.yaml中加入dependencies:

dependencies:
  aliyun_httpdns: x.y.z

请在pub仓库查看最新版本号,替换x.y.z。

添加依赖之后需要执行一次 pub get。

iOS 平台指定阿里云仓库

flutter目标项目的ios/Podfile中添加阿里云仓库, 如下:

# Uncomment this line to define a global platform for your project
# platform :ios, '12.0'
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/aliyun/aliyun-specs.git'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.

如果不存在ios/Podfile文件,请先在flutter目标项目目录下执行以下命令:

flutter build ios --no-codesign --config-only

在执行命令时,如果出现以下错误,不用处理,先在ios/Podfile文件中,添加阿里云仓库即可。

[!] Unable to find a specification for `AlicloudHTTPDNS (= x.y.z)` depended upon by `aliyun_httpdns`

三、配置和使用

3.1 初始化配置

应用启动后,需要先初始化插件,才能调用HTTPDNS能力。 初始化主要是配置AccountId/SecretKey/AESSecretKey等信息及功能开关。 API参考5.2 初始化,示例代码如下:

final _aliyunHttpDns = AliyunHttpDns();
_aliyunHttpDns.init(
    accountId,
    secretKey: secretKey,
    aesSecretKey: aesSecretKey,
    region: "",
    timeout: 2000,
    enableHttps: true,
    enableExpireIp: true,
    enableCacheIp: true,
    enableDegradationLocalDns: true,
    preResolveAfterNetworkChanged: true,
    ipRankingMap: {
    "www.aliyun.com": 80
    },
    sdnsGlobalParam: {
    "aa":"bb",
    "cc":"dd"
    },
    bizTags: ["hh", "gg"]
    ).then((_) => {
      print("init success")
    });

3.1.1 日志配置

应用开发过程中,如果要输出HTTPDNS的日志,可以调用5.1 日志输出控制方法,开启日志,示例代码如下:

 _aliyunHttpDns.enableLog(true).then((_) {
  print("enableLog success");
});

3.1.2 sessionId记录

应用在运行过程中,可以调用5.6 获取SessionId方法获取sessionId,记录到应用的数据采集系统中。 sessionId用于表示标识一次应用运行,线上排查时,可以用于查询应用一次运行过程中的解析日志,示例代码如下:

final _aliyunHttpDns = AliyunHttpDns();
_aliyunHttpDns.getSessionId(accountId).then((sessionId) => {
  print("SessionId = $sessionId")
});

3.2 域名解析

3.2.1 预解析

当需要提前解析域名时,可以调用5.4 预解析域名方法,示例代码如下:

final _aliyunHttpDns = AliyunHttpDns();
_aliyunHttpDns.setPreResolveHosts(accountId, ["www.aliyun.com","www.example.com"]).then((_) {
  print("preResolveHosts success");
});

调用之后,插件会发起域名解析,并把结果缓存到内存,用于后续请求时直接使用。

3.2.2 域名解析

当需要解析域名时,可以通过调用5.3 域名解析方法解析域名获取IP,示例代码如下:

Future<void> _resolve() async {
  final _aliyunHttpDns = AliyunHttpDns();
  _result = await _aliyunHttpDns.resolve(accountId, "www.aliyun.com", kRequestIpv4AndIpv6);
}

四、Flutter最佳实践

4.1 原理说明

  1. 在应用中创建本地http代理服务

  2. 需要使用httpdns解析的请求,配置走本地代理

  3. 在代理中获取请求域名,通过httpdns解析为IP

  4. 在代理中创建指定IPsocket连接,用于发起真正的请求,从而避免走localdns进行解析

4.2 示例说明

完整应用示例请参考插件包中example应用。

4.2.1 代理实现

代理的实现请参考插件包中example/lib/custom_https_proxy.dart文件。 代理内部需要识别请求的域名,并调用HTTPDNS进行解析,并使用解析后的IP创建socket连接用于请求。 代码如下:

import 'dart:io';
import 'dart:convert';

import 'package:aliyun_httpdns/aliyun_httpdns.dart';

import 'constants.dart';

class CustomHttpsProxy {
  static final int PROXY_PORT = 4041;
  static final String PROXY_CONFIG = 'PROXY localhost:$PROXY_PORT';
  ServerSocket? serverSocket;

  CustomHttpsProxy._();

  static final CustomHttpsProxy _instance = CustomHttpsProxy._();

  factory CustomHttpsProxy() => _instance;

  Future init() async {
    await ServerSocket.bind(InternetAddress.anyIPv4, PROXY_PORT).then((serverSocket) {
      this.serverSocket = serverSocket;
      serverSocket.listen((client) {
        try {
          ClientConnectionHandler(client).handle();
        } catch (e) {
          print('ClientConnectionHandler exception $e');
        }
      });
    }).catchError((e) {
      print('serverSocket 处理异常$e');
    });
    return serverSocket;
  }

  void close() {
    if (serverSocket != null) {
      serverSocket?.close();
    }
  }
}

class ClientConnectionHandler {
  final RegExp regx = RegExp(r'CONNECT ([^ :]+)(?::([0-9]+))? HTTP/1.1\r\n');
  Socket? server;
  Socket? client;
  String content = '';
  String? host;
  int? port;

  final _aliyunHttpDns = AliyunHttpDns();

  ClientConnectionHandler(this.client);

  void closeSockets() {
    if (server != null) {
      server?.destroy();
    }
    client?.destroy();
  }

  Future<void> dataHandler(data) async {
    if (server == null) {
      content += utf8.decode(data);
      final m = regx.firstMatch(content);
      if (m != null) {
        host = m.group(1);
        port = m.group(2) == null ? 443 : int.parse(m.group(2)!);
        String? resultIP = await _getIp(host!);
        print("resultIp = $resultIP");
        String realHost = resultIP ?? host!;
        try {
          ServerConnectionHandler(realHost, port!, this)
              .handle()
              .catchError((e) {
            print('Server error $e');
            closeSockets();
          });
        } catch (e) {
          print('Server exception $e');
          closeSockets();
        }
      }
    } else {
      try {
        server?.add(data);
      } catch (e) {
        print('sever has been shut down');
        closeSockets();
      }
    }
  }

  Future<String?> _getIp(String host) async {
    String resolveResult = await _aliyunHttpDns.resolve(accountId, host, kRequestIpv4AndIpv6);
    Map<String, dynamic> map = json.decode(resolveResult);
    if (map.containsKey('ipv4')) {
      print(map['ipv4']);
      List<String> ipv4s = List<String>.from(map['ipv4']);
      if (ipv4s.isNotEmpty) {
        return ipv4s.first;
      }
    } else if (map.containsKey('ipv6')) {
      List<String> ipv6s = List<String>.from(map['ipv6']);
      if (ipv6s.isNotEmpty) {
        return ipv6s.first;
      }
    }
    return null;
  }

  void errorHandler(error, StackTrace trace) {
    print('client socket error: $error');
  }

  void doneHandler() {
    closeSockets();
  }

  void handle() {
    client?.listen(dataHandler,
        onError: errorHandler, onDone: doneHandler, cancelOnError: true);
  }
}

class ServerConnectionHandler {
  final String RESPONSE = 'HTTP/1.1 200 Connection Established\r\n\r\n';
  final String host;
  final int port;
  final ClientConnectionHandler handler;
  Socket? server;
  Socket? client;
  String content = '';

  ServerConnectionHandler(this.host, this.port, this.handler) {
    client = handler.client;
  }

  //接收报文
  void dataHandler(data) {
    try {
      client?.add(data);
    } on Exception catch (e) {
      print('client has been shut down $e');
      handler.closeSockets();
    }
  }

  void errorHandler(error, StackTrace trace) {
    print('server socket error: $error');
  }

  void doneHandler() {
    handler.closeSockets();
  }

  Future handle() async {
    print('尝试建立连接: $host:$port');
    server = await Socket.connect(host, port, timeout: Duration(seconds: 60));
    server?.listen(dataHandler,
        onError: errorHandler, onDone: doneHandler, cancelOnError: true);
    handler.server = server;
    client?.write(RESPONSE);

  }
}

4.2.2 代理创建和使用

代理的创建请参考插件包example/lib/practice.dart文件。 首先需要创建代理服务,并初始化,示例代码如下:

  var proxy = CustomHttpsProxy();

  @override
  void initState() {
    super.initState();
    proxy.init();
  }

具体创建和使用的位置需要根据应用的需要调整。

在请求时根据业务需要配置代理,以控制请求是否使用HTTPDNS。 以get请求为例,代码如下:

Future _doHttpGet(String url, bool useProxy) async {
    var httpClient = HttpClient();
    httpClient.connectionTimeout = Duration(seconds: 30);
    httpClient.idleTimeout = Duration(seconds: 30);
    if (useProxy) {
      httpClient.findProxy = (uri) => CustomHttpsProxy.PROXY_CONFIG;
    }
    var request = await httpClient.getUrl(Uri.parse(url));
    var response = await request.close();
    //读取响应内容
    var responseBody = await response.transform(Utf8Decoder()).join();
    print("response:$responseBody");
}

4.2.3 代理销毁

在应用销毁时,需要销毁代理服务,避免内存泄漏。 参考如下:

  @override
  void dispose() {
    super.dispose();
    proxy.close();
  }

五、API

5.1 日志输出控制

控制是否打印Log。

 _aliyunHttpDns.enableLog(true).then((_) {
  print("enableLog success");
});

5.2 初始化

初始化配置, 在应用启动时调用。

final _aliyunHttpDns = AliyunHttpDns();
_aliyunHttpDns.init(
    accountId,
    secretKey: secretKey,
    aesSecretKey: aesSecretKey,
    region: "",
    timeout: 2000,
    enableHttps: true,
    enableExpireIp: true,
    enableCacheIp: true,
    enableDegradationLocalDns: true,
    preResolveAfterNetworkChanged: true,
    ipRankingMap: {
    "www.aliyun.com": 80
    },
    sdnsGlobalParam: {
    "aa":"bb",
    "cc":"dd"
    },
    bizTags: ["hh", "gg"]
    ).then((_) => {
      print("init success")
    });

参数:

参数名

类型

是否必须

功能

支持平台

参数名

类型

是否必须

功能

支持平台

accountId

String

必选参数

Account ID

Android/iOS

secretKey

String

可选参数

加签密钥

Android/iOS

aesSecretKey

String

可选参数

加密密钥

Android/iOS

region

String

可选参数

解析节点

Android/iOS

timeout

int

可选参数

解析超时时间

Android/iOS

enableHttps

bool

可选参数

是否使用HTTPS

Android/iOS

enableExpireIp

bool

可选参数

是否使用过期IP

Android/iOS

enableCacheIp

bool

可选参数

是否缓存IP

Android/iOS

enableDegradationLocalDns

bool

可选参数

是否使用LocalDns

Android/iOS

preResolveAfterNetworkChanged

bool

可选参数

是否在网络变化时,刷新缓存

Android/iOS

ipRankingMap

Map

可选参数

IP优选列表

Android/iOS

sdnsGlobalParam

Map

可选参数

sdns全局参数

Android/iOS

bizTags

List

可选参数

bizTags

Android

5.3 域名解析

解析指定域名。

Future<void> _resolve() async {
  final _aliyunHttpDns = AliyunHttpDns();
  _result = await _aliyunHttpDns.resolve(accountId, "www.aliyun.com", kRequestIpv4AndIpv6);
}

参数:

参数名

类型

是否必须

功能

参数名

类型

是否必须

功能

accountId

String

必选参数

Account ID

host

String

必选参数

域名

requestIpType

String

必选参数

请求IP类型, 可选值: kRequestIpv4, kRequestIpv6, kRequestIpv4AndIpv6

params

Map<String, String>

可选参数

自定义解析参数,如果要使用自定义解析,cacheKey是必传的

cacheKey

String

可选参数

缓存Key

返回JSON字段说明:

字段名

类型

功能

字段名

类型

功能

host

String

域名

ipv4

String

v4类型的ip列表 示例: ["1.1.1.1","2.2.2.2"]

ipv6

String

v6类型的ip列表 示例: ["1:1:1:1:1:1:1:1","2:2:2:2:2:2:2:2"]

extra

String

ttl

int

过期时间

5.4 预解析域名

预解析域名, 解析后缓存在SDK中,下次解析时直接从缓存中获取,提高解析速度。

final _aliyunHttpDns = AliyunHttpDns();
_aliyunHttpDns.setPreResolveHosts(accountId, ["www.aliyun.com","www.example.com"]).then((_) {
  print("preResolveHosts success");
});

参数:

参数名

类型

是否必须

功能

参数名

类型

是否必须

功能

accountId

String

必选参数

Account ID

hosts

List

必选参数

预解析域名列表

requestIpType

String

可选参数

请求IP类型, 可选值: kRequestIpv4, kRequestIpv6, kRequestIpv4AndIpv6

5.5 校正签名时间

校正客户端时间戳, 防止解析签名失败。

final _aliyunHttpDns = AliyunHttpDns();
 _aliyunHttpDns.setAuthCurrentTime(accountId, 1640000000).then((_) {
  print("设置auth time success");
});

参数说明:

参数名

类型

是否必须

功能

参数名

类型

是否必须

功能

accountId

String

必选参数

Account ID

currentTime

String

必选参数

正确的时间戳, 单位为秒

5.6 获取SessionId

获取SessionId, 用于排查追踪问题。

final _aliyunHttpDns = AliyunHttpDns();
_aliyunHttpDns.getSessionId(accountId).then((sessionId) => {
  print("SessionId = $sessionId")
});

参数说明:

参数名

类型

是否必须

功能

参数名

类型

是否必须

功能

accountId

String

必选参数

Account ID

  • 本页导读 (0)
  • 一、快速入门
  • 1.1 开通服务
  • 1.2 获取配置
  • 二、安装
  • iOS 平台指定阿里云仓库
  • 三、配置和使用
  • 3.1 初始化配置
  • 3.2 域名解析
  • 3.2.2 域名解析
  • 四、Flutter最佳实践
  • 4.1 原理说明
  • 4.2 示例说明
  • 五、API
  • 5.1 日志输出控制
  • 5.2 初始化
  • 5.3 域名解析
  • 5.4 预解析域名
  • 5.5 校正签名时间
  • 5.6 获取SessionId
AI助理

点击开启售前

在线咨询服务

你好,我是AI助理

可以解答问题、推荐解决方案等