有关wifi配置工具wpa_cli以及wpa_supplicant简单分析

最近在公司开发新产品智能Android机器人,开始使用的是rk3229开发板,在调试wifi的时候经常用到工具wap_cli,开始使用demo板调试的时候wpa_cli可以正常使用。但是由于产品需要支持蓝牙功能,所以换了博通的AP6212的wifi模块。demo使用的rtl8616wifi模块。后来发现新硬件上的wap_cli不能使用。很影响调试进度。报错如下

Failed to connect to non-global ctrl_ifname: wlan0  error: No such file or directory
Could not connect to wpa_supplicant: wlan0 - re-trying

报这个错的原因是找不到相关的waln0这个socket通信节点,正常情况下这个socket会在此目录下建立通信节点 /data/misc/wifi/sockets/wlan0,但是我发现
在新硬件下面根本没有建立相关节点。通过不断调试添加log信息原来是ctrl_interface没有定义锁导致。只要把params.override_ctrl_interface定义了就ok了。
wifi的 supplicant模块启动在init.rc中触发的,不同wifi芯片 触发的service是不一样的。比如下面是rtl wifi

service rtw_suppl_con /system/bin/wpa_supplicant_rtl \
    -ip2p0 -Dnl80211 -c/data/misc/wifi/p2p_supplicant.conf \
    -e/data/misc/wifi/entropy.bin -N \
    -iwlan0 -Dnl80211 -c/data/misc/wifi/wpa_supplicant.conf \
    -O/data/misc/wifi/sockets \
    -g@android:wpa_wlan0
    class main
    socket wpa_wlan0 dgram 660 wifi wifi
    disabled
    oneshot

这个是博通的

service wpa_supplicant /system/bin/wpa_supplicant \
    -iwlan0 -Dnl80211 -c/data/misc/wifi/wpa_supplicant.conf \
    -I/system/etc/wifi/wpa_supplicant_overlay.conf \
    -e/data/misc/wifi/entropy.bin -g@android:wpa_wlan0
    class main
    socket wpa_wlan0 dgram 660 wifi wifi
    disabled
    oneshot

-i 指定网络socket通信节点 这里指定的是 wlan0

-D 指定wifi驱动的的接口 这里是nl80211
-C 指定 wpa_supplicant 初始化的时候需要读取 配置文件 
这里是/data/misc/wifi/wpa_supplicant.conf
-I 备用读取配置文件
-g 指定 ctrl_interface =  @android:wpa_ 和 -i 指定waln0组合

这里有点不是很明白 ctrl_interface = @android:wpa_wlan0,但是真用在代码中ctrl_interface 会变为ctrl_interface = /data/misc/wifi/sockets
具体相关转换原因自己没有多关注。有了解的可以可以说一下。
一般情况下配置文件的内容如下:

ctrl_interface=/data/misc/wifi/sockets
update_config=1
ap_scan=1

ctrl_interface=/data/misc/wifi/sockets 和 -g@android:wpa_wlan0 在源码中的体现

case 'g':
            params.ctrl_interface = optarg;
            break;

这就是疑惑的地方 两个明显不同确实指的同一个ctrl_interface
ap_scan=1
1—wpas负责扫描和选择网络
0、2—驱动负责扫描和连接
update_config=1 允许更新配置文件

int main(int argc, char *argv[])
{
    case 'c':
            iface->confname = optarg;
            break;
        case 'C':
            iface->ctrl_interface = optarg;
            break;
        case 'D':
            iface->driver = optarg;
            break;
        case 'd':
#ifdef CONFIG_NO_STDOUT_DEBUG
            printf("Debugging disabled with "
                   "CONFIG_NO_STDOUT_DEBUG=y build time "
                   "option.\n");
            goto out;
#else /* CONFIG_NO_STDOUT_DEBUG */
            params.wpa_debug_level--;
            break;
#endif /* CONFIG_NO_STDOUT_DEBUG */
        case 'e':
            params.entropy_file = optarg;
            break;
#ifdef CONFIG_DEBUG_FILE
        case 'f':
            params.wpa_debug_file_path = optarg;
            break;
#endif /* CONFIG_DEBUG_FILE */
        case 'g':
            params.ctrl_interface = optarg;
            break;
        case 'G':
            params.ctrl_interface_group = optarg;
            break;
        case 'h':
            usage();
            exitcode = 0;
            goto out;
        case 'i':
            iface->ifname = optarg;
    、、、、、、、、
    global = wpa_supplicant_init(&params);
    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_supplicant_add_iface
        wpa_s = wpa_supplicant_add_iface(global, &ifaces[i]);
}
if (exitcode == 0)
        exitcode = wpa_supplicant_run(global);

wpa_supplicant_init函数功能是初始化struct wpa_global *global这个结构体
wpa_global是一个全局性质的上下文信息。它通过ifaces变量指向一个wpa_supplicant对象。
(1)wpa_interface用于描述一个无线设备。该参数在初始化时用到。
(2)wpa_global是一个全局性质的上下文信息。它通过ifaces变量指向一个wpa_supplicant对象。
(3)wpa_supplicant是wpa_supplicant的核心数据结构。一个interface对应一个wpa_supplicant对象,其内部包含非常多的成员变量。
(4)ctrl_iface_global_priv是全局控制接口的信息,内部包含一个用于通信的socket句柄

wpa_supplicant_init()
{
    全局结构体指针 return 返回用
    struct wpa_global *global;
        global = os_zalloc(sizeof(*global));
    if (global == NULL)
        return NULL;
    dl_list_init(&global->p2p_srv_bonjour);
    dl_list_init(&global->p2p_srv_upnp);
    global->params.daemonize = params->daemonize;
    global->params.wait_for_monitor = params->wait_for_monitor;
    global->params.dbus_ctrl_interface = params->dbus_ctrl_interface;
    if (params->pid_file)
        global->params.pid_file = os_strdup(params->pid_file);
    if (params->ctrl_interface)
        global->params.ctrl_interface =
            os_strdup(params->ctrl_interface);
            这句话很重要。如果为空的话。相关的socket就不会建立 就会出现上面的报错
    if (params->ctrl_interface_group)
        global->params.ctrl_interface_group =
            os_strdup(params->ctrl_interface_group);
    if (params->override_driver)
        global->params.override_driver =
            os_strdup(params->override_driver);
    if (params->override_ctrl_interface)
        global->params.override_ctrl_interface =
            os_strdup(params->override_ctrl_interface);
将 params 都复制到global 的wpa_params中
初始化ctrl_iface_global_priv,最重要的是建立socket服务端,并且设置socket节点的地址ctrl_interface。
global->ctrl_iface = wpa_supplicant_global_ctrl_iface_init(global);

}
wpa_supplicant_global_ctrl_iface_init(struct wpa_global *global)
{
    struct ctrl_iface_global_priv *priv;

    priv = os_zalloc(sizeof(*priv));
    if (priv == NULL)
        return NULL;
    dl_list_init(&priv->ctrl_dst);
    priv->global = global;
    priv->sock = -1;

    if (global->params.ctrl_interface == NULL)
    这里判断ctrl_interface是否为空。为空直接返回。所以就不会建立相关socket
        return priv;
建立socket服务客户端 用来接收 wpa_cli 和framework传来的命名
    if (wpas_global_ctrl_iface_open_sock(global, priv) < 0) {
        os_free(priv);
        return NULL;
    }

    wpa_msg_register_cb(wpa_supplicant_ctrl_iface_msg_cb);

    return priv;
}

static int wpas_global_ctrl_iface_open_sock(struct wpa_global *global,
                        struct ctrl_iface_global_priv *priv)
{
    struct sockaddr_un addr;
    这里应该是ctrl_interface=/data/misc/wifi/sockets
    const char *ctrl = global->params.ctrl_interface;
    int flags;

#ifdef ANDROID
这里应该是ctrl=@android:wpa_wlan0,所以 ctrl + 9 = wlan0
    if (os_strncmp(ctrl, "@android:", 9) == 0) {
    建立服务端的 socket
        priv->sock = android_get_control_socket(ctrl + 9);
        if (priv->sock < 0) {
            wpa_printf(MSG_ERROR, "Failed to open Android control "
                   "socket '%s'", ctrl + 9);
            goto fail;
        }
        wpa_printf(MSG_DEBUG, "Using Android control socket '%s'",
               ctrl + 9);
        goto havesock;
    }

    if (os_strncmp(ctrl, "@abstract:", 10) != 0) {
        /*
         * Backwards compatibility - try to open an Android control
         * socket and if that fails, assume this was a UNIX domain
         * socket instead.
         */
        priv->sock = android_get_control_socket(ctrl);
        if (priv->sock >= 0) {
            wpa_printf(MSG_DEBUG,
                   "Using Android control socket '%s'",
                   ctrl);
            goto havesock;
        }
    }
#endif /* ANDROID */



havesock:

    flags = fcntl(priv->sock, F_GETFL);
    if (flags >= 0) {
        flags |= O_NONBLOCK;
        if (fcntl(priv->sock, F_SETFL, flags) < 0) {
            wpa_printf(MSG_INFO, "fcntl(ctrl, O_NONBLOCK): %s",
                   strerror(errno));
            /* Not fatal, continue on.*/
        }
    }
//这里会进行conf文件的保存。之前通过 init.rc service启动时候添加的参数和wpa_supplicant 初始化默认的参数会保存到data/misc/wifi/wpa_supplicant.conf中
    eloop_register_read_sock(priv->sock,
                 wpa_supplicant_global_ctrl_iface_receive,
                 global, priv);

    return 0;

fail:
    if (priv->sock >= 0) {
        close(priv->sock);
        priv->sock = -1;
    }
    return -1;
}

wpa_supplicant_global_ctrl_iface_receive()可以接受两类命令,接口命令和全局命令。一类命令前面有“IFNAME= ”指明处理该命令的接口,然后调用每个接口对应的 wpa_supplicant_ctrl_iface_process()来进行相应处理。另一类未指定接口,由wpa_supplicant_global_ctrl_iface_process()直接处理。

static void wpa_supplicant_global_ctrl_iface_receive(int sock, void *eloop_ctx,
                             void *sock_ctx)
{
    struct wpa_global *global = eloop_ctx;
    struct ctrl_iface_global_priv *priv = sock_ctx;
    char buf[4096];
    int res;
    struct sockaddr_un from;
    socklen_t fromlen = sizeof(from);
    char *reply = NULL, *reply_buf = NULL;
    size_t reply_len;

    res = recvfrom(sock, buf, sizeof(buf) - 1, 0,
               (struct sockaddr *) &from, &fromlen);
    if (res < 0) {
        wpa_printf(MSG_ERROR, "recvfrom(ctrl_iface): %s",
               strerror(errno));
        return;
    }
    buf[res] = '\0';

    if (os_strcmp(buf, "ATTACH") == 0) {
        if (wpa_supplicant_ctrl_iface_attach(&priv->ctrl_dst, &from,
                             fromlen))
            reply_len = 1;
        else
            reply_len = 2;
    } else if (os_strcmp(buf, "DETACH") == 0) {
        if (wpa_supplicant_ctrl_iface_detach(&priv->ctrl_dst, &from,
                             fromlen))
            reply_len = 1;
        else
            reply_len = 2;
    } else {
    //wpa_supplicant_global_ctrl_iface_process分发处理传入的命令
        reply_buf = wpa_supplicant_global_ctrl_iface_process(
            global, buf, &reply_len);
        reply = reply_buf;
    }

    if (!reply && reply_len == 1) {
        reply = "FAIL\n";
        reply_len = 5;
    } else if (!reply && reply_len == 2) {
        reply = "OK\n";
        reply_len = 3;
    }

    if (reply) {
        if (sendto(sock, reply, reply_len, 0, (struct sockaddr *) &from,
               fromlen) < 0) {
            wpa_printf(MSG_DEBUG, "ctrl_iface sendto failed: %s",
                strerror(errno));
        }
    }
    os_free(reply_buf);
}
char * wpa_supplicant_global_ctrl_iface_process(struct wpa_global *global,
                        char *buf, size_t *resp_len)
{
if (os_strcmp(buf, "PING") == 0) {
        os_memcpy(reply, "PONG\n", 5);
        reply_len = 5;
    } else if (os_strncmp(buf, "INTERFACE_ADD ", 14) == 0) {
        if (wpa_supplicant_global_iface_add(global, buf + 14))
            reply_len = -1;
    } else if (os_strncmp(buf, "INTERFACE_REMOVE ", 17) == 0) {
        if (wpa_supplicant_global_iface_remove(global, buf + 17))
            reply_len = -1;
    } else if (os_strcmp(buf, "INTERFACE_LIST") == 0) {
        reply_len = wpa_supplicant_global_iface_list(
            global, reply, reply_size);
    } else if (os_strcmp(buf, "INTERFACES") == 0) {
        reply_len = wpa_supplicant_global_iface_interfaces(
            global, reply, reply_size);
    } else if (os_strcmp(buf, "TERMINATE") == 0) {
        wpa_supplicant_terminate_proc(global);
    } else if (os_strcmp(buf, "SUSPEND") == 0) {
        wpas_notify_suspend(global);
    } else if (os_strcmp(buf, "RESUME") == 0) {
        wpas_notify_resume(global);
    } else if (os_strncmp(buf, "SET ", 4) == 0) {
        if (wpas_global_ctrl_iface_set(global, buf + 4)) {
#ifdef CONFIG_P2P
            if (global->p2p_init_wpa_s) {
                os_free(reply);
                /* Check if P2P redirection would work for this
                 * command. */
                return wpa_supplicant_ctrl_iface_process(
                    global->p2p_init_wpa_s,
                    buf, resp_len);
            }
#endif /* CONFIG_P2P */
            reply_len = -1;
        }
#ifndef CONFIG_NO_CONFIG_WRITE
    } else if (os_strcmp(buf, "SAVE_CONFIG") == 0) {
        if (wpas_global_ctrl_iface_save_config(global))
            reply_len = -1;
#endif /* CONFIG_NO_CONFIG_WRITE */
    } else if (os_strcmp(buf, "STATUS") == 0) {
        reply_len = wpas_global_ctrl_iface_status(global, reply,
                              reply_size);
#ifdef CONFIG_MODULE_TESTS
    } else if (os_strcmp(buf, "MODULE_TESTS") == 0) {
        int wpas_module_tests(void);
        if (wpas_module_tests() < 0)
            reply_len = -1;
#endif /* CONFIG_MODULE_TESTS */
    } else {
        os_memcpy(reply, "UNKNOWN COMMAND\n", 16);
        reply_len = 16;
    }

    if (reply_len < 0) {
        os_memcpy(reply, "FAIL\n", 5);
        reply_len = 5;
    }

    *resp_len = reply_len;
    return reply;
}

wpa_supplicant_global_ctrl_iface_process主要是命令解析,
根据wpa_cli或者framework传递过来的参数。然后执行相关的函数。
在这里我们关注

if (os_strcmp(buf, "SAVE_CONFIG") == 0) {
        if (wpas_global_ctrl_iface_save_config(global))
            reply_len = -1;

保存我们设置的参数。不论是通过settings.apk还是wpa_cli工具联网,最后相关的ssid和password都会保存到wpa_supplicant.conf中。这也是为啥市面上简单很多共享wifi需要root权限原因。没有root权限根本读取不了你的配置文件。
真正执行保存文件的代码是

int wpa_config_write(const char *name, struct wpa_config *config)
{
f = fopen(tmp_name, "w");
wpa_config_write_global(f, config);
//这里保存network={
//  ssid="yzs_test"
//  psk="12345ABCDE"
//  key_mgmt=WPA-PSK
//  priority=10
//}
for (ssid = config->ssid; ssid; ssid = ssid->next) {
        if (ssid->key_mgmt == WPA_KEY_MGMT_WPS || ssid->temporary)
            continue; /* do not save temporary networks */
        if (wpa_key_mgmt_wpa_psk(ssid->key_mgmt) && !ssid->psk_set &&
            !ssid->passphrase)
            continue; /* do not save invalid network */
        fprintf(f, "\nnetwork={\n");
        wpa_config_write_network(f, ssid);
        fprintf(f, "}\n");
    }
    fflush(f);
}

wpa_config_write_global会保存除network热点之外的其他属性比如
**ctrl_interface=/data/misc/wifi/sockets
update_config=1
device_name=rk322x_box
manufacturer=rockchip
model_name=rk322x-box
model_number=rk322x-box
serial_number=FNM88CD8WC**

wpa_cli是shell情况下使用的wifi网络配置工具。和framework下的wifi.c是共用一个socket服务。一般情况下使用 示例

wpa_cli -i wlan0 -p /data/misc/wifi/sockets 

然后进入交互模式。若此时没有建立相关wpa_supplicant服务,此时就会报
Failed to connect to wpa_supplicant
此行代码在wpa_cli.c下

int main(int argc, char *argv[])
{
    int c;
    int daemonize = 0;
    int ret = 0;
    const char *global = NULL;

    if (os_program_init())
        return -1;

    for (;;) {
        c = getopt(argc, argv, "a:Bg:G:hi:p:P:v");
        if (c < 0)
            break;
        switch (c) {
        case 'a':
            action_file = optarg;
            break;
        case 'B':
            daemonize = 1;
            break;
        case 'g':
            global = optarg;
            break;
        case 'G':
            ping_interval = atoi(optarg);
            break;
        case 'h':
            usage();
            return 0;
        case 'v':
            printf("%s\n", wpa_cli_version);
            return 0;
        case 'i':
            os_free(ctrl_ifname);
            ctrl_ifname = os_strdup(optarg);
            printf("ctrl_ifname is ===== %s \n", ctrl_ifname);
            break;
        case 'p':
            ctrl_iface_dir = optarg;
            break;
        case 'P':
            pid_file = optarg;
            break;
        default:
            usage();
            return -1;
        }
    }

    interactive = (argc == optind) && (action_file == NULL);

    if (interactive)
        printf("%s\n\n%s\n\n", wpa_cli_version, wpa_cli_license);

    if (eloop_init())
        return -1;

    if (global) {
#ifdef CONFIG_CTRL_IFACE_NAMED_PIPE
        ctrl_conn = wpa_ctrl_open(NULL);
#else /* CONFIG_CTRL_IFACE_NAMED_PIPE */
        ctrl_conn = wpa_ctrl_open(global);
#endif /* CONFIG_CTRL_IFACE_NAMED_PIPE */
        if (ctrl_conn == NULL) {
            fprintf(stderr, "Failed to connect to wpa_supplicant "
                "global interface: %s  error: %s\n",
                global, strerror(errno));
            return -1;
        }

        if (interactive) {
            update_ifnames(ctrl_conn);
            mon_conn = wpa_ctrl_open(global);
            if (mon_conn) {
                if (wpa_ctrl_attach(mon_conn) == 0) {
                    wpa_cli_attached = 1;
                    eloop_register_read_sock(
                        wpa_ctrl_get_fd(mon_conn),
                        wpa_cli_mon_receive,
                        NULL, NULL);
                } else {
                    printf("Failed to open monitor "
                           "connection through global "
                           "control interface\n");
                }
            }
        }
    }

    eloop_register_signal_terminate(wpa_cli_terminate, NULL);

    if (ctrl_ifname == NULL)
        ctrl_ifname = wpa_cli_get_default_ifname();

    if (interactive) {
    //初始化socket通信的交互接口,这里会打开两个socket ctrl_conn 和mon_conn ,主要用户
        wpa_cli_interactive();
    } else {
    //这里就是报Failed to connect to non-global ctrl_ifname: wlan0  error: No such file or directory 原因
        if (!global &&
            wpa_cli_open_connection(ctrl_ifname, 0) < 0) {
            fprintf(stderr, "Failed to connect to non-global "
                "ctrl_ifname: %s  error: %s\n",
                ctrl_ifname, strerror(errno));
            return -1;
        }

        if (action_file) {
            if (wpa_ctrl_attach(ctrl_conn) == 0) {
                wpa_cli_attached = 1;
            } else {
                printf("Warning: Failed to attach to "
                       "wpa_supplicant.\n");
                return -1;
            }
        }

        if (daemonize && os_daemonize(pid_file))
            return -1;

        if (action_file)
            wpa_cli_action(ctrl_conn);
        else
            ret = wpa_request(ctrl_conn, argc - optind,
                      &argv[optind]);
    }

    os_free(ctrl_ifname);
    eloop_destroy();
    wpa_cli_cleanup();

    return ret;
}

wpa_cli_open_connection 打开socket服务失败就会报错。
ctrl_conn 用于wpa_cli往下发命令
monitor_conn 用于接收wpa_supplicant发送过来的信息

static int wpa_cli_open_connection(const char *ifname, int attach)
{
    ctrl_conn = wpa_ctrl_open(ifname);
    mon_conn = wpa_ctrl_open(ifname)
    }
struct wpa_ctrl * wpa_ctrl_open(const char *ctrl_path)
{
    //建立本地socket客户端
    ctrl->s = socket(PF_UNIX, SOCK_DGRAM, 0);
    ctrl->local.sun_family = AF_UNIX;
    //这里的addr.sun_path is data/misc/wifi/sockets/wlan0
    ret = os_snprintf(ctrl->local.sun_path, sizeof(ctrl->local.sun_path),
              CONFIG_CTRL_IFACE_CLIENT_DIR "/"
              CONFIG_CTRL_IFACE_CLIENT_PREFIX "%d-%d",
              (int) getpid(), counter);
//绑定socket地址
bind(ctrl->s, (struct sockaddr *) &ctrl->local,
            sizeof(ctrl->local)) < 0)
            //连接服务端
            connect(ctrl->s, (struct sockaddr *) &ctrl->dest,
            sizeof(ctrl->dest)) < 0)
            }
Android调试WiFi,通常会使用wpa_supplicantwpa_cli。下面是一些总结和步骤: 1. 在设备上运行wpa_supplicant 在设备上运行wpa_supplicant,它是一个用于处理WiFi连接的后台进程。可以在终端中使用以下命令启动它: ``` wpa_supplicant -iwlan0 -Dwext -c /data/misc/wifi/wpa_supplicant.conf ``` 其中,`wlan0` 是设备的WiFi接口名称,`wext` 是驱动程序的类型,`/data/misc/wifi/wpa_supplicant.conf` 是包含WiFi配置信息的文件路径。 2. 使用wpa_cli进行WiFi连接 wpa_cli是一个命令行工具,用于与wpa_supplicant进行交互并管理WiFi连接。可以在终端中使用以下命令启动它: ``` wpa_cli -iwlan0 ``` 此时会进入wpa_cli交互模式。可以使用以下命令执行一些操作: - `scan` 扫描可用的WiFi网络 - `scan_results` 查看扫描结果 - `add_network` 添加一个新的WiFi网络配置 - `set_network` 设置WiFi网络配置 - `enable_network` 启用WiFi网络 - `disable_network` 禁用WiFi网络 - `status` 查看当前连接状态 例如,如果要连接到名为“mywifi”的WiFi网络,可以执行以下步骤: 1. 扫描可用的WiFi网络 ``` > scan ``` 2. 查看扫描结果 ``` > scan_results ``` 会显示可用的WiFi网络列表,找到名为“mywifi”的网络并记下其network id。 3. 添加一个新的WiFi网络配置 ``` > add_network ``` 会返回一个新的network id。 4. 设置WiFi网络配置 ``` > set_network <network_id> ssid "mywifi" > set_network <network_id> psk "mypassword" ``` 其中,`<network_id>` 是上一步中返回的新的network id,`mywifi` 是WiFi网络的SSID,`mypassword` 是WiFi网络的密码。 5. 启用WiFi网络 ``` > enable_network <network_id> ``` 6. 查看连接状态 ``` > status ``` 如果连接成功,会显示类似于以下内容的信息: ``` wpa_state=COMPLETED ip_address=192.168.1.100 ```
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

九霄的爸爸

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

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

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

打赏作者

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

抵扣说明:

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

余额充值