ESP32 开发之旅⑦ TCP Client

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

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

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

1. 前言

    通常,为了让手机连上一个WiFi热点,基本上都是打开手机设置里面的WiFi设置功能,然后会看到里面有个WiFi热点列表,然后选择你要的连接上去。

    基本上你只要打开手机连接WiFi功能,都会发现附近有超级多的各种来路不明的WiFi热点(连接有风险需谨慎),那么手机是怎么知道附近的WiFi的呢?

    通常,无线网络提供的WiFi热点,大部分都开放了SSID广播(记得之前楼主讲过WiFi热点也可以隐藏的),Scan WiFi的功能就是扫描出所有附近的WiFi热点的SSID信息,这样一来,客户端就可以根据需要选择不同的SSID连入对应的无线网络中。

    在前面章节里面,博主讲解了ESP32WiFi库里面的一些重要内容。这里回顾一下博主讲了哪些重要内容:

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

注意点:

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

    当然,ESP32WiFi库里面还有其他重要内容,比如跟http相关的 WiFiClientWiFiServer,跟https相关的 WiFiClientSecure

    终于,到这篇,可以看到跟网络请求有关的东西了。

    那肯定就会有很多人会问:到底什么时候用到哪个呢?

    在这里,博主给大家概括了以下几点,希望深入理解核心:

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

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

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

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

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

2. TCP client

概念图(博哥找不到esp32的图,原理和esp8266一样):

image

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

3. WiFiClient库

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

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

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 ip    IPAddress of tcpserver
 * @param port  port of tcpserver
 * @param timeout connnet timeout
 * @return  result of tcp connect
 *          1 --- success
 *          0 --- fail
 */
int connect(IPAddress ip, uint16_t port, int32_t timeout);

/**
 * 建立一个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
 * @param timeout connnet timeout
 * @return  result of tcp connect
 *          1 --- success
 *          0 --- fail
 */
int connect(const char *host, uint16_t port, int32_t timeout);

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.2 发送数据操作

函数说明:

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

/**
 * 发送数据
 * @param buf 需要发送数组
 * @param size 数据字节数
 * @return size_t 成功写入发送缓冲区的字节数
 */
size_t write(const uint8_t *buf, size_t size)

/**
 * 发送数据
 * @param buf 需要发送字符串或者字符数组(Program flash)
 * @param size 数据字节数
 * @return size_t 成功写入发送缓冲区的字节数
 */
size_t write_P(PGM_P buf, size_t size);

/**
 * 发送数据
 * @param stream 数据流,比如文件流
 * @return size_t 成功写入发送缓冲区的字节数
 */
size_t write(Stream& stream);

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 read() - 读取接收缓冲区一个字节

函数说明:

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

注意点:

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

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

函数说明:

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

注意点:

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

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

函数说明:

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

注意点:

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

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

函数说明:

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

3.3.6 readBytesUntil- 读取数据数组直到某个终止符为止

函数说明:

/**
 * 读取数据数组直到某个终止符为止
 * @param terminator 结束字符
 * @param buffer 存储数据的缓冲区
 * @param length 缓冲区大小
 */
size_t readBytesUntil(char terminator, char *buffer, size_t length); // as readBytes with terminator character
size_t readBytesUntil(char terminator, uint8_t *buffer, size_t length)

3.3.7 find - 查找某个字符串

函数说明:

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

注意点:

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

3.3.8 flush - 清除接收缓冲区

函数说明:

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

注意点:

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

3.4 普通设置

3.4.1 setNoDelay - 是否禁用 Nagle 算法。

函数说明:

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

底层源码:

int WiFiClient::setNoDelay(bool nodelay)
{
    int flag = nodelay;
    return setOption(TCP_NODELAY, &flag);
}

注意点:

  • 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 2020/1/8
 */
#include <WiFi.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);
}

测试结果:
image

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

例子介绍:
    通过TCP client包装Http请求协议去调用天气接口获取天气信息
源码:

/**
 * Demo:
 *    演示Http请求天气接口信息
 * @author 单片机菜鸟
 * @date 2020/01/08
 */
#include <WiFi.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     = "xxxx";         // XXXXXX -- 使用时请修改为当前你的 wifi ssid
const char* password = "xxxxxxx";         // 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.readBytes(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版本改变很大,可能很多方法对不上;

测试结果:
image

注意点:

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

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

5. 总结

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

发布了110 篇原创文章 · 获赞 488 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/dpjcn1990/article/details/103775241