本文介紹如何在Flutter應用開發中整合使用HTTPDNS。
Flutter是Google開源的應用開發架構,僅通過一套程式碼程式庫,就能構建精美的、原生平台編譯的多平台應用。
我們提供Flutter架構下的HTTPDNS外掛程式,並展示如何在Flutter常用的網路架構中整合使用HTTPDNS。本外掛程式已在GitHub和pub.dev發布。
以下是外掛程式的使用說明和最佳實務:
一、快速入門
1.1 開通服務
請參考快速入門開通HTTPDNS。
1.2 擷取配置
請參考開發配置在EMAS控制台開發配置中擷取AccountId/SecretKey/AESSecretKey等資訊,用於初始化SDK。
二、安裝配置
2.1 添加Flutter依賴
在您的Flutter專案的pubspec.yaml中加入dependencies:
dependencies:
flutter:
sdk: flutter
aliyun_httpdns: ^1.0.2 # 從pub.dev擷取最新版本號碼
dio: ^5.9.0 # Dio網路程式庫
http: ^1.2.0 # http包
http2: ^2.3.1 # 可選,支援HTTP/2添加依賴之後需要執行一次 flutter pub get。
2.2 原生SDK版本說明
2.2.1 驗證原生SDK版本
外掛程式已整合了對應平台的HTTPDNS原生SDK,目前的版本:
Android:
com.aliyun.ams:alicloud-android-httpdns:2.6.7iOS:
AlicloudHTTPDNS:3.4.0
如需更新SDK版本,請參考下面的版本更新指導。
如果要更新原生SDK版本,請使用GitHub fork或本地源碼方式引入外掛程式,以便修改外掛程式內部的build.gradle/podspec檔案。
2.2.2 更新Android SDK版本
編輯 packages/aliyun_httpdns/android/build.gradle 檔案,修改依賴版本:
dependencies {
implementation 'androidx.annotation:annotation:1.8.0'
// 更新為您需要的版本
implementation 'com.aliyun.ams:alicloud-android-httpdns:2.6.7'
}可用版本請參考:Android SDK發布說明
2.2.3 更新iOS SDK版本
編輯 packages/aliyun_httpdns/ios/aliyun_httpdns.podspec 檔案,修改依賴版本:
Pod::Spec.new do |s|
# ... 其他配置 ...
s.dependency 'Flutter'
# 更新為您需要的版本
s.dependency 'AlicloudHTTPDNS', '3.4.0'
# ... 其他配置 ...
end可用版本請參考:iOS SDK發布說明
2.2.4 重新構建專案
更新版本後,需要重新構建專案:
Android:
flutter clean
flutter pub get
flutter build apk # 或其他構建命令iOS:
flutter clean
flutter pub get
cd ios
pod install
cd ..
flutter build ios三、配置和使用
3.1 初始化配置
應用啟動後,需要先初始化外掛程式,才能調用HTTPDNS能力。 初始化主要是配置AccountId/SecretKey等資訊及功能開關。 範例程式碼如下:
// 初始化 HTTPDNS
await AliyunHttpdns.init(
accountId: '您的AccountId',
secretKey: '您的SecretKey',
);
// 設定功能選項
await AliyunHttpdns.setHttpsRequestEnabled(true);
await AliyunHttpdns.setLogEnabled(true);
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
await AliyunHttpdns.setReuseExpiredIPEnabled(true);
await AliyunHttpdns.setPreResolveAfterNetworkChanged(true);
await AliyunHttpdns.setIPRankingList({
'www.aliyun.com': 443,
});
// 構建服務
await AliyunHttpdns.build();
// 設定預解析網域名稱
await AliyunHttpdns.setPreResolveHosts(['www.aliyun.com'], ipType: 'both');
print("init success");setHttpsRequestEnabled參數設定為true後,計費會增加,請仔細閱讀產品計費文檔。
如果您對網域名稱資訊或SDNS參數有更高的安全訴求,可以配置aesSecretKey參數啟用對解析請求進行內容層加密,使用內容加密後計費會增加,請仔細閱讀產品計費文檔。
3.1.1 日誌配置
應用開發過程中,如果要輸出HTTPDNS的日誌,可以調用日誌輸出控制方法,開啟日誌,範例程式碼如下:
await AliyunHttpdns.setLogEnabled(true);
print("enableLog success");3.1.2 sessionId記錄
應用在運行過程中,可以調用擷取SessionId方法擷取sessionId,記錄到應用的資料擷取系統中。 sessionId用於表示標識一次應用運行,線上排查時,可以用於查詢應用一次運行過程中的解析日誌,範例程式碼如下:
final sessionId = await AliyunHttpdns.getSessionId();
print("SessionId = $sessionId");3.2 網域名稱解析
3.2.1 預解析
當需要提前解析網域名稱時,可以調用預解析網域名稱方法,範例程式碼如下:
await AliyunHttpdns.setPreResolveHosts(["www.aliyun.com", "www.example.com"], ipType: 'both');
print("preResolveHosts success");調用之後,外掛程式會發起網域名稱解析,並把結果緩衝到記憶體,用於後續請求時直接使用。
3.2.2 網域名稱解析
當需要解析網域名稱時,可以通過調用網域名稱解析方法解析網域名稱擷取IP,範例程式碼如下:
Future<void> _resolve() async {
final res = await AliyunHttpdns.resolveHostSyncNonBlocking('www.aliyun.com', ipType: 'both');
final ipv4List = res['ipv4'] ?? [];
final ipv6List = res['ipv6'] ?? [];
print('IPv4: $ipv4List');
print('IPv6: $ipv6List');
}四、Flutter最佳實務
4.1 原理說明
本樣本展示了一種更直接的整合方式,通過自訂HTTP用戶端適配器來實現HTTPDNS整合:
建立自訂的HTTP用戶端適配器,攔截網路請求
在適配器中調用HTTPDNS外掛程式解析網域名稱為IP地址
使用解析得到的IP地址建立直接的Socket串連
對於HTTPS串連,確保正確設定SNI(Server Name Indication)為原始網域名稱
這種方式避免了建立本地代理服務的複雜性,直接在HTTP用戶端層面整合HTTPDNS功能。
4.2 樣本說明
本樣本提供了一個完整的Flutter應用,展示如何整合HTTPDNS功能。
4.2.1 自訂HTTP用戶端適配器實現
自訂配接器的實現請參考 lib/net/httpdns_http_client_adapter.dart 檔案。本方案由EMAS團隊設計實現,參考請註明出處。 適配器內部會攔截HTTP請求,調用HTTPDNS進行網域名稱解析,並使用解析後的IP建立socket串連。
本樣本支援三種網路程式庫:Dio、HttpClient、http包。代碼如下:
import 'dart:io';
import 'package:dio/io.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
import 'package:flutter/foundation.dart';
import 'package:aliyun_httpdns/aliyun_httpdns.dart';
// Dio 適配器
IOHttpClientAdapter buildHttpdnsHttpClientAdapter() {
final HttpClient client = HttpClient();
_configureHttpClient(client);
_configureConnectionFactory(client);
final IOHttpClientAdapter adapter = IOHttpClientAdapter(createHttpClient: () => client)
..validateCertificate = (cert, host, port) => true;
return adapter;
}
// 原生 HttpClient
HttpClient buildHttpdnsNativeHttpClient() {
final HttpClient client = HttpClient();
_configureHttpClient(client);
_configureConnectionFactory(client);
return client;
}
// http 包適配器
http.Client buildHttpdnsHttpPackageClient() {
final HttpClient httpClient = buildHttpdnsNativeHttpClient();
return IOClient(httpClient);
}
// HttpClient 基礎配置
void _configureHttpClient(HttpClient client) {
client.findProxy = (Uri _) => 'DIRECT';
client.idleTimeout = const Duration(seconds: 90);
client.maxConnectionsPerHost = 8;
}
// 配置基於 HTTPDNS 的串連工廠
// 本方案由EMAS團隊設計實現,參考請註明出處。
void _configureConnectionFactory(HttpClient client) {
client.connectionFactory = (Uri uri, String? proxyHost, int? proxyPort) async {
final String domain = uri.host;
final bool https = uri.scheme.toLowerCase() == 'https';
final int port = uri.port == 0 ? (https ? 443 : 80) : uri.port;
final List<InternetAddress> targets = await _resolveTargets(domain);
final Object target = targets.isNotEmpty ? targets.first : domain;
if (!https) {
return Socket.startConnect(target, port);
}
// HTTPS:先 TCP,再 TLS(SNI=網域名稱),並保持可取消
bool cancelled = false;
final Future<ConnectionTask<Socket>> rawStart = Socket.startConnect(target, port);
final Future<Socket> upgraded = rawStart.then((task) async {
final Socket raw = await task.socket;
if (cancelled) {
raw.destroy();
throw const SocketException('Connection cancelled');
}
final SecureSocket secure = await SecureSocket.secure(
raw,
host: domain, // 重要:使用原始網域名稱作為SNI
);
if (cancelled) {
secure.destroy();
throw const SocketException('Connection cancelled');
}
return secure;
});
return ConnectionTask.fromSocket(
upgraded,
() {
cancelled = true;
try {
rawStart.then((t) => t.cancel());
} catch (_) {}
},
);
};
}
// 通過 HTTPDNS 解析目標 IP 列表
Future<List<InternetAddress>> _resolveTargets(String domain) async {
try {
final res = await AliyunHttpdns.resolveHostSyncNonBlocking(domain, ipType: 'both');
final List<String> ipv4 = res['ipv4'] ?? [];
final List<String> ipv6 = res['ipv6'] ?? [];
final List<InternetAddress> targets = [
...ipv4.map(InternetAddress.tryParse).whereType<InternetAddress>(),
...ipv6.map(InternetAddress.tryParse).whereType<InternetAddress>(),
];
if (targets.isEmpty) {
debugPrint('[dio] HTTPDNS no result for $domain, fallback to system DNS');
} else {
debugPrint('[dio] HTTPDNS resolved $domain -> ${targets.first.address}');
}
return targets;
} catch (e) {
debugPrint('[dio] HTTPDNS resolve failed: $e, fallback to system DNS');
return const <InternetAddress>[];
}
}4.2.2 適配器整合和使用
適配器的整合請參考 lib/main.dart 檔案。 首先需要初始化HTTPDNS,然後配置網路程式庫使用自訂配接器,範例程式碼如下:
class _MyHomePageState extends State<MyHomePage> {
late final Dio _dio;
late final HttpClient _httpClient;
late final http.Client _httpPackageClient;
@override
void initState() {
super.initState();
// 初始化 HTTPDNS
_initHttpDnsOnce();
// 配置網路程式庫使用 HTTPDNS 適配器
_dio = Dio();
_dio.httpClientAdapter = buildHttpdnsHttpClientAdapter();
_dio.options.headers['Connection'] = 'keep-alive';
_httpClient = buildHttpdnsNativeHttpClient();
_httpPackageClient = buildHttpdnsHttpPackageClient();
}
Future<void> _initHttpDnsOnce() async {
try {
await AliyunHttpdns.init(
accountId: 139450,
secretKey: '您的SecretKey',
);
await AliyunHttpdns.setHttpsRequestEnabled(true);
await AliyunHttpdns.setLogEnabled(true);
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
await AliyunHttpdns.setReuseExpiredIPEnabled(true);
await AliyunHttpdns.build();
// 設定預解析網域名稱
await AliyunHttpdns.setPreResolveHosts(['www.aliyun.com'], ipType: 'both');
} catch (e) {
debugPrint('[httpdns] init failed: $e');
}
}
}使用配置好的網路程式庫發起請求時,會自動使用HTTPDNS進行網域名稱解析:
// 使用 Dio
final response = await _dio.get('https://www.aliyun.com');
// 使用 HttpClient
final request = await _httpClient.getUrl(Uri.parse('https://www.aliyun.com'));
final response = await request.close();
// 使用 http 包
final response = await _httpPackageClient.get(Uri.parse('https://www.aliyun.com'));4.2.3 資源清理
在組件銷毀時,記得清理相關資源:
@override
void dispose() {
_urlController.dispose();
_httpClient.close();
_httpPackageClient.close();
super.dispose();
}五、API
5.1 日誌輸出控制
控制是否列印Log。
await AliyunHttpdns.setLogEnabled(true);
print("enableLog success");5.2 初始化
初始化配置, 在應用啟動時調用。
// 基礎初始化
await AliyunHttpdns.init(
accountId: 139450,
secretKey: 'your_secret_key',
aesSecretKey: 'your_aes_secret_key', // 可選
);
// 配置功能選項
await AliyunHttpdns.setHttpsRequestEnabled(true);
await AliyunHttpdns.setLogEnabled(true);
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
await AliyunHttpdns.setReuseExpiredIPEnabled(true);
await AliyunHttpdns.setPreResolveAfterNetworkChanged(true);
await AliyunHttpdns.setIPRankingList({
'www.aliyun.com': 443,
});
// 構建服務執行個體
await AliyunHttpdns.build();
print("init success");初始化參數:
參數名 | 類型 | 是否必須 | 功能 | 支援平台 |
accountId | int | 必選參數 | Account ID | Android/iOS |
secretKey | String | 選擇性參數 | 加簽密鑰 | Android/iOS |
aesSecretKey | String | 選擇性參數 | 加密金鑰 | Android/iOS |
功能配置方法:
setHttpsRequestEnabled(bool)- 設定是否使用HTTPS解析鏈路setLogEnabled(bool)- 設定是否開啟日誌setPersistentCacheIPEnabled(bool)- 設定是否開啟持久化緩衝setReuseExpiredIPEnabled(bool)- 設定是否允許複用到期IPsetPreResolveAfterNetworkChanged(bool)- 設定網路切換時是否自動重新整理解析setIPRankingList(hostPortMap)- 設定IP優選網域名稱列表
setHttpsRequestEnabled參數設定為true後,計費會增加,請仔細閱讀產品計費文檔。
如果您對網域名稱資訊或SDNS參數有更高的安全訴求,可以配置aesSecretKey參數啟用對解析請求進行內容層加密,使用內容加密後計費會增加,請仔細閱讀產品計費文檔。
5.3 網域名稱解析
解析指定網域名稱。
Future<void> _resolve() async {
final res = await AliyunHttpdns.resolveHostSyncNonBlocking(
'www.aliyun.com',
ipType: 'both', // 'auto', 'ipv4', 'ipv6', 'both'
);
final ipv4List = res['ipv4'] ?? [];
final ipv6List = res['ipv6'] ?? [];
print('IPv4: $ipv4List');
print('IPv6: $ipv6List');
}參數:
參數名 | 類型 | 是否必須 | 功能 |
hostname | String | 必選參數 | 要解析的網域名稱 |
ipType | String | 選擇性參數 | 請求IP類型: 'auto', 'ipv4', 'ipv6', 'both' |
返回資料結構:
欄位名 | 類型 | 功能 |
ipv4 | List | IPv4地址清單,如: ["1.1.1.1", "2.2.2.2"] |
ipv6 | List | IPv6地址清單,如: ["::1", "::2"] |
5.4 預解析網域名稱
預解析網域名稱, 解析後緩衝在SDK中,下次解析時直接從緩衝中擷取,提高解析速度。
await AliyunHttpdns.setPreResolveHosts(
["www.aliyun.com"],
ipType: 'both'
);
print("preResolveHosts success");參數:
參數名 | 類型 | 是否必須 | 功能 |
hosts | List | 必選參數 | 預解析網域名稱列表 |
ipType | String | 選擇性參數 | 請求IP類型: 'auto', 'ipv4', 'ipv6', 'both' |
5.5 擷取SessionId
擷取SessionId, 用於排查追蹤問題。
final sessionId = await AliyunHttpdns.getSessionId();
print("SessionId = $sessionId");無需參數,直接返回當前會話ID。
5.6 清除緩衝
清除所有DNS解析緩衝。
await AliyunHttpdns.cleanAllHostCache();
print("緩衝清除成功");5.7 網路變化時自動重新整理預解析
設定在網路環境變化時是否自動重新整理預解析網域名稱的緩衝。
await AliyunHttpdns.setPreResolveAfterNetworkChanged(true);
print("網路變化自動重新整理已啟用");5.8 持久化緩衝配置
設定是否開啟持久化緩衝功能。開啟後,SDK 會將解析結果儲存到本地,App 重啟後可以從本地載入緩衝。
await AliyunHttpdns.setPersistentCacheIPEnabled(true);
await AliyunHttpdns.setPersistentCacheIPEnabled(
true,
discardExpiredAfterSeconds: 86400 // 選擇性參數
);
print("持久化緩衝已開啟");參數:
參數名 | 類型 | 是否必須 | 功能 |
enabled | bool | 必選參數 | 是否開啟持久化緩衝 |
discardExpiredAfterSeconds | int | 選擇性參數 | 到期時間閾值(秒),App 啟動時會丟棄到期超過此時間長度的緩衝記錄 |
5.9 IP 優選
設定需要進行 IP 優選的網域名稱列表。開啟後,SDK 會對解析返回的 IP 列表進行 TCP 測速並排序,以保證第一個 IP 是可用性最好的 IP。
await AliyunHttpdns.setIPRankingList({
'www.aliyun.com': 443,
});
print("IP 優選配置成功");參數:
參數名 | 類型 | 是否必須 | 功能 |
hostPortMap | Map<String, int> | 必選參數 | 網域名稱和連接埠的映射,如:{'www.aliyun.com': 443} |
六、舊版SDK升級到1.0.0以上指南
1.0.0 版本進行了全面的架構重構和介面最佳化。這次升級旨在:
統一配置模式:從分散的運行時配置改為兩階段初始化模式(init + build),解決配置時序問題,提升 SDK 穩定性
標準化解析介面:重新設計解析介面架構,提供統一的
resolveHostSyncNonBlocking方法,返回結構化資料而非 JSON 字串靜態方法設計:從單例模式改為靜態方法調用,簡化使用方式,無需建立執行個體
效能最佳化:通過介面最佳化和內部實現改進,提升解析效能和資源利用效率
這是一次不相容的重大變更,雖然變更較大,但您只需要修改應用中實際使用的介面。通過下面的升級步驟和新舊 API 映射表,您可以系統性地完成這次重要升級。
升級步驟詳解
1. 更新依賴版本
pubspec.yaml
dependencies:aliyun_httpdns: ^1.0.0執行更新:
flutter pub upgrade aliyun_httpdns2. 重構初始化代碼
升級前
// 舊版本:單例模式 + 一步初始化
final _aliyunHttpDns = AliyunHttpDns();
await _aliyunHttpDns.init(
"YOUR_ACCOUNT_ID", // String 類型
secretKey: "your_secret_key",
aesSecretKey: "your_aes_key",
region: "",
timeout: 2000,
enableHttps: true,
enableExpireIp: true,
enableCacheIp: true,
enableDegradationLocalDns: true,
preResolveAfterNetworkChanged: true,
ipRankingMap: {"www.aliyun.com": 80},
sdnsGlobalParam: {"aa": "bb"},
bizTags: ["tag1", "tag2"]
);升級後
// 新版本:靜態方法 + 兩階段初始化
// 第一階段:初始化基本配置
await AliyunHttpdns.init(
accountId: your_account_id, // int 類型,必須設定
secretKey: "your_secret_key", // 可選
aesSecretKey: "your_aes_key", // 可選
);
// 第二階段:設定功能選項
await AliyunHttpdns.setHttpsRequestEnabled(true); // 替代 enableHttps
await AliyunHttpdns.setLogEnabled(true); // 替代 enableLog
await AliyunHttpdns.setPersistentCacheIPEnabled(true); // 替代 enableCacheIp
await AliyunHttpdns.setReuseExpiredIPEnabled(true); // 替代 enableExpireIp
await AliyunHttpdns.setPreResolveAfterNetworkChanged(true); // 替代 preResolveAfterNetworkChanged
// 第三階段:構建服務(必須調用)
await AliyunHttpdns.build();說明:
移除了
region、timeout、enableDegradationLocalDns、ipRankingMap、sdnsGlobalParam、bizTags等參數新增
build()方法,必須在配置完成後調用所有方法改為靜態方法,無需建立執行個體
3. 更新解析介面
升級前
// 同步非阻塞,返回 JSON 字串
String result = await _aliyunHttpDns.resolve(
"YOUR_ACCOUNT_ID", // accountId
"www.aliyun.com", // host
kRequestIpv4AndIpv6, // requestIpType
);
// 需要手動解析 JSON
Map<String, dynamic> map = json.decode(result);
List<String> ipv4s = List<String>.from(map['ipv4'] ?? []);
List<String> ipv6s = List<String>.from(map['ipv6'] ?? []);
// 使用第一個 IP
String ip = ipv4s.isNotEmpty ? ipv4s.first : '';升級後
// 同步非阻塞,返回結構化資料
Map<String, List<String>> result = await AliyunHttpdns.resolveHostSyncNonBlocking(
'www.aliyun.com', // hostname(無需 accountId)
ipType: 'both', // 替代 kRequestIpv4AndIpv6
);
// 直接擷取 IP 列表
List<String> ipv4s = result['ipv4'] ?? [];
List<String> ipv6s = result['ipv6'] ?? [];
// 使用第一個 IP
String ip = ipv4s.isNotEmpty ? ipv4s.first : '';自訂解析參數:
// 升級前
String result = await _aliyunHttpDns.resolve(
"YOUR_ACCOUNT_ID",
"www.aliyun.com",
kRequestIpv4AndIpv6,
params: {"key": "value"},
cacheKey: "custom_key"
);
// 升級後
Map<String, List<String>> result = await AliyunHttpdns.resolveHostSyncNonBlocking(
'www.aliyun.com',
ipType: 'both',
sdnsParams: {"key": "value"}, // 參數名變更
cacheKey: "custom_key"
);4. 更新預解析介面
升級前
await _aliyunHttpDns.setPreResolveHosts(
"YOUR_ACCOUNT_ID", // accountId
["www.aliyun.com"],
kRequestIpv4AndIpv6 // requestIpType
);升級後
await AliyunHttpdns.setPreResolveHosts(
["www.aliyun.com"], // 無需 accountId
ipType: 'both' // 替代 kRequestIpv4AndIpv6
);5. 更新日誌配置
升級前
await _aliyunHttpDns.enableLog(true);升級後
await AliyunHttpdns.setLogEnabled(true);6. 更新 SessionId 擷取
升級前
String sessionId = await _aliyunHttpDns.getSessionId("YOUR_ACCOUNT_ID");升級後
String? sessionId = await AliyunHttpdns.getSessionId();7. 新增功能使用
清除緩衝
await AliyunHttpdns.cleanAllHostCache();持久化緩衝配置
await AliyunHttpdns.setPersistentCacheIPEnabled(true);若您的應用未使用到不相容的 API,可以不用處理。
API 升級映射表
介面分類 | 升級前 | 升級後 |
建立執行個體 |
| 無需建立,直接使用靜態方法 |
初始化 |
|
|
構建服務 | 無 |
|
啟用 HTTPS 請求 |
|
|
啟用到期 IP |
|
|
開啟本機快取 |
|
|
網路切換預解析 |
|
|
控制日誌輸出 |
|
|
同步非阻塞解析 |
|
|
IPv4 解析 |
|
|
IPv6 解析 |
|
|
IPv4 和 IPv6 解析 |
|
|
自動選擇 | 無 |
|
自訂解析參數 |
|
|
設定預解析網域名稱 |
|
|
擷取 SessionId |
|
|
清除緩衝 | 無 |
|
校正簽章時間 |
| 已移除 |