【Android-WIFI】(五) Roaming机制HAL层源码分析

在这里插入图片描述

一 wpa_supplicant 进程

我们首先来看看 wpa_supplicant 进程的初始化。

int main(int argc, char *argv[])
{
    int c, i;
    struct wpa_interface *ifaces, *iface;
    int iface_count, exitcode = -1;
    struct wpa_params params;
    struct wpa_global *global;

    //...

    //处理进程启动参数

    exitcode = 0;
    global = wpa_supplicant_init(&params);
    //创建了 wpa_global 对象,内部有一个 wpa_supplicant 队列。
    //创建了进程的epoll 文件描述符
    //...

    for (i = 0; exitcode == 0 && i < iface_count; i++) {
        struct wpa_supplicant *wpa_s;

        if ((ifaces[i].confname == NULL &&
             ifaces[i].ctrl_interface == NULL) ||
            ifaces[i].ifname == NULL) {
            if (iface_count == 1 && (params.ctrl_interface ||
                         params.dbus_ctrl_interface))
                break;
            usage();
            exitcode = -1;
            break;
        }
        wpa_s = wpa_supplicant_add_iface(global, &ifaces[i], NULL);
        if (wpa_s == NULL) {
            exitcode = -1;
            break;
        }
    }

    if (exitcode == 0)
        //开始事件循环监听
        exitcode = wpa_supplicant_run(global);

    //...

    return exitcode;
}

在 wpa_supplicant 进程的 main 函数中,初始化了 wpa_global 对象供整个进程使用。wpa_supplicant 进程主要是向上接受上层关于 WiFi 的请求,然后向下层内核驱动发送通信。所以这里通过 IO 多路复用来轮询监听相应的事件。

wpa_supplicant_add_iface ->
wpa_supplicant_init_iface ->
wpas_init_driver ->
wpa_supplicant_set_driver ->
//external/wpa_supplicant_8/src/drivers/drivers.c
const struct wpa_driver_ops *const wpa_drivers[] =
{
#ifdef CONFIG_DRIVER_NL80211
    &wpa_driver_nl80211_ops,
#endif /* CONFIG_DRIVER_NL80211 */
#ifdef CONFIG_DRIVER_WEXT
    &wpa_driver_wext_ops,
#endif /* CONFIG_DRIVER_WEXT */
#ifdef CONFIG_DRIVER_HOSTAP
    &wpa_driver_hostap_ops,
#endif /* CONFIG_DRIVER_HOSTAP */
#ifdef CONFIG_DRIVER_BSD
    &wpa_driver_bsd_ops,
#endif /* CONFIG_DRIVER_BSD */
#ifdef CONFIG_DRIVER_OPENBSD
    &wpa_driver_openbsd_ops,
#endif /* CONFIG_DRIVER_OPENBSD */
#ifdef CONFIG_DRIVER_NDIS
    &wpa_driver_ndis_ops,
#endif /* CONFIG_DRIVER_NDIS */
#ifdef CONFIG_DRIVER_WIRED
    &wpa_driver_wired_ops,
#endif /* CONFIG_DRIVER_WIRED */
#ifdef CONFIG_DRIVER_MACSEC_LINUX
    &wpa_driver_macsec_linux_ops,
#endif /* CONFIG_DRIVER_MACSEC_LINUX */
#ifdef CONFIG_DRIVER_MACSEC_QCA
    &wpa_driver_macsec_qca_ops,
#endif /* CONFIG_DRIVER_MACSEC_QCA */
#ifdef CONFIG_DRIVER_ROBOSWITCH
    &wpa_driver_roboswitch_ops,
#endif /* CONFIG_DRIVER_ROBOSWITCH */
#ifdef CONFIG_DRIVER_ATHEROS
    &wpa_driver_atheros_ops,
#endif /* CONFIG_DRIVER_ATHEROS */
#ifdef CONFIG_DRIVER_NONE
    &wpa_driver_none_ops,
#endif /* CONFIG_DRIVER_NONE */
    NULL
};

wpa_global 中的 每一个 wpa_supplicant 对象都是一个 wpa_supplicant 接口,其中都配置了对应的驱动接口。可选驱动如上图所示

二 Framework->HAL

//external/wpa_supplicant_8/wap_supplicant/hidl/1.4/sta_iface.cpp
SupplicantStatus StaIface::reassociateInternal()
{
    //从 wpa_global 中获取到 wpa_supplicant
    struct wpa_supplicant *wpa_s = retrieveIfacePtr();
    if (wpa_s->wpa_state == WPA_INTERFACE_DISABLED) {
        return {SupplicantStatusCode::FAILURE_IFACE_DISABLED, ""};
    }
    wpas_request_connection(wpa_s);
    return {SupplicantStatusCode::SUCCESS, ""};
}

通过 HIDL 调用到 HAL 层中。在 wpa_suplicant 中请求连接到指定网络。

//external/wpa_supplicant_8/wap_supplicant/wap_supplicant.c
void wpas_request_connection(struct wpa_supplicant *wpa_s)
{
    //指定连接类型为 reassociate
    wpa_s->normal_scans = 0;
    wpa_s->scan_req = NORMAL_SCAN_REQ;
    wpa_supplicant_reinit_autoscan(wpa_s);
    wpa_s->disconnected = 0;
    wpa_s->reassociate = 1;
    wpa_s->last_owe_group = 0;

    //尝试关联到特定 AP,失败则请求重新扫描
    if (wpa_supplicant_fast_associate(wpa_s) != 1)
        wpa_supplicant_req_scan(wpa_s, 0, 0);
    else
        wpa_s->reattach = 0;
}

//external/wpa_supplicant_8/wap_supplicant/event.c
int wpa_supplicant_fast_associate(struct wpa_supplicant *wpa_s)
{
    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);
    //当前时间和上一次扫描时间是否超过了指定的时间阈值scan_res_valid_for_connect
    if (os_reltime_expired(&now, &wpa_s->last_scan,
                   wpa_s->conf->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);
}

//external/wpa_supplicant_8/wap_supplicant/events.c
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);

    //...
    wpa_s->owe_transition_search = 0;
    //从最新的扫描结果中选择一个网络,并更新selected和ssid
    selected = wpa_supplicant_pick_network(wpa_s, &ssid);


    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++;
        
        //如果当前连接的SSID与选定的SSID不同
        //并且WPA状态为WPA_AUTHENTICATING或更高级别
        //设置wpa_s->own_disconnect_req标志为1,向当前网络发送断开请求
        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;
        }
        //...
        return 1;
    } else {
        //...
        //network not found
    }
    return 0;
}
  1. wpas_request_connection函数用于请求建立连接,它重置一些相关的标志和状态,设置关联和扫描请求,并调用wpa_supplicant_fast_associate()函数。
  2. wpa_supplicant_fast_associate函数检查上次扫描结果的有效性,如果结果过期或不存在,则返回-1。否则,它调用wpas_select_network_from_last_scan()函数选择一个网络进行关联。
  3. wpas_select_network_from_last_scan()函数从上次扫描结果中选择一个网络,并判断是否需要进行漫游。如果不需要漫游,则返回0;如果需要漫游,则调用wpa_supplicant_connect()函数与选定的网络进行连接。
//external/wpa_supplicant_8/wap_supplicant/events.c
int wpa_supplicant_connect(struct wpa_supplicant *wpa_s,
               struct wpa_bss *selected,
               struct wpa_ssid *ssid)
{
    //...
    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)))) {
        //...
        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;
}

//external/wpa_supplicant_8/wap_supplicant/wap_supplicant.c
void wpa_supplicant_associate(struct wpa_supplicant *wpa_s,
                  struct wpa_bss *bss, struct wpa_ssid *ssid)
{
    //...

    //如果上一次连接的SSID与当前SSID相同,则标记进行ESS的重新关联
    //如果当前连接的BSS与之前相同,还会标记进行相同BSS的重新关联
    //如果当前连接的BSS与之前不同,记录漫游开始的时间。
    if (wpa_s->last_ssid == ssid) {
        wpa_dbg(wpa_s, MSG_DEBUG, "Re-association to the same ESS");
        wpa_s->reassoc_same_ess = 1;
        if (wpa_s->current_bss && wpa_s->current_bss == bss) {
            wmm_ac_save_tspecs(wpa_s);
            wpa_s->reassoc_same_bss = 1;
        } else if (wpa_s->current_bss && wpa_s->current_bss != bss) {
            os_get_reltime(&wpa_s->roam_start);
        }
    } else {

    }

    //如果rand_style大于0且不是进行ESS的重新关联
    //则使用wpas_update_random_addr函数根据给定的rand_style生成随机MAC地址
    //并且刷新WPA状态机的PMKSA缓存。
    if (rand_style > 0 && !wpa_s->reassoc_same_ess) {
        if (wpas_update_random_addr(wpa_s, rand_style) < 0)
            return;
        wpa_sm_pmksa_cache_flush(wpa_s->wpa, ssid);
    //如果rand_style等于0且之前发生了MAC地址的更改
    //则使用wpas_restore_permanent_mac_addr函数恢复永久MAC地址。
    } else if (rand_style == 0 && wpa_s->mac_addr_changed) {
        if (wpas_restore_permanent_mac_addr(wpa_s) < 0)
            return;
    }
    wpa_s->last_ssid = ssid;

    //...

    wpa_supplicant_rsn_supp_set_config(wpa_s, ssid);
    //...
    wpas_abort_ongoing_scan(wpa_s);
    cwork = os_zalloc(sizeof(*cwork));
    if (cwork == NULL)
        return;

    cwork->bss = bss;
    cwork->ssid = ssid;

    //将连接操作添加到对应的设备的工作队列中,以便在适当的时间执行。
    if (radio_add_work(wpa_s, bss ? bss->freq : 0, "connect", 1,
               wpas_start_assoc_cb, cwork) < 0) {
        os_free(cwork);
    }
}
  1. 重置相关标志和状态,准备进行新的关联过程。
  2. 生成随机的MAC地址或恢复永久MAC地址。
  3. 标记是否进行相同ESS或相同BSS的重新关联。
  4. 设置WPA状态机的配置。
  5. 终止正在进行的扫描操作。
  6. 创建连接工作对象,并将其添加到设备的工作队列中,以便在适当的时间执行关联操作。
//external/wpa_supplicant_8/wap_supplicant/wap_supplicant.c
static void wpas_start_assoc_cb(struct wpa_radio_work *work, int deinit)
{
    struct wpa_connect_work *cwork = work->ctx;
    struct wpa_bss *bss = cwork->bss;
    struct wpa_ssid *ssid = cwork->ssid;
    struct wpa_supplicant *wpa_s = work->wpa_s;
    u8 *wpa_ie;
    const u8 *edmg_ie_oper;
    int use_crypt, ret, bssid_changed;
    unsigned int cipher_pairwise, cipher_group, cipher_group_mgmt;
    struct wpa_driver_associate_params params;
    int assoc_failed = 0;
    struct wpa_ssid *old_ssid;
    u8 prev_bssid[ETH_ALEN];

    //...
    wpa_s->connect_work = work;

    //...
    os_memcpy(prev_bssid, wpa_s->bssid, ETH_ALEN);
    os_memset(&params, 0, sizeof(params));
    wpa_s->reassociate = 0;
    wpa_s->eap_expected_failure = 0;

    //...

    //根据当前的 BSS 和 SSID 信息,构建关联所需的 IEs(Information Elements)
    wpa_ie = wpas_populate_assoc_ies(wpa_s, bss, ssid, &params, NULL);
    if (!wpa_ie) {
        wpas_connect_work_done(wpa_s);
        return;
    }

    //如果存在有效的 BSS,并且驱动程序允许选择 BSS(如非手动指定 BSSID 或正在进行 WPS 搜索),则尝试关联该 BSS;
    //否则,尝试关联指定的 SSID。
    if (bss &&
        (!wpas_driver_bss_selection(wpa_s) || wpas_wps_searching(wpa_s))) {
        wpa_msg(wpa_s, MSG_INFO, "Trying to associate with " MACSTR
            " (SSID='%s' freq=%d MHz)", MAC2STR(bss->bssid),
            wpa_ssid_txt(bss->ssid, bss->ssid_len), bss->freq);
        bssid_changed = !is_zero_ether_addr(wpa_s->bssid);
        os_memset(wpa_s->bssid, 0, ETH_ALEN);
        os_memcpy(wpa_s->pending_bssid, bss->bssid, ETH_ALEN);
        if (bssid_changed)
            wpas_notify_bssid_changed(wpa_s);
    } else {
        wpa_msg(wpa_s, MSG_INFO, "Trying to associate with SSID '%s'",
            wpa_ssid_txt(ssid->ssid, ssid->ssid_len));
        if (bss)
            os_memcpy(wpa_s->pending_bssid, bss->bssid, ETH_ALEN);
        else
            os_memset(wpa_s->pending_bssid, 0, ETH_ALEN);
    }
    
    //取消可能存在的扫描和计划扫描操作。清除之前的关联过程中的密钥。
    if (!wpa_s->pno)
        wpa_supplicant_cancel_sched_scan(wpa_s);

    wpa_supplicant_cancel_scan(wpa_s);

    wpa_clear_keys(wpa_s, bss ? bss->bssid : NULL);
    use_crypt = 1;
    cipher_pairwise = wpa_s->pairwise_cipher;
    cipher_group = wpa_s->group_cipher;
    cipher_group_mgmt = wpa_s->mgmt_group_cipher;
    if (wpa_s->key_mgmt == WPA_KEY_MGMT_NONE ||
        wpa_s->key_mgmt == WPA_KEY_MGMT_IEEE8021X_NO_WPA) {
        if (wpa_s->key_mgmt == WPA_KEY_MGMT_NONE)
            use_crypt = 0;
    }
    if (wpa_s->key_mgmt == WPA_KEY_MGMT_WPS)
        use_crypt = 0;


    //如果key管理方式是WPA_NONE,表示不使用任何加密,设置一个无加密的key
    if (wpa_s->key_mgmt == WPA_KEY_MGMT_WPA_NONE) {
        /* Set the key before (and later after) association */
        wpa_supplicant_set_wpa_none_key(wpa_s, ssid);
    }

    wpa_supplicant_set_state(wpa_s, WPA_ASSOCIATING);
    //...
    //配置params

    ret = wpa_drv_associate(wpa_s, &params);
    os_free(wpa_ie);

    //...
    //设置相关密钥信息

    if (!wpas_driver_bss_selection(wpa_s) || ssid->bssid_set) {
        wpa_s->current_bss = bss;
    }

    wpa_supplicant_rsn_supp_set_config(wpa_s, wpa_s->current_ssid);
    wpa_supplicant_initiate_eapol(wpa_s);
    if (old_ssid != wpa_s->current_ssid)
        wpas_notify_network_changed(wpa_s);
}
  1. 准备关联所需的信息,例如关联帧需要携带的信息元素。
  2. 根据要关联的网络和BSS的情况,设置关联的目标BSSID和SSID。
  3. 取消已有的扫描操作,并清除相关的密钥信息。
  4. 根据网络的安全配置和认证方式,确定是否需要加密。
  5. 根据不同的认证方式,设置相关的密钥信息。
  6. 将驱动状态设置为关联中,调用驱动程序进行关联操作。
  7. 清理关联所用的临时变量和内存。
  8. 更新当前的BSS和SSID信息。
  9. 配置RSN(Robust Secure Network)相关参数。
  10. 初始化EAPOL(EAP over LAN)过程,以进行后续的认证和密钥协商。
  11. 如果当前网络与之前连接的网络不同,通知网络发生了变化。
//external/wpa_supplicant_8/wap_supplicant/driver_i.h
static inline int wpa_drv_associate(struct wpa_supplicant *wpa_s,
                    struct wpa_driver_associate_params *params)
{
    if (wpa_s->driver->associate) {
        return wpa_s->driver->associate(wpa_s->drv_priv, params);
    }
    return -1;
}

//external/wpa_supplicant_8/src/drivers/driver.h
struct wpa_driver_ops {

        /**
         * associate - Request driver to associate
         * @priv: private driver interface data
         * @params: association parameters
         *
         * Returns: 0 on success, -1 on failure
         */
        int (*associate)(void *priv,
                         struct wpa_driver_associate_params *params);

        /**
         * roaming - Set roaming policy for driver-based BSS selection
         * @priv: Private driver interface data
         * @allowed: Whether roaming within ESS is allowed
         * @bssid: Forced BSSID if roaming is disabled or %NULL if not set
         * Returns: Length of written status information or -1 on failure
         *
         * This optional callback can be used to update roaming policy from the
         * associate() command (bssid being set there indicates that the driver
         * should not roam before getting this roaming() call to allow roaming.
         * If the driver does not indicate WPA_DRIVER_FLAGS_BSS_SELECTION
         * capability, roaming policy is handled within wpa_supplicant and there
         * is no need to implement or react to this callback.
         */
        int (*roaming)(void *priv, int allowed, const u8 *bssid);

};

wpa_driver_ops 中定义了一些驱动接口 API。在 wpa_s 初始化的过程中,配置了一些驱动程序,调用驱动中的 associate 方法。

假定这里的驱动应该是wpa_driver_nl80211_ops

//external/wpa_supplicant_8/src/drivers/driver_nl80211.c
const struct wpa_driver_ops wpa_driver_nl80211_ops = {
    .name = "nl80211",
    .desc = "Linux nl80211/cfg80211",
    //...
    .associate = wpa_driver_nl80211_associate,
    //...
#ifdef CONFIG_DRIVER_NL80211_QCA
    .roaming = nl80211_roaming,
    //...
#endif /* CONFIG_DRIVER_NL80211_QCA */
    //...
};

static int wpa_driver_nl80211_associate(
    void *priv, struct wpa_driver_associate_params *params)
{
    struct i802_bss *bss = priv;
    struct wpa_driver_nl80211_data *drv = bss->drv;
    int ret = -1;
    struct nl_msg *msg;

    nl80211_unmask_11b_rates(bss);

    if (params->mode == IEEE80211_MODE_AP)
        return wpa_driver_nl80211_ap(drv, params);

    if (params->mode == IEEE80211_MODE_IBSS)
        return wpa_driver_nl80211_ibss(drv, params);

    //如果驱动程序不支持SME特性则调用wpa_driver_nl80211_connect 执行关联操作
    //并传递get_connect_handle(bss)的值作为参数。
    //最终也是向 Netlink 发送消息。
    if (!(drv->capa.flags & WPA_DRIVER_FLAGS_SME)) {
        enum nl80211_iftype nlmode = params->p2p ?
            NL80211_IFTYPE_P2P_CLIENT : NL80211_IFTYPE_STATION;

        if (wpa_driver_nl80211_set_mode(priv, nlmode) < 0)
            return -1;
        if (params->key_mgmt_suite == WPA_KEY_MGMT_SAE ||
            params->key_mgmt_suite == WPA_KEY_MGMT_FT_SAE)
            bss->use_nl_connect = 1;
        else
            bss->use_nl_connect = 0;

        return wpa_driver_nl80211_connect(drv, params,
                          get_connect_handle(bss));
    }

    nl80211_mark_disconnected(drv);

    wpa_printf(MSG_DEBUG, "nl80211: Associate (ifindex=%d)",
           drv->ifindex);
    msg = nl80211_drv_msg(drv, 0, NL80211_CMD_ASSOCIATE);
    if (!msg)
        return -1;

    //处理关联过程的共同部分 msg 部分
    //传入驱动程序数据结构、关联参数和Netlink消息作为参数
    ret = nl80211_connect_common(drv, params, msg);
    if (ret)
        goto fail;

    //...

    //发送Netlink消息,并等待关联过程完成。
    //传入驱动程序数据结构、消息、关联操作的句柄、以及一些回调函数指针作为参数。
    ret = send_and_recv_msgs_owner(drv, msg,
                       get_connect_handle(drv->first_bss), 1,
                       NULL, NULL, NULL, NULL);
    msg = NULL;
    if (ret) {
        wpa_dbg(drv->ctx, MSG_DEBUG,
            "nl80211: MLME command failed (assoc): ret=%d (%s)",
            ret, strerror(-ret));
        nl80211_dump_scan(drv);
    } else {
        wpa_printf(MSG_DEBUG,
               "nl80211: Association request send successfully");
    }

fail:
    nlmsg_free(msg);
    return ret;
}

这段代码根据驱动程序的支持情况和关联参数的配置,执行Wi-Fi网络的关联过程。它包括了根据不同的情况调用不同的处理函数,配置相应参数,构建即将发送的 msg,向Netlink 发送关联请求、处理关联结果等步骤,并提供了相应的错误处理和调试输出。

成功调用 wpa_supplicant_associate 后,将等待 NL80211_CMD_CONNECT 命令的处理结果。该结果由 wlan driver通过 NL80211_CMD_CONNECT 类型的消息返回给 wpa_supplicant driver。

static void do_process_drv_event(struct i802_bss *bss, int cmd,
                 struct nlattr **tb)
{
    struct wpa_driver_nl80211_data *drv = bss->drv;
    int external_scan_event = 0;
    struct nlattr *frame = tb[NL80211_ATTR_FRAME];

    if (drv->ap_scan_as_station != NL80211_IFTYPE_UNSPECIFIED &&
        (cmd == NL80211_CMD_NEW_SCAN_RESULTS ||
         cmd == NL80211_CMD_SCAN_ABORTED))
        nl80211_restore_ap_mode(bss);

    switch (cmd) {
    //...
    case NL80211_CMD_CONNECT:
    case NL80211_CMD_ROAM:
        mlme_event_connect(drv, cmd,
                   tb[NL80211_ATTR_STATUS_CODE],
                   tb[NL80211_ATTR_MAC],
                   tb[NL80211_ATTR_REQ_IE],
                   tb[NL80211_ATTR_RESP_IE],
                   tb[NL80211_ATTR_TIMED_OUT],
                   tb[NL80211_ATTR_TIMEOUT_REASON],
                   NULL, NULL, NULL,
                   tb[NL80211_ATTR_FILS_KEK],
                   NULL,
                   tb[NL80211_ATTR_FILS_ERP_NEXT_SEQ_NUM],
                   tb[NL80211_ATTR_PMK],
                   tb[NL80211_ATTR_PMKID]);
        break;
    //...
    default:
        wpa_dbg(drv->ctx, MSG_DEBUG, "nl80211: Ignored unknown event "
            "(cmd=%d)", cmd);
        break;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值