查看: 2937|回复: 6

ESP32 开发之旅⑤ Station——WiFiSTA库的使用

[复制链接]
  • TA的每日心情
    开心
    2021-1-4 08:50
  • 签到天数: 827 天

    [LV.10]以坛为家III

    发表于 2020-1-10 09:50 | 显示全部楼层 |阅读模式

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力。希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石。。。

    快速导航
    单片机菜鸟的博客快速索引(快速找到你要的)

    如果觉得有用,麻烦点赞收藏,您的支持是博主创作的动力。

    1. 前言

        在前面的篇章中,博主给大家讲解了ESP32的软硬件配置以及基本功能使用,目的就是想让大家有个初步认识。并且,博主一直重点强调 ESP32 WiFi模块有三种工作模式:

    1. Station模式,也叫站点模式;
    2. Soft-Access Point模式,也叫Soft-AP模式,可以理解为WiFi热点模式;
    3. 以上两种的集合模式,Station 兼Soft-Access Point,也是Mesh NetWork的实现基础;  

        任何基于ESP32的WiFi功能开发,都是基于上面其中一种工作模式来进行开发。所以,它们是我们WiFi基础学习的重点。
        本篇章将讲解Station模式。

    2. 回顾Station模式 —— 我想连上谁

        Station模式又叫做站点工作模式,类似于无线终端,有图有真相:
    Station模式又叫做站点工作模式,类似于无线终端,有图有真相:
    image
        处于Station模式下的ESP32,可以连接到AP。

    通过Station(简称为“STA”)模式,ESP32作为客户端连接到路由的wifi信号。   

    以下黑色字体内容摘录于博主自建qq群里面在arduino 联网.pdf文件,博主觉得讲得挺生动,故分享给大家:  

    1. 首先AP发出信标帧(beacon),意思就是我在这里,谁来泡我啊,移动工作站也会发出探(probe)帧,意思是有谁我可以泡啊,每隔一定时间发出一次。(后面会讲到Scan功能)

    2. 所以,根据这一点,可以产生很多有价值的应用。比如你的带WiFi功能的手机,即使在不连接wifi的情况下,只要打开WiFi功能,就可以被路由器截获这帧信息,路由器收集之后,你的信息就会被一个审计的东西发到服务器上,你手机号xxx上线时间xxx下线时间xxx都浏览了哪些网页,WiFi建立连接过程都一目了然,你的位置也全都暴露了,这就叫WiFi探针。具体流程如下所示

      • STA -------->  Probe Request  ----------> AP
      • STA <--------  Probe Response <---------- AP,这个是由wifi返回的应答帧;
        接下来是身份验证过程,可以使用诸如WEP、WEP2、WPA等加密方式应用到认证请求上:
      • STA -------->  Authentication Request  ----------> AP
        认证请求中包含认证Auth类型,OpenSystem,sharedKey等信息,路由器返回认证结果:
      • STA <--------  Authentication Response <---------- AP
        连接请求:
      • STA -------->  Association Request  ----------> AP
        请求与AP建立关联,从而可以进行数据交互;
        认证通过连接请求OK返回。
      • STA <--------  Association Response <---------- AP  

        通过这个连接过程分析,也证明了我们经常听到的一句话,不要轻易连接不明WiFi热点,各种盗取信息层出不穷。
        同时,学过网络的同学都知道,每台设备都会有个一个IP地址,用来在整个网络环境下的唯一标识。
        而处于Station模式下的ESP32,可以使用DHCP Client的方式,由上级路由分配的ip,或者设置成静态ip。
        如果是采用DHCP分配的方式,获取的ip是动态的,在一些需要知道设备ip才能通信的场合下,就需要通过其他手段来获取ip(一种思路是通过访问webserver去获取模块信息);
        如果是采用设置静态ip的方式,那么就有一个要求前提,要求连接AP设置的网段和静态的要一致,在不能固定AP网段的情况下,这种方式不可取。
        Station模式下的WiFi模块,有几个特点,以便用来管理WiFi连接:

    1. 当最近使用的可接入点连接断开,但后面重新可用,那么ESP32会自动去重新连接它(名词解析,最近使用的可接入点,就是ESP32 最后连接的WiFi热点);

    2. 第一点说到的情况,对于ESP32模块重新启动也一样适用;

    3. 这两点实现的原理就是:ESP32会把最近使用的可接入点的校验信息(ssid账号和psw密码)会存到flash 存储中。使用保存在flash中的校验信息,ESP32就可以重新连接到最近使用的可接入点,尽管你再次改变代码烧写进去,只要你不改变WiFi原来的工作模式和校验信息(说简单点就是,如果你烧写代码不擦除所有flash空间并且设置了自动连接,那么在仍然是station模式下就会自动连接wifi热点,不过在这里,博主建议大家在begin之前先调用 WiFi.disconnect(),可以避免一些奇怪的连接问题)。

    3. WiFiSTA库

        有了前面的理论基础,那么我们开始详解一下ESP32 station模式的专用库——WiFiSTA库,大家使用的时候不需要

    #include <WiFiSTA.h>

        只需要引入

    #include<WiFi.h>

    至于原因,请看源码:
    在这里插入图片描述
        首先,对于STA类库的描述,可以拆分为四个部分:

    1. 第一部分方法,和一个接入点(Access Point,wifi热点)建立连接;
    2. 第二部分方法,管理第一部分方法建立的连接;
    3. 第三部分方法,提供一些关于这个连接的信息,包括MAC地址、IP地址等;
    4. 第四部分方法,提供一些备用方法去连接WiFi-Protected Setup(WPS)以及智能配置方法(SmartConfig);  

    3.1 第一部分方法——建立连接

    3.1.1 begin —— 建立连接

        建立连接,ESP32模块切换工作模式为Station模式。
        会用到以下方法:

    class WiFiSTAClass
    {
        // ----------------------------------------------------------------------------------------------
        // ---------------------------------------- STA function ----------------------------------------
        // ----------------------------------------------------------------------------------------------
    
    public:
    
        wl_status_t begin(const char* ssid, const char *passphrase = NULL, int32_t channel = 0, const uint8_t* bssid = NULL, bool connect = true);
        wl_status_t begin(char* ssid, char *passphrase = NULL, int32_t channel = 0, const uint8_t* bssid = NULL, bool connect = true);
        wl_status_t begin();
      ....... 省略
    };

    下面是函数详解:

    /**
    * 切换工作模式到STA模式,并自动连接到最近接入的wifi热点
    * @param void
    * home.php?mod=space&uid=134873 void
    * @note 调用这个方法就会切换到STA模式,并且连接到最近使用的接入点(会从flash中读取之前存储的配置信息)
    *       如果没有配置信息,那么这个方法基本上没有什么用。
    */
    wl_status_t begin()
    
    /**
    * 切换工作模式到STA模式,并根据connect属性来判断是否连接wifi
    * @param ssid       wifi热点名字
    * @param password   wifi热点密码
    * @param channel    wifi热点的通道号,用特定通信通信,可选参数
    * @param bssid      wifi热点的mac地址,可选参数
    * @param connect    boolean参数,默认等于true,当设置为false,不会去连接wifi热点,会建立module保存上面参数
    * @return wl_status_t  wifi状态
    * @note 调用这个方法就会切换到STA模式。
    *       如果connect等于true,会连接到ssid的wifi热点。
    *       如果connect等于false,不会连接到ssid的wifi热点,会建立module保存上面参数。
    */
    wl_status_t begin(char* ssid, char *passphrase = NULL, int32_t channel = 0, const uint8_t* bssid = NULL, bool connect = true

    注意点:

    • 如果ESP32模块之前处于AP模式,那么当你调用begin()有可能进入到STA+softAP模式;
    • 当你发现一些操作异常,那么你就检测一下当前处于什么模式(WiFi.mode());

    3.1.2 config —— 配置IP地址

    函数说明

    /**
    * 禁止DHCP client,设置station 模式下的IP配置
    * @param  local_ip    station固定的ip地址
    * @param  gateway     网关
    * @param  subnet      子网掩码
    * @param  dns1,dns2  可选参数定义域名服务器(dns)的ip地址,这些域名服务器
    *                     维护一个域名目录(如www.google.co.uk),并将它们翻译成ip地址  
    * @return boolean值,如果配置成功,返回true;
    *         如果配置没成功(模块没处于station或者station+soft AP模式),返回false;
    */
    bool config(IPAddress local_ip, IPAddress gateway, IPAddress subnet, IPAddress dns1 = (uint32_t)0x00000000, IPAddress dns2 = (uint32_t)0x00000000)
    {
        esp_err_t err = ESP_OK;
    
        if(!WiFi.enableSTA(true)) {
            return false;
        }
    
        tcpip_adapter_ip_info_t info;
    
        if(local_ip != (uint32_t)0x00000000){
            info.ip.addr = static_cast<uint32_t>(local_ip);
            info.gw.addr = static_cast<uint32_t>(gateway);
            info.netmask.addr = static_cast<uint32_t>(subnet);
        } else {
            info.ip.addr = 0;
            info.gw.addr = 0;
            info.netmask.addr = 0;
        }
    
        err = tcpip_adapter_dhcpc_stop(TCPIP_ADAPTER_IF_STA);
        if(err != ESP_OK && err != ESP_ERR_TCPIP_ADAPTER_DHCP_ALREADY_STOPPED){
            log_e("DHCP could not be stopped! Error: %d", err);
            return false;
        }
    
        err = tcpip_adapter_set_ip_info(TCPIP_ADAPTER_IF_STA, &info);
        if(err != ERR_OK){
            log_e("STA IP could not be configured! Error: %d", err);
            return false;
        }
    
        if(info.ip.addr){
            _useStaticIp = true;
        } else {
            err = tcpip_adapter_dhcpc_start(TCPIP_ADAPTER_IF_STA);
            if(err == ESP_ERR_TCPIP_ADAPTER_DHCPC_START_FAILED){
                log_e("dhcp client start failed!");
                return false;
            }
            _useStaticIp = false;
        }
    
        ip_addr_t d;
        d.type = IPADDR_TYPE_V4;
    
        if(dns1 != (uint32_t)0x00000000) {
            // Set DNS1-Server
            d.u_addr.ip4.addr = static_cast<uint32_t>(dns1);
            dns_setserver(0, &d);
        }
    
        if(dns2 != (uint32_t)0x00000000) {
            // Set DNS2-Server
            d.u_addr.ip4.addr = static_cast<uint32_t>(dns2);
            dns_setserver(1, &d);
        }
    
        return true;
    }

    注意点:

    • 有着固定IP配置地址的station,通常会更快连接上网络,原因是通过DHCP client获得IP配置这一步被跳过了。如果你把三个参数(local_ip, gateway and subnet)设置为0.0.0.0,那么它会重新启动DHCP,这时你需要重新连接wifi以拿到最新的IP。

    3.2 第二部分方法——管理连接

    3.2.1 reconnect —— 重新连接网络

    函数说明:

    /**
    * 断开连接并且重新连接station到同一个AP
    * @param void
    * @return false or true
    *         返回false,意味着ESP8266不处于STA模式或者说Station在此之前没有连接到一个可接入点。
    *         返回true,意味着已经成功重新启动连接,但是用户仍应该去检测网络连接状态指导WL_CONNECTED。
    */
    bool reconnect()
    {
        if(WiFi.getMode() & WIFI_MODE_STA) {
            if(esp_wifi_disconnect() == ESP_OK) {
                return esp_wifi_connect() == ESP_OK;
            }
        }
        return false;
    }

    案例使用:

    /**
    * 使用案例
    */
    WiFi.reconnect();
    while (WiFi.status() != WL_CONNECTED)
    {
      delay(500);
      Serial.print(".");
    }

    3.2.2 disconnect —— 断开网络连接

    函数说明:

    /**
    * 断开wifi连接
    * @param wifioff 可选参数,设置为true,那么就会关闭Station模式
    * @param eraseap 可选参数,设置为true,清除ssid和psw
    * @return false or true 返回wl_status_t状态
    */
    bool disconnect(bool wifioff = false, bool eraseap = false);
    {
        wifi_config_t conf;
    
        if(WiFi.getMode() & WIFI_MODE_STA){
            if(eraseap){
                memset(&conf, 0, sizeof(wifi_config_t));
                if(esp_wifi_set_config(WIFI_IF_STA, &conf)){
                    log_e("clear config failed!");
                }
            }
            if(esp_wifi_disconnect()){
                log_e("disconnect failed!");
                return false;
            }
            if(wifioff) {
                 return WiFi.enableSTA(false);
            }
            return true;
        }
    
        return false;
    }
    • 对于STA模式下如果出现WiFi账号密码正确的前提下仍然连接不上WiFi热点,可以尝试在begin方法之前调用该方法。当然,博主建议在begin之前都尽量调用该方法。

    3.2.3 isConnected —— 是否连接上网络

    函数讲解:

    
    /**
    * 判断STA模式下是否连接上AP
    * @return 如果STA连接上AP,那么就返回true
    */
    bool isConnected()
    {
        return (status() == WL_CONNECTED);
    }

    3.2.4 setAutoConnect —— 设置是否自动连接到最近接入点

    函数讲解:

    /**
    * 当电源启动后,设置ESP32在STA模式下是否自动连接flash中存储的AP
    * @param autoConnect bool 默认是自动连接
    * @return 返回保存状态 true or false
    */
    bool setAutoConnect(bool autoConnect)
    {
        /*bool ret;
        ret = esp_wifi_set_auto_connect(autoConnect);
        return ret;*/
        return false;//now deprecated
    }

    注意:

    • 此方法已经废弃

    3.2.5 getAutoConnect —— 判断是否设置了自动连接

    函数讲解:

    /**
    * 检测ESP8266 station模式下是否启动自动连接
    * @return 返回自动连接状态 true or false
    */
    bool getAutoConnect()
    {
        /*bool autoConnect;
        esp_wifi_get_auto_connect(&autoConnect);
        return autoConnect;*/
        return false;//now deprecated
    }

    注意:

    • 此方法已经废弃

    3.2.6 setAutoReconnect —— 设置是否自动重新连接到最近接入点

    函数讲解:

    /**
    * 设置当断开连接的时候是否自动重连
    * @param autoConnect bool
    * @return 返回保存状态 true or false
    */
    bool setAutoReconnect(bool autoReconnect)
    {
        _autoReconnect = autoReconnect;
        return true;
    }

    注意点:

    • 如果在网络已经断开了之后才去设置setAutoReconnect(true),这是无效的
    • autoReconnect默认是true

    3.2.7 waitForConnectResult —— 等待网络连接结果

    函数讲解:

    /**
    * 等待直到ESP8266连接AP返回结果
    * @return uint8_t 连接结果
    *         1.WL_CONNECTED 成功连接
    *         2.WL_NO_SSID_AVAIL  匹配SSID失败(账号错误)
    *         3.WL_CONNECT_FAILED psw错误
    *         4.WL_IDLE_STATUS 当wi-fi正在不同的状态中变化
    *         5.WL_DISCONNECTED 这个模块没有配置STA模式
    */
    uint8_t waitForConnectResult();

    3.3 第三部分方法——连接信息

        提供一些关于这个连接的信息,包括MAC地址、IP地址等;

    3.3.1 macAddress —— 获取mac地址

    获取mac地址有2个函数,请看函数讲解:

    /**
     * 获取ESP station下的Mac地址
     * @param mac   uint8_t数组的指针,数组长度为Mac地址的长度,这里为6
     * @return      返回uint8_t数组的指针
     */
    uint8_t * macAddress(uint8_t* mac);
    
    /**
     * 获取ESP station下的Mac地址
     * @return  返回String的Mac地址
     */
    String macAddress();

    应用实例:

    //实例代码1 这只是部分代码 不能直接使用
    if (WiFi.status() == WL_CONNECTED)
    {
      uint8_t macAddr[6];
      WiFi.macAddress(macAddr);
      Serial.printf("Connected, mac address: %02x:%02x:%02x:%02x:%02x:%02x\n", macAddr[0], macAddr[1], macAddr[2], macAddr[3], macAddr[4], macAddr[5]);
      //Connected, mac address: 5C:CF:7F:08:11:17
    }
    
    //实例代码2 这只是部分代码 不能直接使用
    if (WiFi.status() == WL_CONNECTED)
    {
      Serial.printf("Connected, mac address: %s\n", WiFi.macAddress().c_str());
      ////Connected, mac address: 5C:CF:7F:08:11:17
    }

    3.3.2 localIP —— 获取IP地址

    函数讲解:

    /**
     * 返回ESP8266 STA模式下的IP地址
     * @return IP地址
     */
    IPAddress localIP();

    应用实例:

    //实例代码 这只是部分代码 不能直接使用
    if (WiFi.status() == WL_CONNECTED)
    {
      Serial.print("Connected, IP address: ");
      Serial.println(WiFi.localIP());
      //Connected, IP address: 192.168.1.10
    }

    3.3.3 subnetMask —— 获取子网掩码

    函数讲解:

    /**
     * 获取子网掩码的地址
     * @return 返回子网掩码的IP地址
     */
    IPAddress subnetMask();

    应用实例:

    //实例代码 这只是部分代码 不能直接使用
    Serial.print("Subnet mask: ");
    Serial.println(WiFi.subnetMask());
    //Subnet mask: 255.255.255.0

    3.3.4 gatewayIP —— 获取网关地址

    函数讲解:

    /**
     * 获取网关IP地址
     * @return 返回网关IP地址
     */
    IPAddress gatewayIP();

    应用实例:

    
    //实例代码 这只是部分代码 不能直接使用
    Serial.printf("Gataway IP: %s\n", WiFi.gatewayIP().toString().c_str());
    //Gataway IP: 192.168.1.9

    3.3.5 dnsIP —— 获取dns地址

    函数讲解:

    /**
     * 获取DNS ip地址
     * @param dns_no dns序列号
     * @return 返回DNS服务的IP地址
     */
    IPAddress dnsIP(uint8_t dns_no = 0);

    应用实例:

    /实例代码 这只是部分代码 不能直接使用
    Serial.print("DNS #1, #2 IP: ");
    WiFi.dnsIP().printTo(Serial);
    Serial.print(", ");
    WiFi.dnsIP(1).printTo(Serial);
    Serial.println();
    //DNS #1, #2 IP: 62.179.1.60, 62.179.1.61

    3.3.6 hostname —— 获取host名字

    函数讲解:

    /**
     * 获取ESP8266 station DHCP的主机名
     * @return 主机名
     */
    String hostname();

    3.3.7 hostname(name) —— 设置host名字

    设置host名字,有3个可用函数,请看函数讲解:

    /**
     * 设置ESP8266 station DHCP的主机名
     * @param aHostname 最大长度:32
     * @return ok
     */
    bool hostname(char* aHostname);
    bool hostname(const char* aHostname);
    bool hostname(String aHostname);

    应用实例:

    //实例代码 这只是部分代码 不能直接使用
    Serial.printf("Default hostname: %s\n", WiFi.hostname().c_str());
    WiFi.hostname("Station_Tester_02");
    Serial.printf("New hostname: %s\n", WiFi.hostname().c_str());
    //Default hostname: ESP_081117
    //New hostname: Station_Tester_02

    3.3.8 status —— 获取当前wifi连接状态

    函数讲解:

    /**
     * 返回wifi的连接状态
     * @return 返回wl_status_t中定义的其中一值,wl_status_t在 wl_definitions.h中定义
     */
    wl_status_t status();

    3.3.9 SSID —— 获取wifi网络的名字

    函数讲解:

    /**
     * 返回当前通信网络的SSID
     * @return SSID
     */
    String SSID() const;

    应用实例:

    //实例代码 这只是部分代码 不能直接使用
    Serial.printf("SSID: %s\n", WiFi.SSID().c_str());
    //SSID: sensor-net

    3.3.10 psk —— 获取wifi网络密码

    函数讲解:

    /**
     * 返回当前通信网络的密码
     * @return psk
     */
    String psk() const;

    3.3.11 BSSIDstr —— 获取wifi网络macaddress

    函数讲解:

    /**
     * 返回当前通信网络的mac地址
     * @return bssid uint8_t *
     */
    uint8_t * BSSID();
    String BSSIDstr();

    应用实例:

    //实例代码 这只是部分代码 不能直接使用
    Serial.printf("BSSID: %s\n", WiFi.BSSIDstr().c_str());
    //BSSID: 00:1A:70E:C1:68

    3.3.12 RSSI —— 获取wifi网络的信号强度

    函数讲解:

    /**
     * Return the current network RSSI.返回当前通信网络的信号强度,单位是dBm
     * @return  RSSI value
     */
    int32_t RSSI();

    应用实例:

    //实例代码 这只是部分代码 不能直接使用
    Serial.printf("RSSI: %d dBm\n", WiFi.RSSI());
    //RSSI: -68 dBm

    3.3.13 enableIpV6 —— 使能IPV6地址

    函数讲解:

    /**
     * Enable IPv6 on the station interface.
     * @return true on success
     */
    bool WiFiSTAClass::enableIpV6()
    {
        if(WiFiGenericClass::getMode() == WIFI_MODE_NULL){
            return false;
        }
        return tcpip_adapter_create_ip6_linklocal(TCPIP_ADAPTER_IF_STA) == 0;
    }

    3.3.14 localIPv6—— 获取IPV6地址

    函数说明:

    /**
     * Get the station interface IPv6 address.
     * @return IPv6Address
     */
    IPv6Address WiFiSTAClass::localIPv6()
    {
        static ip6_addr_t addr;
        if(WiFiGenericClass::getMode() == WIFI_MODE_NULL){
            return IPv6Address();
        }
        if(tcpip_adapter_get_ip6_linklocal(TCPIP_ADAPTER_IF_STA, &addr)){
            return IPv6Address();
        }
        return IPv6Address(addr.addr);
    }

    3.4 第四部分方法——智能配置

        第四部分方法,智能配置方法(SmartConfig);

    /**
     * 启动 SmartConfig
     */
    bool beginSmartConfig();
    /**
     * 停止 SmartConfig
     */
    bool stopSmartConfig();
    /**
     * 查找SmartConfig状态来决定是否停止配置
     * @return smartConfig Done
     */
    bool smartConfigDone();

        这部分不是本篇的重点,暂时忽略,后续章节会详细讲解。

    4. 实例操作

        上面博主讲了一堆方法理论的知识,下面我们开始讲解操作实例,博主尽量都在代码中注释,直接看代码就好。

    4.1 应用实例1

        statin模式下,创建一个连接到可接入点(wifi热点),并且打印IP地址。

    源码

    4.2 应用实例2

        statin模式下,配置IP地址,网关地址,子网掩码,并且打印IP地址。

    源码

    4.3 应用实例3

        station模式下,创建一个连接到可接入点(wifi热点),并且打印station信息。

    源码

    5. 总结

        本节主要是基于ESP8266WiFiSTA库来讲解Station模式下的函数使用,并且给大家提供了三个实例,至于更多例子我就不继续举例,只能说引导大家入门使用。

    该用户从未签到

    发表于 2020-11-6 10:57 | 显示全部楼层
    讲得很细致
  • TA的每日心情
    开心
    2020-12-4 22:09
  • 签到天数: 1 天

    [LV.1]初来乍到

    发表于 2020-11-28 17:13 | 显示全部楼层
    真详细,这才是干货

    该用户从未签到

    发表于 2020-12-22 23:43 | 显示全部楼层
    好贴 收藏了

    该用户从未签到

    发表于 2021-1-17 20:13 | 显示全部楼层
    4.2的实例怎么看不到啊?

    该用户从未签到

    发表于 2021-1-18 11:18 | 显示全部楼层
    good  学习学习。。。系列文章。
    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    热门推荐

    制作番茄计时器 提高学习效率        番茄钟   计时器
    制作番茄计时器 提高学习
    *学业重,本贴在未来几个月内无法及时维护 #视频介绍(推荐) https://www.bilibili.co
    关于arduino开发的工程方面的一些探索
    关于arduino开发的工程方
    刚开始用Arduino IDE写代码,连个代码提示都没有,后来发现在vscode上有arduino的插件
    雄霸Arduino中文开发平台
    雄霸Arduino中文开发平台
    雄霸Arduino中文开发平台适合于小白学习,中英文无缝切换。由于文件较大无法上传,请
    点灯·blinker esp8266加上DHT_11温度传感器
    点灯·blinker esp8266加
    #定义 BLINKER_WIFI #定义 BLINKER_MIOT_SENSOR //小爱同学定义为传感器设备
    【花雕动手做】太搞笑啦,一支胶管制成二只蠕动机器人
    【花雕动手做】太搞笑啦,
    装修屋子,用完了一筒千里马密封胶,偶然脑洞大开,想要试试看......
    Copyright   ©2015-2016  Arduino中文社区  Powered by©Discuz!   
    快速回复 返回顶部 返回列表