wpa_supplicant用法及分析

本文详细介绍在Android系统中手动配置WIFI的过程,包括加载WIFI驱动、启动wpa_supplicant及客户端配置等步骤,并深入解析了wpa_supplicant的启动与初始化流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 Android系统中对于WIFI的设置集成到了“设置”中,其实跟手动设置差不多。这里介绍下如何手动连接WIFI,以方便以后调试WIFI。

        第一步要做的就是要加载WIFI模块驱动了。当然如果你的WIFI是编译到内核里面的,就不需要的。我们的WIFI芯片用的是BCM4330,编译为模块。

insmod /system/lib/modules/kernel/drivers/net/wireless/bcm4330/bcm4330.ko firmware_path=/system/vendor/firmware/bcm4330.bin \ nvram_path=/system/vendor/firmware/nvram.txt

       这样驱动模块加载后,需要启动wpa_supplicant

root@android:/ # wpa_supplicant -Dwext -iwlan0 -C/data/system/wpa_supplicant -c/data/misc/wifi/wpa_supplicant.conf &  

然后ps|grep wpa看看有没有起来,在wifi工作过程中,这个进程要始终都在的。

        接着启动客户端wpa_cli进行配置并连接wifi热点

130|root@android:/ # wpa_cli -p/data/system/wpa_supplicant -iwlan0
wpa_cli v0.8.x
Copyright (c) 2004-2011, Jouni Malinen <j@w1.fi> and contributors
This program is free software. You can distribute it and/or modify it
under the terms of the GNU General Public License version 2.
Alternatively, this software may be distributed under the terms of the
BSD license. See README and COPYING for more details.

Interactive mode

其中/data/system/wpa_supplicant 是刚刚启动wpa_supplicant的时候创建的一个套接字

接着进行搜索wifi

> scan
OK
<3>CTRL-EVENT-SCAN-RESULTS 

查看搜到的有哪些热点

> scan_result
bssid / frequency / signal level / flags / ssid
40:16:9f:67:0f:00       2462    -42     [WPA-PSK-TKIP+CCMP][WPA2-PSK-TKIP+CCMP][WPS][ESS]       HHCN-NET
b0:48:7a:49:44:68       2437    -58     [WPA2-PSK-CCMP][WPS][ESS]       HHTech.Arch
e0:05:c5:97:d8:5a       2412    -71     [WPA-PSK-CCMP][WPA2-PSK-CCMP-preauth][ESS]      1103-5
40:16:9f:67:0c:a6       2412    -74     [WPA-PSK-TKIP+CCMP][WPA2-PSK-TKIP+CCMP][WPS][ESS]       hardware
b0:48:7a:49:65:54       2437    -58     [WPA-PSK-CCMP][WPA2-PSK-CCMP][WPS][ESS] Dept_driver

接着设置wifi,就连接Dept_driver这个热点吧

> add_net
3
> set_net 3 ssid "Dept_driver"
OK


Dept_driver的加密方式是WPA2-PSK

> set_net 3 psk "password"
OK
> select_net 3
OK
<3>CTRL-EVENT-STATE-CHANGE id=0 state=0 BSSID=00:00:00:00:0[ 2752.332061] dhd_aoe_hostip_clr failed code -23
0:00
<3>CTR[ 2752.337768] dhd_aoe_arp_clr failed code 1
L-EVENT-STATE-CHANGE id=-1 state=3 BSSID=00:00:00:00:00:00
<3>CTRL-EVENT-DISCONNECTED bssid=00:00:00:00:00:00 reason=0
<3>CTRL-EVENT-STATE-CHANGE id=-1 state=0 BSSID=00:00:00:00:00:00
<3>CTRL-EVENT-STATE-CHANGE id=-1 state=3 BSSID=00:00:00:00:00:00
<3>CTRL-EVENT-SCAN-RESULTS 
<3>WPS-AP-AVAILABLE 
<3>Trying to associate with b0:48:7a:49:65:54 (SSID='Dept_driver' freq=2437 MHz)
<3>CTRL-EVENT-STATE-CHANGE id=-1 state=5 BSSID=b0:48:7a:49:65:54
[ 2753.153717] wl_iw_set_essid: join SSID=Dept_driver ch=6
<3>CTRL-EVENT-STATE-CHANGE id=3 state=6 BSSID=b0:48:7a:49:65:54
<3>Associated with b0:48:7a:49:65:54
<3>CTRL-EVENT-STATE-CHANGE id=3 state=7 BSSID=00:00:00:00:00:00
<3>CTRL-EVENT-STATE-CHANGE id=3 state=8 BSSID=00:00:00:00:00:00
<3>WPA: Key negotiation completed with b0:48:7a:49:65:54 [PTK=CCMP GTK=CCMP]
<3>CTRL-EVENT-CONNECTED - Connection to b0:48:7a:49:65:54 completed (reauth) [id=3 id_str=]
<3>CTRL-EVENT-STATE-CHANGE id=3 state=9 BSSID=00:00:00:00:00:00

> enable_net 3
OK
> q


这样,就都设置好了,接着用dhcpd分配一个IP地址

root@android:/ # dhcpcd wlan0                                                  
dhcpcd[3940]: version 5.2.10 starting
dhcpcd[3940]: host does not support a monotonic clock - timing can skew
dhcpcd[3940]: wlan0: rebinding lease of 192.168.1.216
dhcpcd[3940]: wlan0: acknowledged 192.168.1.216 from 192.168.1.1 `�'
dhcpcd[3940]: wlan0: leased 192.168.1.216 for 7200 seconds
dhcpcd[3940]: forked to background, child pid 3971
root@android:/ # busybox ifconfig wlan0                                        
wlan0     Link encap:Ethernet  HWaddr AC:E8:7B:89:D3:C0  
          inet addr:192.168.1.216  Bcast:255.255.255.255  Mask:255.255.255.0
          inet6 addr: fe80::aee8:7bff:fe89:d3c0/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:6637 errors:0 dropped:25 overruns:0 frame:0
          TX packets:4357 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000 
          RX bytes:3825961 (3.6 MiB)  TX bytes:704832 (688.3 KiB)

这样就完成了。可以上网了。

wpa supplicant  架构分析

1. 启动命令 

wpa supplicant 在启动时,启动命令可以带有很多参数,目前我们的启动命令如下: 
wpa_supplicant /system/bin/wpa_supplicant -Dwext -ieth0 -c/data/wifi/wpa_supplicant.conf -f/data/wifi/wpa_log.txt 

wpa_supplicant对于启动命令带的参数,用了两个数据结构来保存, 
一个是 wpa_params, 另一个是wpa_interface. 
这主要是考虑到wpa_supplicant是可以同时支持多个网络接口的。 
wpa_params数据结构主要记录与网络接口无关的一些参数设置。 
而每一个网络接口就用一个wpa_interface数据结构来记录。 
在启动命令行中,可以用-N来指定将要描述一个新的网络接口,对于一个新的网络接口,可以用下面六个参数描述: 
-i<ifname> : 网络接口名称 
-c<conf>: 配置文件名称 
-C<ctrl_intf>: 控制接口名称 
-D<driver>: 驱动类型 
-p<driver_param>: 驱动参数 
-b<br_ifname>: 桥接口名称 

2. wpa_supplicant 初始化流程 

2.1. main()函数: 

在这个函数中,主要做了四件事。 
a. 解析命令行传进的参数。 
b. 调用wpa_supplicant_init()函数,做wpa_supplicant的初始化工作。 
c. 调用wpa_supplicant_add_iface()函数,增加网络接口。 
d. 调用wpa_supplicant_run()函数,让wpa_supplicant真正的run起来。 

2.2. wpa_supplicant_init()函数: 

a. 打开debug 文件。 
b. 注册EAP peer方法。 
c. 申请wpa_global内存,该数据结构作为统领其他数据结构的一个核心, 主要包括四个部分: 
wpa_supplicant *ifaces   /*每个网络接口都有一个对应的wpa_supplicant数据结构,该指针指向最近加入的一个,在wpa_supplicant数据结构中有指针指向next*/ 
wpa_params params   /*启动命令行中带的通用的参数*/ 
ctrl_iface_global_priv *ctrl_iface  /*global 的控制接口*/ 
ctrl_iface_dbus_priv *dbus_ctrl_iface  /*dbus 的控制接口*/ 
d. 设置wpa_global中的wpa_params中的参数。 
e. 调用eloop_init函数将全局变量eloop中的user_data指针指向wpa_global。 
f. 调用wpa_supplicant_global_ctrl_iface_init函数初始化global 控制接口。 
g. 调用wpa_supplicant_dbus_ctrl_iface_init函数初始化dbus 控制接口。 
h. 将该daemon的pid写入pid_file中。 

2.3. wpa_supplicant_add_iface()函数: 

该函数根据启动命令行中带有的参数增加网络接口, 有几个就增加几个。 
a. 因为wpa_supplicant是与网络接口对应的重要的数据结构,所以,首先分配一个wpa_supplicant数据结构的内存。 
b. 调用wpa_supplicant_init_iface() 函数来做网络接口的初始工作,主要包括: 
设置驱动类型,默认是wext; 
读取配置文件,并将其中的信息设置到wpa_supplicant数据结构中的conf 指针指向的数据结构,它是一个wpa_config类型; 
命令行设置的控制接口ctrl_interface和驱动参数driver_param覆盖配置文件里设置,命令行中的优先; 
拷贝网络接口名称和桥接口名称到wpa_config数据结构; 
对于网络配置块有两个链表描述它,一个是 config->ssid,它按照配置文件中的顺序依次挂载在这个链表上,还有一个是pssid,它是一个二级指针,指向一个指针数组,该指针数组按照优先级从高到底的顺序依次保存wpa_ssid指针,相同优先级的在同一链表中挂载。 
c. 调用wpa_supplicant_init_iface2() 函数,主要包括: 
调用wpa_supplicant_init_eapol()函数来初始化eapol; 
调用相应类型的driver的init()函数; 
设置driver的param参数; 
调用wpa_drv_get_ifname()函数获得网络接口的名称,对于wext类型的driver,没有这个接口函数;
调用wpa_supplicant_init_wpa()函数来初始化wpa,并做相应的初始化工作; 
调用wpa_supplicant_driver_init()函数,来初始化driver接口参数;在该函数的最后,会 
wpa_s->prev_scan_ssid = BROADCAST_SSID_SCAN; 
wpa_supplicant_req_scan(wpa_s, interface_count, 100000); 
来主动发起scan, 
调用wpa_supplicant_ctrl_iface_init()函数,来初始化控制接口;对于UNIX SOCKET这种方式,其本地socket文件是由配置文件里的ctrl_interface参数指定的路径加上网络接口名称; 

2.4. wpa_supplicant_run()函数: 

初始化完成之后,让wpa_supplicant的main event loop run起来。 
在wpa_supplicant中,有许多与外界通信的socket,它们都是需要注册到eloop event模块中的,具体地说,就是在eloop_sock_table中增加一项记录,其中包括了sock_fd, handle, eloop_data, user_data。 
eloop event模块就是将这些socket组织起来,统一管理,然后在eloop_run中利用select机制来管理socket的通信。 

3. Wpa_supplicant提供的接口 

从通信层次上划分,wpa_supplicant提供向上的控制接口 control interface,用于与其他模块(如UI)进行通信,其他模块可以通过control interface 来获取信息或下发命令。Wpa_supplicant通过socket通信机制实现下行接口,与内核进行通信,获取信息或下发命令。 

3.1 上行接口 

Wpa_supplicant提供两种方式的上行接口。一种基于传统dbus机制实现与其他进程间的IPC通信;另一种通过Unix domain socket机制实现进程间的IPC通信。 
3.1.1 Dbus接口 

该接口主要在文件“ctrl_iface_dbus.h”,“ctrl_iface_dbus.c”,“ctrl_iface_dbus_handler.h”和“ctrl_iface_dbus_handler.c”中实现,提供一些基本的控制方法。 

DBusMessage * wpas_dbus_new_invalid_iface_error(DBusMessage *message); 

DBusMessage * wpas_dbus_global_add_interface(DBusMessage *message, 
                                        struct wpa_global *global); 

DBusMessage * wpas_dbus_global_remove_interface(DBusMessage *message, 
                                          struct wpa_global *global); 

DBusMessage * wpas_dbus_global_get_interface(DBusMessage *message, 
                                        struct wpa_global *global); 

DBusMessage * wpas_dbus_global_set_debugparams(DBusMessage *message, 
                                          struct wpa_global *global); 

DBusMessage * wpas_dbus_iface_scan(DBusMessage *message, 
                               struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_scan_results(DBusMessage *message, 
                                      struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_bssid_properties(DBusMessage *message, 
                                    struct wpa_supplicant *wpa_s, 
                                    struct wpa_scan_res *res); 

DBusMessage * wpas_dbus_iface_capabilities(DBusMessage *message, 
                                      struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_add_network(DBusMessage *message, 
                                     struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_remove_network(DBusMessage *message, 
                                        struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_set_network(DBusMessage *message, 
                                     struct wpa_supplicant *wpa_s, 
                                     struct wpa_ssid *ssid); 

DBusMessage * wpas_dbus_iface_enable_network(DBusMessage *message, 
                                        struct wpa_supplicant *wpa_s, 
                                        struct wpa_ssid *ssid); 

DBusMessage * wpas_dbus_iface_disable_network(DBusMessage *message, 
                                         struct wpa_supplicant *wpa_s, 
                                         struct wpa_ssid *ssid); 

DBusMessage * wpas_dbus_iface_select_network(DBusMessage *message, 
                                             struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_disconnect(DBusMessage *message, 
                                    struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_set_ap_scan(DBusMessage *message, 
                                          struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_set_smartcard_modules( 
       DBusMessage *message, struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_get_state(DBusMessage *message, 
                                   struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_get_scanning(DBusMessage *message, 
                                      struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_set_blobs(DBusMessage *message, 
                                    struct wpa_supplicant *wpa_s); 

DBusMessage * wpas_dbus_iface_remove_blobs(DBusMessage *message, 
                                      struct wpa_supplicant *wpa_s); 

3.1.2 Unix domain socket 接口 

该接口主要在文件“wpa_ctrl.h”,“wpa_ctrl.c”,“ctrl_iface_unix.c”,“ctrl_iface.h”和“ctrl_iface.c”实现。 

(1)“wpa_ctrl.h”,“wpa_ctrl.c”完成对control interface的封装,对外提供统一的接口。其主要的工作是通过Unix domain socket建立一个control interface 的client结点,与作为server的wpa_supplicant结点通信。 

主要功能函数: 
struct wpa_ctrl * wpa_ctrl_open(const char *ctrl_path); 
/* 建立并初始化一个Unix domain socket的client结点,并与作为server的wpa_supplicant结点绑定 */

void wpa_ctrl_close(struct wpa_ctrl *ctrl); 
/* 撤销并销毁已建立的Unix domain socket的client结点 */ 

int wpa_ctrl_request(struct wpa_ctrl *ctrl, const char *cmd, size_t cmd_len, 
                   char *reply, size_t *reply_len, 
                   void (*msg_cb)(char *msg, size_t len)); 

/* 用户模块直接调用该函数对wpa_supplicant发送命令并获取所需信息 
* 可以发送的命令如附件1所示 */ 
Note: 
       Wpa_supplicant 提供两种由外部模块获取信息的方式:一种是外部模块通过发送request 命令然后获取response的问答模式,另一种是wpa_supplicant主动向外部发送event事件,由外部模块监听接收。 

       一般的常用做法是外部模块通过调用wpa_ctrl_open()两次,建立两个control interface接口,一个为ctrl interface,用于发送命令,获取信息,另一个为monitor interface,用于监听接收来自于wpa_supplicant的event时间。此举可以降低通信的耦合性,避免response和event的相互干扰。 

int wpa_ctrl_attach(struct wpa_ctrl *ctrl); 
/* 注册 某个 control interface 作为 monitor interface */ 

int wpa_ctrl_detach(struct wpa_ctrl *ctrl); 
/* 撤销某个 monitor interface 为 普通的 control interface  */ 

int wpa_ctrl_pending(struct wpa_ctrl *ctrl); 
/* 判断是否有挂起的event 事件 */ 

int wpa_ctrl_recv(struct wpa_ctrl *ctrl, char *reply, size_t *reply_len); 
/* 获取挂起的event 事件 */ 

(2)“ctrl_iface_unix.c”实现wpa_supplicant的Unix domain socket通信机制中server结点,完成对client结点的响应。 
       其中最主要的两个函数为: 
static void wpa_supplicant_ctrl_iface_receive(int sock, void *eloop_ctx, 
                                         void *sock_ctx) 
/* 接收并解析client发送request命令,然后根据不同的命令调用底层不同的处理函数; 
* 然后将获得response结果回馈到 client 结点。 
*/ 

static void wpa_supplicant_ctrl_iface_send(struct ctrl_iface_priv *priv, 
                                      int level, const char *buf, 
                                      size_t len) 
/* 向注册的monitor interfaces 主动发送event事件 */ 

(3)“ctrl_iface.h”和“ctrl_iface.c”主要实现了各种request命令的底层处理函数。 

3.2 下行接口 

Wpa_supplicant提供的下行接口主要用于和kernel(driver)进行通信,下发命令和获取信息。 
Wpa_supplicant下行接口主要包括三种重要的接口: 
1.    PF_INET socket接口,主要用于向kernel 发送ioctl命令,控制并获取相应信息。 
2.    PF_NETLINK socket接口,主要用于接收kernel发送上来的event 事件。 
3.    PF_PACKET socket接口,主要用于向driver传递802.1X报文。 

主要涉及到的文件包括:“driver.h”,“drivers.c”,“driver_wext.h”,“driver_wext.c”,“l2_packet.h”和“l2_packet_linux.c”。其中“driver.h”,“drivers.c”,“driver_wext.h”和“driver_wext.c”实现PF_INET socket接口和PF_NETLINK socket接口;“l2_packet.h”和“l2_packet_linux.c”实现PF_PACKET socket接口。 

(1)“driver.h”,“drivers.c”主要用于封装底层差异对外显示一个相同的wpa_driver_ops接口。Wpa_supplicant可支持atmel, Broadcom, ipw, madwifi, ndis, nl80211, wext等多种驱动。 
其中一个最主要的数据结构为wpa_driver_ops, 其定义了driver相关的各种操作接口。 

(2)“driver_wext.h”,“driver_wext.c”实现了wext形式的wpa_driver_ops,并创建了PF_INET socket接口和PF_NETLINK socket接口,然后通过这两个接口完成与kernel的信息交互。 

Wext提供的一个主要数据结构为: 
struct wpa_driver_wext_data { 
       void *ctx; 
       int event_sock; 
       int ioctl_sock; 
       int mlme_sock; 
       char ifname[IFNAMSIZ + 1]; 
       int ifindex; 
       int ifindex2; 
       int if_removed; 
       u8 *assoc_req_ies; 
       size_t assoc_req_ies_len; 
       u8 *assoc_resp_ies; 
       size_t assoc_resp_ies_len; 
       struct wpa_driver_capa capa; 
       int has_capability; 
       int we_version_compiled; 

       /* for set_auth_alg fallback */ 
       int use_crypt; 
       int auth_alg_fallback; 

       int operstate; 

       char mlmedev[IFNAMSIZ + 1]; 

       int scan_complete_events; 
}; 
其中event_sock 为PF_NETLINK socket接口,ioctl_sock为PF_INET socket借口。 

Driver_wext.c实现了大量底层处理函数用于实现wpa_driver_ops操作参数,其中比较重要的有: 
void * wpa_driver_wext_init(void *ctx, const char *ifname); 
/* 初始化wpa_driver_wext_data 数据结构,并创建PF_NETLINK socket和 PF_INET socket 接口 */ 

void wpa_driver_wext_deinit(void *priv); 
/* 销毁wpa_driver_wext_data 数据结构,PF_NETLINK socket和 PF_INET socket 接口 */ 

static void wpa_driver_wext_event_receive(int sock, void *eloop_ctx, 
                                     void *sock_ctx); 
/* 处理kernel主动发送的event事件的 callback 函数 */ 

最后,将实现的操作函数映射到一个全局的wpa_driver_ops类型数据结构 wpa_driver_wext_ops中。 

const struct wpa_driver_ops wpa_driver_wext_ops = { 
       .name = "wext", 
       .desc = "Linux wireless extensions (generic)", 
       .get_bssid = wpa_driver_wext_get_bssid, 
       .get_ssid = wpa_driver_wext_get_ssid, 
       .set_wpa = wpa_driver_wext_set_wpa, 
       .set_key = wpa_driver_wext_set_key, 
       .set_countermeasures = wpa_driver_wext_set_countermeasures, 
       .set_drop_unencrypted = wpa_driver_wext_set_drop_unencrypted, 
       .scan = wpa_driver_wext_scan, 
       .get_scan_results2 = wpa_driver_wext_get_scan_results, 
       .deauthenticate = wpa_driver_wext_deauthenticate, 
       .disassociate = wpa_driver_wext_disassociate, 
       .set_mode = wpa_driver_wext_set_mode, 
       .associate = wpa_driver_wext_associate, 
       .set_auth_alg = wpa_driver_wext_set_auth_alg, 
       .init = wpa_driver_wext_init, 
       .deinit = wpa_driver_wext_deinit, 
       .add_pmkid = wpa_driver_wext_add_pmkid, 
       .remove_pmkid = wpa_driver_wext_remove_pmkid, 
       .flush_pmkid = wpa_driver_wext_flush_pmkid, 
       .get_capa = wpa_driver_wext_get_capa, 
       .set_operstate = wpa_driver_wext_set_operstate, 
}; 

(3)“l2_packet.h”和“l2_packet_linux.c”主要用于实现PF_PACKET socket接口,通过该接口,wpa_supplicant可以直接将802.1X packet发送到L2层,而不经过TCP/IP协议栈。 

其中主要的功能函数为: 
struct l2_packet_data * l2_packet_init( 
       const char *ifname, const u8 *own_addr, unsigned short protocol, 
       void (*rx_callback)(void *ctx, const u8 *src_addr, 
                         const u8 *buf, size_t len), 
       void *rx_callback_ctx, int l2_hdr); 
/* 创建并初始化PF_PACKET socket接口,其中rx_callback 为从L2接收到的packet 处理callback函数 */ 

void l2_packet_deinit(struct l2_packet_data *l2); 
/* 销毁 PF_PACKET socket接口 */ 

int l2_packet_send(struct l2_packet_data *l2, const u8 *dst_addr, u16 proto, 
                 const u8 *buf, size_t len); 
/* L2层packet发送函数,wpa_supplicant用此发送L2层 802.1X packet  */ 

static void l2_packet_receive(int sock, void *eloop_ctx, void *sock_ctx); 
/*  L2层packet接收函数,接收来自L2层数据后,将其发送到上层  */ 
4. Control interface commands 

       PING 
       MIB 
       STATUS 
       STATUS-VERBOSE 
       PMKSA 
       SET <variable> <valus> 
       LOGON 
       LOGOFF 
       REASSOCIATE 
       RECONNECT 
       PREAUTH <BSSID> 
       ATTACH 
       DETACH 
       LEVEL <debug level> 
       RECONFIGURE 
       TERMINATE 
       BSSID <network id> <BSSID> 
       LIST_NETWORKS 
       DISCONNECT 
       SCAN 
       SCAN_RESULTS 
       BSS 
       SELECT_NETWORK <network id> 
       ENABLE_NETWORK <network id> 
       DISABLE_NETWORK <network id> 
       ADD_NETWORK 
       REMOVE_NETWORK <network id> 
       SET_NETWORK <network id> <variable> <value> 
       GET_NETWORK <network id> <variable> 
       SAVE_CONFIG 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值