授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力。希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石。。。
QQ技术互动交流群:ESP8266&32 物联网开发 群号622368884,不喜勿喷
一、你如果想学基于Arduino的ESP8266开发技术
一、基础篇
二、网络篇
- ESP8266开发之旅 网络篇① 认识一下Arduino Core For ESP8266
- ESP8266开发之旅 网络篇② ESP8266 工作模式与ESP8266WiFi库
- ESP8266开发之旅 网络篇③ Soft-AP——ESP8266WiFiAP库的使用
- ESP8266开发之旅 网络篇④ Station——ESP8266WiFiSTA库的使用
- ESP8266开发之旅 网络篇⑤ Scan WiFi——ESP8266WiFiScan库的使用
- ESP8266开发之旅 网络篇⑥ ESP8266WiFiGeneric——基础库
- ESP8266开发之旅 网络篇⑦ TCP Server & TCP Client
- ESP8266开发之旅 网络篇⑧ SmartConfig——一键配网
- ESP8266开发之旅 网络篇⑨ HttpClient——ESP8266HTTPClient库的使用
- ESP8266开发之旅 网络篇⑩ UDP服务
- ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用
- ESP8266开发之旅 网络篇⑫ 域名服务——ESP8266mDNS库
- ESP8266开发之旅 网络篇⑬ SPIFFS——ESP8266 Flash文件系统
- ESP8266开发之旅 网络篇⑭ web配网
- ESP8266开发之旅 网络篇⑮ 真正的域名服务——DNSServer
- ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新
三、应用篇
四、高级篇
1. 前言
通常,为了让手机连上一个WiFi热点,基本上都是打开手机设置里面的WiFi设置功能,然后会看到里面有个WiFi热点列表,然后选择你要的连接上去。
基本上你只要打开手机连接WiFi功能,都会发现附近有超级多的各种来路不明的WiFi热点(连接有风险需谨慎),那么手机是怎么知道附近的WiFi的呢?
通常,无线网络提供的WiFi热点,大部分都开放了SSID广播(记得之前楼主讲过WiFi热点也可以隐藏的),Scan WiFi的功能就是扫描出所有附近的WiFi热点的SSID信息,这样一来,客户端就可以根据需要选择不同的SSID连入对应的无线网络中。
在前面章节里面,博主讲解了ESP8266WiFi库里面的一些重要内容。这里回顾一下博主讲了哪些重要内容:
- ESP8266WiFiSTA库 ------ STA模式专用库;
- ESP8266WiFiAP库 ------ soft-AP模式专用库;
- ESP8266WiFiScan库 ------ WiFi扫描功能库;
- ESP8266WiFiGeneric库 ------ WiFi基础功能库(WiFi事件、WiFi模式);
- WiFi模块的工作模式:STA模式、soft-AP模式和STA兼soft-AP模式;
注意点:
- 这些功能的引入都是一句简单的代码
#include <ESP8266WiFi.h>
当然,ESP8266WiFi库里面还有其他重要内容,比如跟http相关的 WiFiClient、WiFiServer,跟https相关的 WiFiClientSecure、WiFiServerSecure。
终于,到这篇,可以看到跟网络请求有关的东西了。
那肯定就会有很多人会问:到底什么时候用到哪个呢?
在这里,博主给大家概括了以下几点,希望深入理解核心:
- WiFi工作模式设置跟网络请求无关,决定于ESP8266模块想以什么角色接入网络中。
- 如果ESP8266只是想静静地做个美男子,不想别人连接你,只是想一味地获取,那么你就果断设置成STA模式;
- 如果ESP8266想做个*空调服务大众收集大众的需求,那么你就果断设置成soft-AP模式;
- WiFi工作模式,博主理解为“物理结构”模式;
- 至于是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的更多相关文章
-
ESP8266开发之旅 网络篇⑯ 无线更新——OTA固件更新
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
-
ESP8266开发之旅 网络篇⑧ SmartConfig——一键配网
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
-
ESP8266开发之旅 网络篇⑨ HttpClient——ESP8266HTTPClient库的使用
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
-
ESP8266开发之旅 网络篇⑩ UDP服务
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
-
ESP8266开发之旅 网络篇⑪ WebServer——ESP8266WebServer库的使用
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
-
ESP8266开发之旅 网络篇⑫ 域名服务——ESP8266mDNS库
1. 前言 前面的博文中,无论是作为client端还是server端,它们之间的通信都是通过具体的IP地址来寻址.通过IP地址来寻址,本身就是一个弊端,用户怎么会去记住这些魔法数字呢?那么有没 ...
-
ESP8266开发之旅 网络篇⑬ SPIFFS——ESP8266 SPIFFS文件系统
授人以鱼不如授人以渔,目的不是为了教会你具体项目开发,而是学会学习的能力.希望大家分享给你周边需要的朋友或者同学,说不定大神成长之路有博哥的奠基石... QQ技术互动交流群:ESP8266&3 ...
-
ESP8266开发之旅 网络篇⑮ DNSServer——真正的域名服务
1. 前言 Arduino for esp8266中有两个DNS服务相关的库: ESP8266mDNS库 这个库是mDNS库,使用这个库的时候ESP8266可以在AP模式或是以STA模式接入局 ...
-
ESP8266开发之旅 网络篇③ Soft-AP——ESP8266WiFiAP库的使用
1. 前言 在前面的篇章中,博主给大家讲解了ESP8266的软硬件配置以及基本功能使用,目的就是想让大家有个初步认识.并且,博主一直重点强调 ESP8266 WiFi模块有三种工作模式: St ...
随机推荐
-
Google C++命名规范
时间:2014.03.02 地点:基地 -------------------------------------------------------------------------------- ...
-
SAP的运输功能(转)
SAP的运输功能(transportation) transportation大体有三个作用: 1.运输计划transportation planning 概念:对outbound delivery ...
-
【RMQ问题】求数组区间最大值,NYOJ-1185-最大最小值
转自:http://blog.csdn.net/lilongherolilong/article/details/6624390 先挖好坑,明天该去郑轻找虐 RMQ(Range Minimum/Max ...
-
抓取dump
1,在程序奔溃前部署.adplus.exe -crash -pn explorer.exe -o d: -crash:当进程挂掉的时候抓取dump,只能抓取到进程报错时的信息,如果进程不报错,就无法抓 ...
-
OpenCL memory object 之 Global memory (2)
转载自:http://www.cnblogs.com/mikewolf2002/archive/2011/12/18/2291584.html 当我们用clCreateBuffer, clCreate ...
-
C51 的重入问题 WARNING L15: MULTIPLE CALL TO SEGMENT
WARNING L15: MULTIPLE CALL TO SEGMENT 这个问题必须注意,可能引起程序冲突,假设你用于自动化领域,则可能导致信号产生尖峰. 产生这一警告的一个根源是:你在主循环里调 ...
-
利用python实现简单登陆注册系统
#!/usr/bin/env python # -*- coding:utf-8 -*- def login(username,password): ''' :param username:用户名 : ...
-
com.android.dex.DexException: Multiple dex files define(jar包重复引用) 错误解决
前段时间开始转入Android studio,不料果真使用时候遇到些错误,在此记下! 出现这个错误往往是在libs目录下有个jar包,然后在gradle文件中又引用了,即: 共同引用了. 解决方法: ...
-
基于FPGA视频时序生成中的库文件
上一篇分享了一个视频时序生成代码,下面我根据之前项目中用到的时序,对各个参数做了库文件,方便调用. -- -- Package File Template -- -- Purpose: This pa ...
-
Oracle调整内存超出限制出现ORA-27100: shared memory realm already exists问题解决办法
今天测试服务器遇到问题 ORA-04030:out of process memory when trying to allocate string bytes 一看就猜到是内存不足了,把Oracle ...