11.TCP server&TCP client


TCP client

client,又名客户端,也就是需要通过获取server提供的服务数据来展示自己。Tcp client,只是架构在tcp协议之上的客户端。上图中,ESP8266作为client端,通过路由,访问局域网内的Pc server或者广域网下的网络服务器信息,server收到请求后会处理请求并且把响应数据返回以供ESP8266使用。

总体上分为4种方法

  • 第一类方法,连接操作;
  • 第二类方法,发送请求操作;
  • 第三类方法,响应操作;
  • 第四类方法,普通设置;

1.建立tcp连接

/**
 * 建立一个tcp连接
 * @param ip    IPAddress of tcpserver
 * @param port  port of tcpserver
 * @return  result of tcp connect
 *          1 --- success
 *          0 --- fail
 */
int connect(IPAddress ip, uint16_t port);

/**

  • 建立一个tcp连接
  • @param host host of tcpserver (192.xx.xx.xx)
  • @param port port of tcpserver
  • @return result of tcp connect
  •      1 --- success
    
  •      0 --- fail
    

*/
int connect(const char *host, uint16_t port)

/**

  • 建立一个tcp连接
  • @param host host of tcpserver (192.xx.xx.xx)
  • @param port port of tcpserver
  • @return result of tcp connect
  •      1 --- success
    
  •      0 --- fail
    

*/
int connect(const String host, uint16_t port);

2.判断client是否还在连接

/**
 * 判断tcp连接是否建立起来(ESTABLISHED)
 * @return  result of tcp connect
 *           1 --- success
 *           0 --- fail
 */
uint8_t connected();

3.停止tcp连接

/**
 * 关闭tcp连接
 */
void stop();

4.获取tcp连接状态

/**
 * 获取tcp连接状态
 * @return  result of tcp connect
 *          CLOSED      = 0,
 *          LISTEN      = 1,
 *          SYN_SENT    = 2,
 *          SYN_RCVD    = 3,
 *          ESTABLISHED = 4,
 *          FIN_WAIT_1  = 5,
 *          FIN_WAIT_2  = 6,
 *          CLOSE_WAIT  = 7,
 *          CLOSING     = 8,
 *          LAST_ACK    = 9,
 *          TIME_WAIT   = 10
 */
uint8_t status();

2.发送数据操作

5.发送数据到client连接的server

/**
 * 发送数据
 * @param str 需要单个字节
 * @return size_t 成功写入发送缓冲区的字节数
 */
size_t write(uint8_t);

/**

  • 发送数据
  • @param str 需要发送字符串或者字符数组
  • @return size_t 成功写入发送缓冲区的字节数
    */
    size_t write(const char *str);

/**

  • 发送数据
  • @param buffer 需要发送字符串或者字符数组
  • @param size 数据字节数
  • @return size_t 成功写入发送缓冲区的字节数
    */
    size_t write(const char *buffer, size_t size)

/**

  • 发送数据
  • @param stream 数据流,比如文件流
  • @return size_t 成功写入发送缓冲区的字节数
    */
    size_t write(Stream& stream);
    //注意:write(uint8_t)函数是发送数据的底层方法,也就是说print、println底层也是调用write;
    write(const char *str) 函数底层是调用 write(const char *buffer, size_t size),通过strlen计算长度;

6.发送数据到client连接的server

/**
 * 发送数据
 * @param FlashStringHelper 需要发送的字符串,字符串存在flash中(PROGMEM)
 * @return size_t 成功写入发送缓冲区的字节数
 */
size_t print(const __FlashStringHelper *);

/**

  • 发送数据
  • @param String 需要发送的字符串,字符串存在内存中
  • @return size_t 成功写入发送缓冲区的字节数
    */
    size_t print(const String &);

/**

  • 发送数据
  • @param String 需要发送的字符数组,字符数组存在内存中
  • @return size_t 成功写入发送缓冲区的字节数
    */
    size_t print(const char[]);

/**

  • 发送数据
  • @param String 需要发送的字符
  • @return size_t 成功写入发送缓冲区的字节数
    */
    size_t print(char);

/**

  • 发送数据
  • @param String 需要发送的数据,多是数字,转成对应的进制,一般都是传输数字型数据
  • @return size_t 成功写入发送缓冲区的字节数
    */
    size_t print(unsigned char, int = DEC);
    size_t print(int, int = DEC);
    size_t print(unsigned int, int = DEC);
    size_t print(long, int = DEC);
    size_t print(unsigned long, int = DEC);
    size_t print(double, int = 2);
    //读者需要特别关注 print(const __FlashStringHelper *) 这个函数,以后代码内存优化需用用到;
    常见用法:
    //实例代码 非完整代码 不可直接使用 理解即可
    WiFiClient client;
    client.print( F(“This is an flash string”)); //字符串“This is an flash string”存在于flash

7.发送数据到client连接的server

/**
  • 发送数据,并且加上换行符 “
  • @param FlashStringHelper 需要发送的字符串,字符串存在flash中(PROGMEM)
  • @return size_t 成功写入发送缓冲区的字节数
    */
    size_t println(const __FlashStringHelper *);
  • /**

    • 发送数据,并且加上换行符 “
    • @param String 需要发送的字符串,字符串存在内存中
    • @return size_t 成功写入发送缓冲区的字节数
      */
      size_t println(const String &s);

    /**

    • 发送数据,并且加上换行符 “
    • @param String 需要发送的字符数组,字符数组存在内存中
    • @return size_t 成功写入发送缓冲区的字节数
      */
      size_t println(const char[]);

    /**

    • 发送数据,并且加上换行符 “
    • @param String 需要发送的字符
    • @return size_t 成功写入发送缓冲区的字节数
      */
      size_t println(char);

    /**

    • 发送数据,并且加上换行符 “
    • @param String 需要发送的数据,多是数字,转成对应的进制,一般都是传输数字型数据
    • @return size_t 成功写入发送缓冲区的字节数
      */
      size_t println(unsigned char, int = DEC);
      size_t println(int, int = DEC);
      size_t println(unsigned int, int = DEC);
      size_t println(long, int = DEC);
      size_t println(unsigned long, int = DEC);
      size_t println(double, int = 2);

    /**

    • 发送换行符 “
    • @return size_t 成功写入发送缓冲区的字节数
      */
      size_t println(void);
      //注意:println系列其实就是在print系列的基础上加上了换行符 “
      ”;

    8.返回接收缓存区可读取字节数

    /**
     * 返回发送缓冲区剩余可写字节数
     * @return int 发送缓冲区剩余可写字节数
     */
    size_t availableForWrite();
    //注意:一般来说,调用发送数据操作之后,并不会立刻发送出去,而是把数据放入发送缓冲区,通过机制不断读取发送缓冲区的数据不断发送出去;
    可以通过此函数判断请求是否发送完毕;

    9.读取接收缓冲区一个字节

    
    /**
     * 读取接收缓冲区一个字节
     * @return int 一字节数据
     */
    int read();
    

    10.读取接收缓冲区size大小的字节数据

    /**
     * 读取接收缓冲区size大小的字节数据
     * @param buf 数据存储到该buf
     * @param size 读取大小
     * @return int 成功读取的大小
     */
    int read(uint8_t *buf, size_t size);
    

    11.读取接收缓冲区size大小的字节数据

    /**
     * 读取接收缓冲区一个字节
     * @return int 一字节数据
     */
    int peek();
    

    12.读取接收缓冲区size大小的字节数据

    /**
     * 读取接收缓冲区length大小的字节数据
     * @param buffer 数据存储到该 buffer
     * @param length 读取大小
     * @return size_t 成功读取的大小
     */
    size_t peekBytes(uint8_t *buffer, size_t length);
    size_t peekBytes(char *buffer, size_t length);
    //注意:此函数读取完数据后,不会把该数据从缓冲区清掉,所以需要特别关注这一点
    

    13.读取响应数据直到某个字符串为止

    /**
     * 读取响应数据直到某个字符串为止
     * @param end 结束字符
     * @return String 读取成功的字符串
     */
    String readStringUntil(char end);
    

    14.查找某个字符串

    /**
     * 判断是否存在某个目标字符串
     * @param buffer 目标字符串
     * @return bool 存在返回true
     */
    bool find(char *buffer);
    //注意:此函数会把数据从缓冲区清掉;
    

    15.清除接收缓冲区

    /**
     * 清除缓冲区
     */
    void flush(void);
    //注意:新版本flush功能是等待缓冲区中的所有传出字符都已发送。所以做不了清除缓冲区的作用;
    可以有以下代替:
    while(client.read()>0);
    

    方法要点:博主建议大家尽量用批量处理的方法,比如 readStringUntil、read(buf,size)、peekBytes(buf,length),性能方面会好很多; 博主通过查看源码,发现client的发送缓冲区的大小是256Bytes;

    16.是否禁用 Nagle 算法。

    /**
     * 是否禁用 Nagle 算法。
     * @param nodelay true表示禁用 Nagle 算法
     */
    void setNoDelay(bool nodelay);
    //注意:Nagle 算法的目的是通过合并一些小的发送消息,然后一次性发送所有的消息来减少通过网络发送的小数据包的tcp/ip流量。这种方法的缺点是延迟了单个消息的发送,直到一个足够大的包被组装。
    

    实例操作

    /**
     * Demo:
     *    STA模式下,演示WiFiClient与TCP server之间的通信功能
     *    本实验需要跟TCP调试助手一起使用。
     * @author 单片机菜鸟
     * @date 2019/1/25
     */
    #include <ESP8266WiFi.h>
     
    

    //以下三个定义为调试定义
    #define DebugBegin(baud_rate) Serial.begin(baud_rate)
    #define DebugPrintln(message) Serial.println(message)
    #define DebugPrint(message) Serial.print(message)

    #define AP_SSID “TP-LINK_5344” //这里改成你的wifi名字
    #define AP_PSW “xxxxxxx”//这里改成你的wifi密码

    const uint16_t port = 8234;
    const char * host = “192.168.1.102”; // ip or dns
    WiFiClient client;//创建一个tcp client连接

    void setup() {
    //设置串口波特率,以便打印信息
    DebugBegin(115200);
    //延时5s 为了演示效果
    delay(5000);
    // 我不想别人连接我,只想做个站点
    WiFi.mode(WIFI_STA);
    WiFi.begin(AP_SSID,AP_PSW);

    DebugPrint(“Wait for WiFi… “);
    //等待wifi连接成功
    while (WiFi.status() != WL_CONNECTED) {
    Serial.print(“.”);
    delay(500);
    }

    DebugPrintln(“”);
    DebugPrintln(“WiFi connected”);
    DebugPrint(“IP address: “);
    DebugPrintln(WiFi.localIP());

    delay(500);
    }

    void loop() {

    DebugPrint(“connecting to “);
    DebugPrintln(host);

    if (!client.connect(host, port)) {
    DebugPrintln(“connection failed”);
    DebugPrintln(“wait 5 sec…”);
    delay(5000);
    return;
    }

    // 发送数据到Tcp server
    DebugPrintln(“Send this data to server”);
    client.println(String(“Send this data to server”));

    //读取从server返回到响应数据
    String line = client.readStringUntil(‘
    ‘);
    DebugPrintln(line);

    DebugPrintln(“closing connection”);
    client.stop();

    DebugPrintln(“wait 5 sec…”);
    delay(5000);
    }

    这里说一下我的具体操作。首先把程序烧到wifi模块里面。(注意自己配置好相关参数)

    然后我们要获取自己的主机的ip地址

     

    从这里我们可以看到,自己主机的IP地址在192.168.137.左右,所以地址位为192.168.127.1

    接下来我们可以打开tcp调试助手

    注意ip地址的配置

    打开后直接烧录程序即可!(会自动连接,然后发送数据)

    演示用http请求调用天气接口信息

    /**
     * Demo:
     *    演示Http请求天气接口信息
     * @author 单片机菜鸟
     * @date 2019/09/04
     */
    #include <ESP8266WiFi.h>
    #include <ArduinoJson.h>
     
    

    //以下三个定义为调试定义
    #define DebugBegin(baud_rate) Serial.begin(baud_rate)
    #define DebugPrintln(message) Serial.println(message)
    #define DebugPrint(message) Serial.print(message)

    const char* ssid = “TP-LINK_5344”; // XXXXXX – 使用时请修改为当前你的 wifi ssid
    const char* password = “6206908you11011010”; // XXXXXX – 使用时请修改为当前你的 wifi 密码
    const char* host = “api.seniverse.com”;
    const char* APIKEY = “wcmquevztdy1jpca”; //API KEY
    const char* city = “guangzhou”;
    const char* language = “zh-Hans”;//zh-Hans 简体中文 会显示乱码

    const unsigned long BAUD_RATE = 115200; // serial connection speed
    const unsigned long HTTP_TIMEOUT = 5000; // max respone time from server
    const size_t MAX_CONTENT_SIZE = 1000; // max size of the HTTP response

    // 我们要从此网页中提取的数据的类型
    struct WeatherData {
    char city[16];//城市名称
    char weather[32];//天气介绍(多云…)
    char temp[16];//温度
    char udate[32];//更新时间
    };

    WiFiClient client;
    char response[MAX_CONTENT_SIZE];
    char endOfHeaders[] = “

    “;

    void setup() {
    // put your setup code here, to run once:
    WiFi.mode(WIFI_STA); //设置esp8266 工作模式
    DebugBegin(BAUD_RATE);
    DebugPrint(“Connecting to “);//写几句提示,哈哈
    DebugPrintln(ssid);
    WiFi.begin(ssid, password); //连接wifi
    WiFi.setAutoConnect(true);
    while (WiFi.status() != WL_CONNECTED) {
    //这个函数是wifi连接状态,返回wifi链接状态
    delay(500);
    DebugPrint(“.”);
    }
    DebugPrintln(“”);
    DebugPrintln(“WiFi connected”);
    delay(500);
    DebugPrintln(“IP address: “);
    DebugPrintln(WiFi.localIP());//WiFi.localIP()返回8266获得的ip地址
    client.setTimeout(HTTP_TIMEOUT);
    }

    void loop() {
    // put your main code here, to run repeatedly:
    //判断tcp client是否处于连接状态,不是就建立连接
    while (!client.connected()){
    if (!client.connect(host, 80)){
    DebugPrintln(“connection….”);
    delay(500);
    }
    }
    //发送http请求 并且跳过响应头 直接获取响应body
    if (sendRequest(host, city, APIKEY) && skipResponseHeaders()) {
    //清除缓冲
    clrEsp8266ResponseBuffer();
    //读取响应数据
    readReponseContent(response, sizeof(response));
    WeatherData weatherData;
    if (parseUserData(response, &weatherData)) {
    printUserData(&weatherData);
    }
    }
    delay(5000);//每5s调用一次
    }

    /**

    • @发送http请求指令
      /
      bool sendRequest(const char
      host, const char* cityid, const char* apiKey) {
      // We now create a URI for the request
      //心知天气 发送http请求
      String GetUrl = “/v3/weather/now.json?key=”;
      GetUrl += apiKey;
      GetUrl += “&location=”;
      GetUrl += city;
      GetUrl += “&language=”;
      GetUrl += language;
      // This will send the request to the server
      client.print(String(“GET “) + GetUrl + “ HTTP/1.1
      “ +
      “Host: “ + host + “

    “ +
    “Connection: close

    “);
    DebugPrintln(“create a request:”);
    DebugPrintln(String(“GET “) + GetUrl + “ HTTP/1.1
    “ +
    “Host: “ + host + “
    “ +
    “Connection: close
    “);
    delay(1000);
    return true;
    }

    /**

    • @Desc 跳过 HTTP 头,使我们在响应正文的开头
      */
      bool skipResponseHeaders() {
      // HTTP headers end with an empty line
      bool ok = client.find(endOfHeaders);
      if (!ok) {
      DebugPrintln(“No response or invalid response!”);
      }
      return ok;
      }

    /**

    • @Desc 从HTTP服务器响应中读取正文
      /
      void readReponseContent(char
      content, size_t maxSize) {
      size_t length = client.peekBytes(content, maxSize);
      delay(100);
      DebugPrintln(“Get the data from Internet!”);
      content[length] = 0;
      DebugPrintln(content);
      DebugPrintln(“Read data Over!”);
      client.flush();//清除一下缓冲
      }

    /**

    • @Desc 解析数据 Json解析
    • 数据格式如下:
    • {
    • “results”: [
    •    {
      
    •        "location": {
      
    •            "id": "WX4FBXXFKE4F",
      
    •            "name": "北京",
      
    •            "country": "CN",
      
    •            "path": "北京,北京,中国",
      
    •            "timezone": "Asia/Shanghai",
      
    •            "timezone_offset": "+08:00"
      
    •        },
      
    •        "now": {
      
    •            "text": "多云",
      
    •            "code": "4",
      
    •            "temperature": "23"
      
    •        },
      
    •        "last_update": "2017-09-13T09:51:00+08:00"
      
    •    }
      
    • ]

    }
    /
    bool parseUserData(char
    content, struct WeatherData
    weatherData) {
    // – 根据我们需要解析的数据来计算JSON缓冲区最佳大小
    // 如果你使用StaticJsonBuffer时才需要
    // const size_t BUFFER_SIZE = 1024;
    // 在堆栈上分配一个临时内存池
    // StaticJsonBuffer jsonBuffer;
    // – 如果堆栈的内存池太大,使用 DynamicJsonBuffer jsonBuffer 代替
    DynamicJsonBuffer jsonBuffer;

    JsonObject& root = jsonBuffer.parseObject(content);

    if (!root.success()) {
    DebugPrintln(“JSON parsing failed!”);
    return false;
    }

    //复制我们感兴趣的字符串
    strcpy(weatherData->city, root[“results”][0][“location”][“name”]);
    strcpy(weatherData->weather, root[“results”][0][“now”][“text”]);
    strcpy(weatherData->temp, root[“results”][0][“now”][“temperature”]);
    strcpy(weatherData->udate, root[“results”][0][“last_update”]);
    // – 这不是强制复制,你可以使用指针,因为他们是指向“内容”缓冲区内,所以你需要确保
    // 当你读取字符串时它仍在内存中
    return true;
    }

    // 打印从JSON中提取的数据
    void printUserData(const struct WeatherData* weatherData) {
    DebugPrintln(“Print parsed data :”);
    DebugPrint(“City : “);
    DebugPrint(weatherData->city);
    DebugPrint(“, “);
    DebugPrint(“Weather : “);
    DebugPrint(weatherData->weather);
    DebugPrint(“, “);
    DebugPrint(“Temp : “);
    DebugPrint(weatherData->temp);
    DebugPrint(“ C”);
    DebugPrint(“, “);
    DebugPrint(“Last Updata : “);
    DebugPrint(weatherData->udate);
    DebugPrintln(“
    “);
    }

    // 关闭与HTTP服务器连接
    void stopConnect() {
    DebugPrintln(“Disconnect”);
    client.stop();
    }

    void clrEsp8266ResponseBuffer(void){
    memset(response, 0, MAX_CONTENT_SIZE); //清空
    }

    这里好像没有用,暂时先不解释,以后在研究

    TCP Server

    在ESP8266上建立TCP Server需要用到WiFiServer库,WiFiServer库也是属于ESP8266WiFi库里面的一部分,主要是负责跟server有关的操作。

     

    总体分为3部分

    • 管理server方法;
    • WiFiClient接入方法;
    • 响应WiFiClient的请求(这部分方法请看上面讲解);

    测试很简单,先获取wifi模块的ip地址

    然后直接用tcp调试助手连接

    发送数据,wifi模块就可以接受到数据了。

    演示webserver功能

    /**
     * Demo:
     *    演示web Server功能
     *    打开PC浏览器 输入IP地址。请求web server
     * @author 单片机菜鸟
     * @date 2019/09/05
     */
    #include <ESP8266WiFi.h>
     
    

    const char* ssid = “TP-LINK_5344”;//wifi账号 这里需要修改
    const char* password = “xxxx”;//wifi密码 这里需要修改

    //创建 tcp server 端口号是80
    WiFiServer server(80);

    void setup(){
    Serial.begin(115200);
    Serial.println();

    Serial.printf(“Connecting to %s “, ssid);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED){
    delay(500);
    Serial.print(“.”);
    }
    Serial.println(“ connected”);
    //启动TCP 连接
    server.begin();
    //打印TCP server IP地址
    Serial.printf(“Web server started, open %s in a web browser
    “, WiFi.localIP().toString().c_str());
    }

    /**

    • 模拟web server 返回http web响应内容
    • 这里是手动拼接HTTP响应内容
    • 后面楼主会继续讲解另外两个专用于http请求的库
      */
      String prepareHtmlPage(){
      String htmlPage =
      String(“HTTP/1.1 200 OK
      “) +
      “Content-Type: text/html

    “ +
    “Connection: close
    “ + // the connection will be closed after completion of the response
    “Refresh: 5
    “ + // refresh the page automatically every 5 sec

    “ +
    ““ +
    ““ +
    “Analog input: “ + String(analogRead(A0)) +
    ““ +

    “;
    return htmlPage;
    }

    void loop(){
    WiFiClient client = server.available();
    // wait for a client (web browser) to connect
    if (client){
    Serial.println(“
    [Client connected]”);
    while (client.connected()){
    // 不断读取请求内容
    if (client.available()){
    String line = client.readStringUntil(‘
    ‘);
    Serial.print(line);
    // wait for end of client’s request, that is marked with an empty line
    if (line.length() == 1 && line[0] == ‘
    ‘){
    //返回响应内容
    client.println(prepareHtmlPage());
    break;
    }
    }
    //由于我们设置了 Connection: close 当我们响应数据之后就会自动断开连接
    }
    delay(100); // give the web browser time to receive the data

    // close the connection:
    client.stop();
    Serial.println("[Client disonnected]");
    

    }
    }

    这里测试成功了,用法很简单,直接烧到自己的wifi模块里面。

    然后在浏览器输入你wifi模块的IP地址就可以看到内容了。

    演示webserver功能,根据响应做不同操作

    /*
    * Demo:
    *    演示简单web Server功能
    *    web server会根据请求来做不同的操作
    *    http://server_ip/gpio/0 打印 /gpio0
    *    http://server_ip/gpio/1 打印 /gpio1
    *    server_ip就是ESP8266的Ip地址
    * @author 单片机菜鸟
    * @date 2019/09/05
    */
     
    

    #include <ESP8266WiFi.h>

    //以下三个定义为调试定义
    #define DebugBegin(baud_rate) Serial.begin(baud_rate)
    #define DebugPrintln(message) Serial.println(message)
    #define DebugPrint(message) Serial.print(message)

    const char* ssid = “TP-LINK_5344”;//wifi账号 这里需要修改
    const char* password = “xxxx”;//wifi密码 这里需要修改

    // 创建tcp server
    WiFiServer server(80);

    void setup() {
    DebugBegin(115200);
    delay(10);

    // Connect to WiFi network
    DebugPrintln(“”);
    DebugPrintln(String(“Connecting to “) + ssid);
    //我只想做个安静的美男子 STA
    WiFi.mode(WIFI_STA);
    //我想连接路由wifi
    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    DebugPrint(“.”);
    }
    DebugPrintln(“”);
    DebugPrintln(“WiFi connected”);

    // 启动server
    server.begin();
    DebugPrintln(“Server started”);

    // 打印IP地址
    DebugPrintln(WiFi.localIP().toString());
    }

    void loop() {
    // 等待有效的tcp连接
    WiFiClient client = server.available();
    if (!client) {
    return;
    }

    DebugPrintln(“new client”);
    //等待client数据过来
    while (!client.available()) {
    delay(1);
    }

    // 读取请求的第一行 会包括一个url,这里只处理url
    String req = client.readStringUntil(‘
    ‘);
    DebugPrintln(req);
    //清掉缓冲区数据 据说这个方法没什么用 可以换种实现方式
    client.flush();

    // 开始匹配
    int val;
    if (req.indexOf(“/gpio/0”) != -1) {
    DebugPrintln(“/gpio0”);
    val = 0;
    } else if (req.indexOf(“/gpio/1”) != -1) {
    DebugPrintln(“/gpio1”);
    val = 1;
    } else {
    DebugPrintln(“invalid request”);
    //关闭这个client请求
    client.stop();
    return;
    }
    //清掉缓冲区数据
    client.flush();

    // 准备响应数据
    String s = “HTTP/1.1 200 OK
    Content-Type: text/html

    GPIO is now "; s += (val) ? "high" : "low"; s += " ";

    // 发送响应数据给client
    client.print(s);
    delay(1);
    DebugPrintln(“Client disonnected”);

    // The client will actually be disconnected
    // when the function returns and ‘client’ object is detroyed
    }

    这里演示一下效果(效果就是这样)

    教程到此结束,本比较参考博哥博客 ,如果对你有帮助,给我几块硬币可好?

     


    文章作者: 小游
    版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 小游 !
      目录