Android wifi — connect流程

0. 前言

  最近发现一些settings调用wifimanager连接wifi后,没有调用到wpa_supplicant的问题,借此梳理下Android wifi连接流程。
注:代码基于Android R(11) qualcomm平台

1. 代码实现及流程

下面主要梳理从settings app到wpa的流程,闲话不多说,先上流程图。
在这里插入图片描述

1.1 Settings App

  Wifi 连接的交互界面位于settings app,一切调用都是从他开始,因此我们先从settings代码进行跟踪。
packages/apps/Settings/src/com/android/settings/wifi/WifiSettings.java

void submit(WifiConfigController configController) {

    final WifiConfiguration config = configController.getConfig();
 if (configController.getMode() == WifiConfigUiBase.MODE_MODIFY) {
        if(!configController.checkWapiParam()) {
            if(configController.getCurSecurity() == AccessPoint.SECURITY_WAPI_CERT) {
                startWapiCertManage();
            }
            return;
        }
        mWifiManager.save(config, mSaveListener);
    } else {
        if(!configController.checkWapiParam()) {
            if(configController.getCurSecurity() == AccessPoint.SECURITY_WAPI_CERT) {
                startWapiCertManage();
            }
            return;
        }
        mWifiManager.save(config, mSaveListener);
        if (mSelectedAccessPoint != null) { // Not an "Add network"
            connect(config, false /* isSavedNetwork */,
                    CONNECT_SOURCE_UNSPECIFIED);
        }
    }

    mWifiTracker.resumeScanning();
}

可以看到点击连接以后,如果config不为null,则先保存网络,再进行连接,所以即使连接失败,此网络依然在已保存网络列表里。

protected void connect(final WifiConfiguration config,
        boolean isSavedNetwork, @ConnectSource int connectSource) {
    // Log subtype if configuration is a saved network.
    mMetricsFeatureProvider.action(getContext(), SettingsEnums.ACTION_WIFI_CONNECT,
            isSavedNetwork);
    mConnectSource = connectSource;
    mWifiManager.connect(config, mConnectListener);
    mClickedConnect = true;
}

1.2 mWifiManager.connect

frameworks/base/wifi/java/android/net/wifi/WifiManager.java

这里我们先看connect是怎么实现的,save的过程最后再看。具体实现还是在service,wifimanager只是一个桥梁。

public void connect(@NonNull WifiConfiguration config, @Nullable ActionListener listener) {
    if (config == null) throw new IllegalArgumentException("config cannot be null");
    connectInternal(config, WifiConfiguration.INVALID_NETWORK_ID, listener);
}
private void connectInternal(@Nullable WifiConfiguration config, int networkId,
        @Nullable ActionListener listener) {
    ActionListenerProxy listenerProxy = null;
    Binder binder = null;
    if (listener != null) {
        listenerProxy = new ActionListenerProxy("connect", mLooper, listener);
        binder = new Binder();
    }
    try {
        mService.connect(config, networkId, binder, listenerProxy,
                listener == null ? 0 : listener.hashCode());
    } catch (RemoteException e) {
        if (listenerProxy != null) listenerProxy.onFailure(ERROR);
    } catch (SecurityException e) {
        if (listenerProxy != null) listenerProxy.onFailure(NOT_AUTHORIZED);
    }
}

1.3 mService.connect

frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiServiceImpl.java

     /**
      * see {@link android.net.wifi.WifiManager#connect(int, WifiManager.ActionListener)}
      */
     @Override
     public void connect(WifiConfiguration config, int netId, IBinder binder,
             @Nullable IActionListener callback, int callbackIdentifier) {
         int uid = Binder.getCallingUid();
         if (!isPrivileged(Binder.getCallingPid(), uid)) {
             throw new SecurityException(TAG + ": Permission denied");
         }
         mLog.info("connect uid=%").c(uid).flush();
         int staId;
         if(config != null) staId = config.staId;
         else staId = getIdentityForNetwork(netId);
         if(staId == STA_PRIMARY) {
             mClientModeImpl.connect(config, netId, binder, callback, callbackIdentifier, uid);
         } else {
             QtiClientModeImpl qtiClientModeImpl = mWifiThreadRunner.call(() ->
                 mActiveModeWarden.getQtiClientModeImpl(), null);
             if (qtiClientModeImpl != null)
                 qtiClientModeImpl.connect(config, netId, binder, callback, callbackIdentifier, uid);
         }
         if (mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)) {
             if (config == null) {
                 mWifiMetrics.logUserActionEvent(UserActionEvent.EVENT_MANUAL_CONNECT, netId);
             } else {
                 mWifiMetrics.logUserActionEvent(
                         UserActionEvent.EVENT_ADD_OR_UPDATE_NETWORK, config.networkId);
             }
         }
         mClientModeImpl.connect(config, netId, binder, callback, callbackIdentifier, uid);
     }

wifiservice会判断uid的权限,然后这里会判断staid,因为android11上层是支持了双wifi的,就是连接俩个AP,当然具体功能还要厂商自己实现,这里开启wifi时就会分配staid。这里如果是AP1则是正常流程走ClientModeImpl,如果是AP2则会走QtiClientModeImpl,我们先只跟踪AP1。

1.4 mClientModeImpl.connect

frameworks/opt/net/wifi/service/java/com/android/server/wifi/ClientModeImpl.java

public void connect(WifiConfiguration config, int netId, @Nullable IBinder binder,
        @Nullable IActionListener callback, int callbackIdentifier, int callingUid) {
    mWifiInjector.getWifiThreadRunner().post(() -> {
        if (callback != null && binder != null) {
            mProcessingActionListeners.add(binder, callback, callbackIdentifier);
        }
        /**
         * The connect message can contain a network id passed as arg1 on message or
         * or a config passed as obj on message.
         * For a new network, a config is passed to create and connect.
         * For an existing network, a network id is passed
         */
        NetworkUpdateResult result = null;
        if (config != null) {
            result = mWifiConfigManager.addOrUpdateNetwork(config, callingUid);
            if (!result.isSuccess()) {
                loge("connectNetwork adding/updating config=" + config + " failed");
                sendActionListenerFailure(callbackIdentifier, WifiManager.ERROR);
                return;
            }
            broadcastWifiCredentialChanged(WifiManager.WIFI_CREDENTIAL_SAVED, config);
        } else {
            if (mWifiConfigManager.getConfiguredNetwork(netId) == null) {
                loge("connectNetwork Invalid network Id=" + netId);
                sendActionListenerFailure(callbackIdentifier, WifiManager.ERROR);
                return;
            }
            result = new NetworkUpdateResult(netId);
        }
        final int networkId = result.getNetworkId();
        mWifiConfigManager.userEnabledNetwork(networkId);
        if (!mWifiConfigManager.enableNetwork(networkId, true, callingUid, null)
                || !mWifiConfigManager.updateLastConnectUid(networkId, callingUid)) {
            logi("connect Allowing uid " + callingUid
                    + " with insufficient permissions to connect=" + networkId);
        } else if (mWifiPermissionsUtil.checkNetworkSettingsPermission(callingUid)) {
            // Note user connect choice here, so that it will be considered in the
            // next network selection.
            mWifiConnectivityManager.setUserConnectChoice(networkId);
        }
        Message message =
                obtainMessage(CMD_CONNECT_NETWORK, -1, callbackIdentifier, result);
        message.sendingUid = callingUid;
        sendMessage(message);
    });
}

  看一下wifi状态机里做了什么事情。

  首先是调用WifiConfigManager.addOrUpdateNetwork来更新网络配置。如果保存成功则发送广播。然后检查网络权限等等各项操作结束以后,发送消息CMD_CONNECT_NETWORK。

  现在wifi状态机应该在ConnectModeState。我们看它怎么处理。

case CMD_CONNECT_NETWORK:
    callbackIdentifier = message.arg2;
    result = (NetworkUpdateResult) message.obj;
    netId = result.getNetworkId();
    connectToUserSelectNetwork(
            netId, message.sendingUid, result.hasCredentialChanged());
    mWifiMetrics.logStaEvent(
            StaEvent.TYPE_CONNECT_NETWORK,
            mWifiConfigManager.getConfiguredNetwork(netId));
    sendActionListenerSuccess(callbackIdentifier);
    break;

这里面主要跟踪connectToUserSelectNetwork

private void connectToUserSelectNetwork(int netId, int uid, boolean forceReconnect) {
    logd("connectToUserSelectNetwork netId " + netId + ", uid " + uid
            + ", forceReconnect = " + forceReconnect);
    if (!forceReconnect && (mLastNetworkId == netId || mTargetNetworkId == netId)) {
        // We're already connecting/connected to the user specified network, don't trigger a
        // reconnection unless it was forced.
        logi("connectToUserSelectNetwork already connecting/connected=" + netId);
    } else {
        mWifiConnectivityManager.prepareForForcedConnection(netId);
        if (uid == Process.SYSTEM_UID) {
            mWifiMetrics.setNominatorForNetwork(netId,
                    WifiMetricsProto.ConnectionEvent.NOMINATOR_MANUAL);
        }
        startConnectToNetwork(netId, uid, SUPPLICANT_BSSID_ANY);
    }
}

这里又发送了CMD_START_CONNECT消息。

public void startConnectToNetwork(int networkId, int uid, String bssid) {
    sendMessage(CMD_START_CONNECT, networkId, uid, bssid);
}

  还是在ConnectModeState中处理。在这里会更新AP的信息,然后计分器打分,从底层获取macaddress,然后开启IPClient。上述完成以后开始connectToNetwork

case CMD_START_CONNECT:
    /* connect command coming from auto-join */
    netId = message.arg1;
    int uid = message.arg2;
    bssid = (String) message.obj;
    mSentHLPs = false;

    if (!hasConnectionRequests()) {
        if (mNetworkAgent == null) {
            loge("CMD_START_CONNECT but no requests and not connected,"
                    + " bailing");
            break;
        } else if (!mWifiPermissionsUtil.checkNetworkSettingsPermission(uid)) {
            loge("CMD_START_CONNECT but no requests and connected, but app "
                    + "does not have sufficient permissions, bailing");
            break;
        }
    }
    config = mWifiConfigManager.getConfiguredNetworkWithoutMasking(netId);
    logd("CMD_START_CONNECT "
            + " my state " + getCurrentState().getName()
            + " nid=" + Integer.toString(netId)
            + " roam=" + Boolean.toString(mIsAutoRoaming));
    if (config == null) {
        loge("CMD_START_CONNECT and no config, bail out...");
        break;
    }
    mTargetNetworkId = netId;
    // Update scorecard while there is still state from existing connection
    int scanRssi = mWifiConfigManager.findScanRssi(netId,
            mWifiHealthMonitor.getScanRssiValidTimeMs());
    mWifiScoreCard.noteConnectionAttempt(mWifiInfo, scanRssi, config.SSID);
    mBssidBlocklistMonitor.updateFirmwareRoamingConfiguration(config.SSID);

    updateWifiConfigOnStartConnection(config, bssid);
    reportConnectionAttemptStart(config, mTargetBssid,
            WifiMetricsProto.ConnectionEvent.ROAM_UNRELATED);

    String currentMacAddress = mWifiNative.getMacAddress(mInterfaceName);
    mWifiInfo.setMacAddress(currentMacAddress);
    Log.i(TAG, "Connecting with " + currentMacAddress + " as the mac address");
    mTargetWifiConfiguration = config;
    /* Check for FILS configuration again after updating the config */
    if (config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.FILS_SHA256)
            || config.allowedKeyManagement.get(
            WifiConfiguration.KeyMgmt.FILS_SHA384)) {

        boolean isIpClientStarted = startIpClient(config, true);
        if (isIpClientStarted) {
            mIpClientWithPreConnection = true;
            break;
        }
    }
    connectToNetwork(config);
    break;
void connectToNetwork(WifiConfiguration config) {
    if ((config != null) && mWifiNative.connectToNetwork(mInterfaceName, config)) {
        mWifiInjector.getWifiLastResortWatchdog().noteStartConnectTime();
        mWifiMetrics.logStaEvent(StaEvent.TYPE_CMD_START_CONNECT, config);
        mLastConnectAttemptTimestamp = mClock.getWallClockMillis();
        mIsAutoRoaming = false;
        if (getCurrentState() != mDisconnectedState) {
            transitionTo(mDisconnectingState);
        }
    } else {
        loge("CMD_START_CONNECT Failed to start connection to network " + config);
        mTargetWifiConfiguration = null;
        stopIpClient();
        reportConnectionAttemptEnd(
                WifiMetrics.ConnectionEvent.FAILURE_CONNECT_NETWORK_FAILED,
                WifiMetricsProto.ConnectionEvent.HLF_NONE,
                WifiMetricsProto.ConnectionEvent.FAILURE_REASON_UNKNOWN);
    }
}

1.5 mWifiNative.connectToNetwork

frameworks/opt/net/wifi/service/java/com/android/server/wifi/WifiNative.java

     /**
      * Add the provided network configuration to wpa_supplicant and initiate connection to it.
      * This method does the following:
      * 1. Abort any ongoing scan to unblock the connection request.
      * 2. Remove any existing network in wpa_supplicant(This implicitly triggers disconnect).
      * 3. Add a new network to wpa_supplicant.
      * 4. Save the provided configuration to wpa_supplicant.
      * 5. Select the new network in wpa_supplicant.
      * 6. Triggers reconnect command to wpa_supplicant.
      *
      * @param ifaceName Name of the interface.
      * @param configuration WifiConfiguration parameters for the provided network.
      * @return {@code true} if it succeeds, {@code false} otherwise
      */
     public boolean connectToNetwork(@NonNull String ifaceName, WifiConfiguration configuration) {
         // Abort ongoing scan before connect() to unblock connection request.
         mWifiCondManager.abortScan(ifaceName);
         if (configuration.staId == STA_PRIMARY && mIfaceBands.containsKey(STA_SECONDARY)) {
             String SecStaifaceName = getSecondaryStaInterfaceName();
             if (SecStaifaceName != null) {
                 Log.d(TAG, "Disconnect STA2 for STA1 connection");
                 disconnect(SecStaifaceName);
             }
         }
         return mSupplicantStaIfaceHal.connectToNetwork(ifaceName, configuration);
     }

我们可以看到connectToNetwork通过WifiNative到了SupplicantStaIfaceHal

1.6 mSupplicantStaIfaceHal.connectToNetwork

frameworks/opt/net/wifi/service/java/com/android/server/wifi/SupplicantStaIfaceHal.java

     /**
      * Add the provided network configuration to wpa_supplicant and initiate connection to it.
      * This method does the following:
      * 1. If |config| is different to the current supplicant network, removes all supplicant
      * networks and saves |config|.
      * 2. Select the new network in wpa_supplicant.
      *
      * @param ifaceName Name of the interface.
      * @param config WifiConfiguration parameters for the provided network.
      * @return {@code true} if it succeeds, {@code false} otherwise
      */
     public boolean connectToNetwork(@NonNull String ifaceName, @NonNull WifiConfiguration config) {
         synchronized (mLock) {
             logd("connectToNetwork " + config.getKey());
             WifiConfiguration currentConfig = getCurrentNetworkLocalConfig(ifaceName);
             if (WifiConfigurationUtil.isSameNetwork(config, currentConfig)) {
                 String networkSelectionBSSID = config.getNetworkSelectionStatus()
                         .getNetworkSelectionBSSID();
                 String networkSelectionBSSIDCurrent =
                         currentConfig.getNetworkSelectionStatus().getNetworkSelectionBSSID();
                 if (Objects.equals(networkSelectionBSSID, networkSelectionBSSIDCurrent)) {
                     logd("Network is already saved, will not trigger remove and add operation.");
                 } else {
                     logd("Network is already saved, but need to update BSSID.");
                     if (!setCurrentNetworkBssid(
                             ifaceName,
                             config.getNetworkSelectionStatus().getNetworkSelectionBSSID())) {
                         loge("Failed to set current network BSSID.");
                         return false;
                     }
                     mCurrentNetworkLocalConfigs.put(ifaceName, new WifiConfiguration(config));
                 }
             } else {
                 mCurrentNetworkRemoteHandles.remove(ifaceName);
                 mCurrentNetworkLocalConfigs.remove(ifaceName);
                 if (!removeAllNetworks(ifaceName)) {
                     loge("Failed to remove existing networks");
                     return false;
                 }
                 Pair<SupplicantStaNetworkHal, WifiConfiguration> pair =
                         addNetworkAndSaveConfig(ifaceName, config); //下一步调用
                 if (pair == null) {
                     loge("Failed to add/save network configuration: " + config.getKey());
                     return false;
                 }
                 mCurrentNetworkRemoteHandles.put(ifaceName, pair.first);
                 mCurrentNetworkLocalConfigs.put(ifaceName, pair.second);
             }
             SupplicantStaNetworkHal networkHandle =
                     checkSupplicantStaNetworkAndLogFailure(ifaceName, "connectToNetwork");
             if (networkHandle == null) {
                 loge("No valid remote network handle for network configuration: "
                         + config.getKey());
                 return false;
             }
 
             PmkCacheStoreData pmkData = mPmkCacheEntries.get(config.networkId);
             if (pmkData != null
                     && !WifiConfigurationUtil.isConfigForPskNetwork(config)
                     && pmkData.expirationTimeInSec > mClock.getElapsedSinceBootMillis() / 1000) {
                 logi("Set PMK cache for config id " + config.networkId);
                 if (networkHandle.setPmkCache(pmkData.data)) {
                     mWifiMetrics.setConnectionPmkCache(true);
                 }
             }
 
             if (!networkHandle.select()) { //1.8 -> sta_network.cpp
                 loge("Failed to select network configuration: " + config.getKey());
                 return false;
             }
             return true;
         }
     }

其中我们需要关注addNetworkAndSaveConfig

     /**
      * Add a network configuration to wpa_supplicant.
      *
      * @param config Config corresponding to the network.
      * @return a Pair object including SupplicantStaNetworkHal and WifiConfiguration objects
      * for the current network.
      */
     private Pair<SupplicantStaNetworkHal, WifiConfiguration>
             addNetworkAndSaveConfig(@NonNull String ifaceName, WifiConfiguration config) {
         synchronized (mLock) {
             logi("addSupplicantStaNetwork via HIDL");
             if (config == null) {
                 loge("Cannot add NULL network!");
                 return null;
             }
             SupplicantStaNetworkHal network = addNetwork(ifaceName);
             if (network == null) {
                 loge("Failed to add a network!");
                 return null;
             }
             boolean saveSuccess = false;
             try {
                 saveSuccess = network.saveWifiConfiguration(config);
             } catch (IllegalArgumentException e) {
                 Log.e(TAG, "Exception while saving config params: " + config, e);
             }
             if (!saveSuccess) {
                 loge("Failed to save variables for: " + config.getKey());
                 if (!removeAllNetworks(ifaceName)) {
                     loge("Failed to remove all networks on failure.");
                 }
                 return null;
             }
             return new Pair(network, new WifiConfiguration(config));
         }
     }

connectToNetwork -> addNetworkAndSaveConfig -> addNetwork -> supplicant

     /**
      * Adds a new network.
      *
      * @return The ISupplicantNetwork object for the new network, or null if the call fails
      */
     private SupplicantStaNetworkHal addNetwork(@NonNull String ifaceName) {
         synchronized (mLock) {
             final String methodStr = "addNetwork";
             ISupplicantStaIface iface = checkSupplicantStaIfaceAndLogFailure(ifaceName, methodStr);
             if (iface == null) return null;
             Mutable<ISupplicantNetwork> newNetwork = new Mutable<>();
             try {
                 iface.addNetwork((SupplicantStatus status,
                         ISupplicantNetwork network) -> {
                     if (checkStatusAndLogFailure(status, methodStr)) {
                         newNetwork.value = network; // 1.7 -> wpa_supplicant
                     }
                 });
             } catch (RemoteException e) {
                 handleRemoteException(e, methodStr);
             }
             if (newNetwork.value != null) {
                 return getStaNetworkMockable(
                         ifaceName,
                         ISupplicantStaNetwork.asInterface(newNetwork.value.asBinder()));
             } else {
                 return null;
             }
         }
     }

1.7 wpa_supplicant

在上一节中我们看到调用了 iface.addNetwork ,跟踪下去会发现这是 wpa_supplicant 中 hidl 接口
external/wpa_supplicant_8/wpa_supplicant/hidl/1.3/sta_iface.cpp

 Return<void> StaIface::addNetwork(addNetwork_cb _hidl_cb)
 {
 	return validateAndCall(
 	    this, SupplicantStatusCode::FAILURE_IFACE_INVALID,
 	    &StaIface::addNetworkInternal, _hidl_cb);
 }
 std::pair<SupplicantStatus, sp<ISupplicantNetwork>>
 StaIface::addNetworkInternal()
 {
 	android::sp<ISupplicantStaNetwork> network;
 	struct wpa_supplicant *wpa_s = retrieveIfacePtr();
 	struct wpa_ssid *ssid = wpa_supplicant_add_network(wpa_s);
 	if (!ssid) {
 		return {{SupplicantStatusCode::FAILURE_UNKNOWN, ""}, network};
 	}
 	HidlManager *hidl_manager = HidlManager::getInstance();
 	if (!hidl_manager ||
 	    hidl_manager->getStaNetworkHidlObjectByIfnameAndNetworkId(
 		wpa_s->ifname, ssid->id, &network)) {
 		return {{SupplicantStatusCode::FAILURE_UNKNOWN, ""}, network};
 	}
 	return {{SupplicantStatusCode::SUCCESS, ""}, network};
 }

external/wpa_supplicant_8/wpa_supplicant/wpa_supplicant.c

 /**
  * wpa_supplicant_add_network - Add a new network
  * @wpa_s: wpa_supplicant structure for a network interface
  * Returns: The new network configuration or %NULL if operation failed
  *
  * This function performs the following operations:
  * 1. Adds a new network.
  * 2. Send network addition notification.
  * 3. Marks the network disabled.
  * 4. Set network default parameters.
  */
 struct wpa_ssid * wpa_supplicant_add_network(struct wpa_supplicant *wpa_s)
 {
 	struct wpa_ssid *ssid;
 
 	ssid = wpa_config_add_network(wpa_s->conf);//config.c
 	if (!ssid)
 		return NULL;
 	wpas_notify_network_added(wpa_s, ssid);//notify.c
 	ssid->disabled = 1;
 	wpa_config_set_network_defaults(ssid);
 
 	return ssid;
 }

external/wpa_supplicant_8/wpa_supplicant/config.c

 /**
  * wpa_config_add_network - Add a new network with empty configuration
  * @config: Configuration data from wpa_config_read()
  * Returns: The new network configuration or %NULL if operation failed
  */
 struct wpa_ssid * wpa_config_add_network(struct wpa_config *config)
 {
 	int id;
 	struct wpa_ssid *ssid, *last = NULL;
 
 	id = -1;
 	ssid = config->ssid;
 	while (ssid) {
 		if (ssid->id > id)
 			id = ssid->id;
 		last = ssid;
 		ssid = ssid->next;
 	}
 	id++;
 
 	ssid = os_zalloc(sizeof(*ssid));
 	if (ssid == NULL)
 		return NULL;
 	ssid->id = id;
 	dl_list_init(&ssid->psk_list);
 	if (last)
 		last->next = ssid;
 	else
 		config->ssid = ssid;
 
 	wpa_config_update_prio_list(config);
 
 	return ssid;
 }

external/wpa_supplicant_8/wpa_supplicant/notify.c

 void wpas_notify_network_added(struct wpa_supplicant *wpa_s,
 			       struct wpa_ssid *ssid)
 {
 	if (wpa_s->p2p_mgmt)
 		return;
 
 	/*
 	 * Networks objects created during any P2P activities should not be
 	 * exposed out. They might/will confuse certain non-P2P aware
 	 * applications since these network objects won't behave like
 	 * regular ones.
 	 */
 	if (!ssid->p2p_group && wpa_s->global->p2p_group_formation != wpa_s) {
 		wpas_dbus_register_network(wpa_s, ssid);
 		wpas_hidl_register_network(wpa_s, ssid);
 	}
 }

这里看到了执行了 wpas_dbus_register_networkwpas_hidl_register_network 但是通过查看log,我们发现,貌似最终执行下去的是wpas_hidl_register_network,先继续跟踪,后续添加打印确认wpas_dbus_register_network是否在执行过程中便return了
在这里插入图片描述
external/wpa_supplicant_8/wpa_supplicant/hidl/1.3/hidl.cpp

int wpas_hidl_register_network(
      struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid)
  {
  	if (!wpa_s || !wpa_s->global->hidl || !ssid)
  		return 1;
  
  	wpa_printf(
  	    MSG_DEBUG, "Registering network to hidl control: %d", ssid->id);
  
  	HidlManager *hidl_manager = HidlManager::getInstance();
  	if (!hidl_manager)
  		return 1;
  
  	return hidl_manager->registerNetwork(wpa_s, ssid);
  }

external/wpa_supplicant_8/wpa_supplicant/hidl/1.3/hidl_manager.cpp

 /**
   * Register a network to hidl manager.
   *
   * @param wpa_s |wpa_supplicant| struct corresponding to the interface on which
   * the network is added.
   * @param ssid |wpa_ssid| struct corresponding to the network being added.
   *
   * @return 0 on success, 1 on failure.
   */
  int HidlManager::registerNetwork(
      struct wpa_supplicant *wpa_s, struct wpa_ssid *ssid)
  {
  	if (!wpa_s || !ssid)
  		return 1;
  
  	// Generate the key to be used to lookup the network.
  	const std::string network_key =
  	    getNetworkObjectMapKey(wpa_s->ifname, ssid->id);
  
  	if (isP2pIface(wpa_s)) {
  		if (addHidlObjectToMap<P2pNetwork>(
  			network_key,
  			new P2pNetwork(wpa_s->global, wpa_s->ifname, ssid->id),
  			p2p_network_object_map_)) {
  			wpa_printf(
  			    MSG_ERROR,
  			    "Failed to register P2P network with HIDL "
  			    "control: %d",
  			    ssid->id);
  			return 1;
  		}
  		p2p_network_callbacks_map_[network_key] =
  		    std::vector<android::sp<ISupplicantP2pNetworkCallback>>();
  		// Invoke the |onNetworkAdded| method on all registered
  		// callbacks.
  		callWithEachP2pIfaceCallback(
  		    wpa_s->ifname,
  		    std::bind(
  			&ISupplicantP2pIfaceCallback::onNetworkAdded,
  			std::placeholders::_1, ssid->id));
  	} else {
  		if (addHidlObjectToMap<StaNetwork>(
  			network_key,
  			new StaNetwork(wpa_s->global, wpa_s->ifname, ssid->id),
  			sta_network_object_map_)) {
  			wpa_printf(
  			    MSG_ERROR,
  			    "Failed to register STA network with HIDL "
  			    "control: %d",
  			    ssid->id);
  			return 1;
  		}
  		sta_network_callbacks_map_[network_key] =
  		    std::vector<android::sp<ISupplicantStaNetworkCallback>>();
  		// Invoke the |onNetworkAdded| method on all registered
  		// callbacks.
  		callWithEachStaIfaceCallback(
  		    wpa_s->ifname,
  		    std::bind(
  			&ISupplicantStaIfaceCallback::onNetworkAdded,
  			std::placeholders::_1, ssid->id));
  	}
  	return 0;
  }

1.8 networkHandle.select()

接着我们回到1.6节中,SupplicantStaIfaceHal中的connectToNetwork最后会执行select,我们看supplicant中select具体做了什么

frameworks/opt/net/wifi/service/java/com/android/server/wifi/SupplicantStaIfaceHal.java

     /**
       * Trigger a connection to this network.
       *
       * @return true if it succeeds, false otherwise.
       */
      public boolean select() {
          synchronized (mLock) {
              final String methodStr = "select";
              if (!checkISupplicantStaNetworkAndLogFailure(methodStr)) return false;
              try {
                  SupplicantStatus status = mISupplicantStaNetwork.select();
                  return checkStatusAndLogFailure(status, methodStr);
              } catch (RemoteException e) {
                  handleRemoteException(e, methodStr);
                  return false;
              }
          }
      }

这里选择AP以后就开始关联了,关联成功就是四次握手。

external/wpa_supplicant_8/wpa_supplicant/hidl/1.3/sta_network.cpp
external/wpa_supplicant_8/wpa_supplicant/wpa_supplicant.c
external/wpa_supplicant_8/wpa_supplicant/events.c
external/wpa_supplicant_8/src/rsn_supp/wpa.c
select->selectInternal->wpa_supplicant_select_network->wpa_supplicant_fast_associate->wpas_select_network_from_last_scan->wpa_supplicant_pick_network->wpa_supplicant_select_bss->wpa_supplicant_connect->wpa_supplicant_associate->wpas_start_assoc_cb->wpa_sm_set_assoc_wpa_ie

external/wpa_supplicant_8/wpa_supplicant/hidl/1.3/sta_network.cpp

 Return<void> StaNetwork::select(select_cb _hidl_cb)
  {
  	return validateAndCall(
  	    this, SupplicantStatusCode::FAILURE_NETWORK_INVALID,
  	    &StaNetwork::selectInternal, _hidl_cb);
  }
 SupplicantStatus StaNetwork::selectInternal()
  {
  	struct wpa_ssid *wpa_ssid = retrieveNetworkPtr();
  	if (wpa_ssid->disabled == 2) {
  		return {SupplicantStatusCode::FAILURE_UNKNOWN, ""};
  	}
  	struct wpa_supplicant *wpa_s = retrieveIfacePtr();
  	wpa_s->scan_min_time.sec = 0;
  	wpa_s->scan_min_time.usec = 0;
  	wpa_supplicant_select_network(wpa_s, wpa_ssid);
  	return {SupplicantStatusCode::SUCCESS, ""};
  }

/external/wpa_supplicant_8/wpa_supplicant/wpa_supplicant.c

 /**
   * wpa_supplicant_select_network - Attempt association with a network
   * @wpa_s: wpa_supplicant structure for a network interface
   * @ssid: wpa_ssid structure for a configured network or %NULL for any network
   */
  void wpa_supplicant_select_network(struct wpa_supplicant *wpa_s,
  				   struct wpa_ssid *ssid)
  {
  
  	struct wpa_ssid *other_ssid;
  	int disconnected = 0;
  
  	if (ssid && ssid != wpa_s->current_ssid && wpa_s->current_ssid) {
  		if (wpa_s->wpa_state >= WPA_AUTHENTICATING)
  			wpa_s->own_disconnect_req = 1;
  		wpa_supplicant_deauthenticate(
  			wpa_s, WLAN_REASON_DEAUTH_LEAVING);
  		disconnected = 1;
  	}
  
  	if (ssid)
  		wpas_clear_temp_disabled(wpa_s, ssid, 1);
  
  	/*
  	 * Mark all other networks disabled or mark all networks enabled if no
  	 * network specified.
  	 */
  	for (other_ssid = wpa_s->conf->ssid; other_ssid;
  	     other_ssid = other_ssid->next) {
  		int was_disabled = other_ssid->disabled;
  		if (was_disabled == 2)
  			continue; /* do not change persistent P2P group data */
  
  		other_ssid->disabled = ssid ? (ssid->id != other_ssid->id) : 0;
  		if (was_disabled && !other_ssid->disabled)
  			wpas_clear_temp_disabled(wpa_s, other_ssid, 0);
  
  		if (was_disabled != other_ssid->disabled)
  			wpas_notify_network_enabled_changed(wpa_s, other_ssid);
  	}
  
  	if (ssid && ssid == wpa_s->current_ssid && wpa_s->current_ssid &&
  	    wpa_s->wpa_state >= WPA_AUTHENTICATING) {
  		/* We are already associated with the selected network */
  		wpa_printf(MSG_DEBUG, "Already associated with the "
  			   "selected network - do nothing");
  		return;
  	}
  
  	if (ssid) {
  		wpa_s->current_ssid = ssid;
  		eapol_sm_notify_config(wpa_s->eapol, NULL, NULL);
  		wpa_s->connect_without_scan =
  			(ssid->mode == WPAS_MODE_MESH) ? ssid : NULL;
  
  		/*
  		 * Don't optimize next scan freqs since a new ESS has been
  		 * selected.
  		 */
  		os_free(wpa_s->next_scan_freqs);
  		wpa_s->next_scan_freqs = NULL;
  	} else {
  		wpa_s->connect_without_scan = NULL;
  	}
   
  	wpa_s->disconnected = 0;
  	wpa_s->reassociate = 1;
  	wpa_s_clear_sae_rejected(wpa_s);
  	wpa_s->last_owe_group = 0;
  	if (ssid) {
  		ssid->owe_transition_bss_select_count = 0;
  		wpa_s_setup_sae_pt(wpa_s->conf, ssid);
  	}
  
  	if (wpa_s->connect_without_scan ||
  	    wpa_supplicant_fast_associate(wpa_s) != 1) {
  		wpa_s->scan_req = NORMAL_SCAN_REQ;
  		wpas_scan_reset_sched_scan(wpa_s);
  		wpa_supplicant_req_scan(wpa_s, 0, disconnected ? 100000 : 0);
  	}
  
  	if (ssid)
  		wpas_notify_network_selected(wpa_s, ssid);
  }

external/wpa_supplicant_8/wpa_supplicant/events.c

int wpa_supplicant_fast_associate(struct wpa_supplicant *wpa_s)
  {
  #ifdef CONFIG_NO_SCAN_PROCESSING
  	return -1;
  #else /* CONFIG_NO_SCAN_PROCESSING */
  	struct os_reltime now;
  
  	wpa_s->ignore_post_flush_scan_res = 0;
  
  	if (wpa_s->last_scan_res_used == 0)
  		return -1;
  
  	os_get_reltime(&now);
  	if (os_reltime_expired(&now, &wpa_s->last_scan,
  			       SCAN_RES_VALID_FOR_CONNECT)) {
  		wpa_printf(MSG_DEBUG, "Fast associate: Old scan results");
  		return -1;
  	}
  
  	return wpas_select_network_from_last_scan(wpa_s, 0, 1);// select and connect
  #endif /* CONFIG_NO_SCAN_PROCESSING */
  }
static int wpas_select_network_from_last_scan(struct wpa_supplicant *wpa_s,
  					      int new_scan, int own_request)
  {
  	struct wpa_bss *selected;
  	struct wpa_ssid *ssid = NULL;
  	int time_to_reenable = wpas_reenabled_network_time(wpa_s);
  
  	if (time_to_reenable > 0) {
  		wpa_dbg(wpa_s, MSG_DEBUG,
  			"Postpone network selection by %d seconds since all networks are disabled",
  			time_to_reenable);
  		eloop_cancel_timeout(wpas_network_reenabled, wpa_s, NULL);
  		eloop_register_timeout(time_to_reenable, 0,
  				       wpas_network_reenabled, wpa_s, NULL);
  		return 0;
  	}
  
  	if (wpa_s->p2p_mgmt)
  		return 0; /* no normal connection on p2p_mgmt interface */
  
  	wpa_s->owe_transition_search = 0;
  	selected = wpa_supplicant_pick_network(wpa_s, &ssid);
  
  #ifdef CONFIG_MESH
  	if (wpa_s->ifmsh) {
  		wpa_msg(wpa_s, MSG_INFO,
  			"Avoiding join because we already joined a mesh group");
  		return 0;
  	}
  #endif /* CONFIG_MESH */
  
  	if (selected) {
  		int skip;
  		skip = !wpa_supplicant_need_to_roam(wpa_s, selected, ssid);
  		if (skip) {
  			if (new_scan)
  				wpa_supplicant_rsn_preauth_scan_results(wpa_s);
  			return 0;
  		}
  
  		wpa_s->suitable_network++;
  
  		if (ssid != wpa_s->current_ssid &&
  		    wpa_s->wpa_state >= WPA_AUTHENTICATING) {
  			wpa_s->own_disconnect_req = 1;
  			wpa_supplicant_deauthenticate(
  				wpa_s, WLAN_REASON_DEAUTH_LEAVING);
  		}
  
  		if (wpa_supplicant_connect(wpa_s, selected, ssid) < 0) {
  			wpa_dbg(wpa_s, MSG_DEBUG, "Connect failed");
  			return -1;
  		}
  		if (new_scan)
  			wpa_supplicant_rsn_preauth_scan_results(wpa_s);
  		/*
  		 * Do not allow other virtual radios to trigger operations based
  		 * on these scan results since we do not want them to start
  		 * other associations at the same time.
  		 */
  		return 1;
  	} else {
  		wpa_s->no_suitable_network++;
  		wpa_dbg(wpa_s, MSG_DEBUG, "No suitable network found");
  		ssid = wpa_supplicant_pick_new_network(wpa_s);
  		if (ssid) {
  			wpa_dbg(wpa_s, MSG_DEBUG, "Setup a new network");
  			wpa_supplicant_associate(wpa_s, NULL, ssid);
  			if (new_scan)
  				wpa_supplicant_rsn_preauth_scan_results(wpa_s);
  		} else if (own_request) {
  			/*
  			 * No SSID found. If SCAN results are as a result of
  			 * own scan request and not due to a scan request on
  			 * another shared interface, try another scan.
  			 */
  			int timeout_sec = wpa_s->scan_interval;
  			int timeout_usec = 0;
  #ifdef CONFIG_P2P
  			int res;
  
  			res = wpas_p2p_scan_no_go_seen(wpa_s);
  			if (res == 2)
  				return 2;
  			if (res == 1)
  				return 0;
  
  			if (wpa_s->p2p_in_provisioning ||
  			    wpa_s->show_group_started ||
  			    wpa_s->p2p_in_invitation) {
  				/*
  				 * Use shorter wait during P2P Provisioning
  				 * state and during P2P join-a-group operation
  				 * to speed up group formation.
  				 */
  				timeout_sec = 0;
  				timeout_usec = 250000;
  				wpa_supplicant_req_new_scan(wpa_s, timeout_sec,
  							    timeout_usec);
  				return 0;
  			}
  #endif /* CONFIG_P2P */
  #ifdef CONFIG_INTERWORKING
  			if (wpa_s->conf->auto_interworking &&
  			    wpa_s->conf->interworking &&
  			    wpa_s->conf->cred) {
  				wpa_dbg(wpa_s, MSG_DEBUG, "Interworking: "
  					"start ANQP fetch since no matching "
  					"networks found");
  				wpa_s->network_select = 1;
  				wpa_s->auto_network_select = 1;
  				interworking_start_fetch_anqp(wpa_s);
  				return 1;
  			}
  #endif /* CONFIG_INTERWORKING */
  #ifdef CONFIG_WPS
  			if (wpa_s->after_wps > 0 || wpas_wps_searching(wpa_s)) {
  				wpa_dbg(wpa_s, MSG_DEBUG, "Use shorter wait during WPS processing");
  				timeout_sec = 0;
  				timeout_usec = 500000;
  				wpa_supplicant_req_new_scan(wpa_s, timeout_sec,
  							    timeout_usec);
  				return 0;
  			}
  #endif /* CONFIG_WPS */
  #ifdef CONFIG_OWE
  			if (wpa_s->owe_transition_search) {
  				wpa_dbg(wpa_s, MSG_DEBUG,
  					"OWE: Use shorter wait during transition mode search");
  				timeout_sec = 0;
  				timeout_usec = 500000;
  				wpa_supplicant_req_new_scan(wpa_s, timeout_sec,
  							    timeout_usec);
  				return 0;
  			}
  #endif /* CONFIG_OWE */
  			if (wpa_supplicant_req_sched_scan(wpa_s))
  				wpa_supplicant_req_new_scan(wpa_s, timeout_sec,
  							    timeout_usec);
  
  			wpa_msg_ctrl(wpa_s, MSG_INFO,
  				     WPA_EVENT_NETWORK_NOT_FOUND);
  		}
  	}
  	return 0;
  }

按照流程图,我们会发现 wpas_select_network_from_last_scan 中有两个比较重要的调用,一个是 wpa_supplicant_pick_network 该函数的作用是从wpa_ssid中选择wpa_bss;一个是 wpa_supplicant_connect 进行关联和握手。我们先看wpa_supplicant_pick_network,代码位置还是在 external/wpa_supplicant_8/wpa_supplicant/events.c

struct wpa_bss * wpa_supplicant_pick_network(struct wpa_supplicant *wpa_s,
  					     struct wpa_ssid **selected_ssid)
  {
  	struct wpa_bss *selected = NULL;
  	size_t prio;
  	struct wpa_ssid *next_ssid = NULL;
  	struct wpa_ssid *ssid;
  
  	if (wpa_s->last_scan_res == NULL ||
  	    wpa_s->last_scan_res_used == 0)
  		return NULL; /* no scan results from last update */
  
  	if (wpa_s->next_ssid) {
  		/* check that next_ssid is still valid */
  		for (ssid = wpa_s->conf->ssid; ssid; ssid = ssid->next) {
  			if (ssid == wpa_s->next_ssid)
  				break;
  		}
  		next_ssid = ssid;
  		wpa_s->next_ssid = NULL;
  	}
  
  	while (selected == NULL) {
  	    //conf是struct wpa_supplicant中表示wpa_supplicant.conf文件的struct wpa_config
        //num_prio表示wpa_config中wpa_ssid中的优先级有几种类型,有可能一个优先级下面挂有多个wpa_ssid
  		for (prio = 0; prio < wpa_s->conf->num_prio; prio++) {
  		    //pssid表示wpa_config中按prio排列的wpa_ssid链表的头节点
  			if (next_ssid && next_ssid->priority ==
  			    wpa_s->conf->pssid[prio]->priority) {
  			    //only_first_ssid = 1表示从last selected的优先级最高的排在优先级链表第一位的ssid中选择bss
  				selected = wpa_supplicant_select_bss(
  					wpa_s, next_ssid, selected_ssid, 1);
  				if (selected)
  					break;
  			}
  			//only_first_ssid = 0表示从整个优先级链表中选择bss
  			selected = wpa_supplicant_select_bss(
  				wpa_s, wpa_s->conf->pssid[prio],
  				selected_ssid, 0);
  			if (selected)
  				break;
  		}
  
  		if (selected == NULL && wpa_s->blacklist &&
  		    !wpa_s->countermeasures) {
  			wpa_dbg(wpa_s, MSG_DEBUG, "No APs found - clear "
  				"blacklist and try again");
  			wpa_blacklist_clear(wpa_s);
  			wpa_s->blacklist_cleared++;
  		} else if (selected == NULL)
  			break;
  	}
  
  	ssid = *selected_ssid;
  	if (selected && ssid && ssid->mem_only_psk && !ssid->psk_set &&
  	    !ssid->passphrase && !ssid->ext_psk) {
  		const char *field_name, *txt = NULL;
  
  		wpa_dbg(wpa_s, MSG_DEBUG,
  			"PSK/passphrase not yet available for the selected network");
  
  		wpas_notify_network_request(wpa_s, ssid,
  					    WPA_CTRL_REQ_PSK_PASSPHRASE, NULL);
  
  		field_name = wpa_supplicant_ctrl_req_to_string(
  			WPA_CTRL_REQ_PSK_PASSPHRASE, NULL, &txt);
  		if (field_name == NULL)
  			return NULL;
  
  		wpas_send_ctrl_req(wpa_s, ssid, field_name, txt);
  
  		selected = NULL;
  	}
  
  	return selected;
  }

wpa_supplicant_select_bss 具体处理如何从priority group中选择bss

static struct wpa_bss *
  wpa_supplicant_select_bss(struct wpa_supplicant *wpa_s,
  			  struct wpa_ssid *group,
  			  struct wpa_ssid **selected_ssid,
  			  int only_first_ssid)
  {
  	unsigned int i;
  
  	if (wpa_s->current_ssid) {
  		struct wpa_ssid *ssid;
  
  		wpa_dbg(wpa_s, MSG_DEBUG,
  			"Scan results matching the currently selected network");
  		//last_scan_res_used表示上次扫描的bss的个数
  		for (i = 0; i < wpa_s->last_scan_res_used; i++) {
  			struct wpa_bss *bss = wpa_s->last_scan_res[i];
  
  			// wpa_scan_res_match用来匹配SSID,加密方式等
  			ssid = wpa_scan_res_match(wpa_s, i, bss, group,
  						  only_first_ssid, 0);
  			if (ssid != wpa_s->current_ssid)
  				continue;
  			wpa_dbg(wpa_s, MSG_DEBUG, "%u: " MACSTR
  				" freq=%d level=%d snr=%d est_throughput=%u",
  				i, MAC2STR(bss->bssid), bss->freq, bss->level,
  				bss->snr, bss->est_throughput);
  		}
  	}
  
  	if (only_first_ssid)
  		wpa_dbg(wpa_s, MSG_DEBUG, "Try to find BSS matching pre-selected network id=%d",
  			group->id);
  	else
  		wpa_dbg(wpa_s, MSG_DEBUG, "Selecting BSS from priority group %d",
  			group->priority);
  
  	for (i = 0; i < wpa_s->last_scan_res_used; i++) {
  		struct wpa_bss *bss = wpa_s->last_scan_res[i];
  
  		wpa_s->owe_transition_select = 1;
  		//selected_ssid不等于NULL表示调用成功,真正的返回值是wpa_bss
  		*selected_ssid = wpa_scan_res_match(wpa_s, i, bss, group,
  						    only_first_ssid, 1);
  		wpa_s->owe_transition_select = 0;
  		if (!*selected_ssid)
  			continue;
  		wpa_dbg(wpa_s, MSG_DEBUG, "   selected %sBSS " MACSTR
  			" ssid='%s'",
  			bss == wpa_s->current_bss ? "current ": "",
  			MAC2STR(bss->bssid),
  			wpa_ssid_txt(bss->ssid, bss->ssid_len));
  		return bss;
  	}
  
  	return NULL;
  }

接着我们回到 wpas_select_network_from_last_scan 看下 wpa_supplicant_connect

int wpa_supplicant_connect(struct wpa_supplicant *wpa_s,
  			   struct wpa_bss *selected,
  			   struct wpa_ssid *ssid)
  {
  	if (wpas_wps_scan_pbc_overlap(wpa_s, selected, ssid)) {
  		wpa_msg(wpa_s, MSG_INFO, WPS_EVENT_OVERLAP
  			"PBC session overlap");
  		wpas_notify_wps_event_pbc_overlap(wpa_s);
  #ifdef CONFIG_P2P
  		if (wpa_s->p2p_group_interface == P2P_GROUP_INTERFACE_CLIENT ||
  		    wpa_s->p2p_in_provisioning) {
  			eloop_register_timeout(0, 0, wpas_p2p_pbc_overlap_cb,
  					       wpa_s, NULL);
  			return -1;
  		}
  #endif /* CONFIG_P2P */
  
  #ifdef CONFIG_WPS
  		wpas_wps_pbc_overlap(wpa_s);
  		wpas_wps_cancel(wpa_s);
  #endif /* CONFIG_WPS */
  		return -1;
  	}
  
  	wpa_msg(wpa_s, MSG_DEBUG,
  		"Considering connect request: reassociate: %d  selected: "
  		MACSTR "  bssid: " MACSTR "  pending: " MACSTR
  		"  wpa_state: %s  ssid=%p  current_ssid=%p",
  		wpa_s->reassociate, MAC2STR(selected->bssid),
  		MAC2STR(wpa_s->bssid), MAC2STR(wpa_s->pending_bssid),
  		wpa_supplicant_state_txt(wpa_s->wpa_state),
  		ssid, wpa_s->current_ssid);
  
  	/*
  	 * Do not trigger new association unless the BSSID has changed or if
  	 * reassociation is requested. If we are in process of associating with
  	 * the selected BSSID, do not trigger new attempt.
  	 */
  	if (wpa_s->reassociate ||
  	    (os_memcmp(selected->bssid, wpa_s->bssid, ETH_ALEN) != 0 &&
  	     ((wpa_s->wpa_state != WPA_ASSOCIATING &&
  	       wpa_s->wpa_state != WPA_AUTHENTICATING) ||
  	      (!is_zero_ether_addr(wpa_s->pending_bssid) &&
  	       os_memcmp(selected->bssid, wpa_s->pending_bssid, ETH_ALEN) !=
  	       0) ||
  	      (is_zero_ether_addr(wpa_s->pending_bssid) &&
  	       ssid != wpa_s->current_ssid)))) {
  		if (wpa_supplicant_scard_init(wpa_s, ssid)) {
  			wpa_supplicant_req_new_scan(wpa_s, 10, 0);
  			return 0;
  		}
  		wpa_msg(wpa_s, MSG_DEBUG, "Request association with " MACSTR,
  			MAC2STR(selected->bssid));
  		wpa_supplicant_associate(wpa_s, selected, ssid);
  	} else {
  		wpa_dbg(wpa_s, MSG_DEBUG, "Already associated or trying to "
  			"connect with the selected AP");
  	}
  
  	return 0;
  }

我们可以看到 wpa_supplicant_connect 会调用 wpa_supplicant.cwpa_supplicant_associate 进行关联,关联成功就是四次握手。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ʚ兔子的先森ɞ

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值