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

QQ技术互动交流群:ESP8266&32 物联网开发 群号622368884,不喜勿喷

一、你如果想学基于Arduino的ESP8266开发技术

一、基础篇

  1. ESP8266开发之旅 基础篇① 走进ESP8266的世界
  2. ESP8266开发之旅 基础篇② 如何安装ESP8266的Arduino开发环境
  3. ESP8266开发之旅 基础篇③ ESP8266与Arduino的开发说明
  4. ESP8266开发之旅 基础篇④ ESP8266与EEPROM
  5. ESP8266开发之旅 基础篇⑤ ESP8266 SPI通信和I2C通信
  6. ESP8266开发之旅 基础篇⑥ Ticker——ESP8266定时库

二、网络篇

  1. ESP8266开发之旅 网络篇① 认识一下Arduino Core For ESP8266
  2. ESP8266开发之旅 网络篇② ESP8266 工作模式与ESP8266WiFi库
  3. ESP8266开发之旅 网络篇③ Soft-AP——ESP8266WiFiAP库的使用
  4. ESP8266开发之旅 网络篇④ Station——ESP8266WiFiSTA库的使用
  5. ESP8266开发之旅 网络篇⑤ Scan WiFi——ESP8266WiFiScan库的使用
  6. ESP8266开发之旅 网络篇⑥ ESP8266WiFiGeneric——基础库
  7. ESP8266开发之旅 网络篇⑦ TCP Server & TCP Client
  8. ESP8266开发之旅 网络篇⑧ SmartConfig——一键配网
  9. ESP8266开发之旅 网络篇⑨ HttpClient——ESP8266HTTPClient库的使用
  10. ESP8266开发之旅 网络篇⑩ UDP服务
  11. ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用
  12. ESP8266开发之旅 网络篇⑫ 域名服务——ESP8266mDNS库
  13. ESP8266开发之旅 网络篇⑬ SPIFFS——ESP8266 Flash文件系统
  14. ESP8266开发之旅 网络篇⑭ web配网
  15. ESP8266开发之旅 网络篇⑮ 真正的域名服务——DNSServer
  16. ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新

三、应用篇

  1. ESP8266开发之旅 应用篇① 局域网应用 ——炫酷RGB彩灯
  2. ESP8266开发之旅 应用篇② OLED显示天气屏
  3. ESP8266开发之旅 应用篇③ 简易版WiFi小车

四、高级篇

  1. ESP8266开发之旅 进阶篇① 代码优化 —— ESP8266内存管理
  2. ESP8266开发之旅 进阶篇② 闲聊Arduino IDE For ESP8266配置
  3. ESP8266开发之旅 进阶篇③ 闲聊 ESP8266 Flash
  4. ESP8266开发之旅 进阶篇④ 常见问题 —— 解决困扰
  5. ESP8266开发之旅 进阶篇⑤ 代码规范 —— 像写文章一样优美
  6. ESP8266开发之旅 进阶篇⑥ ESP-specific APIs说明

1. 前言

    通常,为了让手机连上一个WiFi热点,基本上都是打开手机设置里面的WiFi设置功能,然后会看到里面有个WiFi热点列表,然后选择你要的连接上去。
    基本上你只要打开手机连接WiFi功能,都会发现附近有超级多的各种来路不明的WiFi热点(连接有风险需谨慎),那么手机是怎么知道附近的WiFi的呢?
    通常,无线网络提供的WiFi热点,大部分都开放了SSID广播(记得之前楼主讲过WiFi热点也可以隐藏的),Scan WiFi的功能就是扫描出所有附近的WiFi热点的SSID信息,这样一来,客户端就可以根据需要选择不同的SSID连入对应的无线网络中。
    在前面章节里面,博主讲解了ESP8266WiFi库里面的一些重要内容。这里回顾一下博主讲了哪些重要内容:

  1. ESP8266WiFiSTA库 ------ STA模式专用库
  2. ESP8266WiFiAP库 ------ soft-AP模式专用库
  3. ESP8266WiFiScan库 ------ WiFi扫描功能库
  4. ESP8266WiFiGeneric库 ------ WiFi基础功能库(WiFi事件、WiFi模式)
  5. WiFi模块的工作模式:STA模式、soft-AP模式和STA兼soft-AP模式

注意点:

  • 这些功能的引入都是一句简单的代码
#include <ESP8266WiFi.h>

    当然,ESP8266WiFi库里面还有其他重要内容,比如跟http相关的 WiFiClientWiFiServer,跟https相关的 WiFiClientSecureWiFiServerSecure
    终于,到这篇,可以看到跟网络请求有关的东西了。
    那肯定就会有很多人会问:到底什么时候用到哪个呢?
    在这里,博主给大家概括了以下几点,希望深入理解核心:

  1. WiFi工作模式设置跟网络请求无关,决定于ESP8266模块想以什么角色接入网络中。
  • 如果ESP8266只是想静静地做个美男子,不想别人连接你,只是想一味地获取,那么你就果断设置成STA模式;
  • 如果ESP8266想做个中央空调服务大众收集大众的需求,那么你就果断设置成soft-AP模式;
  • WiFi工作模式,博主理解为“物理结构”模式;
  1. 至于是client还是Server,取决于ESP8266开发需求;
  • 如果业务要求是获取其他server提供的数据(发送请求,比如请求天气信息),那么你就可以使用Client模式;
  • 如果业务要求是别人请求你获取某些数据(web请求),那么你可以使用Server模式;
  • client or server,取决于你的业务需求;

    这一章节,我们讲讲解两大模块:

  • TCP client,对应 WiFiClient
  • TCP Server,对应 WiFiServer 库。

    至于什么是TCP传输协议,大家执行查资料吧。

  • TCP是底层通讯协议,定义的是数据传输和连接方式的规范;
  • HTTP是应用层协议,定义的是传输数据的内容的规范;
  • HTTP协议中的数据是利用TCP协议传输的,所以支持HTTP也就一定支持TCP;

2. TCP client

概念图:

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

3. WiFiClient库

    博主总结了 WiFiClient 百度脑图:

    整体上来说,方法可以分为4类:

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

3.1 连接操作

3.1.1 connect - 启动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);

3.1.2 connected - 判断client是否还在连接

函数说明:

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

3.1.3 stop - 停止tcp连接

函数说明:

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

3.1.4 status - 连接状态

函数说明:

/**
 * 获取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();

3.2 发送数据操作

发送操作的源码可以查阅 Print.cpp

3.2.1 write - 发送数据到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计算长度;
size_t write(const char *str) {
    if(str == NULL)
        return 0;
    return write((const uint8_t *) str, strlen(str));
}

3.2.2 print - 发送数据到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

3.2.3 println - 发送数据到client连接的server

函数说明:

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

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

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

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

/**
 * 发送数据,并且加上换行符 "\r\n"
 * @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);

/**
 * 发送换行符 "\r\n"
 * @return size_t 成功写入发送缓冲区的字节数
 */
size_t println(void);

注意点:

  • println系列其实就是在print系列的基础上加上了换行符 "\r\n";

3.3 响应操作

3.3.1 available() - 返回接收缓存区可读取字节数

函数说明:

/**
 * 返回接收缓存区可读取字节数
 * @return int 接收缓冲区可读取字节数
 */
int available();

注意点:

  • 通过此方法,我们可以判断发送出去的请求是否有响应信息;

3.3.2 availableForWrite() - 返回发送缓冲区剩余可写字节数

函数说明:

/**
 * 返回发送缓冲区剩余可写字节数
 * @return int 发送缓冲区剩余可写字节数
 */
size_t availableForWrite();

注意点:

  • 一般来说,调用发送数据操作之后,并不会立刻发送出去,而是把数据放入发送缓冲区,通过机制不断读取发送缓冲区的数据不断发送出去;
  • 可以通过此函数判断请求是否发送完毕;

3.3.3 read() - 读取接收缓冲区一个字节

函数说明:

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

注意点:

  • 此函数读取完数据后,会把该数据从缓冲区清掉;

3.3.4 read(buf,size) - 读取接收缓冲区size大小的字节数据

函数说明:

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

注意点:

  • 此函数读取完数据后,会把该数据从缓冲区清掉;

3.3.5 peek() - 读取接收缓冲区一个字节

函数说明:

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

注意点:

  • 此函数读取完数据后,不会把该数据从缓冲区清掉,所以需要特别关注这一点;

3.3.6 peekBytes(buf,size) - 读取接收缓冲区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);

注意点:

  • 此函数读取完数据后,不会把该数据从缓冲区清掉,所以需要特别关注这一点;

3.3.7 readStringUntil - 读取响应数据直到某个字符串为止

函数说明:

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

3.3.8 find - 查找某个字符串

函数说明:

/**
 * 判断是否存在某个目标字符串
 * @param buffer 目标字符串
 * @return bool 存在返回true
 */
bool find(char *buffer);

注意点:

  • 此函数会把数据从缓冲区清掉;

3.3.9 flush - 清除接收缓冲区

函数说明:

/**
 * 清除缓冲区
 */
void flush(void);

注意点:

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

方法要点

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

3.4 普通设置

3.4.1 setNoDelay - 是否禁用 Nagle 算法。

函数说明:

/**
 * 是否禁用 Nagle 算法。
 * @param nodelay true表示禁用 Nagle 算法
 */
void setNoDelay(bool nodelay);

底层源码:

void setNoDelay(bool nodelay)
{
        if(!_pcb) {
            return;
        }
        if(nodelay) {
            tcp_nagle_disable(_pcb);
        } else {
            tcp_nagle_enable(_pcb);
        }
}

注意点:

  • Nagle 算法的目的是通过合并一些小的发送消息,然后一次性发送所有的消息来减少通过网络发送的小数据包的tcp/ip流量。这种方法的缺点是延迟了单个消息的发送,直到一个足够大的包被组装。

4. 实例操作

    前面讲了这么多理论内容,接下来用几个例子来说明一下。

4.1 演示 WiFiClient 与 TCP server 之间的通信功能

例子介绍:
    本实验演示 WiFiClient 与 TCP server 之间的通信功能,需要使用到TCP调试助手,请在TCP调试助手上建立一个Tcp server,ip地址是192.168.1.102,端口号是8234。

源码:

/**
 * 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('\r');
  DebugPrintln(line);

  DebugPrintln("closing connection");
  client.stop();

  DebugPrintln("wait 5 sec...");
  delay(5000);
}

测试结果:

4.2 演示 Http请求天气接口信息

例子介绍:
    通过TCP client包装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[] = "\r\n\r\n";

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\r\n" +
               "Host: " + host + "\r\n" +
               "Connection: close\r\n\r\n");
  DebugPrintln("create a request:");
  DebugPrintln(String("GET ") + GetUrl + " HTTP/1.1\r\n" +
               "Host: " + host + "\r\n" +
               "Connection: close\r\n");
  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<BUFFER_SIZE> 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(", \t");
  DebugPrint("Weather : ");
  DebugPrint(weatherData->weather);
  DebugPrint(",\t");
  DebugPrint("Temp : ");
  DebugPrint(weatherData->temp);
  DebugPrint(" C");
  DebugPrint(",\t");
  DebugPrint("Last Updata : ");
  DebugPrint(weatherData->udate);
  DebugPrintln("\r\n");
}

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

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

注意点:

  • 这里用到了ArduinoJson库,大家可以通过 ArduinoJson,后面博主也计划专门出一篇讲解它;尽量使用ArduinoJson 5.x版本,6.x版本改变很大,可能很多方法对不上;

测试结果:

注意点:

  • Http协议,最好还是要了解的;
  • 可能很多人觉得这样拼装请求很麻烦,所以请关注HttpClient篇章,简化请求;

    Tcpclient就介绍到这里,博主只是带领大家做简单学习,深入的理解还请自行查阅源码;

5. TCP Server

    接下来,博主开始介绍TCP Client的重要伙伴 —— Tcp Server。
    现在,手机上网已经是人们每天必不可少的事情。比如刷微博,刷朋友圈,刷新闻等等; 那么这些朋友圈、微博、新闻内容都是从哪里来的呢?做个App开发的同学都应该知道,手机App属于client端,属于UI端,展示UI内容,而显示什么UI内容基本上都是发送一些http请求到后端服务(server),服务器根据具体的请求内容返回对应的响应内容。
    所谓server,可以简单理解为提供服务,提供数据的一个地方。
    先来理解一下概念图:

    mobile phone作为client端,通过路由热点,向Server端的ESP8266请求数据,8266获取到请求后解析请求然后返回响应数据。
    但是,请开发者注意:ESP8266上建立一个server是比较简单的,不过是属于局域网内的server,因为真正意义上的server并不是这样的,大伙了解一个这样的概念就好

6. WiFiServer库

    在ESP8266上建立TCP Server需要用到WiFiServer库,WiFiServer库也是属于ESP8266WiFi库里面的一部分,主要是负责跟server有关的操作。
    先来了解一下整体函数结构,博主总结了一波百度脑图:

    方法总体上可以分为三部分:

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

6.1 管理server

6.1.1 WiFiServer server(port) —— 创建TCP server

函数说明:

/**
 * 函数功能:创建TCP server
 * @param addr server的ip地址
 * @param port server的端口
 */
WiFiServer(IPAddress addr, uint16_t port);

/**
 * 函数功能:创建TCP server
 * @param port server的端口
 */
WiFiServer(uint16_t port);

6.1.2 begin() —— 启动TCP server

函数说明:

/**
 * 函数功能:启动TCP server
 */
void begin();
/**
 * 函数功能:启动TCP server
 * @param port server端口号
 */
void begin(uint16_t port);

注意点:

  • begin()和 WiFiServer(addr, port)或者WiFiServer(port)一起使用;

6.1.3 setNoDelay() —— 关闭延时发送功能

函数说明:

/**
 * 是否禁用 Nagle 算法。
 * @param nodelay true表示禁用 Nagle 算法
 */
void setNoDelay(bool nodelay);

注意点:

  • Nagle 算法的目的是通过合并一些小的发送消息,然后一次性发送所有的消息来减少通过网络发送的小数据包的tcp/ip流量。这种方法的缺点是延迟了单个消息的发送,直到一个足够大的包被组装。

6.1.4 close() —— 关闭TCP server

函数说明:

/**
 * 关闭TCP server
 */
void close();

6.1.5 stop() —— 停止TCP server

函数说明:

/**
 * 停止TCP server
 */
void stop();

注意点:

  • stop()和 close()是同样的功能,所以调用哪一个都没有问题;
void WiFiServer::stop() {
    close();
}

6.1.1 status() ——返回TCP server状态

函数说明:

/**
 * 返回TCP server状态
 * @return wl_tcp_state tcp状态
 */
uint8_t status();

wl_tcp_state 包括:

//博主暂时没理解具体每一个怎么用
enum wl_tcp_state {
  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
};

6.2 WiFiClient接入

6.2.1 available —— 获取有效的wificlient连接

函数说明:

/**
 * 获取有效的wificlient连接
 * @return 如果存在有效的wificlient连接,就返回WiFilient对象,如果没有那就返回一个无效的wificlient(connected等于false,开发者可以通过判断connected()
 */
WiFiClient available(uint8_t* status = NULL);

函数源码:

WiFiClient WiFiServer::available(byte* status) {
    (void) status;
    //判断是否有非空的连接对象
    if (_unclaimed) {
        WiFiClient result(_unclaimed);
        _unclaimed = _unclaimed->next();
        result.setNoDelay(_noDelay);
        DEBUGV("WS:av\r\n");
        return result;
    }

    optimistic_yield(1000);
    //没有连接对象就返回无用的wificlient对象
    return WiFiClient();
}

6.2.2 hasClient —— 判断是否有client连接

函数说明:

/**
 * 判断是否有client连接
 * @return bool 如果有client连接就返回true
 */
bool hasClient();

注意点:

  • 开发者可以通过判断这个函数来判断是否有client连接,然后调用available() 方法来获取连接,这样拿到wificlient之后就可以调用wificlient的方法;

7. 实例操作

    前面讲了这么多理论内容,接下来用几个例子来说明一下。

7.1 演示WiFiServer功能

例子介绍:
    8266作为WiFiServer端,打开TCP调试助手,模拟TCP Client的请求。
例子源码:

/**
 * Demo:
 *    演示WiFiServer功能
 *    打开TCP调试助手 模拟TCP client请求
 * @author 单片机菜鸟
 * @date 2019/09/04
 */
#include <ESP8266WiFi.h>

//定义最多多少个client可以连接本server(一般不要超过4个)
#define MAX_SRV_CLIENTS 1
//以下三个定义为调试定义
#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";
const char* password = "6206908you11011010";

//创建server 端口号是23
WiFiServer server(23);
//管理clients
WiFiClient serverClients[MAX_SRV_CLIENTS];

void setup() {
  DebugBegin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  DebugPrint("\nConnecting to ");
  DebugPrintln(ssid);
  uint8_t i = 0;
  while (WiFi.status() != WL_CONNECTED && i++ < 20) {
    delay(500);
  }
  if (i == 21) {
    DebugPrint("Could not connect to");
    DebugPrintln(ssid);
    while (1) {
      delay(500);
    }
  }
  //启动server
  server.begin();
  //关闭小包合并包功能,不会延时发送数据
  server.setNoDelay(true);

  DebugPrint("Ready! Use 'telnet ");
  DebugPrint(WiFi.localIP());
  DebugPrintln(" 23' to connect");
}

void loop() {
  uint8_t i;
  //检测是否有新的client请求进来
  if (server.hasClient()) {
    for (i = 0; i < MAX_SRV_CLIENTS; i++) {
      //释放旧无效或者断开的client
      if (!serverClients[i] || !serverClients[i].connected()) {
        if (serverClients[i]) {
          serverClients[i].stop();
        }
        //分配最新的client
        serverClients[i] = server.available();
        DebugPrint("New client: ");
        DebugPrint(i);
        break;
      }
    }
    //当达到最大连接数 无法释放无效的client,需要拒绝连接
    if (i == MAX_SRV_CLIENTS) {
      WiFiClient serverClient = server.available();
      serverClient.stop();
      DebugPrintln("Connection rejected ");
    }
  }
  //检测client发过来的数据
  for (i = 0; i < MAX_SRV_CLIENTS; i++) {
    if (serverClients[i] && serverClients[i].connected()) {
      if (serverClients[i].available()) {
        //get data from the telnet client and push it to the UART
        while (serverClients[i].available()) {
          //发送到串口调试器
          Serial.write(serverClients[i].read());
        }
      }
    }
  }

  if (Serial.available()) {
    //把串口调试器发过来的数据 发送给client
    size_t len = Serial.available();
    uint8_t sbuf[len];
    Serial.readBytes(sbuf, len);
    //push UART data to all connected telnet clients
    for (i = 0; i < MAX_SRV_CLIENTS; i++) {
      if (serverClients[i] && serverClients[i].connected()) {
        serverClients[i].write(sbuf, len);
        delay(1);
      }
    }
  }
}

测试结果:

7.2 演示web Server功能

例子介绍:
    8266作为web server端,打开PC浏览器输入IP地址,请求web server。
例子源码:

/**
 * 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\n", WiFi.localIP().toString().c_str());
}

/**
 * 模拟web server 返回http web响应内容
 * 这里是手动拼接HTTP响应内容
 * 后面楼主会继续讲解另外两个专用于http请求的库
 */
String prepareHtmlPage(){
  String htmlPage =
     String("HTTP/1.1 200 OK\r\n") +
            "Content-Type: text/html\r\n" +
            "Connection: close\r\n" +  // the connection will be closed after completion of the response
            "Refresh: 5\r\n" +  // refresh the page automatically every 5 sec
            "\r\n" +
            "<!DOCTYPE HTML>" +
            "<html>" +
            "Analog input:  " + String(analogRead(A0)) +
            "</html>" +
            "\r\n";
  return htmlPage;
}

void loop(){
  WiFiClient client = server.available();
  // wait for a client (web browser) to connect
  if (client){
    Serial.println("\n[Client connected]");
    while (client.connected()){
      // 不断读取请求内容
      if (client.available()){
        String line = client.readStringUntil('\r');
        Serial.print(line);
        // wait for end of client's request, that is marked with an empty line
        if (line.length() == 1 && line[0] == '\n'){
          //返回响应内容
          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]");
  }
}

测试结果:

7.3 演示简单web Server功能,webserver会根据请求来做不同的操作

例子介绍:
    8266作为WiFiServer端,演示简单web Server功能,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('\r');
  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\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>\r\nGPIO is now ";
  s += (val) ? "high" : "low";
  s += "</html>\n";

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

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

测试结果:

8. 总结

    这一篇章,博主主要讲了TCP通信的两大角色——client和server。大家需要区分tcp http。并且也要区分工作模式和client server不是一个概念,两者没有必然的联系。这篇算是入门http请求的重点内容,希望读者可以仔细研读,并结合源码去理解。

ESP8266开发之旅 网络篇⑦ TCP Server & TCP Client的更多相关文章

  1. ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  2. ESP8266开发之旅 网络篇⑧ SmartConfig——一键配网

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  3. ESP8266开发之旅 网络篇⑨ HttpClient——ESP8266HTTPClient库的使用

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  4. ESP8266开发之旅 网络篇⑩ UDP服务

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  5. ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  6. ESP8266开发之旅 网络篇⑫ 域名服务——ESP8266mDNS库

    1. 前言     前面的博文中,无论是作为client端还是server端,它们之间的通信都是通过具体的IP地址来寻址.通过IP地址来寻址,本身就是一个弊端,用户怎么会去记住这些魔法数字呢?那么有没 ...

  7. ESP8266开发之旅 网络篇⑬ SPIFFS——ESP8266 SPIFFS文件系统

    授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...

  8. ESP8266开发之旅 网络篇⑮ DNSServer——真正的域名服务

    1. 前言     Arduino for esp8266中有两个DNS服务相关的库: ESP8266mDNS库 这个库是mDNS库,使用这个库的时候ESP8266可以在AP模式或是以STA模式接入局 ...

  9. ESP8266开发之旅 网络篇③ Soft-AP——ESP8266WiFiAP库的使用

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

随机推荐

  1. select2的多选下拉框上传

    1.加入multiple: true,属性实现多选下拉框样式 2.下拉框选择后的值是数组类型不要经过数据处理才能进行表单提交 提交的时候原下拉框所在的标签不提交,而是将多选后的值存入页面中的一个隐藏标 ...

  2. 【linux】【FastDFS】FastDFS安装

    前言 FastDFS是一个开源的轻量级分布式文件系统,由跟踪服务器(tracker server).存储服务器(storage server)和客户端(client)三个部分组成,主要解决了海量数据存 ...

  3. 2019windows上安装Mac OS 10.14过程详细截图

    之前VMware12里面的Mac OS10.10升级后,键盘鼠标就用不了了.试了几次都这样,只能重装VMware14, 安装Mac OS 10.14系统.把步骤截下图,分享一下. 一.材料准备 1.虚 ...

  4. net core WebApi——定时任务Quartz

    目录 前言 Quartz 测试 问题及解决方法 小结 前言 本来打算昨天都开始写这篇,就因为要把小团队的博客整理汇总,一看二哈的博客那么多,一个个复制粘贴肯定麻烦(其实是我自己觉得复制麻烦),所以穿插 ...

  5. easyui-datetimebox 控件绑定双击事件实现自动选中当前日期时间

    本方法是在不改变原 js 的情况下,通过扩展方法来实现本目的 首先在 datetimebox 控件中扩展一个 绑定双击事件 的方法 $.extend($.fn.datetimebox.methods, ...

  6. Python学习笔记整理总结【Django】:中间件、CSRF、缓存

     一.中间件  中间件是一类,在请求到来和结束后,django会根据自己的规则在合适的时机执行中间件中相应的方法:在django项目的settings模块中,有一个 MIDDLEWARE 变量,其中每 ...

  7. gcc编译命令总结

    一步到位编译:gcc hello.c -o hello 预处理 -E (.i) 编译 -S (.s) 汇编-c (.o) 连接-o 预处理 gcc -E hello.c -o hello.i -E:仅 ...

  8. python教程 - 猿说python

    一.简介         知识改变命运,程序改变世界.互联网时代潜移默化的改变着我们的生活,伴随技术的进步,我想下一个时代应该属于人工智能和机器学习,属于python.           pytho ...

  9. 百万it资源百度网盘链接分享

    自己大量时间整理的优质资源,容量达3000多G,有需要的朋友可以微我,资源截图:  面试资料: 书籍类: 视频类: 以上只是部分资源,想要资源的亲请加微信咨询. 欢迎加微信咨询,请备注资源: 独乐乐不 ...

  10. mysql 查询常见时间段数据

    1.今天 select * from 表名 where to_days(时间字段名) = to_days(now()); 2.昨天 SELECT * FROM 表名 WHERE TO_DAYS( NO ...