本文介绍如何在Flutter应用开发中集成使用HTTPDNS。
Flutter是Google开源的应用开发框架,仅通过一套代码库,就能构建精美的、原生平台编译的多平台应用。
我们提供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 原理说明
在应用中创建本地http代理服务
需要使用httpdns解析的请求,配置走本地代理
在代理中获取请求域名,通过httpdns解析为IP
在代理中创建指定IP的socket连接,用于发起真正的请求,从而避免走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