🔍 Android 网络有效性检测(Network Validation)实现机制
核心架构
┌──────────────────────────────────────┐
│ ConnectivityService (CS) │
│ • 网络连接后触发验证 │
│ • 接收验证结果 │
└──────────┬───────────────────────────┘
│ 创建并通信
▼
┌──────────────────────────────────────┐
│ NetworkMonitor (NM) │ ◄─ 独立进程(NetworkStack)
│ • 执行多种探测 │
│ • 判断网络可用性 │
│ • 检测Captive Portal │
└──────────┬───────────────────────────┘
│ 5种探测
┌──────┴──────┬─────────┬──────────┬───────┐
▼ ▼ ▼ ▼ ▼
┌──────┐ ┌──────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ DNS │ │ HTTPS │ │ HTTP │ │Fallback│ │PrivDNS │
│ 探测 │ │ 探测 │ │ 探测 │ │ 探测 │ │ 探测 │
└──────┘ └──────────┘ └────────┘ └────────┘ └────────┘
1️⃣ 五种探测类型
/**
* The overall validation result for the Network being reported on.
*
* <p>The possible values for this key are:
* {@link #NETWORK_VALIDATION_RESULT_INVALID},
* {@link #NETWORK_VALIDATION_RESULT_VALID},
* {@link #NETWORK_VALIDATION_RESULT_PARTIALLY_VALID},
* {@link #NETWORK_VALIDATION_RESULT_SKIPPED}.
*
* @see android.net.NetworkCapabilities#NET_CAPABILITY_VALIDATED
*/
@NetworkValidationResult
public static final String KEY_NETWORK_VALIDATION_RESULT = "networkValidationResult";
/** DNS probe. */
// TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_DNS
public static final int NETWORK_PROBE_DNS = 0x04;
/** HTTP probe. */
// TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTP
public static final int NETWORK_PROBE_HTTP = 0x08;
/** HTTPS probe. */
// TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_HTTPS;
public static final int NETWORK_PROBE_HTTPS = 0x10;
/** Captive portal fallback probe. */
// TODO: link to INetworkMonitor.NETWORK_VALIDATION_FALLBACK
public static final int NETWORK_PROBE_FALLBACK = 0x20;
/** Private DNS (DNS over TLS) probd. */
// TODO: link to INetworkMonitor.NETWORK_VALIDATION_PROBE_PRIVDNS
public static final int NETWORK_PROBE_PRIVATE_DNS = 0x40;
/** @hide */
@IntDef(
prefix = {"NETWORK_PROBE_"},
value = {
NETWORK_PROBE_DNS,
NETWORK_PROBE_HTTP,
NETWORK_PROBE_HTTPS,
NETWORK_PROBE_FALLBACK,
NETWORK_PROBE_PRIVATE_DNS
})
@Retention(RetentionPolicy.SOURCE)
public @interface NetworkProbe {}
| 探测类型 | 目的 | 标志位 |
|---|---|---|
| DNS Probe | 验证 DNS 解析是否工作 | 0x04 |
| HTTP Probe | 检测 Captive Portal(强制门户) | 0x08 |
| HTTPS Probe | 验证互联网连接性 | 0x10 |
| Fallback Probe | HTTP 备用探测 | 0x20 |
| Private DNS Probe | 验证 DoT (DNS over TLS) | 0x40 |
2️⃣ HTTP/HTTPS 探测 URL
默认探测 URL
/**
* The URL used for HTTP captive portal detection upon a new connection.
* A 204 response code from the server is used for validation.
*
* @hide
*/
public static final String CAPTIVE_PORTAL_HTTP_URL = "captive_portal_http_url";
private static final String DEFAULT_TEST_URL =
"https://connectivitycheck.android.com/generate_204";
public static final String TEST_HOST = "connectivitycheck.gstatic.com";
public static final String HTTP_REQUEST =
"GET /generate_204 HTTP/1.0\r\n" +
"Host: " + TEST_HOST + "\r\n" +
"Connection: keep-alive\r\n\r\n";
关键点:
- HTTP URL:
http://connectivitycheck.gstatic.com/generate_204 - HTTPS URL:
https://www.google.com/generate_204 - 期望响应:
HTTP 204 No Content(表示网络畅通,无内容返回)
204 响应的含义
private void testForCaptivePortal() {
mTestingThread = new Thread(new Runnable() {
public void run() {
// Give time for captive portal to open.
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
}
if (isFinishing() || isDestroyed()) return;
HttpURLConnection urlConnection = null;
int httpResponseCode = 500;
int oldTag = TrafficStats.getAndSetThreadStatsTag(
NetworkStackConstants.TAG_SYSTEM_PROBE);
try {
urlConnection = (HttpURLConnection) mNetwork.openConnection(
new URL(mCm.getCaptivePortalServerUrl()));
urlConnection.setInstanceFollowRedirects(false);
urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
urlConnection.setUseCaches(false);
urlConnection.getInputStream();
httpResponseCode = urlConnection.getResponseCode();
} catch (IOException e) {
loge(e.getMessage());
} finally {
if (urlConnection != null) urlConnection.disconnect();
TrafficStats.setThreadStatsTag(oldTag);
}
if (httpResponseCode == 204) {
done(true);
}
}
});
mTestingThread.start();
}
3️⃣ 验证流程
标准网络(Valid Network)验证
void setNetworkValid(boolean privateDnsProbeSent) {
mNmValidationResult = NETWORK_VALIDATION_RESULT_VALID;
mNmValidationRedirectUrl = null;
int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS;
if (privateDnsProbeSent) {
probesSucceeded |= NETWORK_VALIDATION_PROBE_PRIVDNS;
}
// The probesCompleted equals to probesSucceeded for the case of valid network, so put
// the same value into two different parameter of the method.
setProbesStatus(probesSucceeded, probesSucceeded);
}
成功条件: DNS + HTTPS 探测都成功 → 网络有效
Captive Portal 检测
void setNetworkPortal(String redirectUrl, boolean privateDnsProbeSent) {
setNetworkInvalid(privateDnsProbeSent);
mNmValidationRedirectUrl = redirectUrl;
// Suppose the portal is found when NetworkMonitor probes NETWORK_VALIDATION_PROBE_HTTP
// in the beginning, so the NETWORK_VALIDATION_PROBE_HTTPS hasn't probed yet.
int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTP;
int probesSucceeded = VALIDATION_RESULT_INVALID;
if (privateDnsProbeSent) {
probesCompleted |= NETWORK_VALIDATION_PROBE_PRIVDNS;
}
setProbesStatus(probesCompleted, probesSucceeded);
}
检测逻辑:
- HTTP 探测返回 302/307 重定向 → 检测到 Captive Portal
redirectUrl存储登录页面地址- 系统弹出通知,引导用户登录
部分连接(Partial Connectivity)
void setNetworkPartial() {
mNmValidationResult = NETWORK_VALIDATION_RESULT_PARTIAL;
mNmValidationRedirectUrl = null;
int probesCompleted = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_HTTPS
| NETWORK_VALIDATION_PROBE_FALLBACK;
int probesSucceeded = NETWORK_VALIDATION_PROBE_DNS | NETWORK_VALIDATION_PROBE_FALLBACK;
setProbesStatus(probesCompleted, probesSucceeded);
}
判断条件:
- DNS 成功 + Fallback 成功
- 但 HTTPS 失败
- → 部分可用(例如只能访问内网或特定站点)
4️⃣ 验证结果处理
private void handleNetworkTested(
@NonNull NetworkAgentInfo nai, int testResult, @NonNull String redirectUrl) {
final boolean valid = (testResult & NETWORK_VALIDATION_RESULT_VALID) != 0;
final boolean partial = (testResult & NETWORK_VALIDATION_RESULT_PARTIAL) != 0;
final boolean portal = !TextUtils.isEmpty(redirectUrl);
// If there is any kind of working networking, then the NAI has been evaluated
// once. {@see NetworkAgentInfo#setEvaluated}, which returns whether this is
// the first time this ever happened.
final boolean someConnectivity = (valid || partial || portal);
final boolean becameEvaluated = someConnectivity && nai.setEvaluated();
// Because of b/245893397, if the score is updated when updateCapabilities is called,
// any callback that receives onAvailable for that rematch receives an extra caps
// callback. To prevent that, update the score in the agent so the updates below won't
// see an update to both caps and score at the same time.
// TODO : fix b/245893397 and remove this.
if (becameEvaluated) nai.updateScoreForNetworkAgentUpdate();
if (!valid && shouldIgnoreValidationFailureAfterRoam(nai)) {
// Assume the validation failure is due to a temporary failure after roaming
// and ignore it. NetworkMonitor will continue to retry validation. If it
// continues to fail after the block timeout expires, the network will be
// marked unvalidated. If it succeeds, then validation state will not change.
return;
}
final boolean wasValidated = nai.isValidated();
final boolean wasPartial = nai.partialConnectivity();
final boolean wasPortal = nai.captivePortalDetected();
nai.setPartialConnectivity(partial);
nai.setCaptivePortalDetected(portal);
nai.updateScoreForNetworkAgentUpdate();
final boolean partialConnectivityChanged = (wasPartial != partial);
final boolean portalChanged = (wasPortal != portal);
if (DBG) {
final String logMsg = !TextUtils.isEmpty(redirectUrl)
? " with redirect to " + redirectUrl
: "";
final String statusMsg;
if (valid) {
statusMsg = "passed";
} else if (!TextUtils.isEmpty(redirectUrl)) {
statusMsg = "detected a portal";
} else {
statusMsg = "failed";
}
log(nai.toShortString() + " validation " + statusMsg + logMsg);
}
if (valid != wasValidated) {
final FullScore oldScore = nai.getScore();
nai.setValidated(valid);
updateCapabilities(oldScore, nai, nai.networkCapabilities);
if (valid) {
handleFreshlyValidatedNetwork(nai);
// Clear NO_INTERNET, PRIVATE_DNS_BROKEN, PARTIAL_CONNECTIVITY and
// LOST_INTERNET notifications if network becomes valid.
mNotifier.clearNotification(nai.network.getNetId(),
NotificationType.NO_INTERNET);
mNotifier.clearNotification(nai.network.getNetId(),
NotificationType.LOST_INTERNET);
mNotifier.clearNotification(nai.network.getNetId(),
处理流程:
- 解析结果: valid(有效) / partial(部分) / portal(强制门户)
- 更新状态:
NetworkCapabilities中的NET_CAPABILITY_VALIDATED - 触发回调: 通知应用网络状态变化
- 清除通知: 如果之前有"无网络"通知,则清除
- 网络切换: 如果当前网络失效,自动切换到其他可用网络
5️⃣ Private DNS 验证
/**
* Return whether validation is required for private DNS in strict mode.
* @param nc Network capabilities of the network to test.
*/
public static boolean isPrivateDnsValidationRequired(@NonNull final NetworkCapabilities nc) {
final boolean isVcnManaged = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
&& !nc.hasCapability(NET_CAPABILITY_NOT_VCN_MANAGED);
final boolean isOemPaid = nc.hasCapability(NET_CAPABILITY_OEM_PAID)
&& nc.hasCapability(NET_CAPABILITY_TRUSTED);
final boolean isDefaultCapable = nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
&& nc.hasCapability(NET_CAPABILITY_TRUSTED);
// TODO: Consider requiring validation for DUN networks.
if (nc.hasCapability(NET_CAPABILITY_INTERNET)
&& (isVcnManaged || isOemPaid || isDefaultCapable)) {
return true;
}
// Test networks that also have one of the major transport types are attempting to replicate
// that transport on a test interface (for example, test ethernet networks with
// EthernetManager#setIncludeTestInterfaces). Run validation on them for realistic tests.
// See also comments on EthernetManager#setIncludeTestInterfaces and on TestNetworkManager.
if (nc.hasTransport(TRANSPORT_TEST) && nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED) && (
nc.hasTransport(TRANSPORT_WIFI)
|| nc.hasTransport(TRANSPORT_CELLULAR)
|| nc.hasTransport(TRANSPORT_BLUETOOTH)
|| nc.hasTransport(TRANSPORT_ETHERNET))) {
return true;
}
return false;
}
验证条件:
- 网络具有
INTERNET能力 - 是 VCN 管理 / OEM付费 / 默认可用网络
- → 需要验证 Private DNS (DoT) 可用性
6️⃣ 配置参数
Captive Portal 模式
* When detecting a captive portal, immediately disconnect from the
* network and do not reconnect to that network in the future.
*
* @hide
*/
public static final int CAPTIVE_PORTAL_MODE_AVOID = 2;
/**
* What to do when connecting a network that presents a captive portal.
* Must be one of the CAPTIVE_PORTAL_MODE_* constants above.
*
* The default for this setting is CAPTIVE_PORTAL_MODE_PROMPT.
* @hide
*/
@Readable
public static final String CAPTIVE_PORTAL_MODE = "captive_portal_mode";
/**
* Setting to turn off captive portal detection. Feature is enabled by
* default and the setting needs to be set to 0 to disable it.
*
| 模式 | 值 | 行为 |
|---|---|---|
CAPTIVE_PORTAL_MODE_IGNORE | 0 | 忽略检测 |
CAPTIVE_PORTAL_MODE_PROMPT | 1 | 弹出登录提示(默认) |
CAPTIVE_PORTAL_MODE_AVOID | 2 | 立即断开并避免该网络 |
7️⃣ 超时机制
"http://connectivitycheck.gstatic.com/generate_204";
// TODO: create better separation between radio types and network types
// how long to wait before switching back to a radio's default network
private static final int RESTORE_DEFAULT_NETWORK_DELAY = 1 * 60 * 1000;
// system property that can override the above value
private static final String NETWORK_RESTORE_DELAY_PROP_NAME =
"android.telephony.apn-restore";
// How long to wait before putting up a "This network doesn't have an Internet connection,
// connect anyway?" dialog after the user selects a network that doesn't validate.
private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;
// How long to wait before considering that a network is bad in the absence of any form
// of connectivity (valid, partial, captive portal). If none has been detected after this
// delay, the stack considers this network bad, which may affect how it's handled in ranking
// according to config_networkAvoidBadWifi.
// Timeout in case the "actively prefer bad wifi" feature is on
private static final int ACTIVELY_PREFER_BAD_WIFI_INITIAL_TIMEOUT_MS = 20 * 1000;
// Timeout in case the "actively prefer bad wifi" feature is off
private static final int DEFAULT_EVALUATION_TIMEOUT_MS = 8 * 1000;
// Default to 30s linger time-out, and 5s for nascent network. Modifiable only for testing.
private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
private static final int DEFAULT_LINGER_DELAY_MS = 30_000;
private static final int DEFAULT_NASCENT_DELAY_MS = 5_000;
// Delimiter used when creating the broadcast delivery group for sending
// CONNECTIVITY_ACTION broadcast.
private static final char DELIVERY_GROUP_KEY_DELIMITER = ';';
// The maximum value for the blocking validation result, in milliseconds.
public static final int MAX_VALIDATION_IGNORE_AFTER_ROAM_TIME_MS = 10000;
- 提示延迟: 8秒无验证结果后提示用户
- 评估超时: 8-20秒判定网络为"差"
- Linger 延迟: 30秒保持连接以观察
🎯 总结
Android 网络有效性检测的完整流程:
1. 网络连接建立 (WiFi/移动数据)
↓
2. ConnectivityService 触发 NetworkMonitor
↓
3. NetworkMonitor 执行多种探测:
├─ DNS 解析测试
├─ HTTPS 探测 (验证互联网)
├─ HTTP 探测 (检测 Captive Portal)
├─ Fallback 探测 (备用)
└─ Private DNS 探测 (DoT)
↓
4. 判断结果:
├─ 204 响应 → ✅ VALID (网络有效)
├─ 302/307 → 🌐 CAPTIVE PORTAL (需登录)
├─ DNS成功但HTTPS失败 → ⚠️ PARTIAL (部分可用)
└─ 全部失败 → ❌ INVALID (无效)
↓
5. 更新 NetworkCapabilities
↓
6. 通知应用/触发网络切换
关键特性:
- ✅ 多层验证: 不只依赖单一探测
- ✅ 自动重试: 失败后定期重新验证
- ✅ 用户友好: Captive Portal 自动弹出登录页面
- ✅ 智能切换: 验证失败自动切换到其他网络
- ✅ 隐私保护: 使用 DoT 验证 Private DNS
这就是 Android 确保用户始终能连接到"真正可用"的网络的核心机制! 🚀