[Android][WIFI]手機作AP,關(guān)閉移動網(wǎng)絡(luò)后,STA端斷開重連問題分析
背景描述
測試平臺
Android版本:Android P(9.0)
復(fù)現(xiàn)步驟
- 準(zhǔn)備兩臺移動設(shè)備,一臺作為AP,一臺作為STA;
- 作為AP的設(shè)備具備移動網(wǎng)絡(luò)上網(wǎng),熱點網(wǎng)絡(luò)分享能力;
- 打開作為AP的設(shè)備的移動網(wǎng)絡(luò)流量開關(guān),再打開熱點;
- 作為STA的設(shè)備接入該AP,并等待其通路判斷完成,確保Internet網(wǎng)絡(luò)訪問能力正常;
- 關(guān)閉作為AP的設(shè)備的移動網(wǎng)絡(luò)流量開關(guān);
- 觀察
期望結(jié)果
STA端網(wǎng)絡(luò)保持連接,狀態(tài)變更為無Internet訪問能力提示
實際結(jié)果
STA端斷開,并隨后自動重連成功,狀態(tài)變更為無Internet訪問能力提示
問題分析
WifiStateMachine
打開STA端WiFi Verbose log后,抓取日志,首先確認(rèn)斷開的原因:
01-04 10:01:38.920 24751 24837 D WifiStateMachine: ConnectedState !CMD_IP_CONFIGURATION_LOST rt=2214117/2214117 0 0 failures: 0/9 7e:d2:c5:43:c8:03 bcn=0
01-04 10:01:38.920 24751 24837 D WifiStateMachine: L2ConnectedState !CMD_IP_CONFIGURATION_LOST rt=2214117/2214117 0 0 failures: 0/9 7e:d2:c5:43:c8:03 bcn=0
...
01-04 10:01:38.921 25250 25250 D wpa_supplicant: wlan0: Request to deauthenticate - bssid=7e:d2:c5:43:c8:03 pending_bssid=00:00:00:00:00:00 reason=3 (DEAUTH_LEAVING) state=COMPLETED
發(fā)現(xiàn)為主動斷開,而WifiStateMachine
顯示狀態(tài)機在L2ConnectedState
處理了CMD_IP_CONFIGURATION_LOST
消息;
結(jié)合代碼來看,WifiStateMachine
在L2ConnectedState
中處理CMD_IP_CONFIGURATION_LOST
消息的邏輯中包含handleIpConfigurationLost()
方法,后者會調(diào)用WifiNative.disconnect()
發(fā)起斷開請求:
//frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java
private void handleIpConfigurationLost() {
mWifiInfo.setInetAddress(null);
mWifiInfo.setMeteredHint(false);
mWifiConfigManager.updateNetworkSelectionStatus(mLastNetworkId,
WifiConfiguration.NetworkSelectionStatus.DISABLED_DHCP_FAILURE);
/* DHCP times out after about 30 seconds, we do a
* disconnect thru supplicant, we will let autojoin retry connecting to the network
*/
mWifiNative.disconnect(mInterfaceName);
}
...
class L2ConnectedState extends State {
...
@Override
public boolean processMessage(Message message) {
...
switch (message.what) {
...
case CMD_IP_CONFIGURATION_LOST:
// Get Link layer stats so that we get fresh tx packet counters.
getWifiLinkLayerStats();
handleIpConfigurationLost();
reportConnectionAttemptEnd(
WifiMetrics.ConnectionEvent.FAILURE_DHCP,
WifiMetricsProto.ConnectionEvent.HLF_NONE);
transitionTo(mDisconnectingState);
break;
...
}
return HANDLED;
}
}
而發(fā)送發(fā)送CMD_IP_CONFIGURATION_LOST
消息是由構(gòu)造IpClient
時傳遞的IpClientCallback
實例對象回調(diào)過來的:
//frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java
private IpClient mIpClient;
...
private FrameworkFacade mFacade;
...
class IpClientCallback extends IpClient.Callback {
...
@Override
public void onProvisioningFailure(LinkProperties newLp) {
mWifiMetrics.logStaEvent(StaEvent.TYPE_CMD_IP_CONFIGURATION_LOST);
sendMessage(CMD_IP_CONFIGURATION_LOST);
}
...
}
...
private void setupClientMode() {
...
mIpClient = mFacade.makeIpClient(mContext, mInterfaceName, new IpClientCallback());
mIpClient.setMulticastFilter(true);
...
}
到這里,基本可以確定,是IpClient
側(cè)的邏輯,觸發(fā)了WifiStateMachine.IpClientCallback
的onProvisioningFailure()
回調(diào)方法,導(dǎo)致STA端WLAN斷開;
接下來跳轉(zhuǎn)到IpClient
繼續(xù)分析:
IpClient
IpClient
側(cè)通常日志較少,需要添加日志:
public class IpClient extends StateMachine {
private static final boolean DBG = true;
...
private void configureAndStartStateMachine() {
...
setInitialState(mStoppedState);
setDbg("wlan0".equals(mInterfaceName));
...
}
...
}
復(fù)現(xiàn)后可知:
01-04 10:01:38.918 24751 25461 D IpClient.wlan0: handleMessage: E msg.what=6
...
01-04 10:01:38.918 24751 25461 D IpClient.wlan0: processMsg: RunningState
01-04 10:01:38.919 24751 25461 D IpClient.wlan0: Netlink-seen LPs: {InterfaceName: wlan0 LinkAddresses: [fe80::ce88:26ff:fefb:a765/64,192.168.98.129/24,240e:476:bbc2:3fcf:ce88:26ff:fefb:a765/64,240e:476:bbc2:3fcf:40ae:51e9:8141:84b7/64,] Routes: [fe80::/64 -> :: wlan0,] DnsAddresses: [240e:476:bbc2:3fcf::e6,] UsePrivateDns: false PrivateDnsServerName: null Domains: null MTU: 0}, new LPs: {InterfaceName: wlan0 LinkAddresses: [fe80::ce88:26ff:fefb:a765/64,192.168.98.129/24,240e:476:bbc2:3fcf:ce88:26ff:fefb:a765/64,240e:476:bbc2:3fcf:40ae:51e9:8141:84b7/64,] Routes: [fe80::/64 -> :: wlan0,192.168.98.0/24 -> 0.0.0.0 wlan0,0.0.0.0/0 -> 192.168.98.27 wlan0,] DnsAddresses: [192.168.98.27,] UsePrivateDns: false PrivateDnsServerName: null Domains: null MTU: 0 TcpBufferSizes: 524288,1048576,2097152,262144,524288,1048576}; old LPs: {InterfaceName: wlan0 LinkAddresses: [fe80::ce88:26ff:fefb:a765/64,192.168.98.129/24,240e:476:bbc2:3fcf:ce88:26ff:fefb:a765/64,240e:476:bbc2:3fcf:40ae:51e9:8141:84b7/64,] Routes: [fe80::/64 -> :: wlan0,::/0 -> fe80::7cd2:c5ff:fe43:c803 wlan0,240e:476:bbc2:3fcf::/64 -> :: wlan0,192.168.98.0/24 -> 0.0.0.0 wlan0,0.0.0.0/0 -> 192.168.98.27 wlan0,] DnsAddresses: [240e:476:bbc2:3fcf::e6,192.168.98.27,] UsePrivateDns: false PrivateDnsServerName: null Domains: null MTU: 0 TcpBufferSizes: 524288,1048576,2097152,262144,524288,1048576}
01-04 10:01:38.920 24751 25461 D IpClient.wlan0: onProvisioningFailure()
第一句日志輸出是在assembleLinkProperties()
方法中:
//frameworks/base/services/net/java/android/net/ip/IpClient.java
private LinkProperties assembleLinkProperties() {
// [1] Create a new LinkProperties object to populate.
LinkProperties newLp = new LinkProperties();
newLp.setInterfaceName(mInterfaceName);
// [2] Pull in data from netlink:
// - IPv4 addresses
// - IPv6 addresses
// - IPv6 routes
// - IPv6 DNS servers
//
// N.B.: this is fundamentally race-prone and should be fixed by
// changing NetlinkTracker from a hybrid edge/level model to an
// edge-only model, or by giving IpClient its own netlink socket(s)
// so as to track all required information directly.
LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties();
newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses());
for (RouteInfo route : netlinkLinkProperties.getRoutes()) {
newLp.addRoute(route);
}
addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers());
// [3] Add in data from DHCPv4, if available.
//
// mDhcpResults is never shared with any other owner so we don't have
// to worry about concurrent modification.
if (mDhcpResults != null) {
for (RouteInfo route : mDhcpResults.getRoutes(mInterfaceName)) {
newLp.addRoute(route);
}
addAllReachableDnsServers(newLp, mDhcpResults.dnsServers);
newLp.setDomains(mDhcpResults.domains);
if (mDhcpResults.mtu != 0) {
newLp.setMtu(mDhcpResults.mtu);
}
}
// [4] Add in TCP buffer sizes and HTTP Proxy config, if available.
if (!TextUtils.isEmpty(mTcpBufferSizes)) {
newLp.setTcpBufferSizes(mTcpBufferSizes);
}
if (mHttpProxy != null) {
newLp.setHttpProxy(mHttpProxy);
}
// [5] Add data from InitialConfiguration
if (mConfiguration != null && mConfiguration.mInitialConfig != null) {
InitialConfiguration config = mConfiguration.mInitialConfig;
// Add InitialConfiguration routes and dns server addresses once all addresses
// specified in the InitialConfiguration have been observed with Netlink.
if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) {
for (IpPrefix prefix : config.directlyConnectedRoutes) {
newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName));
}
}
addAllReachableDnsServers(newLp, config.dnsServers);
}
final LinkProperties oldLp = mLinkProperties;
if (DBG) {
Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s",
netlinkLinkProperties, newLp, oldLp));
}
// TODO: also learn via netlink routes specified by an InitialConfiguration and specified
// from a static IP v4 config instead of manually patching them in in steps [3] and [5].
return newLp;
}
結(jié)合狀態(tài)機的日志,可知是RunningState
處理EVENT_NETLINK_LINKPROPERTIES_CHANGED
消息時調(diào)用的assembleLinkProperties()
方法:
//frameworks/base/services/net/java/android/net/ip/IpClient.java
...
class RunningState extends State {
private ConnectivityPacketTracker mPacketTracker;
private boolean mDhcpActionInFlight;
...
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
...
case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
if (!handleLinkPropertiesUpdate(SEND_CALLBACKS)) {
transitionTo(mStoppingState);
}
break;
...
}
...
}
}
結(jié)合代碼分析,可知這部分邏輯應(yīng)該是這樣的:
- AP端關(guān)閉移動網(wǎng)絡(luò)流量開關(guān)后,STA端IPV6地址與路由表發(fā)生改變;
-
NetlinkTracker
從Netd
接收到這些改變的事件,并通過NetlinkTracker.Callback.update()
回調(diào)給到IpClient.mNetlinkTracker
; -
IpClient.mNetlinkTracker
收到update()
事件回調(diào)后,向IpClient
狀態(tài)機發(fā)送EVENT_NETLINK_LINKPROPERTIES_CHANGED
消息; -
IpClient
如果此時處于RunningState
,那么在處理EVENT_NETLINK_LINKPROPERTIES_CHANGED
消息時,會觸發(fā)onProvisioningFailure()
方法回調(diào); -
onProvisioningFailure()
方法回調(diào)會通過構(gòu)造時注冊進來的IpClient.Callback
回調(diào)實例,通知到WifiStateMachine
,后者會執(zhí)行斷開邏輯;
梳理完了整個流程,接下來就需要分析上面的第4步中最后一個疑點——onProvisioningFailure()
為何會執(zhí)行;
前面已經(jīng)分析到了: IpClient
在RunningState
下處理EVENT_NETLINK_LINKPROPERTIES_CHANGED
消息會調(diào)用handleLinkPropertiesUpdate()
方法,參數(shù)為SEND_CALLBACKS
,恒為true
;
//frameworks/base/services/net/java/android/net/ip/IpClient.java
private void dispatchCallback(ProvisioningChange delta, LinkProperties newLp) {
switch (delta) {
...
case LOST_PROVISIONING:
if (DBG) { Log.d(mTag, "onProvisioningFailure()"); }
recordMetric(IpManagerEvent.PROVISIONING_FAIL);
mCallback.onProvisioningFailure(newLp);
break;
...
}
}
...
// Returns false if we have lost provisioning, true otherwise.
private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
final LinkProperties newLp = assembleLinkProperties();
if (Objects.equals(newLp, mLinkProperties)) {
return true;
}
final ProvisioningChange delta = setLinkProperties(newLp);
if (sendCallbacks) {
dispatchCallback(delta, newLp);
}
return (delta != ProvisioningChange.LOST_PROVISIONING);
}
而handleLinkPropertiesUpdate()
方法實現(xiàn)內(nèi)部,有一個名為delta
的局部常量,類型為ProvisioningChange
枚舉,當(dāng)delta
這個局部常量賦值為setLinkProperties()
方法的返回結(jié)果;
如果返回結(jié)果為LOST_PROVISIONING
,則會通過dispatchCallback()
方法觸發(fā)onProvisioningFailure()
回調(diào),從而出現(xiàn)上面整個鏈路,導(dǎo)致STA斷連;
在handleLinkPropertiesUpdate()
方法內(nèi)部,最重要的兩個方法是:assembleLinkProperties()
與setLinkProperties(newLp)
;
依次來看:
//frameworks/base/services/net/java/android/net/ip/IpClient.java
private LinkProperties assembleLinkProperties() {
// [1] Create a new LinkProperties object to populate.
LinkProperties newLp = new LinkProperties();
newLp.setInterfaceName(mInterfaceName);
// [2] Pull in data from netlink:
// - IPv4 addresses
// - IPv6 addresses
// - IPv6 routes
// - IPv6 DNS servers
//
// N.B.: this is fundamentally race-prone and should be fixed by
// changing NetlinkTracker from a hybrid edge/level model to an
// edge-only model, or by giving IpClient its own netlink socket(s)
// so as to track all required information directly.
LinkProperties netlinkLinkProperties = mNetlinkTracker.getLinkProperties();
newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses());
for (RouteInfo route : netlinkLinkProperties.getRoutes()) {
newLp.addRoute(route);
}
addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers());
// [3] Add in data from DHCPv4, if available.
//
// mDhcpResults is never shared with any other owner so we don't have
// to worry about concurrent modification.
if (mDhcpResults != null) {
for (RouteInfo route : mDhcpResults.getRoutes(mInterfaceName)) {
newLp.addRoute(route);
}
addAllReachableDnsServers(newLp, mDhcpResults.dnsServers);
newLp.setDomains(mDhcpResults.domains);
if (mDhcpResults.mtu != 0) {
newLp.setMtu(mDhcpResults.mtu);
}
}
// [4] Add in TCP buffer sizes and HTTP Proxy config, if available.
if (!TextUtils.isEmpty(mTcpBufferSizes)) {
newLp.setTcpBufferSizes(mTcpBufferSizes);
}
if (mHttpProxy != null) {
newLp.setHttpProxy(mHttpProxy);
}
// [5] Add data from InitialConfiguration
if (mConfiguration != null && mConfiguration.mInitialConfig != null) {
InitialConfiguration config = mConfiguration.mInitialConfig;
// Add InitialConfiguration routes and dns server addresses once all addresses
// specified in the InitialConfiguration have been observed with Netlink.
if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) {
for (IpPrefix prefix : config.directlyConnectedRoutes) {
newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName));
}
}
addAllReachableDnsServers(newLp, config.dnsServers);
}
final LinkProperties oldLp = mLinkProperties;
if (DBG) {
Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s",
netlinkLinkProperties, newLp, oldLp));
}
// TODO: also learn via netlink routes specified by an InitialConfiguration and specified
// from a static IP v4 config instead of manually patching them in in steps [3] and [5].
return newLp;
}
assembleLinkProperties()
這個方法主要完成了如下任務(wù):
- 通過
mNetlinkTracker.getLinkProperties()
獲取當(dāng)前探測到的最新的鏈路信息,并封裝為LinkProperties
返回,賦值給netlinkLinkProperties
; - 將
netlinkLinkProperties
中的需要關(guān)注的信息(LinkAddresses
、RouteInfo
等)拷貝到newLp
這個局部變量中; - 返回
newLp
;
handleLinkPropertiesUpdate()
方法內(nèi)在收到assembleLinkProperties()
方法的返回值后,會判斷與當(dāng)前的成員變量mLinkProperties
是否相同;
LinkProperties
重寫了equals
方法:
//frameworks/base/core/java/android/net/LinkProperties.java
...
@Override
/**
* Compares this {@code LinkProperties} instance against the target
* LinkProperties in {@code obj}. Two LinkPropertieses are equal if
* all their fields are equal in values.
*
* For collection fields, such as mDnses, containsAll() is used to check
* if two collections contains the same elements, independent of order.
* There are two thoughts regarding containsAll()
* 1. Duplicated elements. eg, (A, B, B) and (A, A, B) are equal.
* 2. Worst case performance is O(n^2).
*
* @param obj the object to be tested for equality.
* @return {@code true} if both objects are equal, {@code false} otherwise.
*/
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof LinkProperties)) return false;
LinkProperties target = (LinkProperties) obj;
/**
* This method does not check that stacked interfaces are equal, because
* stacked interfaces are not so much a property of the link as a
* description of connections between links.
*/
return isIdenticalInterfaceName(target)
&& isIdenticalAddresses(target)
&& isIdenticalDnses(target)
&& isIdenticalPrivateDns(target)
&& isIdenticalValidatedPrivateDnses(target)
&& isIdenticalRoutes(target)
&& isIdenticalHttpProxy(target)
&& isIdenticalStackedLinks(target)
&& isIdenticalMtu(target)
&& isIdenticalTcpBufferSizes(target);
}
...
由此可見,滿足equals
的要求非??量?,只要有任何變動,都會導(dǎo)致返回值為false
;
回到此問題,由于IPV6的相關(guān)地址變更,其關(guān)聯(lián)的路由規(guī)則與地址信息均會發(fā)生改變,因此這里必然會返回false
;
由此,我們來看第二個重要的方法——setLinkProperties()
:
//frameworks/base/services/net/java/android/net/ip/IpClient.java
...
static boolean isProvisioned(LinkProperties lp, InitialConfiguration config) {
// For historical reasons, we should connect even if all we have is
// an IPv4 address and nothing else.
if (lp.hasIPv4Address() || lp.isProvisioned()) {
return true;
}
if (config == null) {
return false;
}
// When an InitialConfiguration is specified, ignore any difference with previous
// properties and instead check if properties observed match the desired properties.
return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes());
}
...
private ProvisioningChange compareProvisioning(LinkProperties oldLp, LinkProperties newLp) {
ProvisioningChange delta;
InitialConfiguration config = mConfiguration != null ? mConfiguration.mInitialConfig : null;
final boolean wasProvisioned = isProvisioned(oldLp, config);
final boolean isProvisioned = isProvisioned(newLp, config);
if (!wasProvisioned && isProvisioned) {
delta = ProvisioningChange.GAINED_PROVISIONING;
} else if (wasProvisioned && isProvisioned) {
delta = ProvisioningChange.STILL_PROVISIONED;
} else if (!wasProvisioned && !isProvisioned) {
delta = ProvisioningChange.STILL_NOT_PROVISIONED;
} else {
...
delta = ProvisioningChange.LOST_PROVISIONING;
}
final boolean lostIPv6 = oldLp.isIPv6Provisioned() && !newLp.isIPv6Provisioned();
final boolean lostIPv4Address = oldLp.hasIPv4Address() && !newLp.hasIPv4Address();
final boolean lostIPv6Router = oldLp.hasIPv6DefaultRoute() && !newLp.hasIPv6DefaultRoute();
...
final boolean ignoreIPv6ProvisioningLoss = (mMultinetworkPolicyTracker != null)
&& !mMultinetworkPolicyTracker.getAvoidBadWifi();
...
if (lostIPv4Address || (lostIPv6 && !ignoreIPv6ProvisioningLoss)) {
delta = ProvisioningChange.LOST_PROVISIONING;
}
...
if (oldLp.hasGlobalIPv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) {
delta = ProvisioningChange.LOST_PROVISIONING;
}
return delta;
}
// Updates all IpClient-related state concerned with LinkProperties.
// Returns a ProvisioningChange for possibly notifying other interested
// parties that are not fronted by IpClient.
private ProvisioningChange setLinkProperties(LinkProperties newLp) {
...
ProvisioningChange delta = compareProvisioning(mLinkProperties, newLp);
mLinkProperties = new LinkProperties(newLp);
...
return delta;
}
...
// Returns false if we have lost provisioning, true otherwise.
private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
...
final ProvisioningChange delta = setLinkProperties(newLp);
if (sendCallbacks) {
dispatchCallback(delta, newLp);
}
return (delta != ProvisioningChange.LOST_PROVISIONING);
}
這里方法跳轉(zhuǎn)比較多,概括一下:
-
setLinkProperties
由于需要一個ProvisioningChange
枚舉的返回結(jié)果,因此不僅僅是將參數(shù)生拷貝到成員變量mLinkProperties
,在此之前需要調(diào)用compareProvisioning
方法對mLinkProperties
與傳入?yún)?shù)newLp
進行對比,并將差異返回,賦值給局部常量delta
,后者也是整個setLinkProperties
方法的返回結(jié)果; -
compareProvisioning
方法主要通過對比兩個傳入?yún)?shù)的isProvisioned
方法返回結(jié)果,來判定這次變動是GAINED_PROVISIONING
,STILL_PROVISIONED
,STILL_NOT_PROVISIONED
,還是LOST_PROVISIONING
; - 此外,如果
compareProvisioning
方法內(nèi)的局部變量ignoreIPv6ProvisioningLoss
不為true
,IPV6地址、網(wǎng)關(guān)、DNS的丟失也會導(dǎo)致返回結(jié)果為LOST_PROVISIONING
而此問題出現(xiàn),就是滿足了第3條條件所致;
解決方案
由上可知,此行為是AOSP原生邏輯,旨在使STA自動規(guī)避無法上網(wǎng)的AP;文章來源:http://www.zghlxwxcb.cn/news/detail-495316.html
因此:文章來源地址http://www.zghlxwxcb.cn/news/detail-495316.html
- 此問題若非強需求,可以不處理;
- 若需要屏蔽掉此行為,只需將
config_networkAvoidBadWifi
修改為0
即可;
到了這里,關(guān)于[Android][WIFI]手機作AP,關(guān)閉移動網(wǎng)絡(luò)后,STA端斷開重連問題分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!