9. 应用层协议

9.1 TCP Client

TCP Client主要是用来访问服务器,很多可以通过外网访问的物联网设备主要就是工作在TCP Client下。设备主动去访问外部的服务器,与服务器建立连接,用户的app也是去访问这个服务器,这样变相实现了用户对设备的访问。

9.1.1 使用说明

TCP Client按如下方式使用:

  1. 引用相关库#include <WiFi.h>。

  2. 连上网 。

  3. 声明WiFiClient对象用于连接服务器。

  4. 使用connect方法连接服务器。

  5. 进行数据读写通讯。

9.1.2 API 参考

connect() - 建立连接

int connect(IPAddress ip, uint16_t port)

int connect(IPAddress ip, uint16_t port, int32_t timeout)

int connect(const char *host, uint16_t port)

int connect(const char *host, uint16_t port, int32_t timeout)

用于和服务器建立连接,可以连接指定IP地址或是指定域名,连接成功返回1,失败返回0;

语法

WiFiClient client;
if (!client.connect(host, port)) {
    Serial.println("Connection failed.");
    Serial.println("Waiting 5 seconds before retrying...");
    delay(5000);
    return;
}

参数

传入值 说明 值范围
IPAddress ip IP地址
const char *host 指定域名
uint16_t port 端口号
int32_t timeout 超时,单位秒

返回

返回值 说明 值范围
int 连接成功返回1,失败返回0 1、0

connected() - 返回当前客户端是否与服务器建立连接

uint8_t connected()

语法

if(client.connected()){
    Serial.println("connected.");
}

返回

返回值 说明 值范围
uint8_t 服务器建立连接返回true true、false

write() - 发送数据

size_t write(uint8_t data)
size_t write(const uint8_t *buf, size_t size)
size_t write_P(PGM_P buf, size_t size)
size_t write(Stream &stream)

发送数据,发送成功返回发送字节数,失败返回0 除了用 write() 方法外也可以用print()等方法发送数据

语法

client.write();

参数

传入值 说明 值范围
uint8_t data 发送的数据
size_t size 数据长度

返回

返回值 说明 值范围
size_t 发送成功返回发送字节数,失败返回0

print() - 将数据打印到客户端连接到的服务器

println() - 打印数据,后跟回车符和换行符

client.print(data)
client.print(data, BASE)

将数据打印到客户端连接到的服务器。将数字打印为数字序列,每个数字为一个ASCII字符(例如,数字123作为三个字符“ 1”,“ 2”,“ 3”发送)。

语法

client.print("GET /index.html HTTP/1.1\n\n");

参数

传入值 说明 值范围
data 要打印的数据(char,byte,int,long或string)
BASE(可选) DEC代表十进制(基数10),OCT代表八进制(基数8),HEX代表十六进制(基数16)

返回

返回值 说明 值范围
返回的字节数写的,可选

available() - 返回可读取数据长度

int available()

返回可读取数据长度,如果没有数据可读取则返回0

语法

while (client.available()) {    
      // 读取一行服务器响应内容,并通过串口打印出来
      // read back one line from the server
      String line = client.readStringUntil('\r');
      Serial.print(line);
    }

返回

返回值 说明 值范围
int 返回可读取数据长度,如果没有数据可读取则返回0

read() - 从接收缓存读取数据并返回读取到的数据字节数

int read()
int read(uint8_t *buf, size_t size)

从接收缓存读取数据并返回读取到的数据字节数,如果返回-1则表示读取失败,读取过的数据会从接收缓存删除。

语法

client.read();

参数

传入值 说明 值范围
uint8_t *buf 读取缓存
size_t size 读取字节数

返回

返回值 说明 值范围
int 读取到的数据字节数,如果返回-1则表示读取失败

readStringUntil() - 将来自缓冲区的字符读取到数组中

stream.readStringUntil(terminator)
readStringUntil()

从流中读取字符到字符串。如果检测到终止符字符或该终止符超时,则该函数终止(请参见setTimeout())。

该函数是Stream类的一部分,并且可以从该类继承的任何类(Wire,Serial等)调用。有关更多信息,请参见Stream类主页。

语法

String line = client.readStringUntil('\r');

参数

传入值 说明 值范围
terminator 要搜索的字符。允许的数据类型:char。

返回

返回值 说明 值范围
String 从流中读取整个String,直至终止符 true、false

注意和警告 终止符将从流中丢弃。

stop() - 关闭客户端,释放资源

void stop()

语法

client.stop();

remoteIP() - 返回服务器IP地址

IPAddress remoteIP() const
IPAddress remoteIP(int fd) const

语法

Serial.println(client.remoteIP());

返回

返回值 说明 值范围
IPAddress 服务器IP地址

remotePort() - 返回服务器端口号

uint16_t remotePort() const
uint16_t remotePort(int fd) const

语法

Serial.println(client.remotePort());

返回

返回值 说明 值范围
uint16_t 服务器端口号

localIP() - 返回本地IP地址

IPAddress localIP() const
IPAddress localIP(int fd) const

语法

Serial.println(client.localIP());

返回

返回值 说明 值范围
IPAddress 本地IP地址

localPort() - 返回本地端口号

uint16_t localPort() const
uint16_t localPort(int fd) const

语法

Serial.println(client.localPort());

返回

返回值 说明 值范围
uint16_t 本地端口号

其他Stream类的方法 可参考串口部分的介绍

readString() - 读取字符串

peek() - 读取首字节数据,但并不从接收缓存中删除它

flush() - 清空当前接收缓存

parseInt() - 从流中查找第一个有效的整型数据

parseFloat() - 从缓冲区返回第一个有效的float型数据。

find() - 从缓冲区读取数据,直到读取到指定的字符串

findUntil() - 从缓冲区读取数据,直到读取到指定的字符串或指定的停止符

setTimeout() - 设置超时时间

例程:TCP Client 连接Server通信

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板;修改主板建立 TCP Client 去连接 PC 软件建立的一个 server ,如果连接成功 每5秒发送一次 “Hello World!” 字符串。

#include <WiFi.h>

const char *ssid = "your_ssid";
const char *password = "your_password";

const IPAddress serverIP(192,168,1,96); //欲访问的地址
uint16_t serverPort = 777;         //服务器端口号

WiFiClient client; //声明一个客户端对象,用于与服务器进行连接

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

    WiFi.mode(WIFI_STA);
    WiFi.setSleep(false); //关闭STA模式下wifi休眠,提高响应速度
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }
    Serial.println("Connected");
    Serial.print("IP Address:");
    Serial.println(WiFi.localIP());
}

void loop()
{
    Serial.println("Trying to access the server"); // 尝试访问服务器
    if (client.connect(serverIP, serverPort)) //尝试访问目标地址
    {
        Serial.println("Access successful");	// 访问成功

        client.print("Hello world!");                    //向服务器发送数据
        while (client.connected() || client.available()) //如果已连接或有收到的未读取的数据
        {
            if (client.available()) //如果有数据可读取
            {
                String line = client.readStringUntil('\n'); //读取数据到换行符
                Serial.print("Read data:");	// 读取到数据
                Serial.println(line);
                client.write(line.c_str()); //将收到的数据回发
            }
        }
        Serial.println("Close current connection");  // 关闭当前连接
        client.stop(); //关闭客户端
    }
    else
    {
        Serial.println("Access loss");	// 访问失
        client.stop(); //关闭客户端
    }
    delay(5000);
}

下载免费的 Packet Sender 软件

打开软件 点击 File -> Settings ,点击 Enable TCP Servers 使能TCP服务器,TCP Server Port 设置一个固定端口号,点击右下方 OK按钮保存后退出设置。

image-20211220150814214

此时在软件界面可以看到 IP 地址和 TCP:777。

image-20211220151217493

将例程中的IP地址和端口号改为 Packet Sender 软件建立的 TCP Servers 的 IP 和 端口号,烧写代码到 Edge101WE 主板。

const IPAddress serverIP(192,168,1,96); //欲访问的地址
uint16_t serverPort = 777;         //服务器端口号

此时主板串口打印连接成功,主板的 IP地址为 192.168.0.221

.....Connected
IP Address:192.168.0.221
Trying to access the server
Access successful
Close current connection

此时可在 Packet Sender 软件上看到主板发送的数据。在发送对话框选中 Persistent TCP (持久TCP连接)

image-20211220160839286

此时收到 Edge101WE 主板发送的数据 “Hello world!”。

在对话框中输入 “Hello Edge101WE” 点击 Send按钮。

image-20211220161150353

此时在串口接收的数据如下,说明主板收到 Packet Sender 软件发送的字符串。

Trying to access the server
Access successful
Read data:Hello Edge101WE

例程:作为WEB TCP Client 向一个TCP服务器发送信息

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WiFi\examples\WiFiClientBasic)

/*
    This sketch sends a message to a TCP server

*/

#include <WiFi.h>
#include <WiFiMulti.h>

WiFiMulti WiFiMulti;

void setup()
{
  Serial.begin(115200);
  delay(10);

  // We start by connecting to a WiFi network
  WiFiMulti.addAP("your_ssid", "your_password");

  Serial.println();
  Serial.println();
  Serial.print("Waiting for WiFi... ");

  while (WiFiMulti.run() != WL_CONNECTED) {
    Serial.print(".");
    delay(500);
  }

  Serial.println("");
  Serial.println("WiFi connected");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());

  delay(500);
}


void loop()
{
  //    const uint16_t port = 80;
  //    const char * host = "192.168.1.1"; // ip or dns
  const uint16_t port = 80;  // 一般网站默认开放端口为80
  const char * host = "www.baidu.com"; // ip or dns,访问百度网站

  Serial.print("Connecting to ");
  Serial.println(host);
  // 用WiFiClient类创建TCP连接
  // Use WiFiClient class to create TCP connections
  WiFiClient client;
    
  // 连接服务器	
  if (!client.connect(host, port)) {
    Serial.println("Connection failed.");
    Serial.println("Waiting 5 seconds before retrying...");
    delay(5000);
    return;
  }
  // 将会向服务器发送请求    
  // This will send a request to the server
    
  // 取消下行注释,即可将任意字符串发送到服务器
  // uncomment this line to send an arbitrary string to the server
  // client.print("Send this data to the server");
  
  // 取消下行注释,发送一个基本的文档请求到服务器
  // uncomment this line to send a basic document request to the server
  client.print("GET /index.html HTTP/1.1\n\n");

  int maxloops = 0;
  // 等待服务器响应
  //wait for the server's reply to become available
  while (!client.available() && maxloops < 1000)
  {
    maxloops++;
    delay(1); //delay 1 msec
  }
  if (client.available() > 0)
  { 
    // 检测接收缓存区是否有内容
    while (client.available()) {    
      // 读取一行服务器响应内容,并通过串口打印出来
      // read back one line from the server
      String line = client.readStringUntil('\r');
      Serial.print(line);
    }
  }
  else
  {
    Serial.println("client.available() timed out ");
  }

  Serial.println("Closing connection.");
  client.stop();

  Serial.println("Waiting 5 seconds before restarting...");
  delay(5000);
}

串口终端打印结果

image-20210328104901131

9.1.3 WiFiClientSecure 客户端加密

WiFiClientSecure类使用TLS(SSL)实现对安全连接的支持。它继承自WiFiClient,因此实现了该类的接口的超集。使用WiFiClientSecure类建立安全连接的方法有三种:使用根证书颁发机构(CA)证书,使用根CA证书加上客户端证书和密钥以及使用预共享密钥(PSK)。

使用根证书颁发机构证书

此方法对服务器进行身份验证并协商加密的连接。连接到HTTPS站点时,它与在Web浏览器中实现的功能相同。

如果要访问自己的服务器:

  • 为您自己的证书颁发机构生成根证书

  • 使用您的服务器的根证书(“自签名证书”)生成证书和私钥

如果要访问公共服务器:

  • 获取签署该服务器证书的公共CA的证书,然后:

  • 在WiFiClientSecure中,使用setCACert(或适当的连接方法)来设置您的CA或公共CA的根证书。

  • 当WiFiClientSecure连接到目标服务器时,它使用CA证书来验证服务器提供的证书,然后协商连接的加密

请参阅WiFiClientSecure示例。

使用根CA证书和客户端证书/密钥

此方法对服务器进行身份验证,此外还向服务器对客户端进行身份验证,然后协商加密连接。

  • 请按照上述步骤

  • 使用您的根CA为您的客户端生成证书/密钥

  • 在您将要访问的服务器上注册密钥,以便服务器可以对您的客户端进行身份验证

  • 在WiFiClientSecure中,使用setCACert(或适当的连接方法)设置您的CA或公共CA的根证书,这用于对服务器进行身份验证

  • 在WiFiClientSecure中,使用setCertificate和setPrivateKey(或适当的connect方法)设置客户端的证书和密钥,这将用于向服务器验证客户端的身份

  • 当WiFiClientSecure连接到目标服务器时,它将使用CA证书来验证服务器提供的证书,它将使用证书/密钥来向服务器验证您的客户端,然后将协商连接的加密。

使用预共享密钥(PSK)

TLS支持使用预共享密钥(即,客户端和服务器都知道的密钥)作为HTTPS上网络上常用的公共密钥加密的替代方法来进行身份验证和加密。PSK已开始用于MQTT(例如在mosquitto中),以简化设置并避免必须经历整个CA,证书和私钥过程。

预共享密钥是最多32个字节的二进制字符串,通常以十六进制形式表示。除了密钥之外,客户端还可以提供ID,通常,服务器允许将不同的密钥与每个客户端ID关联。实际上,这与用户名和密码对非常相似,除了与密码不同,密钥不会直接传输到服务器,因此与恶意服务器的连接不会泄露密码。另外,服务器还向客户端进行了身份验证。

要使用PSK,请执行以下操作:

  • 生成随机的十六进制字符串(为某些文件生成MD5或SHA是这样做的一种方法)

  • 为您的客户端提供一个字符串ID,并将您的服务器配置为接受ID /密钥对

  • 在WiFiClientSecure中,使用setPreSharedKey(或适当的连接方法)来设置ID /密钥组合

  • 当WiFiClientSecure连接到目标服务器时,它使用id / key组合对服务器进行身份验证(必须证明它也具有密钥),对客户端进行身份验证,然后协商连接的加密

API参考

connect() - 与指定主机的端口建立TCP连接
#include <WiFiClientSecure.h>

int WiFiClientSecure::connect(IPAddress ip, uint16_t port);
int WiFiClientSecure::connect(IPAddress ip, uint16_t port, int32_t timeout);
int WiFiClientSecure::connect(const char *host, uint16_t port);
int WiFiClientSecure::connect(const char *host, uint16_t port, int32_t timeout);
int WiFiClientSecure::connect(IPAddress ip, uint16_t port, const char *CA_cert, const char *cert, const char *private_key);
int WiFiClientSecure::connect(const char *host, uint16_t port, const char *CA_cert, const char *cert, const char *private_key);
int WiFiClientSecure::connect(IPAddress ip, uint16_t port, const char *pskIdent, const char *psKey);
int WiFiClientSecure::connect(const char *host, uint16_t port, const char *pskIdent, const char *psKey);

语法

// 连接到指定主机上的指定端口。connect() 成功返回1
  if (!client.connect(server, 443))
    Serial.println("Connection failed!");
  else {
    Serial.println("Connected to server!");

参数

传入值 说明 值范围
IPAddress ip IP地址
uint16_t port 端口号
int32_t timeout 超时时间(单位:毫秒)
const char *host 主机名
const char *CA_cert 根证书
const char *cert 客户端证书
const char *private_key 客户端证书的私钥
char *pskIdent PSK身份
const char *psKey 预共享密钥

返回

返回值 说明 值范围
int 如果连接成功,则为1;如果连接失败,则为0。 0,1
setInsecure() - 通过HTTPS连接时,请勿执行证书验证

语法

#include <WiFiClientSecure.h>

void WiFiClientSecure::setInsecure();

参数

返回

setCACert() - 设置访问目标主机的根证书

语法

#include <WiFiClientSecure.h>

void WiFiClientSecure::setCACert (const char *rootCA);

参数

传入值 说明 值范围
const char *rootCA 根证书

返回

connected() - 返回与服务器的连接状态

语法

#include <WiFiClientSecure.h>

uint8_t WiFiClientSecure::connected();

参数

返回

返回值 说明 值范围
uint8_t 连接状态。连接时为true,断开连接时为false。 true、false
available() - 获取流中可用的字节数

语法

#include <WiFiClientSecure.h>

int WiFiClientSecure::available();

参数

返回

返回值 说明 值范围
int 可以读取的字节数
read() - 从流中读取数据

对于未设置参数的调用,将读取一个字符。如果发生错误,请调用stop()断开与主机的TCP连接。

语法

#include <WiFiClientSecure.h>

int WiFiClientSecure::read();

int WiFiClientSecure::read(uint8_t *buf, size_t size);

参数

传入值 说明 值范围
uint8_t *buf 读取缓冲区
size_t size 缓冲区大小

返回

返回值 说明 值范围
int 读取的字节数。发生错误时为负数。
stop() - 断开与主机的TCP连接

语法

#include <WiFiClientSecure.h>

void WiFiClientSecure::stop();

参数

返回

例程:WiFiClientSecure 证书认证

实现 Edge101WE 主板通过HTTPS协议与www.howsmyssl.com网站服务器进行通讯(该网站专门用于HTTPS通讯测试)。服务器响应信息将会通过串口监视器显示以便我们查阅。例程使用证书认证可以提高HTTPS通讯安全性。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WiFiClientSecure\examples\WiFiClientSecure)

/*
  Wifi secure connection example for ESP32
  Running on TLS 1.2 using mbedTLS
  Suporting the following chipersuites:
  "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_256_CCM","TLS_DHE_RSA_WITH_AES_256_CCM","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384","TLS_DHE_RSA_WITH_AES_256_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_DHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8","TLS_DHE_RSA_WITH_AES_256_CCM_8","TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_CAMELLIA_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_CAMELLIA_256_CBC_SHA384","TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256","TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CCM","TLS_DHE_RSA_WITH_AES_128_CCM","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_RSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8","TLS_DHE_RSA_WITH_AES_128_CCM_8","TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_ECDHE_RSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA","TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA","TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA","TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA","TLS_DHE_PSK_WITH_AES_256_GCM_SHA384","TLS_DHE_PSK_WITH_AES_256_CCM","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384","TLS_DHE_PSK_WITH_AES_256_CBC_SHA384","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA","TLS_DHE_PSK_WITH_AES_256_CBC_SHA","TLS_DHE_PSK_WITH_CAMELLIA_256_GCM_SHA384","TLS_ECDHE_PSK_WITH_CAMELLIA_256_CBC_SHA384","TLS_DHE_PSK_WITH_CAMELLIA_256_CBC_SHA384","TLS_PSK_DHE_WITH_AES_256_CCM_8","TLS_DHE_PSK_WITH_AES_128_GCM_SHA256","TLS_DHE_PSK_WITH_AES_128_CCM","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256","TLS_DHE_PSK_WITH_AES_128_CBC_SHA256","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA","TLS_DHE_PSK_WITH_AES_128_CBC_SHA","TLS_DHE_PSK_WITH_CAMELLIA_128_GCM_SHA256","TLS_DHE_PSK_WITH_CAMELLIA_128_CBC_SHA256","TLS_ECDHE_PSK_WITH_CAMELLIA_128_CBC_SHA256","TLS_PSK_DHE_WITH_AES_128_CCM_8","TLS_ECDHE_PSK_WITH_3DES_EDE_CBC_SHA","TLS_DHE_PSK_WITH_3DES_EDE_CBC_SHA","TLS_RSA_WITH_AES_256_GCM_SHA384","TLS_RSA_WITH_AES_256_CCM","TLS_RSA_WITH_AES_256_CBC_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_256_CCM_8","TLS_RSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_RSA_WITH_CAMELLIA_256_CBC_SHA256","TLS_RSA_WITH_CAMELLIA_256_CBC_SHA","TLS_ECDH_RSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_ECDH_RSA_WITH_CAMELLIA_256_CBC_SHA384","TLS_ECDH_ECDSA_WITH_CAMELLIA_256_GCM_SHA384","TLS_ECDH_ECDSA_WITH_CAMELLIA_256_CBC_SHA384","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_128_CCM","TLS_RSA_WITH_AES_128_CBC_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA","TLS_RSA_WITH_AES_128_CCM_8","TLS_RSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_RSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_RSA_WITH_CAMELLIA_128_CBC_SHA","TLS_ECDH_RSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_ECDH_RSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_ECDH_ECDSA_WITH_CAMELLIA_128_GCM_SHA256","TLS_ECDH_ECDSA_WITH_CAMELLIA_128_CBC_SHA256","TLS_RSA_WITH_3DES_EDE_CBC_SHA","TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA","TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA","TLS_RSA_PSK_WITH_AES_256_GCM_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA","TLS_RSA_PSK_WITH_CAMELLIA_256_GCM_SHA384","TLS_RSA_PSK_WITH_CAMELLIA_256_CBC_SHA384","TLS_RSA_PSK_WITH_AES_128_GCM_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA","TLS_RSA_PSK_WITH_CAMELLIA_128_GCM_SHA256","TLS_RSA_PSK_WITH_CAMELLIA_128_CBC_SHA256","TLS_RSA_PSK_WITH_3DES_EDE_CBC_SHA","TLS_PSK_WITH_AES_256_GCM_SHA384","TLS_PSK_WITH_AES_256_CCM","TLS_PSK_WITH_AES_256_CBC_SHA384","TLS_PSK_WITH_AES_256_CBC_SHA","TLS_PSK_WITH_CAMELLIA_256_GCM_SHA384","TLS_PSK_WITH_CAMELLIA_256_CBC_SHA384","TLS_PSK_WITH_AES_256_CCM_8","TLS_PSK_WITH_AES_128_GCM_SHA256","TLS_PSK_WITH_AES_128_CCM","TLS_PSK_WITH_AES_128_CBC_SHA256","TLS_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_WITH_CAMELLIA_128_GCM_SHA256","TLS_PSK_WITH_CAMELLIA_128_CBC_SHA256","TLS_PSK_WITH_AES_128_CCM_8","TLS_PSK_WITH_3DES_EDE_CBC_SHA","TLS_EMPTY_RENEGOTIATION_INFO_SCSV"]
  2017 - Evandro Copercini - Apache 2.0 License.
*/

#include <WiFiClientSecure.h>

const char* ssid     = "your_ssid";     // your network SSID (name of wifi network)
const char* password = "your_password"; // your network password

const char*  server = "www.howsmyssl.com";  // Server URL

// www.howsmyssl.com root certificate authority, to verify the server
// change it to your server root CA
// SHA1 fingerprint is broken now!

// Web服务器的根证书
// 注意:出于安全原因,CA会定期更新根证书信息。因此本程序中的证书可能已经过期。
// 请使用浏览器获取最新的网站根证书并复制粘贴到程序中相应位置。如需了解如何执行这一操作,“查看网站根证书”页面。
const char* test_root_ca= \
     "-----BEGIN CERTIFICATE-----\n" \
     "MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\n" \
     "MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\n" \
     "DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\n" \
     "PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\n" \
     "Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\n" \
     "AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\n" \
     "rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\n" \
     "OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\n" \
     "xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n" \
     "7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\n" \
     "aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\n" \
     "HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\n" \
     "SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\n" \
     "ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\n" \
     "AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\n" \
     "R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\n" \
     "JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\n" \
     "Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n" \
     "-----END CERTIFICATE-----\n";

// You can use x.509 client certificates if you want
//const char* test_client_key = "";   //to verify the client
//const char* test_client_cert = "";  //to verify the client


WiFiClientSecure client;  // client 是 WiFiClientSecure 类型变量,通过HTTPS连接时使用。

void setup() {
  //Initialize serial and wait for port to open:
  Serial.begin(115200);
  delay(100);

  Serial.print("Attempting to connect to SSID: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);  // 连接到ssid指定的访问点。此时的密码是密码。WiFi是WiFi.cpp中定义的变量

  // attempt to connect to Wifi network:
  // 返回当前的连接状态。连接到接入点时,将返回WL_CONNECTED  
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    // wait 1 second for re-trying
    delay(1000);
  }

  Serial.print("Connected to ");
  Serial.println(ssid);

  client.setCACert(test_root_ca);  // 设置Web服务器的根证书
  //client.setCertificate(test_client_cert); // for client verification
  //client.setPrivateKey(test_client_key);	// for client verification

  Serial.println("\nStarting connection to server...");
  // 连接到指定主机上的指定端口。connect() 成功返回1
  if (!client.connect(server, 443))
    Serial.println("Connection failed!");
  else {
    Serial.println("Connected to server!");
    // Make a HTTP request:
    // 编写一个HTTP请求。由于WiFiClientSecure继承了Print类,因此您可以使用诸如Serial之类的println() 等  
    client.println("GET https://www.howsmyssl.com/a/check HTTP/1.0");
    client.println("Host: www.howsmyssl.com");
    client.println("Connection: close");
    client.println();
	// 返回与服务器的连接状态。连接后,在while语句中执行
    while (client.connected()) {
      String line = client.readStringUntil('\n');  // WiFiClientSecure也继承自Stream类,因此您可以使用readStringUntil
      if (line == "\r") {
        Serial.println("headers received");
        break;
      }
    }
    // if there are incoming bytes available
    // from the server, read them and print them:
    // 返回可以读取的字节数
    while (client.available()) {
      char c = client.read();  //  从服务器读取一个字符
      Serial.write(c);
    }

    client.stop(); // 与服务器断开连接
  }
}

void loop() {
  // do nothing
}

串口返回

Attempting to connect to SSID: dfrobotOffice
.....Connected to dfrobotOffice

Starting connection to server...
Connected to server!
headers received
{"given_cipher_suites":["TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_256_CCM","TLS_DHE_RSA_WITH_AES_256_CCM","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384","TLS_DHE_RSA_WITH_AES_256_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_DHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8","TLS_DHE_RSA_WITH_AES_256_CCM_8","TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CCM","TLS_DHE_RSA_WITH_AES_128_CCM","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_RSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8","TLS_DHE_RSA_WITH_AES_128_CCM_8","TLS_DHE_PSK_WITH_AES_256_GCM_SHA384","TLS_DHE_PSK_WITH_AES_256_CCM","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384","TLS_DHE_PSK_WITH_AES_256_CBC_SHA384","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA","TLS_DHE_PSK_WITH_AES_256_CBC_SHA","TLS_PSK_DHE_WITH_AES_256_CCM_8","TLS_DHE_PSK_WITH_AES_128_GCM_SHA256","TLS_DHE_PSK_WITH_AES_128_CCM","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256","TLS_DHE_PSK_WITH_AES_128_CBC_SHA256","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA","TLS_DHE_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_DHE_WITH_AES_128_CCM_8","TLS_RSA_WITH_AES_256_GCM_SHA384","TLS_RSA_WITH_AES_256_CCM","TLS_RSA_WITH_AES_256_CBC_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_256_CCM_8","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_128_CCM","TLS_RSA_WITH_AES_128_CBC_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA","TLS_RSA_WITH_AES_128_CCM_8","TLS_RSA_PSK_WITH_AES_256_GCM_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA","TLS_RSA_PSK_WITH_AES_128_GCM_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_WITH_AES_256_GCM_SHA384","TLS_PSK_WITH_AES_256_CCM","TLS_PSK_WITH_AES_256_CBC_SHA384","TLS_PSK_WITH_AES_256_CBC_SHA","TLS_PSK_WITH_AES_256_CCM_8","TLS_PSK_WITH_AES_128_GCM_SHA256","TLS_PSK_WITH_AES_128_CCM","TLS_PSK_WITH_AES_128_CBC_SHA256","TLS_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_WITH_AES_128_CCM_8","TLS_EMPTY_RENEGOTIATION_INFO_SCSV"],"ephemeral_keys_supported":true,"session_ticket_supported":true,"tls_compression_supported":false,"unknown_cipher_suite_supported":false,"beast_vuln":false,"able_to_detect_n_minus_one_splitting":false,"insecure_cipher_suites":{},"tls_version":"TLS 1.2","rating":"Probably Okay"}⸮

例程:WiFiClientInsecure 通过HTTPS连接时,跳过执行证书验证

使用 setInsecure 将会让主板不进行服务器身份认证,而直接与服务器进行通讯。注意:此方法跳过了 HTTPS 协议中的安全加密措施,因此仅可用于测试使用,而不适合传输需要保密的信息。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WiFiClientSecure\examples\WiFiClientInsecure)

#include <WiFiClientSecure.h>

const char* ssid     = "your-ssid";     // your network SSID (name of wifi network)
const char* password = "your-password"; // your network password

const char*  server = "www.howsmyssl.com";  // Server URL

WiFiClientSecure client;

void setup() {
  //Initialize serial and wait for port to open:
  Serial.begin(115200);
  delay(100);

  Serial.print("Attempting to connect to SSID: ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);

  // attempt to connect to Wifi network:
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print(".");
    // wait 1 second for re-trying
    delay(1000);
  }

  Serial.print("Connected to ");
  Serial.println(ssid);

  Serial.println("\nStarting connection to server...");
  // 通过HTTPS连接时,请勿执行证书验证  
  client.setInsecure();//skip verification
    
  if (!client.connect(server, 443))
    Serial.println("Connection failed!");
  else {
    Serial.println("Connected to server!");
    // Make a HTTP request:
    client.println("GET https://www.howsmyssl.com/a/check HTTP/1.0");
    client.println("Host: www.howsmyssl.com");
    client.println("Connection: close");
    client.println();

    while (client.connected()) {
      String line = client.readStringUntil('\n');
      if (line == "\r") {
        Serial.println("headers received");
        break;
      }
    }
    // if there are incoming bytes available
    // from the server, read them and print them:
    while (client.available()) {
      char c = client.read();
      Serial.write(c);
    }

    client.stop();
  }
}

void loop() {
  // do nothing
}

串口输出

Attempting to connect to SSID: dfrobotOffice
...Connected to dfrobotOffice

Starting connection to server...
Connected to server!
headers received
{"given_cipher_suites":["TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384","TLS_DHE_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDHE_ECDSA_WITH_AES_256_CCM","TLS_DHE_RSA_WITH_AES_256_CCM","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384","TLS_DHE_RSA_WITH_AES_256_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA","TLS_DHE_RSA_WITH_AES_256_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_256_CCM_8","TLS_DHE_RSA_WITH_AES_256_CCM_8","TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256","TLS_DHE_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CCM","TLS_DHE_RSA_WITH_AES_128_CCM","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256","TLS_DHE_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA","TLS_DHE_RSA_WITH_AES_128_CBC_SHA","TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8","TLS_DHE_RSA_WITH_AES_128_CCM_8","TLS_DHE_PSK_WITH_AES_256_GCM_SHA384","TLS_DHE_PSK_WITH_AES_256_CCM","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA384","TLS_DHE_PSK_WITH_AES_256_CBC_SHA384","TLS_ECDHE_PSK_WITH_AES_256_CBC_SHA","TLS_DHE_PSK_WITH_AES_256_CBC_SHA","TLS_PSK_DHE_WITH_AES_256_CCM_8","TLS_DHE_PSK_WITH_AES_128_GCM_SHA256","TLS_DHE_PSK_WITH_AES_128_CCM","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256","TLS_DHE_PSK_WITH_AES_128_CBC_SHA256","TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA","TLS_DHE_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_DHE_WITH_AES_128_CCM_8","TLS_RSA_WITH_AES_256_GCM_SHA384","TLS_RSA_WITH_AES_256_CCM","TLS_RSA_WITH_AES_256_CBC_SHA256","TLS_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_RSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_RSA_WITH_AES_256_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_256_GCM_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA384","TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA","TLS_RSA_WITH_AES_256_CCM_8","TLS_RSA_WITH_AES_128_GCM_SHA256","TLS_RSA_WITH_AES_128_CCM","TLS_RSA_WITH_AES_128_CBC_SHA256","TLS_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_RSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_RSA_WITH_AES_128_CBC_SHA","TLS_ECDH_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA256","TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA","TLS_RSA_WITH_AES_128_CCM_8","TLS_RSA_PSK_WITH_AES_256_GCM_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA384","TLS_RSA_PSK_WITH_AES_256_CBC_SHA","TLS_RSA_PSK_WITH_AES_128_GCM_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA256","TLS_RSA_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_WITH_AES_256_GCM_SHA384","TLS_PSK_WITH_AES_256_CCM","TLS_PSK_WITH_AES_256_CBC_SHA384","TLS_PSK_WITH_AES_256_CBC_SHA","TLS_PSK_WITH_AES_256_CCM_8","TLS_PSK_WITH_AES_128_GCM_SHA256","TLS_PSK_WITH_AES_128_CCM","TLS_PSK_WITH_AES_128_CBC_SHA256","TLS_PSK_WITH_AES_128_CBC_SHA","TLS_PSK_WITH_AES_128_CCM_8","TLS_EMPTY_RENEGOTIATION_INFO_SCSV"],"ephemeral_keys_supported":true,"session_ticket_supported":true,"tls_compression_supported":false,"unknown_cipher_suite_supported":false,"beast_vuln":false,"able_to_detect_n_minus_one_splitting":false,"insecure_cipher_suites":{},"tls_version":"TLS 1.2","rating":"Probably Okay"}⸮

9.2 TCP Server

9.2.1 使用说明

TCP Server按如下方式使用:

  1. 引用相关库#include <WiFi.h>

  2. 声明WiFiServer对象

  3. 使用begin方法启动监听

  4. 监听客户端连接并处理数据通讯;

9.2.2 常用方法

WiFiServer() - 声明WiFiServer对象

WiFiServer(uint16_t port=80, uint8_t max_clients=4)

在声明WiFiServer对象可以选择输入要监听的端口号和最大接入客户数量。

语法

#include <WiFi.h>
WiFiServer server; // 声明服务器对象

参数

传入值 说明 值范围
port 服务器使用的端口号
max_clients 最大客户端数,默认值 4。 1~4

返回

begin() - 服务器启动监听

void begin(uint16_t port=0)

语法

server.begin(55555); // 服务器启动监听端口号55555

参数

传入值 说明 值范围
服务器使用的端口号。如果省略,则使用构造函数中指定的值。
指定重用本地地址的权限。

返回

available() - 尝试建立客户对象

WiFiClient available()
WiFiClient accept(){return available();}

语法

WiFiClient client = server.available(); // 尝试建立客户对象
if (client) // 如果当前客户可用
{
    
}

参数

返回

返回值 说明 值范围
返回与客户端的连接的 WiFi 客户端类型信息

setNoDelay() - 设置是否延时发送

void setNoDelay(bool nodelay)

此函数用于设置 server 是否使用Nagle算法来将发送的信息先拆分成小包再发送。

使用Nagle算法来拆包发送的优点是可以将较大的数据信息拆分,从而让信息传输的网络利用率更加优化。但缺点是比起不拆包发送的模式来说,拆包发送的速度要慢一些。

语法

server.setNoDelay(true);  // 停止小包合并发送

参数

传入值 说明 值范围
nodelay true: 不使用Nagle算法来将发送的信息先拆分成小包再发送。
false: 使用Nagle算法来将发送的信息先拆分成小包再发送。

返回

getNoDelay() - 返回是否延时发送

bool getNoDelay()

语法

status = client.getNoDelay()

参数

返回

返回值 说明 值范围
bool true:禁用延迟发送

hasClient() - 返回是否有客户端尝试接入

bool hasClient()

检查是否有客户端访问主板建立的网络服务器。

语法

if (server.hasClient()) {}

参数

返回

返回值 说明 值范围
bool true:有client连接server
false:无client连接server

write() - 发送数据

size_t write(const uint8_t *data, size_t len)
size_t write(uint8_t data){return write(&data, 1);}

发送数据,发送成功返回发送字节数,失败返回0。 除了用 write() 方法外也可以用 print() 等方法发送数据。

语法

uint8_t sbuf;
serverClient.wrte(sbuf,len);

参数

传入值 说明 值范围
const uint8_t *data 指向数组的指针
uint8_t data 数据
size_t len 数据长度

返回

返回值 说明 值范围
size_t 发送成功返回发送字节数,失败返回0

setTimeout() - 设置超时时间

int setTimeout(uint32_t seconds)

语法

client.setTimeout(5); 

参数

传入值 说明 值范围
uint32_t seconds 设备等待数据流的最大时间,单位

返回

返回值 说明 值范围
int

stop() - 停止当前监听

void stop() 

语法

server.stop();

参数

返回

例程:建立TCP Server 等待TCP Client连接通信

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>

const char *ssid = "your_ssid";
const char *password = "your_password";

WiFiServer server; // 声明服务器对象

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

    WiFi.mode(WIFI_STA);
    WiFi.setSleep(false); //关闭STA模式下wifi休眠,提高响应速度
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED)
    {
        delay(500);
        Serial.print(".");
    }
    Serial.println("Connected");
    Serial.print("IP Address:");
    Serial.println(WiFi.localIP());

    server.begin(55555); //服务器启动监听端口号55555
}

void loop()
{
    WiFiClient client = server.available(); //尝试建立客户对象
    if (client) //如果当前客户可用
    {
        Serial.println("[Client connected]");
        String readBuff;
        while (client.connected()) //如果客户端处于连接状态
        {
            if (client.available()) //如果有可读数据
            {
                char c = client.read(); //读取一个字节
                                        //也可以用readLine()等其他方法
                readBuff += c;
                if(c == '\r') //接收到回车符
                {
                    client.print("Received: " + readBuff); //向客户端发送
                    Serial.println("Received: " + readBuff); //从串口打印
                    readBuff = "";
                }
            }
        }
        client.stop(); //结束当前连接:
        Serial.println("[Client disconnected]");
    }
}

打开串口终端,将主板复位,串口将打印出主板TCP Server的IP地址。使用 Packet Sender 软件,发送区 Address 设置为 串口终端打印出来的地址,填写程序设定的端口号 55555,协议选择TCP点击发送。

此时在串口中断可看到主板 TCP Server收到相同的数据。

...........Connected
IP Address:192.168.0.221
[Client connected]
Received: Edge101WE

在 Packet Sender 软件建立的TCP Client上也可接收到主板 TCP Server 回发的数据。

image-20211220164713279

例程:SimpleWiFiServer - 网页控制LED灯

例程通过 STA 方式联网后建立一个简单的服务端,例程将打印主板IP地址到串行监视器。 你可以在浏览器中访问该ip地址来打开和关闭LED。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

注意:需要修改例程中的 LED 引脚为 15

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WiFi/examples/SimpleWiFiServe)

/*
 WiFi Web Server LED Blink
 A simple web server that lets you blink an LED via the web.
 This sketch will print the IP address of your WiFi Shield (once connected)
 to the Serial monitor. From there, you can open that address in a web browser
 to turn on and off the LED on pin 15.
 If the IP address of your shield is yourAddress:
 http://yourAddress/H turns the LED on
 http://yourAddress/L turns it off
 This example is written for a network using WPA encryption. For
 WEP or WPA, change the Wifi.begin() call accordingly.
 Circuit:
 * WiFi shield attached
 * LED attached to pin 15
 created for arduino 25 Nov 2012
 by Tom Igoe
ported for sparkfun esp32 
31.01.2017 by Jan Hendrik Berlin
 
 */

#include <WiFi.h>

const char* ssid     = "your_ssid";
const char* password = "your_password";

WiFiServer server(80);

void setup()
{
    Serial.begin(115200);
    pinMode(15, OUTPUT);      // set the LED pin mode

    delay(10);

    // We start by connecting to a WiFi network

    Serial.println();
    Serial.println();
    Serial.print("Connecting to ");
    Serial.println(ssid);

    WiFi.begin(ssid, password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }

    Serial.println("");
    Serial.println("WiFi connected.");
    Serial.println("IP address: ");
    Serial.println(WiFi.localIP());
    
    server.begin();

}

int value = 0;

void loop(){
    
 // 监听新连接的客户端
 WiFiClient client = server.available();   // listen for incoming clients
  if (client) {                             // if you get a client(如果有客户端连接)
    Serial.println("New Client.");           // print a message out the serial port(打印信息到串口)
    // 创建一个字符串来保存来自客户端的传入数据  
    String currentLine = "";                // make a String to hold incoming data from the client
    while (client.connected()) {            // loop while the client's connected (当客户端连接时保持循环)
      // 如果有来自客户端的数据可以读取
      if (client.available()) {             // if there's bytes to read from the client          
        char c = client.read();             // read a byte, then (读取一个字节)
        Serial.write(c);                    // print it out the serial monitor (打印到串口)
        if (c == '\n') {                    // if the byte is a newline character (如果是换行符)
          // 如果当前行为空,在一行中会有两个换行符。
          // 这是客户端HTrP请求的结束标志,所以发送一个响应:  
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP 头经常以一串响应代码开始,即(e.g. HTTP/1.1 200 OK)
            // 然后是一个内容类型,这样客户就知道收到了什么,然后是一个空白行:
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println();
			// 紧接着http响应的内容是:	
            // the content of the HTTP response follows the header:
            client.print("Click <a href=\"/H\">here</a> to turn the LED on pin 0 on.<br>");
            client.print("Click <a href=\"/L\">here</a> to turn the LED on pin 0 off.<br>");
			// HTTP响应以另一个空行结束	
            // The HTTP response ends with another blank line:
            client.println();
            //跳出循环
            // break out of the while loop:
            break;
          } else {    // if you got a newline, then clear currentLine:(如果你收到了新行,则清空currentLine:)
            currentLine = "";
          }
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
           
           // 如果收到除回车符以外的其他字符,将其加到currentLine的末尾
           currentLine += c;      // add it to the end of the currentLine
        }
		// 检查来自客户端的请求是否是"GET /H" 或者 "GET /L":
        // Check to see if the client request was "GET /H" or "GET /L":
        if (currentLine.endsWith("GET /H")) {
          digitalWrite(15, HIGH);               // GET /H turns the LED on
        }
        if (currentLine.endsWith("GET /L")) {
          digitalWrite(15, LOW);                // GET /L turns the LED off
        }
      }
    }
    // 关闭连接  
    // close the connection:
    client.stop();
    Serial.println("Client Disconnected.");
  }
}

打开串口终端,将主板复位,串口将打印出主板TCP Server的IP地址,设置的端口号是80端口。一般80作为网页服务器的默认访问端口。

Connecting to dfrobotOffice
.......
WiFi connected.
IP address: 
192.168.0.221
New Client.
GET / HTTP/1.1
Host: 192.168.0.221
Connection: keep-alive
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

Client Disconnected.
New Client.
GET /favicon.ico HTTP/1.1
Host: 192.168.0.221
Connection: keep-alive
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
Accept: image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8
Referer: http://192.168.0.221/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9

Client Disconnected.

使用网页浏览器访问TCP Web Server 的IP,点击页面,可开关LED.

image-20211220170814222

例程:WiFi Telnet to Serial

程序建立一个 server,并在 debug 串口打印出IP地址和端口号。PC 在同一个网段,通过 TCP Client 或 Telent 工具连接这个 IP 和端口后,即可建立无线 WiFi Telnet 和 串口2 收发数据的功能,为了便于调试可以将代码中串口收发数据改为 debug 串口。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WiFi/examples/WiFiTelnetToSerial)

/*
  WiFiTelnetToSerial - Example Transparent UART to Telnet Server for ESP32

  Copyright (c) 2017 Hristo Gochkov. All rights reserved.
  This file is part of the ESP32 WiFi library for Arduino environment.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.

  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/
#include <WiFi.h>
#include <WiFiMulti.h>

WiFiMulti wifiMulti;

//how many clients should be able to telnet to this ESP32
#define MAX_SRV_CLIENTS 1
const char* ssid = "**********";
const char* password = "**********";

WiFiServer server(23);
WiFiClient serverClients[MAX_SRV_CLIENTS];

void setup() {
  Serial.begin(115200);
  Serial.println("\nConnecting");

  wifiMulti.addAP(ssid, password);
  wifiMulti.addAP("ssid_from_AP_2", "your_password_for_AP_2");
  wifiMulti.addAP("ssid_from_AP_3", "your_password_for_AP_3");

  Serial.println("Connecting Wifi ");
  for (int loops = 10; loops > 0; loops--) {
    if (wifiMulti.run() == WL_CONNECTED) {
      Serial.println("");
      Serial.print("WiFi connected ");
      Serial.print("IP address: ");
      Serial.println(WiFi.localIP());
      break;
    }
    else {
      Serial.println(loops);
      delay(1000);
    }
  }
  if (wifiMulti.run() != WL_CONNECTED) {
    Serial.println("WiFi connect failed");
    delay(1000);
    ESP.restart();
  }

  //start UART and the server
  Serial1.begin(9600);
  server.begin();
  WiFi.setSleep(false);
  server.setNoDelay(true);

  Serial.print("Ready! Use 'telnet ");
  Serial.print(WiFi.localIP());
  Serial.println(" 23' to connect");
}

void loop() {
  uint8_t i;
  if (wifiMulti.run() == WL_CONNECTED) {
    //check if there are any new clients
    if (server.hasClient()){
      for(i = 0; i < MAX_SRV_CLIENTS; i++){
        //find free/disconnected spot
        if (!serverClients[i] || !serverClients[i].connected()){
          if(serverClients[i]) serverClients[i].stop();
          serverClients[i] = server.available();
          if (!serverClients[i]) Serial.println("available broken");
          Serial.print("New client: ");
          Serial.print(i); Serial.print(' ');
          Serial.println(serverClients[i].remoteIP());
          break;
        }
      }
      if (i >= MAX_SRV_CLIENTS) {
        //no free/disconnected spot so reject
        server.available().stop();
      }
    }
    //check clients for data
    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()) Serial1.write(serverClients[i].read());
        }
      }
      else {
        if (serverClients[i]) {
          serverClients[i].stop();
        }
      }
    }
    //check UART for data
    if(Serial1.available()){
      size_t len = Serial1.available();
      uint8_t sbuf[len];
      Serial1.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);
        }
      }
    }
  }
  else {
    Serial.println("WiFi not connected!");
    for(i = 0; i < MAX_SRV_CLIENTS; i++) {
      if (serverClients[i]) serverClients[i].stop();
    }
    delay(1000);
  }
}

9.3 同步UDP通信

UDP协议是User Datagram Protocol的简称,中文名是用户数据协议,是一种无连接的传输层协议。UDP协议具有较好的实时性,效率比TCP协议高,适用于对高速传输和实时性有较高要求的通信。但是UDP协议不保证绝对的可靠,需要用户通过其他方式实现可靠性。

9.3.1 UDP 几种通信方式

只有UDP有广播和多播, TCP只能进行点对点的单播, 多播的重点是高效的把同一个包尽可能多的发送到不同的,甚至可能是未知的设备。但是TCP连接是一对一明确的,只能单播。

UDP单播(Unicast)

单播是客户端与服务器之间的点到点连接。

UDP广播(broadcast)

广播UDP与单播UDP的区别就是IP地址不同,广播使用广播地址255.255.255.255,将消息发送到在同一广播网络上的每个主机。值得强调的是:本地广播信息是不会被路由器转发。当然这是十分容易理解的,因为如果路由器转发了广播信息,那么势必会引起网络瘫痪。这也是为什么IP协议的设计者故意没有定义互联网范围的广播机制。

广播地址通常用于在网络游戏中处于同一本地网络的玩家之间交流状态信息等。

其实广播顾名思义,就是想局域网内所有的人说话,但是广播还是要指明接收者的端口号的,因为不可能接受者的所有端口都来收听广播。

UDP多播(multicast)

多播,也称为“组播”,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。

在广域网上广播的时候,其中的交换机和路由器只向需要获取数据的主机复制并转发数据。主机可以向路由器请求加入或退出某个组,网络中的路由器和交换机有选择地复制并传输数据,将数据仅仅传输给组内的主机。多播的这种功能,可以一次将数据发送到多个主机,又能保证不影响其他不需要(未加入组)的主机的其他通 信。

相对于传统的一对一的单播,多播具有如下的优点:

  • 具有同种业务的主机加入同一数据流,共享同一通道,节省了带宽和服务器的优点,具有广播的优点而又没有广播所需要的带宽。

  • 服务器的总带宽不受客户端带宽的限制。由于组播协议由接收者的需求来确定是否进行数据流的转发,所以服务器端的带宽是常量,与客户端的数量无关。

  • 与单播一样,多播是允许在广域网即Internet上进行传输的,而广播仅仅在同一局域网上才能进行。

多播的缺点:

  • 多播与单播相比没有纠错机制,当发生错误的时候难以弥补,但是可以在应用层来实现此种功能。

  • 多播的网络支持存在缺陷,需要路由器及网络协议栈的支持。

  • 多播的应用主要有网上视频、网上会议等。

地址规范

多播的地址是特定的,D类地址用于多播。D类IP地址就是多播IP地址,即224.0.0.0至239.255.255.255之间的IP地址,并被划分为局部连接多播地址、预留多播地址和管理权限多播地址3类:

  • 局部多播地址:在224.0.0.0~224.0.0.255之间,这是为路由协议和其他用途保留的地址,路由器并不转发属于此范围的IP包。

  • 预留多播地址:在224.0.1.0~238.255.255.255之间,可用于全球范围(如Internet)或网络协议。

  • 管理权限多播地址:在239.0.0.0~239.255.255.255之间,可供组织内部使用,类似于私有IP地址,不能用于Internet,可限制多播范围。

多播程序设计的框架

要进行多播的编程,需要遵从一定的编程框架。多播程序框架主要包含套接字初始化、设置多播超时时间、加入多播组、发送数据、接收数据以及从多播组中离开几个方面。其步骤如下:

(1)建立一个socket。

(2)然后设置多播的参数,例如超时时间TTL、本地回环许可LOOP等。

(3)加入多播组。

(4)发送和接收数据。

(5)从多播组离开。

9.3.1 API 参考

begin() - 启动监听某个端口

uint8_t begin(uint16_t p)
uint8_t begin(IPAddress a, uint16_t p)

启动监听某个端口,或者来自某地址发送给某端口的数据。

语法

#include <WiFiUdp.h>

WiFiUDP Udp;                         // 实例化WiFiUDP对象
unsigned int localUdpPort = 1234;    // 自定义本地监听端口

if(Udp.begin(localUdpPort)){  // 启动Udp监听服务
	Serial.println("Listening Success");
    // 打印本地的ip地址,在UDP工具中会使用到
    Serial.printf("UDP Listening on IP:%s, UDP port:%d\n", WiFi.localIP().toString().c_str(), localUdpPort);
}

参数

传入值 说明 值范围
IPAddress a UDP的IP地址
uint16_t p UDP的端口号

返回

返回值 说明 值范围
uint8_t 1:启动UDP服务成功
0:启动UDP服务失败

beginMulticast() - 多播模式启动监听

uint8_t beginMulticast(IPAddress a, uint16_t p);

语法

#include <WiFiUdp.h>

WiFiUDP Udp;                         // 实例化WiFiUDP对象
unsigned int localUdpPort = 1234;    // 自定义本地监听端口
IPAddress multicast_ip(239, 1, 2, 3);

if(Udp.beginMulticast(multicast_ip,localUdpPort)){  // 启动Udp多播监听服务
	Serial.println("Listening Success");
    // 打印本地的ip地址,在UDP工具中会使用到
    Serial.printf("UDP Listening on IP:%s, UDP port:%d\n", WiFi.localIP().toString().c_str(), localUdpPort);
}

参数

传入值 说明 值范围
IPAddress a UDP的IP地址
uint16_t p UDP的端口号

返回

返回值 说明 值范围
uint8_t 1:启动UDP服务成功
0:启动UDP服务失败

stop() - 停止监听

void stop()

停止监听,释放资源。

语法

Udp.stop(); // 关闭udp监听

参数

返回

beginPacket() - 准备发送数据包

int beginPacket()
// 准备发送数据包(仅在运行parsePacket()方法且返回值大于0时可用)   
    
int beginPacket(IPAddress ip, uint16_t port)
int beginPacket(const char *host, uint16_t port);
// 准备发送数据包,参数分别为目标IP和目标端口号

语法

Udp.beginPacket(Udp.remoteIP(), remoteUdpPort); // 配置远端ip地址和端口

参数

传入值 说明 值范围
IPAddress ip UDP数据接收设备的IP地址
uint16_t port UDP数据接收设备的监听端口号

返回

返回值 说明 值范围
int 1:配置数据接收设备的IP地址和监听端口号成功
0:配置数据接收设备的IP地址和监听端口号失败

beginMulticastPacket() - 多播模式准备发送数据包

int beginMulticastPacket();

语法

udp.beginMulticast();

参数

返回

返回值 说明 值范围
int 1:配置数据接收设备的IP地址和监听端口号成功
0:配置数据接收设备的IP地址和监听端口号失败

parsePacket() - 获取接收数据信息

int parsePacket()

获取接收数据信息,如果有数据包可用,则返回队首数据包长度,否则返回0。

语法

int packetSize = Udp.parsePacket(); // 获得解析包
  if (packetSize)  // 解析包不为空
  {
  }

参数

返回

返回值 说明 值范围
int n:数据包的大小(以字节为单位)
0:没有可用的数据包

available() - 检查是否有数据被接收

int available();

检查设备是否接收到数据。函数将会返回等待读取的数据字节数。请注意,使用本函数以前需要先调用parsePacket函数。

语法

void loop()
{
  int packetSize = Udp.parsePacket();  // 获得解析包
  Serial.printf("UDP data size: %d\n", packetSize);
  int n = Udp.available();
  Serial.printf("UDP buffer size: %d\n", n);
  delay(3000);
}

参数

返回

返回值 说明 值范围
int 等待读取的数据字节数。
返回值数据类型:int

endPacket() - 发送UDP数据包

int endPacket()

语法

char  replyPacket[] = "FireBeetle MESH\n";  //发送的消息,仅支持英文
// 向udp工具发送消息
Udp.beginPacket(Udp.remoteIP(), remoteUdpPort); // 配置远端ip地址和端口
Udp.write(replyPacket); // 把数据写入发送缓冲区
Udp.endPacket(); // 发送数

参数

返回

返回值 说明 值范围
int 1:数据发送成功
0:数据发送失败

remoteIP() - 返回远端地址

IPAddress remoteIP()

返回远端地址(仅在运行parsePacket()方法且返回值大于0时可用)。

语法

Serial.printf("received from remote IP:%s(remote port:%d)Number of packet bytes is:%d\n", Udp.remoteIP().toString().c_str(), Udp.remotePort(), packetSize);

参数

返回

返回值 说明 值范围
IPAddress 返回值是向主板发送UDP数据包的远端设备的IP地址。

remotePort() - 返回远端端口号

uint16_t remotePort()

返回远端端口号(仅在运行parsePacket()方法且返回值大于0时可用)。

语法

Serial.printf("received from remote IP:%s(remote port:%d)Number of packet bytes is:%d\n", Udp.remoteIP().toString().c_str(), Udp.remotePort(), packetSize);

参数

返回

返回值 说明 值范围
uint16_t 返回值是向主板发送UDP数据包的远端设备的端口。

write() - 复制数据到发送缓冲区

size_t write(uint8_t)
size_t write(const uint8_t *buffer, size_t size)

复制数据到发送缓冲区(同一数据包发送缓存最大1460字节)。请注意:此函数仅仅将数据写入发送缓冲区,但是数据不会发送。

语法

Udp.write(replyPacket); // 把数据写入发送缓冲区

参数

传入值 说明 值范围
uint8_t 发送的数据字节
const uint8_t *buffer 指向发送数据的指针
size_t size 数据字节数

返回

返回值 说明 值范围
size_t 写入发送缓冲区的数据大小(单位:字节)。

read() - 读取数据

int read()
// 读取首字节数据(仅在运行parsePacket()方法且返回值大于0时可用)

int read(unsigned char* buffer, size_t len)
int read(char* buffer, size_t len)
// 读取数据(仅在运行parsePacket()方法且返回值大于0时可用)

语法

if (Udp.available()) {  // 判断是否有UDP数据包
    char a = Udp.read();//连续调用read
    Serial.printf("READ: %c\n", a);
    a = Udp.read();
    Serial.printf("READ: %c\n", a);
    a = Udp.read();
    Serial.printf("READ: %c\n", a);
    a = Udp.read();
    Serial.printf("READ: %c\n", a);
    char incomingPacket[255];  // 存储Udp客户端发过来的数据
    int len = Udp.read(incomingPacket, 255);
    Serial.printf("READ: %s   len:%d\n", incomingPacket, len);
}  

参数

传入值 说明 值范围
* buffer 保存传入数据包的内存指针
size_t len 传入数据包的大小(单位是字节)

返回

返回值 说明 值范围
int 在没有使用任何参数的情况下调用此函数,函数的返回值情况如下所述:
设备没有接收到数据时,返回值为 -1
设备接收到数据时,返回值为接收到的数据包中的第1个字符的ASCII码数值。

在使用了参数buffer和len调用此函数,函数的返回值情况如下所述:
设备没有接收到数据时,返回值为 -1
设备接收到数据时,返回值为接收到的数据包的大小(单位是字节)。

peek() - 读取数据,但不清除数据

int peek()

语法

int data = Udp.peek();

参数

返回

返回值 说明 值范围
int 设备没有接收到数据时,返回值为-1
设备接收到数据时,返回值为接收到的数据流中的第1个字符。

flush() - 待发数据发送完毕前,保持等待状态

void flush();

等待所有“发送缓存”中数据都发送完毕以后,再执行后续的程序内容。

语法

Udp.flush();

参数

返回

其他读写等操作可参考 串口部分 Stream类中的函数。

例程:建立UDP Server

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <WiFiUdp.h>
 
#define ssid      "your_ssid"  		// 这里改成你的设备当前环境下WIFI名字
#define password  "your_password"   // 这里改成你的设备当前环境下WIFI密码
 
WiFiUDP Udp; // 实例化WiFiUDP对象
unsigned int localUdpPort = 1234;  // 自定义本地监听端口
// unsigned int remoteUdpPort = 4321;  // 自定义远程监听端口
uint8_t  incomingPacket[255];  // 保存Udp工具发过来的消息
uint8_t  replyPacket[] = "This is Edge101WE Board speaking.";  //发送的消息,仅支持英文
 
void setup()
{
  Serial.begin(115200); // 打开串口
  Serial.println();
 
  Serial.printf("connecting... %s ", ssid);
  WiFi.begin(ssid, password); // 连接到wifi
  while (WiFi.status() != WL_CONNECTED) // 等待连接
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("connected");
 
  if(Udp.begin(localUdpPort)){//启动Udp监听服务
    Serial.println("Monitoring succeeded");
    // 打印本地的ip地址,在UDP工具中会使用到
    // WiFi.localIP().toString().c_str()用于将获取的本地IP地址转化为字符串    
    Serial.printf("Now listen to IP:%s, UDP port:%d\n", WiFi.localIP().toString().c_str(), localUdpPort);
  }else{
    Serial.println("Monitoring failed");
  }
}
 
void loop()
{
  // 解析Udp数据包
  int packetSize = Udp.parsePacket(); // 获得解析包
  if (packetSize) // 解析包不为空
  {
    //收到Udp数据包
    //Udp.remoteIP().toString().c_str()用于将获取的远端IP地址转化为字符串
    Serial.printf("Received from remote IP:%s(remote port:%d)packet bytes:%d\n", Udp.remoteIP().toString().c_str(), Udp.remotePort(), packetSize);
      
    // 读取Udp数据包并存放在incomingPacket
    int len = Udp.read(incomingPacket, 255);//返回数据包字节数
    if (len > 0)
    { 
      incomingPacket[len] = 0; // 清空缓存
    }
    // 向串口打印信息
    Serial.printf("UDP data is: %s\n", incomingPacket);
 
    // 向udp工具发送消息
    Udp.beginPacket(Udp.remoteIP(), Udp.remotePort()); // 配置远端ip地址和端口
    Udp.write(replyPacket,39); // 把数据写入发送缓冲区
    Udp.endPacket(); // 发送数据
  }
}

将程序下载到 Edge101WE 主板。打开串口终端,提示已经连接到WiFi,并且打开监听,IP:192.168.0.73, UDP port:1234

connecting... dfrobotOffice .....connected
Monitoring succeeded
Now listen to IP:192.168.0.221, UDP port:1234

打开 Packet Sender 软件,通过 UDP 发送字符串到 Edge101WE 主板的 IP:192.168.0.221 和 UDP port:1234。

image-20211220172558910

串口终端将会收到发送的字符串“This is Packet Sender speaking.”。

此时可在 Packet Sender 软件 上看到 Edge101WE 主板返回的字符串”This is Edge101WE Board speaking.”。

connecting... dfrobotOffice .....connected
Monitoring succeeded
Now listen to IP:192.168.0.221, UDP port:1234
Received from remote IP:192.168.1.96(remote port:50760)packet bytes:31
UDP data is: This is Packet Sender speaking.

9.4 异步 UDP 通信

异步 UDP 理论上可以提供比一般 UDP 更加优异的性能。

API参考

AsyncUDP类:

onPacket() - 注册事件,绑定回调函数
void onPacket(AuPacketHandlerFunctionWithArg cb, void * arg=NULL);
void onPacket(AuPacketHandlerFunction cb);
listen() - 开启监听
bool listen(const ip_addr_t *addr, uint16_t port);
bool listen(const IPAddress addr, uint16_t port);
bool listen(const IPv6Address addr, uint16_t port);
bool listen(uint16_t port);
listenMulticast() - 开启多播监听
bool listenMulticast(const ip_addr_t *addr, uint16_t port, uint8_t ttl=1, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
bool listenMulticast(const IPAddress addr, uint16_t port, uint8_t ttl=1, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
bool listenMulticast(const IPv6Address addr, uint16_t port, uint8_t ttl=1, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
connect() - 作为客户端连接到服务器
bool connect(const ip_addr_t *addr, uint16_t port);
bool connect(const IPAddress addr, uint16_t port);
bool connect(const IPv6Address addr, uint16_t port);

作为客户端连接到服务器(连接成功后可以直接使用write、send方法,而不用writeTo、sendTo)。

close() - 关闭UDP
void close()
write() - 发送数据
size_t writeTo(const uint8_t *data, size_t len, const ip_addr_t *addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t writeTo(const uint8_t *data, size_t len, const IPAddress addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t writeTo(const uint8_t *data, size_t len, const IPv6Address addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t write(const uint8_t *data, size_t len);
size_t write(uint8_t data);
broadcast() - 广播数据
size_t broadcastTo(uint8_t *data, size_t len, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t broadcastTo(const char * data, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t broadcast(uint8_t *data, size_t len);
size_t broadcast(const char * data);
send() - 发送数据
size_t sendTo(AsyncUDPMessage &message, const ip_addr_t *addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t sendTo(AsyncUDPMessage &message, const IPAddress addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t sendTo(AsyncUDPMessage &message, const IPv6Address addr, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t send(AsyncUDPMessage &message);
broadcast() - 广播数据
size_t broadcastTo(AsyncUDPMessage &message, uint16_t port, tcpip_adapter_if_t tcpip_if=TCPIP_ADAPTER_IF_MAX);
size_t broadcast(AsyncUDPMessage &message);

AsyncUDPPacket类:

AsyncUDPPacket() - 构造函数
AsyncUDPPacket(AsyncUDPPacket &packet);
AsyncUDPPacket(AsyncUDP *udp, pbuf *pb, const ip_addr_t *addr, uint16_t port, struct netif * netif);
data() - 返回数据指针
uint8_t * data()
length() - 返回数据长度
size_t length()
isBroadcast() - 查询是否为广播数据
bool isBroadcast()
isMulticast() - 查询是否为组播地址
bool isMulticast()
localIP() - 返回目标(本地)IP
IPAddress localIP()

返回目标(本地)IP

localPort() - 返回目标(本地)端口号
uint16_t localPort()
remoteIP() - 返回远端IP
IPAddress remoteIP()
remotePort() - 返回远端端口号
uint16_t remotePort()
remoteMac() - 返回远端MAC地址
 void remoteMac(uint8_t * mac);
send() - 发送UDP数据
size_t send(AsyncUDPMessage &message);
available() - 查询缓存区数据
int available();
read() - 读取数据
size_t read(uint8_t *data, size_t len)  // 读取数据
    
int read();  // 读取首字节数据  
peek() - 读取首字节数据,但并不从接收缓存中删除它
int peek()
flush() - 待发数据发送完毕前,保持等待状态
void flush()

等待所有“发送缓存”中数据都发送完毕以后,再执行后续的程序内容。

语法

Udp.flush();

参数

返回

write() - 向远端发数据
size_t write(const uint8_t *data, size_t len)
size_t write(uint8_t data)

AsyncUDPMessage类:

AsyncUDPMessage() - 构造函数
AsyncUDPMessage(size_t size=CONFIG_TCP_MSS)

构造函数(相当于一个缓存,把要发送的数据放到这里,然后通过相应方法发送)

write() - 将数据写到AsyncUDPMessage对象
size_t write(const uint8_t *data, size_t len)
size_t write(uint8_t data)
space() - 返回AsyncUDPMessage对象剩余可用空间
size_t space()
data() - 返回AsyncUDPMessage对象中数据首指针
uint8_t * data()
length() - 返回AsyncUDPMessage对象当前已用长度
size_t length()
flush() - 待发AsyncUDPMessage对象中的数据发送完毕前,保持等待状态
void flush()

参数

返回

例程:AsyncUDPServer

建立一个异步UDP服务器,监听端口1234。将收到的数据从串口打印出来。同时每秒钟发送一次广播。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->AsyncUDP\examples\AsyncUDPServer)

#include "WiFi.h"
#include "AsyncUDP.h"

const char * ssid = "***********";
const char * password = "***********";

AsyncUDP udp;

void setup()
{
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.println("WiFi Failed");
        while(1) {
            delay(1000);
        }
    }
    if(udp.listen(1234)) {
        Serial.print("UDP Listening on IP: ");
        Serial.println(WiFi.localIP());
        udp.onPacket([](AsyncUDPPacket packet) {
            Serial.print("UDP Packet Type: ");
            Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
            Serial.print(", From: ");
            Serial.print(packet.remoteIP());
            Serial.print(":");
            Serial.print(packet.remotePort());
            Serial.print(", To: ");
            Serial.print(packet.localIP());
            Serial.print(":");
            Serial.print(packet.localPort());
            Serial.print(", Length: ");
            Serial.print(packet.length());
            Serial.print(", Data: ");
            Serial.write(packet.data(), packet.length());
            Serial.println();
            //reply to the client
            packet.printf("Got %u bytes of data", packet.length());
        });
    }
}

void loop()
{
    delay(1000);
    //Send broadcast
    udp.broadcast("Anyone here?");
}

打开 Packet Sender 软件,发送数据到 Edge101WE 主板建立的 UDP server。

image-20211220173613936

Edge101WE 主板串口终端打印,接收到 Packet Sender 软件发出的字符串 “This is Packet Sender speaking.”

同时 Edge101WE 主板将发送获取的字符串长度信息给 Packet Sender 软件。

UDP Listening on IP: 192.168.0.221
UDP Packet Type: Unicast, From: 192.168.1.96:63983, To: 192.168.0.221:1234, Length: 31, Data: This is Packet Sender speaking.

例程:UDP多播服务器

多播(组播)的概念 多播,也称为“组播”,将网络中同一业务类型主机进行了逻辑上的分组,进行数据收发的时候其数据仅仅在同一分组中进行,其他的主机没有加入此分组不能收发对应的数据。

在广域网上广播的时候,其中的交换机和路由器只向需要获取数据的主机复制并转发数据。主机可以向路由器请求加入或退出某个组,网络中的路由器和交换机有选择地复制并传输数据,将数据仅仅传输给组内的主机。多播的这种功能,可以一次将数据发送到多个主机,又能保证不影响其他不需要(未加入组)的主机的其他通信。

相对于传统的一对一的单播,多播具有如下的优点:

  • 具有同种业务的主机加入同一数据流,共享同一通道,节省了带宽和服务器的优点,具有广播的优点而又没有广播所需要的带宽。

  • 服务器的总带宽不受客户端带宽的限制。由于组播协议由接收者的需求来确定是否进行数据流的转发,所以服务器端的带宽是常量,与客户端的数量无关。

  • 与单播一样,多播是允许在广域网即 Internet 上进行传输的,而广播仅仅在同一局域网上才能进行。

组播的缺点:

  • 多播与单播相比没有纠错机制,当发生错误的时候难以弥补,但是可以在应用层来实现此种功能。

  • 多播的网络支持存在缺陷,需要路由器及网络协议栈的支持。

  • 多播的应用主要有网上视频、网上会议等。

广域网的多播 多播的地址是特定的,D类地址用于多播。D类IP地址就是多播IP地址,即 224.0.0.0至239.255.255.255 之间的 IP 地址,并被划分为局部连接多播地址、预留多播地址和管理权限多播地址3类:

  • 局部多播地址:在 224.0.0.0~224.0.0.255 之间,这是为路由协议和其他用途保留的地址,路由器并不转发属于此范围的IP包。

  • 预留多播地址:在 224.0.1.0~238.255.255.255 之间,可用于全球范围(如 Internet)或网络协议。

  • 管理权限多播地址:在 239.0.0.0~239.255.255.255 之间,可供组织内部使用,类似于私有IP地址,不能用于Internet,可限制多播范围。

在一块 Edge101WE 主板A 烧写如下程序:

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->AsyncUDP\examples\AsyncUDPMulticastServer)

#include "WiFi.h"
#include "AsyncUDP.h"

const char * ssid = "***********";
const char * password = "***********";

AsyncUDP udp;

void setup()
{
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.println("WiFi Failed");
        while(1) {
            delay(1000);
        }
    }
    if(udp.listenMulticast(IPAddress(239,1,2,3), 1234)) {
        Serial.print("UDP Listening on IP: ");
        Serial.println(WiFi.localIP());
        udp.onPacket([](AsyncUDPPacket packet) {
            Serial.print("UDP Packet Type: ");
            Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
            Serial.print(", From: ");
            Serial.print(packet.remoteIP());
            Serial.print(":");
            Serial.print(packet.remotePort());
            Serial.print(", To: ");
            Serial.print(packet.localIP());
            Serial.print(":");
            Serial.print(packet.localPort());
            Serial.print(", Length: ");
            Serial.print(packet.length());
            Serial.print(", Data: ");
            Serial.write(packet.data(), packet.length());
            Serial.println();
            //reply to the client
            packet.printf("Got %u bytes of data", packet.length());
        });
        //Send multicast
        udp.print("Hello!");
    }
}

void loop()
{
    delay(1000);
    //Send multicast
    udp.print("Anyone here?");
}

在另外一块 Edge101WE 主板B 烧写如下程序。为了防止重复的应答,去掉了应答和广播的代码。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码

#include "WiFi.h"
#include "AsyncUDP.h"

const char * ssid = "***********";
const char * password = "***********";

AsyncUDP udp;

void setup()
{
    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.println("WiFi Failed");
        while(1) {
            delay(1000);
        }
    }
    if(udp.listenMulticast(IPAddress(239,1,2,3), 1234)) {
        Serial.print("UDP Listening on IP: ");
        Serial.println(WiFi.localIP());
        udp.onPacket([](AsyncUDPPacket packet) {
            Serial.print("UDP Packet Type: ");
            Serial.print(packet.isBroadcast()?"Broadcast":packet.isMulticast()?"Multicast":"Unicast");
            Serial.print(", From: ");
            Serial.print(packet.remoteIP());
            Serial.print(":");
            Serial.print(packet.remotePort());
            Serial.print(", To: ");
            Serial.print(packet.localIP());
            Serial.print(":");
            Serial.print(packet.localPort());
            Serial.print(", Length: ");
            Serial.print(packet.length());
            Serial.print(", Data: ");
            Serial.write(packet.data(), packet.length());
            Serial.println();
            //reply to the client
            //packet.printf("Got %u bytes of data", packet.length());
        });
        //Send multicast
        //udp.print("Hello!");
    }
}

void loop()
{
    delay(1000);
    //Send multicast
    //udp.print("Anyone here?");
}

此时在 软件上 建立一个 多播,发送信息,点击发送后会弹出多播的设置页面,将 239.1.2.3 加入。

然后点击 Send 按钮,在两个 Edge101WE 主板都将收到相同的信息。

image-20211221104336798

主板A串口终端打印

UDP Listening on IP: 192.168.0.221
UDP Packet Type: Multicast, From: 192.168.1.96:51296, To: 239.1.2.3:1234, Length: 31, Data: This is Packet Sender speaking.
UDP Packet Type: Multicast, From: 192.168.1.96:51296, To: 239.1.2.3:1234, Length: 31, Data: This is Packet Sender speaking.

主板B串口终端打印

UDP Listening on IP: 192.168.0.218
UDP Packet Type: Multicast, From: 192.168.0.221:1234, To: 239.1.2.3:1234, Length: 12, Data: Anyone here?
UDP Packet Type: Multicast, From: 192.168.1.96:51296, To: 239.1.2.3:1234, Length: 31, Data: This is Packet Sender speaking.
UDP Packet Type: Multicast, From: 192.168.0.221:1234, To: 239.1.2.3:1234, Length: 12, Data: Anyone here?
UDP Packet Type: Multicast, From: 192.168.1.96:51296, To: 239.1.2.3:1234, Length: 31, Data: This is Packet Sender speaking

9.4 Web Server

Web Server 就是提供 Web 服务的服务器。Web服务器的主要功能是:存储、处理和传递网页给客户。Web服务器可以解析(handles)HTTP协议。当Web服务器接收到一个HTTP请求(request),会返回一个HTTP响应(response),例如送回一个HTML页面。为了处理一个请求(request),Web服务器可以响应(response)一个静态页面或图片,进行页面跳转(redirect),或者把动态响应(dynamic response)的产生委托(delegate)给一些其它的程序例如CGI脚本、JSP(JavaServer Pages)脚本、servlets、ASP(Active Server Pages)脚本、服务器端(server-side) JavaScript,或者一些其它的服务器端(server-side)技术。无论脚本的目的如何,这些服务器端(server-side)的程序通常产生一个HTML的响应(response)来让浏览器可以浏览。

Edge101WE 主板可作为存储网页的 Web 服务器,有人通过局域网请求一个网页时,Edge101WE 主板将为该网页提供服务。对于开发来说主要处理的就是注册链接并编写用户访问该链接时需要执行的操作。

使用步骤如下:

  • 引入相应库#include <WebServer.h>。

  • 声明WebServer对象并设置端口号,一般WebServer端口号使用80。

  • 使用on()方法注册链接与回调函数。

  • 使用begin()方法启动服务器进行请求监听。

  • 使用handleClient()处理来自客户端的请求。

9.4.1 API 参考

WebServer() - 构造方法

WebServer(int port = 80)
WebServer(IPAddress addr, int port = 80);

语法

WebServer server(80);

参数

传入值 说明 值范围
IPAddress addr 设置服务器IP地址。
int port 设置服务端口号,默认为80端口。

返回

begin() - 服务器启动监听

void begin()
void begin(uint16_t port)

语法

WebServer server(80);
server.begin();

参数

传入值 说明 值范围
uint16_t port 服务端口号

返回

close() - 停止当前监听

stop() - 停止当前监听

void close()
void stop()

语法

server.close();
server.stop();

参数

返回

on() - 注册链接与回调函数

void on(const String &uri, THandlerFunction handler)
void on(const String &uri, HTTPMethod method, THandlerFunction fn)
void on(const String &uri, HTTPMethod method, THandlerFunction fn, THandlerFunction ufn)

设置HTTP请求回调函数。

语法

void homepage() {
  server.send(200, "text/plain", "This is homepage!");
  Serial.println("This is homepage");
}
server.on("/", homepage);

参数

传入值 说明 值范围
uri HTTP请求客户端所请求的uri
handler HTTP请求回调函数
method 向客户端发送响应信息时所使用的HTTP方法 HTTP_ANY
HTTP_GET
HTTP_POST
HTTP_PUT
HTTP_PATCH
HTTP_DELETE
HTTP_OPTIONS

返回

onNotFound() - 注册未注册链接回调函数

void onNotFound(THandlerFunction fn)

设置HTTP请求无效地址的回调函数。通过无效地址回调函数,我们可以将404页面信息发送给客户端。

语法

// 设置处理404情况的函数'handleNotFound'
void handleNotFound() { // 当浏览器请求的网络资源无法在服务器找到时将调用此函数
  server.send(404, "text/plain", "404: Not found"); 
}
server.onNotFound(handleNotFound);

参数

传入值 说明 值范围
fn 处理无效地址请求的回调函数

返回

onFileUpload() - 注册文件上传回调函数

void onFileUpload(THandlerFunction fn)

当 FireBeetle MESH 主板建立的网络服务器收到了客户端的文件上传请求时,可利用此函数来配置处理文件上传请求的回调函数。

语法

void handleonFileUpload() { // 当收到了客户端的文件上传请求时时将调用此函数
    Serial.println("FileUpload called");
}
server.onFileUpload(handleonFileUpload);

参数

传入值 说明 值范围
fn 处理文件上传请求的回调函数

返回

handleClient() - 处理来自客户端的请求

void handleClient();

检查有没有客户端设备通过网络向 FireBeetle MESH 主板建立的网络服务器发送请求。每一次 handleClient 函数被调用时,FireBeetle MESH 的网络服务器都会检查一下是否有客户端发送 HTTP 请求。因此建议将该函数放在 loop 函数中,从而确保它能经常被调用。

假如 loop 函数里有类似 delay 一类的函数延迟程序运行,那么就一定要注意了。如果 handleClient 函数长时间得不到调用,FireBeetle MESH 网络服务器会因为无法经常检查 HTTP 客户端请求而导致服务器响应变慢,严重的情况下,会导致服务器工作不稳定。

语法

void loop(void){
  server.handleClient();     // 处理 http 服务器访问
}

参数

返回

uri() - 返回客户端请求的url

String uri()  

获取客户端发送的HTTP请求行中的请求资源路径信息。

语法

void homepage() {
  Serial.println(server.uri());  // 通过串口监视器输出浏览器请求资源路径
  server.send(200, "text/plain", "This is homepage!");
}
server.on("/", homepage);

参数

返回

返回值 说明 值范围
String 客户端请求行中的请求资源路径信息。

method() - 返回客户端请求方法

HTTPMethod method()

语法

void homepage() {
  Serial.println(server.method());  // 串口输出当前客户端的请求方法
  server.send(200, "text/plain", "This is homepage!");
}
server.on("/", homepage);

参数

返回

返回值 说明 值范围
HTTPMethod HTTP请求方法代表值 HTTP_ANY 1
HTTP_GET 1
HTTP_POST 3
HTTP_PUT 4
HTTP_PATCH 5
HTTP_DELETE 6
HTTP_OPTIONS 7

collectHeaders() - 设置需要收集哪些请求头信息

void collectHeaders(const char* headerKeys[], const size_t headerKeysCount); 

使用 Edge101WE 主板实现的物联网服务器收集客户端发送的请求头信息。使用这个函数设置需要获取的HTTP请求头信息。

注意:在我们使用WebServer库中的headers、header 、headerName、hasHeader 函数来获取浏览器请求头以前,需要首先调用本函数来设置主板具体收集哪些请求头信息。

语法

#include <WiFi.h>
#include <WebServer.h>

// 设置WiFi接入信息
const char* ssid = "your_ssid";
const char* password = "your_password";

// 创建 WebServer 对象
WebServer server(80);

// 设置访问次数变量
int repeat = 0;
// 设置需要收集的请求头信息
const char *headerKeys[] = {"Content-Length", "Content-Type", "Connection", "Date"};

void homepage() {
  Serial.println(server.method());  // 串口输出当前客户端的请求方法
  server.send(200, "text/plain", "This is homepage!");
  if (server.hasHeader("Connection")) { // 判断该请求头是否存在

    // 使用示例,打印当前收集的请求头的数量
    Serial.print("Number of request headers:"); Serial.println(server.headers());

    // 打印当前请求中所收集的请求头指定项的值
    Serial.print("Value of request headers Connection:"); Serial.println(server.header("Connection"));

    // 打印当前请求中所收集的Host
    Serial.print("request headers Host :"); Serial.println(server.hostHeader());

    // 分隔空行
    Serial.println("\r\n");
  }
}

void setup(void) {
  // 初始化串口
  Serial.begin(115200);
  Serial.println("");

  // 初始化网络
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("IP Address: ");
  Serial.println(WiFi.localIP());

  //初始化WebServer
  server.begin();
  server.on("/", homepage);
  server.collectHeaders(headerKeys, sizeof(headerKeys) / sizeof(headerKeys[0]));
}

void loop(void) {
  // 监听客户请求并处理
  server.handleClient();
}

参数

传入值 说明 值范围
headerKeys[] 请求头名数组。此数组中的元素即是需要主板处理或收集的请求头信息。
headerKeysCount 需要获取/处理的请求头个数

返回

headers() - 获得请求头数量

int headers();

语法

// 使用示例,打印当前收集的请求头的数量
Serial.print("Number of request headers:"); Serial.println(server.headers());

参数

返回

返回值 说明 值范围
int 获取的响应头的数量

headerName() - 获取指定请求头的名称

String headerName(int i);

语法

// 打印当前请求中所收集的请求头的值信息,请求头名称+请求头值
Serial.print(server.headerName(1)); 
Serial.print(" = "); 
Serial.println(server.header(1));

参数

传入值 说明 值范围
int i 请求头序号

返回

返回值 说明 值范围
String 指定请求头名称

hasHeader() - 确定是否包含指定请求头

bool hasHeader(String name);

语法

// 判断该请求头是否存在Connection信息
if (server.hasHeader("Connection")) {
    Serial.println("Include Connection"); 
} else {
    Serial.println("Not included Connection"); 
} 

参数

传入值 说明 值范围
String name 需要确认客户端请求中是否存在的请求头信息名

返回

返回值 说明 值范围
bool true:请求头中是否存在指定请求头信息存在。
false:请求头中是否存在指定请求头信息不存在

arg() - 请求中指定参数的数值

String arg(String name);        // get request argument value by name
String arg(int i);              // get request argument value by number

语法

void homepage() {
	server.send(200, "text/plain", "This is homepage!");
 	Serial.print("parameter a:"); Serial.println(server.arg("a"));
  	Serial.print("second parameter:"); Serial.println(server.arg(2));	   
}

server.on("/", homepage);

参数

传入值 说明 值范围
String name 请求参数的名称
int i 请求参数序列号

返回

返回值 说明 值范围
String 请求参数的值。如果没有指定的请求参数号/请求参数名称对应的请求参数,则返回空字符串(“”)。

args() - 请求包含的参数数量

int args(); 

语法

void homepage() {
	server.send(200, "text/plain", "This is homepage!");
	Serial.print("Number of parameters:"); Serial.println(server.args());  
}

server.on("/", homepage);

参数

返回

返回值 说明 值范围
int 请求参数的数量

argName() - 请求中参数名

String argName(int i);

语法

void homepage() {
	server.send(200, "text/plain", "This is homepage!");
  	Serial.print("The name of parameter 2:");
  	Serial.println(server.argName(2));
}

server.on("/", homepage);

参数

传入值 说明 值范围
int i 请求体中的参数序列号

返回

返回值 说明 值范围
String 请求信息中指定参数的名称

hasArg () - 判断请求中是否包含某个参数名

bool hasArg(String name);

语法


参数

传入值 说明 值范围
name 需要确认的请求体中的参数名

返回

返回值 说明 值范围
bool 返回是否存在指定参数

send() - 向客户端(浏览器)发送数据

void send(int code, const char* content_type = NULL, const String& content = String(""))
void send(int code, char* content_type, const String& content)
void send(int code, const String& content_type, const String& content)

语法

void homepage() {
  server.send(200, "text/plain", "This is homepage!");
  Serial.println("This is homepage");
}
server.on("/", homepage);

参数响应内容类型

传入值 说明 值范围
code 响应状态码
content_type 响应内容类型
content 响应内容

返回

sendHeader() - 发送响应头

void sendHeader(const String& name, const String& value, bool first = false)

语法

void homepage() {
  // 设置自定义响应头内容
  server.sendHeader("device", "FireBeetle_MESH");
    
  server.send(200, "text/plain", "This is homepage!");
  Serial.println("This is homepage");
}
server.on("/", homepage);

参数

传入值 说明 值范围
name 自定义的响应头信息的名称,可使用字符串类型。
value 自定义的响应头值,可使用字符串类型。
first 设置该响应头是否需要放在第一行,默认为false。

返回

sendContent() - 发送响应正文内容

void sendContent(const String& content);
void sendContent(const char* content, size_t contentLength);

向客户端发送响应信息有以下三种方式:

sendContent:

sendContent函数所发送的信息通常是程序中的一个字符串。该函数的优点是直接调用程序内字符串,这个操作比起两外两种方法来说更加简单直接。但是其缺点是,由于存储发送信息的字符串是在程序中的,这会占用主板的动态内存空间。因此,使用sendContent函数时,发送信息的大小受到了限制。

sendContent_P:

当我们使用sendContent_P时,发送的响应信息必须存储在程序存储空间。这一特点大大优化了程序内存占用。因此sendContent_P对于我们在发送较大的响应信息时非常有帮助。但是sendContent_P的信息仍是写在程序中,如果发送的信息需要分为多个文件存储,使用sendContent_P函数是无法胜任的。

streamFile:

使用streamFile函数来发送响应信息是最推荐的操作方法。因为streamFile利用了主板的闪存文件系统来存储发送的信息内容。可以说streamFile函数既可以节省程序内存空间,又允许我们将需要发送的信息分为多个文件进行保存。但是使用streamFile时需要我们使用Arduino IDE的闪存文件上传工具预先将文件上传到闪存中。

语法

void homepage() {
  // 设置响应体内容以及响应体长度
  server.sendContent("sendContent_test_OK");     
  server.send(200, "text/plain", "This is homepage!");
  Serial.println("This is homepage");
}
server.on("/", homepage);

参数

传入值 说明 值范围
content 响应体信息,可使用字符串格式
contentLength 响应体信息长度

返回

sendContent_P() - 发送响应正文内容

void sendContent_P(PGM_P content);
void sendContent_P(PGM_P content, size_t size);

语法

// 建立保存在程序存储空间中的字符串
const char reponseContent[] PROGMEM = "FireBeetle MESH IIoT mainboard";
void homepage() {
  // 设置响应体内容
  server.sendContent_P(reponseContent);    
  server.send(200, "text/plain", "This is homepage!");
  Serial.println("This is homepage");
}
server.on("/", homepage);

参数

传入值 说明 值范围
content 响应体信息,该信息必须存储在程序存储空间的字符数组。
size_t size 响应体信息长度

返回

streamFile() - 发送文件,返回发送的字节数

template<typename T>
size_t streamFile(T &file, const String& contentType)

语法

if (SPIFFS.exists(path)) {                     // 如果访问的文件可以在SPIFFS中找到
    File file = SPIFFS.open(path, "r");          // 则尝试打开该文件
    server.streamFile(file, contentType);// 并且将该文件返回给浏览器

参数

传入值 说明 值范围
file 存储有响应信息的闪存文件对象
contentType 响应信息系类型

返回

返回值 说明 值范围
size_t 发送的文件大小

setContentLength() - 设置响应体长度

void setContentLength(const size_t contentLength);

语法

void homepage() {
    // 设置响应体内容以及响应体长度
    server.sendContent("sendContent_test_OK");
    // 不确定响应体信息长度,使用 CONTENT_LENGTH_UNKNOWN 关键词作为参数    	
 	server.setContentLength(CONTENT_LENGTH_UNKNOWN); 
    
    server.send(200, "text/plain", "This is homepage!");
  	Serial.println("This is homepage");
}
server.on("/", homepage);

参数

传入值 说明 值范围
contentLength 响应体的长度。
不确定响应体信息长度,使用 CONTENT_LENGTH_UNKNOWN 关键词作为参数 。

返回

upload() - 处理文件上传

HTTPUpload& upload() { return *_currentUpload; }

语法

// 处理上传文件函数
void handleFileUpload(){  
  
  HTTPUpload& upload = server.upload();

参数

传入值 说明 值范围
server.upload()

返回

authenticate() - 请求认证校验

bool authenticate(const char * username, const char * password)

通过本API建立加密网页,设置用户名和密码。用户必须正确输入访问用户名和密码方可访问物联网服务器所建立的页面内容。

语法

//校验用户登录账号和密码,(使用Basic方式),若输入错误则继续返回认证界面
if (!server.authenticate("FireBeetleMESH", "123456")) return server.requestAuthentication();

参数

传入值 说明 值范围
username 客户端访问物联网服务器加密页面时的认证用户名
password 客户端访问物联网服务器加密页面时的认证密码

返回

返回值 说明 值范围
bool true:用户密码输入正确
false:用户密码输入错误

requestAuthentication() - 请求进行用户登录认证

void requestAuthentication(HTTPAuthMethod mode = BASIC_AUTH, const char* realm = NULL, const String& authFailMsg = String(""))

请求进行用户登录认证(浏览器端会打开登录窗口)。

语法

//校验用户登录账号和密码,(使用Basic方式),若输入错误则继续返回认证界面
if (!server.authenticate("FireBeetleMESH", "123456")) return server.requestAuthentication();

参数

传入值 说明 值范围
mode HTTP验证方式 BASIC_AUTH、 DIGEST_AUTH(默认为BASIC_AUTH)
realm 认证范围
authFailMsg 认证失败提示消息

返回

HTTP状态码说明

上面的200、404等是HTTP状态码。用户在请求访问某个地址的时候,WebServer需要进行响应,发送响应头,响应头中第一行一般像是这样的HTTP/1.1 200 OK,其中200就是状态码。常见的状态码如下:

200服务器成功返回网页; 404请求的网页不存在; 301本网页被永久性转移到另一个URL; 503服务器目前不可用; 401请求未经授权,需要登录认证; ……

Content-Type说明

上面的text/plain、text/html这些是Content-Type,具体说明如下:

Content-Type(MediaType),即是Internet Media Type,互联网媒体类型,也叫做MIME类型。在互联网中有成百上千中不同的数据类型,HTTP在传输数据对象时会为他们打上称为MIME的数据格式标签,用于区分数据类型。最初MIME是用于电子邮件系统的,后来HTTP也采用了这一方案。 在HTTP协议消息头中,使用Content-Type来表示请求和响应中的媒体类型信息。它用来告诉服务端如何处理请求的数据,以及告诉客户端(一般是浏览器)如何解析响应的数据,比如显示图片,解析并展示html等等。

Content-Type举例:

text/plain纯文本文件; text/htmlhtml文件; application/jsonjson形式数据文件; application/xmlxml形式数据文件; image/pngpng格式图片; ……

9.4.2 同步 Web Server 例程

例程:最简单的 Web Server 例子

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <WebServer.h> // 引入相应库

// 连接WiFi网络的SSID和密码
const char *ssid = "your_ssid";	
const char *password = "your_password";

WebServer server(80); // 声明WebServer对象

void handleRoot() // 回调函数
{
  server.send(200, "text/plain", "This is the root directory");
}

void handleP1() // 回调函数
{
  server.send(200, "text/plain", "This is the Page 1");
}

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

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());  // 从串口打印出 Web Server的IP,用于访问

  server.on("/", handleRoot); // 注册链接"/"与对应回调函数

  server.on("/p1", handleP1); // 注册链接"/p1"与对应回调函数

  server.on("/p2", []() { // 注册链接"/p2",对应回调函数通过内联函数声明
    server.send(200, "text/plain", "This is the Page 2");
  });

  server.begin(); // 启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); // 处理来自客户端的请求
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks  
}

下载程序后将会从串口打印出Web Server的IP。

image-20210420104952508

使用网页浏览器访问 http://192.168.1.9/ 可看到 This is the root directory

image-20210420105805663

访问 http://192.168.1.9/p1 可看到 This is the Page 1

image-20210420105932129

访问 http://192.168.1.9/p2 可看到 This is the Page 2

image-20210420110121071

例程:设置未注册页面响应

如果用户访问了未注册的的链接时我们最好能给个提示,比如我们在上网时经常能见到的“网页不存在”、“404 Not Found”等。在这里我们可以用onNotFound()方法来给出这样的提示,用户在访问不存在的链接时会跳转到该方法所绑定的回调函数上。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <WebServer.h> //引入相应库

// 连接WiFi网络的SSID和密码
const char *ssid = "your_ssid";	
const char *password = "your_password";

WebServer server(80); //声明WebServer对象

void handleRoot() //回调函数
{
  server.send(200, "text/plain", "This is the root directory");
}

void handleNotFound() //未注册链接回调函数
{
  server.send(404, "text/plain", "Sorry,this page cannot be found.");
}

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

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot);    //注册链接"/"与对应回调函数
  server.onNotFound(handleNotFound); //未注册链接回调函数注册

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks
}

当访问串口打印出来的 Web Server IP 可打开根目录网页

image-20210420105805663

当访问其他网页 例如 http://192.168.1.9/p1 时提醒找不到这个网页

image-20210420135006797

例程:用户认证

用户认证可以提供一定的安全性,这里提供了BASIC_AUTHDIGEST_AUTH两种方式,一般来说DIGEST_AUTH方式安全性稍高些。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <WebServer.h> //引入相应库

// 连接WiFi网络的SSID和密码
const char *ssid = "your_ssid";	
const char *password = "your_password";

WebServer server(80); //声明WebServer对象


const char *username = "admin";     //用户名
const char *userpassword = "admin"; //用户密码

void handleRoot() //回调函数
{
  if (!server.authenticate(username, userpassword)) //校验用户是否登录
  {
    return server.requestAuthentication(); //请求进行用户登录认证
  }
  server.send(200, "text/plain", "Login successful."); //登录成功则显示真正的内容
}

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

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot); //注册链接和回调函数

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks
}

访问串口打印出来的 Web Server IP 要求输入用户名和密码,此时输入程序中的用户名和密码,点击登录。

image-20210420143521767

登录后显示登录成功。

image-20210420143634840

例程:获取访问服务器的客户端信息

客户端请求链接服务器,也能够获取客户端请求的一些信息。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include <WebServer.h> //引入相应库

// 连接WiFi网络的SSID和密码
const char *ssid = "your_ssid";
const char *password = "your_password";

WebServer server(80); //声明WebServer对象

void handleRoot() //回调函数
{
  server.send(200, "text/plain", "This is the root directory");
}

void handleClientMessage() //回调函数
{
  String message = "Client information: ";
  message += "\nClient IP: ";
  IPAddress addr = server.client().remoteIP(); //客户端ip
  message += String(addr[0]) + "." + String(addr[1]) + "." + String(addr[2]) + "." + String(addr[3]);
  message += "\nURI: ";
  message += server.uri(); //打印当前url
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST"; //判断http请求方法
  message += "\nArguments: ";  // 参数
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++)
  {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(200, "text/plain", message);
}

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

  WiFi.mode(WIFI_STA);
  WiFi.setSleep(false);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED)
  {
    delay(500);
    Serial.print(".");
  }
  Serial.println("Connected");
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());

  server.on("/", handleRoot); //注册链接"/"与对应回调函数
  server.on("/msg", HTTP_GET, handleClientMessage); //注册链接和回调函数

  server.begin(); //启动服务器
  Serial.println("Web server started");
}

void loop()
{
  server.handleClient(); //处理来自客户端的请求
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks    
}

当访问串口打印出来的 Web Server IP 时可显示客户端的信息

image-20210420145715367

例程:HelloServer mDNS方式的Web Server

注意:如果您使用的是Windows电脑需要设置mDNS环境,详情见 9.5.2 mDNS 通过STA方式。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WebServer\examples\HelloServer)

#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>

const char *ssid = "your_ssid";
const char *password = "your_password";

WebServer server(80);

const int led = 15;

void handleRoot() {
  digitalWrite(led, 1);
  server.send(200, "text/plain", "hello from Edge101WE!");
  digitalWrite(led, 0);
}

void handleNotFound() {
  digitalWrite(led, 1);
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
  digitalWrite(led, 0);
}

void setup(void) {
  pinMode(led, OUTPUT);
  digitalWrite(led, 0);
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  if (MDNS.begin("edge101we")) {
    Serial.println("MDNS responder started");
  }

  server.on("/", handleRoot);

  server.on("/inline", []() {
    server.send(200, "text/plain", "this works as well");
  });

  server.onNotFound(handleNotFound);

  server.begin();
  Serial.println("HTTP server started");
}

void loop(void) {
  server.handleClient();
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks
}

访问 http://edge101we.local/ 可显示 “hello from Edge101WE!”,同时每次刷新网页可看到用户指示灯闪烁一次。

例程:高级Web Server

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WebServer\examples\AdvancedWebServer)

/*
   Copyright (c) 2015, Majenko Technologies
   All rights reserved.

   Redistribution and use in source and binary forms, with or without modification,
   are permitted provided that the following conditions are met:

 * * Redistributions of source code must retain the above copyright notice, this
     list of conditions and the following disclaimer.

 * * Redistributions in binary form must reproduce the above copyright notice, this
     list of conditions and the following disclaimer in the documentation and/or
     other materials provided with the distribution.

 * * Neither the name of Majenko Technologies nor the names of its
     contributors may be used to endorse or promote products derived from
     this software without specific prior written permission.

   THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
   ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
   DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
   ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
   (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
   LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
   ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
   (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/


#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>


//#define ETH_MODE   //Using Ethernet
#ifdef ETH_MODE
#include <ETH.h>
static bool eth_connected = false;
void ETHEvent(WiFiEvent_t event)
{
  switch (event) {
    case ARDUINO_EVENT_ETH_START:
      //Serial.println("ETH Started");
      //set eth hostname here
      ETH.setHostname("Edge101WE");
      break;
    case ARDUINO_EVENT_ETH_CONNECTED:
      //Serial.println("ETH Connected");
      break;
    case ARDUINO_EVENT_ETH_GOT_IP:
      eth_connected = true;
      break;
    case ARDUINO_EVENT_ETH_DISCONNECTED:
      Serial.println("ETH Disconnected");
      eth_connected = false;
      break;
    case ARDUINO_EVENT_ETH_STOP:
      Serial.println("ETH Stopped");
      eth_connected = false;
      break;
    default:
      break;
  }
}
#else
#include <WiFi.h>
const char *ssid = "your_ssid";
const char *password = "your_password";
#endif
WebServer server(80);

const int led = 15;

void handleRoot() {
  digitalWrite(led, 1);
  char temp[400];
  int sec = millis() / 1000;
  int min = sec / 60;
  int hr = min / 60;

  snprintf(temp, 400,

           "<html>\
  <head>\
    <meta http-equiv='refresh' content='5'/>\
    <title>Edge101WE Demo</title>\
    <style>\
      body { background-color: #cccccc; font-family: Arial, Helvetica, Sans-Serif; Color: #000088; }\
    </style>\
  </head>\
  <body>\
    <h1>Hello from Edge101WE!</h1>\
    <p>Uptime: %02d:%02d:%02d</p>\
    <img src=\"/test.svg\" />\
  </body>\
</html>",

           hr, min % 60, sec % 60
          );
  server.send(200, "text/html", temp);
  digitalWrite(led, 0);
}

void handleNotFound() {
  digitalWrite(led, 1);
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";

  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }

  server.send(404, "text/plain", message);
  digitalWrite(led, 0);
}

void setup(void) {
  pinMode(led, OUTPUT);
  digitalWrite(led, 0);
  Serial.begin(115200);
#ifdef ETH_MODE
  WiFi.onEvent(ETHEvent);
  ETH.begin();
  Serial.print("Please wait for Ethernet connection");
  while(!eth_connected){
    delay(1000);
    Serial.print(".");
  }
  Serial.println("succeed");
  Serial.print("ETH MAC: ");
  Serial.print(ETH.macAddress());
  Serial.print(", IPv4: ");
  Serial.print(ETH.localIP());
  if (ETH.fullDuplex()) {
    Serial.print(", FULL_DUPLEX");
  }
  Serial.print(", ");
  Serial.print(ETH.linkSpeed());
  Serial.println("Mbps");
#else
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  Serial.print("Please wait for WiFi connection");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }

  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
#endif
  if (MDNS.begin("edge101we")) {
    Serial.println("MDNS responder started");
  }

  server.on("/", handleRoot);
  server.on("/test.svg", drawGraph);
  server.on("/inline", []() {
    server.send(200, "text/plain", "this works as well");
  });
  server.onNotFound(handleNotFound);
  server.begin();
  Serial.println("HTTP server started");
}

void loop(void) {
  server.handleClient();
  delay(2);//allow the cpu to switch to other tasks
}

void drawGraph() {
  String out = "";
  char temp[100];
  out += "<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" width=\"400\" height=\"150\">\n";
  out += "<rect width=\"400\" height=\"150\" fill=\"rgb(250, 230, 210)\" stroke-width=\"1\" stroke=\"rgb(0, 0, 0)\" />\n";
  out += "<g stroke=\"black\">\n";
  int y = rand() % 130;
  for (int x = 10; x < 390; x += 10) {
    int y2 = rand() % 130;
    sprintf(temp, "<line x1=\"%d\" y1=\"%d\" x2=\"%d\" y2=\"%d\" stroke-width=\"1\" />\n", x, 140 - y, x + 10, 140 - y2);
    out += temp;
    y = y2;
  }
  out += "</g>\n</svg>\n";

  server.send(200, "image/svg+xml", out);
}

访问串口打印出来的 Web Server IP ,或访问 http://edge101we.local/ 。每10秒钟主动更新一次网页显示

image-20211221112156647

例程:网页控制LED

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com  
*********/

// Load Wi-Fi library
#include <WiFi.h>

// Replace with your network credentials
const char *ssid = "your_ssid";
const char *password = "your_password";

// Set web server port number to 80
WiFiServer server(80);

// Variable to store the HTTP request
String header;

// Auxiliar variables to store the current output state
String output15State = "off";
String output33State = "off";

// Assign output variables to GPIO pins
const int output15 = 15;
const int output33 = 33;

// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0; 
// Define timeout time in milliseconds (example: 2000ms = 2s)
const long timeoutTime = 2000;

void setup() {
  Serial.begin(115200);
  // Initialize the output variables as outputs
  pinMode(output15, OUTPUT);
  pinMode(output33, OUTPUT);
  // Set outputs to LOW
  digitalWrite(output15, LOW);
  digitalWrite(output33, LOW);

  // Connect to Wi-Fi network with SSID and password
  Serial.print("Connecting to ");
  Serial.println(ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  // Print local IP address and start web server
  Serial.println("");
  Serial.println("WiFi connected.");
  Serial.println("IP address: ");
  Serial.println(WiFi.localIP());
  server.begin();
}

void loop(){
  WiFiClient client = server.available();   // Listen for incoming clients

  if (client) {                             // If a new client connects,
    currentTime = millis();
    previousTime = currentTime;
    Serial.println("New Client.");          // print a message out in the serial port
    String currentLine = "";                // make a String to hold incoming data from the client
    while (client.connected() && currentTime - previousTime <= timeoutTime) {  // loop while the client's connected
      currentTime = millis();
      if (client.available()) {             // if there's bytes to read from the client,
        char c = client.read();             // read a byte, then
        Serial.write(c);                    // print it out the serial monitor
        header += c;
        if (c == '\n') {                    // if the byte is a newline character
          // if the current line is blank, you got two newline characters in a row.
          // that's the end of the client HTTP request, so send a response:
          if (currentLine.length() == 0) {
            // HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
            // and a content-type so the client knows what's coming, then a blank line:
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println("Connection: close");
            client.println();
            
            // turns the GPIOs on and off
            if (header.indexOf("GET /15/on") >= 0) {
              Serial.println("GPIO 15 on");
              output15State = "on";
              digitalWrite(output15, HIGH);
            } else if (header.indexOf("GET /15/off") >= 0) {
              Serial.println("GPIO 15 off");
              output15State = "off";
              digitalWrite(output15, LOW);
            } else if (header.indexOf("GET /33/on") >= 0) {
              Serial.println("GPIO 33 on");
              output33State = "on";
              digitalWrite(output33, HIGH);
            } else if (header.indexOf("GET /33/off") >= 0) {
              Serial.println("GPIO 33 off");
              output33State = "off";
              digitalWrite(output33, LOW);
            }
            
            // Display the HTML web page
            client.println("<!DOCTYPE html><html>");
            client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
            client.println("<link rel=\"icon\" href=\"data:,\">");
            // CSS to style the on/off buttons 
            // Feel free to change the background-color and font-size attributes to fit your preferences
            client.println("<style>html { font-family: Helvetica; display: inline-block; margin: 0px auto; text-align: center;}");
            client.println(".button { background-color: #4CAF50; border: none; color: white; padding: 16px 40px;");
            client.println("text-decoration: none; font-size: 30px; margin: 2px; cursor: pointer;}");
            client.println(".button2 {background-color: #555555;}</style></head>");
            
            // Web Page Heading
            client.println("<body><h1>Edge101WE Web Server</h1>");
            
            // Display current state, and ON/OFF buttons for GPIO 15  
            client.println("<p>GPIO 15 - State " + output15State + "</p>");
            // If the output15State is off, it displays the ON button       
            if (output15State=="off") {
              client.println("<p><a href=\"/15/on\"><button class=\"button\">ON</button></a></p>");
            } else {
              client.println("<p><a href=\"/15/off\"><button class=\"button button2\">OFF</button></a></p>");
            } 
               
            // Display current state, and ON/OFF buttons for GPIO 33  
            client.println("<p>GPIO 33 - State " + output33State + "</p>");
            // If the output33State is off, it displays the ON button       
            if (output33State=="off") {
              client.println("<p><a href=\"/33/on\"><button class=\"button\">ON</button></a></p>");
            } else {
              client.println("<p><a href=\"/33/off\"><button class=\"button button2\">OFF</button></a></p>");
            }
            client.println("</body></html>");
            
            // The HTTP response ends with another blank line
            client.println();
            // Break out of the while loop
            break;
          } else { // if you got a newline, then clear currentLine
            currentLine = "";
          }
        } else if (c != '\r') {  // if you got anything else but a carriage return character,
          currentLine += c;      // add it to the end of the currentLine
        }
      }
    }
    // Clear the header variable
    header = "";
    // Close the connection
    client.stop();
    Serial.println("Client disconnected.");
    Serial.println("");
  }
}

访问串口打印出来的 Web Server IP 可点击按钮控制两个GPIO状态,控制板载 LED 灯亮灭。

image-20211221113902834

例程:FSBrowser 文件浏览器

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WebServer\examples\FSBrowser)

/*
  FSWebServer - Example WebServer with FS backend for esp8266/esp32
  Copyright (c) 2015 Hristo Gochkov. All rights reserved.
  This file is part of the WebServer library for Arduino environment.

  This library is free software; you can redistribute it and/or
  modify it under the terms of the GNU Lesser General Public
  License as published by the Free Software Foundation; either
  version 2.1 of the License, or (at your option) any later version.
  This library is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  Lesser General Public License for more details.
  You should have received a copy of the GNU Lesser General Public
  License along with this library; if not, write to the Free Software
  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA

  upload the contents of the data folder with MkSPIFFS Tool ("ESP32 Sketch Data Upload" in Tools menu in Arduino IDE)
  or you can upload the contents of a folder if you CD in that folder and run the following command:
  for file in `ls -A1`; do curl -F "file=@$PWD/$file" esp32fs.local/edit; done

  access the sample web page at http://esp32fs.local
  edit the page by going to http://esp32fs.local/edit
*/
#include <WiFi.h>
#include <WiFiClient.h>
#include <WebServer.h>
#include <ESPmDNS.h>

#define FILESYSTEM SPIFFS
// You only need to format the filesystem once
#define FORMAT_FILESYSTEM false
#define DBG_OUTPUT_PORT Serial
#define A0 34
#if FILESYSTEM == FFat
#include <FFat.h>
#endif
#if FILESYSTEM == SPIFFS
#include <SPIFFS.h>
#endif

const char* ssid = "wifi-ssid";
const char* password = "wifi-password";
const char* host = "esp32fs";
WebServer server(80);
//holds the current upload
File fsUploadFile;

//format bytes
String formatBytes(size_t bytes) {
  if (bytes < 1024) {
    return String(bytes) + "B";
  } else if (bytes < (1024 * 1024)) {
    return String(bytes / 1024.0) + "KB";
  } else if (bytes < (1024 * 1024 * 1024)) {
    return String(bytes / 1024.0 / 1024.0) + "MB";
  } else {
    return String(bytes / 1024.0 / 1024.0 / 1024.0) + "GB";
  }
}

String getContentType(String filename) {
  if (server.hasArg("download")) {
    return "application/octet-stream";
  } else if (filename.endsWith(".htm")) {
    return "text/html";
  } else if (filename.endsWith(".html")) {
    return "text/html";
  } else if (filename.endsWith(".css")) {
    return "text/css";
  } else if (filename.endsWith(".js")) {
    return "application/javascript";
  } else if (filename.endsWith(".png")) {
    return "image/png";
  } else if (filename.endsWith(".gif")) {
    return "image/gif";
  } else if (filename.endsWith(".jpg")) {
    return "image/jpeg";
  } else if (filename.endsWith(".ico")) {
    return "image/x-icon";
  } else if (filename.endsWith(".xml")) {
    return "text/xml";
  } else if (filename.endsWith(".pdf")) {
    return "application/x-pdf";
  } else if (filename.endsWith(".zip")) {
    return "application/x-zip";
  } else if (filename.endsWith(".gz")) {
    return "application/x-gzip";
  }
  return "text/plain";
}

bool exists(String path){
  bool yes = false;
  File file = FILESYSTEM.open(path, "r");
  if(!file.isDirectory()){
    yes = true;
  }
  file.close();
  return yes;
}

bool handleFileRead(String path) {
  DBG_OUTPUT_PORT.println("handleFileRead: " + path);
  if (path.endsWith("/")) {
    path += "index.htm";
  }
  String contentType = getContentType(path);
  String pathWithGz = path + ".gz";
  if (exists(pathWithGz) || exists(path)) {
    if (exists(pathWithGz)) {
      path += ".gz";
    }
    File file = FILESYSTEM.open(path, "r");
    server.streamFile(file, contentType);
    file.close();
    return true;
  }
  return false;
}

void handleFileUpload() {
  if (server.uri() != "/edit") {
    return;
  }
  HTTPUpload& upload = server.upload();
  if (upload.status == UPLOAD_FILE_START) {
    String filename = upload.filename;
    if (!filename.startsWith("/")) {
      filename = "/" + filename;
    }
    DBG_OUTPUT_PORT.print("handleFileUpload Name: "); DBG_OUTPUT_PORT.println(filename);
    fsUploadFile = FILESYSTEM.open(filename, "w");
    filename = String();
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    //DBG_OUTPUT_PORT.print("handleFileUpload Data: "); DBG_OUTPUT_PORT.println(upload.currentSize);
    if (fsUploadFile) {
      fsUploadFile.write(upload.buf, upload.currentSize);
    }
  } else if (upload.status == UPLOAD_FILE_END) {
    if (fsUploadFile) {
      fsUploadFile.close();
    }
    DBG_OUTPUT_PORT.print("handleFileUpload Size: "); DBG_OUTPUT_PORT.println(upload.totalSize);
  }
}

void handleFileDelete() {
  if (server.args() == 0) {
    return server.send(500, "text/plain", "BAD ARGS");
  }
  String path = server.arg(0);
  DBG_OUTPUT_PORT.println("handleFileDelete: " + path);
  if (path == "/") {
    return server.send(500, "text/plain", "BAD PATH");
  }
  if (!exists(path)) {
    return server.send(404, "text/plain", "FileNotFound");
  }
  FILESYSTEM.remove(path);
  server.send(200, "text/plain", "");
  path = String();
}

void handleFileCreate() {
  if (server.args() == 0) {
    return server.send(500, "text/plain", "BAD ARGS");
  }
  String path = server.arg(0);
  DBG_OUTPUT_PORT.println("handleFileCreate: " + path);
  if (path == "/") {
    return server.send(500, "text/plain", "BAD PATH");
  }
  if (exists(path)) {
    return server.send(500, "text/plain", "FILE EXISTS");
  }
  File file = FILESYSTEM.open(path, "w");
  if (file) {
    file.close();
  } else {
    return server.send(500, "text/plain", "CREATE FAILED");
  }
  server.send(200, "text/plain", "");
  path = String();
}

void handleFileList() {
  if (!server.hasArg("dir")) {
    server.send(500, "text/plain", "BAD ARGS");
    return;
  }

  String path = server.arg("dir");
  DBG_OUTPUT_PORT.println("handleFileList: " + path);


  File root = FILESYSTEM.open(path);
  path = String();

  String output = "[";
  if(root.isDirectory()){
      File file = root.openNextFile();
      while(file){
          if (output != "[") {
            output += ',';
          }
          output += "{\"type\":\"";
          output += (file.isDirectory()) ? "dir" : "file";
          output += "\",\"name\":\"";
          output += String(file.path()).substring(1);
          output += "\"}";
          file = root.openNextFile();
      }
  }
  output += "]";
  server.send(200, "text/json", output);
}

void setup(void) {
  DBG_OUTPUT_PORT.begin(115200);
  DBG_OUTPUT_PORT.print("\n");
  DBG_OUTPUT_PORT.setDebugOutput(true);
  if (FORMAT_FILESYSTEM) FILESYSTEM.format();
  FILESYSTEM.begin();
  {
      File root = FILESYSTEM.open("/");
      File file = root.openNextFile();
      while(file){
          String fileName = file.name();
          size_t fileSize = file.size();
          DBG_OUTPUT_PORT.printf("FS File: %s, size: %s\n", fileName.c_str(), formatBytes(fileSize).c_str());
          file = root.openNextFile();
      }
      DBG_OUTPUT_PORT.printf("\n");
  }


  //WIFI INIT
  DBG_OUTPUT_PORT.printf("Connecting to %s\n", ssid);
  if (String(WiFi.SSID()) != String(ssid)) {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
  }

  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    DBG_OUTPUT_PORT.print(".");
  }
  DBG_OUTPUT_PORT.println("");
  DBG_OUTPUT_PORT.print("Connected! IP address: ");
  DBG_OUTPUT_PORT.println(WiFi.localIP());

  MDNS.begin(host);
  DBG_OUTPUT_PORT.print("Open http://");
  DBG_OUTPUT_PORT.print(host);
  DBG_OUTPUT_PORT.println(".local/edit to see the file browser");


  //SERVER INIT
  //list directory
  server.on("/list", HTTP_GET, handleFileList);
  //load editor
  server.on("/edit", HTTP_GET, []() {
    if (!handleFileRead("/edit.htm")) {
      server.send(404, "text/plain", "FileNotFound");
    }
  });
  //create file
  server.on("/edit", HTTP_PUT, handleFileCreate);
  //delete file
  server.on("/edit", HTTP_DELETE, handleFileDelete);
  //first callback is called after the request has ended with all parsed arguments
  //second callback handles file uploads at that location
  server.on("/edit", HTTP_POST, []() {
    server.send(200, "text/plain", "");
  }, handleFileUpload);

  //called when the url is not defined here
  //use it to load content from FILESYSTEM
  server.onNotFound([]() {
    if (!handleFileRead(server.uri())) {
      server.send(404, "text/plain", "FileNotFound");
    }
  });

  //get heap status, analog input value and all GPIO statuses in one json call
  server.on("/all", HTTP_GET, []() {
    String json = "{";
    json += "\"heap\":" + String(ESP.getFreeHeap());
    json += ", \"analog\":" + String(analogRead(A0));
    json += ", \"gpio\":" + String((uint32_t)(0));
    json += "}";
    server.send(200, "text/json", json);
    json = String();
  });
  server.begin();
  DBG_OUTPUT_PORT.println("HTTP server started");

}

void loop(void) {
  server.handleClient();
  delay(2);//allow the cpu to switch to other tasks
}
安装 ESP32 Sketch Data Upload

要使用FSBrowser,首先需要在Arduino IDE上安装一个文件上传的小工具。

  • 确保使用受支持的Arduino IDE版本之一,并已安装ESP32内核。

  • 最新版本下载esp32fs.zip压缩工具

    image-20210513173004079

  • 在您的Arduino sketchbook目录中,创建一个tools目录(如果尚不存在)。

  • 将工具解压缩到“ Arduino安装目录-> Arduino” / tools目录中。例如:<home_dir>/Arduino/tools/ESP32FS/tool/esp32fs.jar

    image-20210513180126454

    或在OSX上/Applications/Arduino.app/Contents/Java/tools/ESP32FS

  • 确保在esp32核心安装文件夹中有mklittlefs [.exe]mkfatfs [.exe]。在**\ AppData \ Local \ Arduino15 …内部**或在zip IDE安装上查看,请参阅“Setup->sketchbook location”hardware\espressif\esp32\tools

  • 作为参考,请参阅以前的发行版,以获取有关已归档二进制文件的副本。

  • 您还可以使用提供的package_esp32_index.template.json来运行get.py并下载缺少的二进制文件

  • 重新启动Arduino IDE。

使用方法
  • 确保已选择board,port,分区方案并关闭了串行监视器。打开FSBrowser.ino,Board信息栏,Flash Size : 4MB,Partition Scheme : Default 4MB with spiffs(1.2MB APP/1.5MB SPIFFS)

    image-20210420192549225

  • 将 FSBrowser.ino 代码下载到 Edge101WE 主板。

  • 选择Tools > ESP32 Sketch Data Upload菜单项。

    image-20210420192705241

  • 在下拉列表中,从/ data文件夹中选择要创建的SPIFFS,LittleFS或FatFS。这里选择SPIFFS。

image-20210513181239051

  • 单击 “确定” 开始将 这个FSBrowser Sketch 目录下 data 文件夹中的文件上传到ESP32 Flash文件系统中。

  • 最后还有一个 !Erase Flash!选项允许在必要时清除整个flash,请谨慎使用。

    完成后,IDE状态栏将显示“图像已上传”消息的状态。对于大型文件系统,可能需要几分钟的时间。

上传项目中的数据。当上传完毕后回显示SPIFFS Image Uploaded。

image-20210420192822507

实际上,上传是在项目下面的一个 data 文件夹的内容。里面包含了一些htm文件、图片和js脚本。

image-20210420192951410

上传完毕后访问 http://esp32fs.local/edit 即可浏览到上传的文件。

image-20210420193035802

还可以通过 选择文件 按钮上传其他的文件,点击 Upload上传。这样我们在编写web服务器程序时就可以通过 esp32FS 工具和 FSBrowser 来制作和上传网页所需要的文件。

例程:HttpAdvancedAuth 高级用户认证

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WebServer\examples\HttpAdvancedAuth)

/*
  HTTP Advanced Authentication example
  Created Mar 16, 2017 by Ahmed El-Sharnoby.
  This example code is in the public domain.
*/

#include <WiFi.h>
#include <ESPmDNS.h>
#include <ArduinoOTA.h>
#include <WebServer.h>

const char *ssid = "your_ssid";
const char *password = "your_password";

WebServer server(80);

const char* www_username = "admin";
const char* www_password = "esp32";
// allows you to set the realm of authentication Default:"Login Required"
const char* www_realm = "Custom Auth Realm";
// the Content of the HTML response in case of Unautherized Access Default:empty
String authFailResponse = "Authentication Failed";

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  if (WiFi.waitForConnectResult() != WL_CONNECTED) {
    Serial.println("WiFi Connect Failed! Rebooting...");
    delay(1000);
    ESP.restart();
  }
  ArduinoOTA.begin();

  server.on("/", []() {
    if (!server.authenticate(www_username, www_password))
      //Basic Auth Method with Custom realm and Failure Response
      //return server.requestAuthentication(BASIC_AUTH, www_realm, authFailResponse);
      //Digest Auth Method with realm="Login Required" and empty Failure Response
      //return server.requestAuthentication(DIGEST_AUTH);
      //Digest Auth Method with Custom realm and empty Failure Response
      //return server.requestAuthentication(DIGEST_AUTH, www_realm);
      //Digest Auth Method with Custom realm and Failure Response
    {
      return server.requestAuthentication(DIGEST_AUTH, www_realm, authFailResponse);
    }
    server.send(200, "text/plain", "Login OK");
  });
  server.begin();

  Serial.print("Open http://");
  Serial.print(WiFi.localIP());
  Serial.println("/ in your browser to see it working");
}

void loop() {
  ArduinoOTA.handle();
  server.handleClient();
  delay(2);//allow the cpu to switch to other tasks
}

用户认证有以下两种方式:

Basic 认证

Basic 认证是一种较为简单的HTTP认证方式,客户端通过明文(Base64 编码格式)传输用户名和密码到服务端进行认证,通常需要配合HTTPS来保证信息传输的安全。输入正确的用户名和密码,认证成功后,浏览器会将凭据信息缓存起来,那么以后再进入时,无需重复手动输入用户名和密码。

Basic 认证是通过将“用户名:密码”格式的字符串经过的Base64编码得到的。而 Base64 不属于加密范畴,可以被逆向解码,等同于明文,因此Basic传输认证信息是不安全的。

Digest 认证(摘要认证)

Digest 认证是为了修复基本认证协议的严重缺陷而设计的,秉承 “绝不通过明文在网络发送密码” 的原则,通过 “密码摘要” 进行认证,大大提高了安全性。Digest 认证同样使用质询 / 响应的方式(challenge/response),但不会像 Basic 认证那样直接发送明文密码。所谓质询响应方式是指,一开始一方会先发送认证要求给另一方,接着使用从另一方那接收到的质询码计算生成响应码。最后将响应码返回给对方进行认证的方式。

相对于Basic 认证,Digest 认证主要有如下改进:

  • 绝不通过明文在网络上发送密码

  • 可以有效防止恶意用户进行重放攻击

  • 可以有选择的防止对报文内容的篡改

需要注意的是,摘要认证除了能够保护密码之外,并不能保护其他内容,与HTTPS配合使用仍是一个良好的选择。

Digest 认证涉及到的参数的含义:

Digest 认证的参数 含义
WWW-Authentication 用来定义使用何种方式(Basic、Digest、Bearer等)去进行认证以获取受保护的资源
realm 表示Web服务器中受保护文档的安全域(比如公司财务信息域和公司员工信息域),用来指示需要哪个域的用户名和密码
qop 保护质量,包含auth(默认的)和auth-int(增加了报文完整性检测)两种策略,(可以为空,但是)不推荐为空值
nonce 服务端向客户端发送质询时附带的一个随机数,这个数会经常发生变化。客户端计算密码摘要时将其附加上去,使得多次生成同一用户的密码摘要各不相同,用来防止重放攻击
nc nonce计数器,是一个16进制的数值,表示同一nonce下客户端发送出请求的数量。例如,在响应的第一个请求中,客户端将发送“nc=00000001”。这个指示值的目的是让服务器保持这个计数器的一个副本,以便检测重复的请求
cnonce 客户端随机数,这是一个不透明的字符串值,由客户端提供,并且客户端和服务器都会使用,以避免用明文文本。这使得双方都可以查验对方的身份,并对消息的完整性提供一些保护
response 这是由用户代理软件计算出的一个字符串,以证明用户知道口令
Authorization-Info 用于返回一些与授权会话相关的附加信息
nextnonce 下一个服务端随机数,使客户端可以预先发送正确的摘要
rspauth 响应摘要,用于客户端对服务端进行认证
stale 当密码摘要使用的随机数过期时,服务器可以返回一个附带有新随机数的401响应,并指定stale=true,表示服务器在告知客户端用新的随机数来重试,而不再要求用户重新输入用户名和密码了

加入认证机制后,浏览器访问串口打印出来的 IP 地址,提示需要输入用户名和密码,用户名输入 admin ,密码输入 esp32。

登录后 页面显示 Login OK 。

例程:SimpleAuthentification 鉴权

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->WebServer\examples\SimpleAuthentification)

#include <WiFiClient.h>
#include <WebServer.h>

//#define ETH_MODE   //Using Ethernet
#ifdef ETH_MODE
#include <ETH.h>
static bool eth_connected = false;
void ETHEvent(WiFiEvent_t event)
{
  switch (event) {
    case ARDUINO_EVENT_ETH_START:
      //Serial.println("ETH Started");
      //set eth hostname here
      ETH.setHostname("esp32-ethernet");
      break;
    case ARDUINO_EVENT_ETH_CONNECTED:
      //Serial.println("ETH Connected");
      break;
    case ARDUINO_EVENT_ETH_GOT_IP:
      eth_connected = true;
      break;
    case ARDUINO_EVENT_ETH_DISCONNECTED:
      Serial.println("ETH Disconnected");
      eth_connected = false;
      break;
    case ARDUINO_EVENT_ETH_STOP:
      Serial.println("ETH Stopped");
      eth_connected = false;
      break;
    default:
      break;
  }
}
#else
#include <WiFi.h>
const char *ssid = "your_ssid";
const char *password = "your_password";
#endif
WebServer server(80);

//Check if header is present and correct
bool is_authentified() {
  Serial.println("Enter is_authentified");
  if (server.hasHeader("Cookie")) {
    Serial.print("Found cookie: ");
    String cookie = server.header("Cookie");
    Serial.println(cookie);
    if (cookie.indexOf("ESPSESSIONID=1") != -1) {
      Serial.println("Authentification Successful");
      return true;
    }
  }
  Serial.println("Authentification Failed");
  return false;
}

//login page, also called for disconnect
void handleLogin() {
  String msg;
  if (server.hasHeader("Cookie")) {
    Serial.print("Found cookie: ");
    String cookie = server.header("Cookie");
    Serial.println(cookie);
  }
  if (server.hasArg("DISCONNECT")) {
    Serial.println("Disconnection");
    server.sendHeader("Location", "/login");
    server.sendHeader("Cache-Control", "no-cache");
    server.sendHeader("Set-Cookie", "ESPSESSIONID=0");
    server.send(301);
    return;
  }
  if (server.hasArg("USERNAME") && server.hasArg("PASSWORD")) {
    if (server.arg("USERNAME") == "admin" &&  server.arg("PASSWORD") == "admin") {
      server.sendHeader("Location", "/");
      server.sendHeader("Cache-Control", "no-cache");
      server.sendHeader("Set-Cookie", "ESPSESSIONID=1");
      server.send(301);
      Serial.println("Log in Successful");
      return;
    }
    msg = "Wrong username/password! try again.";
    Serial.println("Log in Failed");
  }
  String content = "<html><body><form action='/login' method='POST'>To log in, please use : admin/admin<br>";
  content += "User:<input type='text' name='USERNAME' placeholder='user name'><br>";
  content += "Password:<input type='password' name='PASSWORD' placeholder='password'><br>";
  content += "<input type='submit' name='SUBMIT' value='Submit'></form>" + msg + "<br>";
  content += "You also can go <a href='/inline'>here</a></body></html>";
  server.send(200, "text/html", content);
}

//root page can be accessed only if authentification is ok
void handleRoot() {
  Serial.println("Enter handleRoot");
  String header;
  if (!is_authentified()) {
    server.sendHeader("Location", "/login");
    server.sendHeader("Cache-Control", "no-cache");
    server.send(301);
    return;
  }
  String content = "<html><body><H2>hello, you successfully connected to esp32!</H2><br>";
  if (server.hasHeader("User-Agent")) {
    content += "the user agent used is : " + server.header("User-Agent") + "<br><br>";
  }
  content += "You can access this page until you <a href=\"/login?DISCONNECT=YES\">disconnect</a></body></html>";
  server.send(200, "text/html", content);
}

//no need authentification
void handleNotFound() {
  String message = "File Not Found\n\n";
  message += "URI: ";
  message += server.uri();
  message += "\nMethod: ";
  message += (server.method() == HTTP_GET) ? "GET" : "POST";
  message += "\nArguments: ";
  message += server.args();
  message += "\n";
  for (uint8_t i = 0; i < server.args(); i++) {
    message += " " + server.argName(i) + ": " + server.arg(i) + "\n";
  }
  server.send(404, "text/plain", message);
}

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

#ifdef ETH_MODE
  WiFi.onEvent(ETHEvent);
  ETH.begin();
  Serial.print("Please wait for Ethernet connection");
  while(!eth_connected){
    delay(1000);
    Serial.print(".");
  }
  Serial.println("succeed");
  Serial.print("ETH MAC: ");
  Serial.print(ETH.macAddress());
  Serial.print(", IPv4: ");
  Serial.print(ETH.localIP());
  if (ETH.fullDuplex()) {
    Serial.print(", FULL_DUPLEX");
  }
  Serial.print(", ");
  Serial.print(ETH.linkSpeed());
  Serial.println("Mbps");
#else
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  Serial.print("Please wait for WiFi connection");
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());
#endif
  server.on("/", handleRoot);
  server.on("/login", handleLogin);
  server.on("/inline", []() {
    server.send(200, "text/plain", "this works without need of authentification");
  });

  server.onNotFound(handleNotFound);
  //here the list of headers to be recorded
  const char * headerkeys[] = {"User-Agent", "Cookie"} ;
  size_t headerkeyssize = sizeof(headerkeys) / sizeof(char*);
  //ask server to track these headers
  server.collectHeaders(headerkeys, headerkeyssize);
  server.begin();
  Serial.println("HTTP server started");
}

void loop(void) {
  server.handleClient();
  delay(2);//allow the cpu to switch to other tasks
}

访问 串口打印出来的IP地址,显示登录界面,只有输入正确的密码才能跳转到 主页。

image-20210421151202898

9.4.3 异步 Web Server

使用异步网络的优点:

  • 使用异步网络意味着您可以同时处理多个连接。

  • 一旦请求准备就绪并解析完毕,您就会被叫回。

  • 当您发送响应时,您将立即准备处理其他连接,而服务器将在后台处理响应,当您发送响应时,您立即就可以处理其他连接,而服务器则负责在后台发送响应。

  • 速度快。

  • 易于使用的API,HTTP基本和摘要MD5身份验证(默认),ChunkedResponse易于使用的API,HTTP基本和摘要MD5身份验证(可选),ChunkedResponse。

  • 易于扩展以处理任何类型的内容很容易扩展以处理任何类型的内容。

  • 支持继续100支持继续100。

  • 异步WebSocket插件无需其他服务器或端口即可提供不同的位置。异步WebSocket插件提供不同的位置,没有额外的服务器或端口。

  • Async EventSource(服务器发送事件)插件可将事件发送到浏览器Asynceventsource(服务器发送事件)插件,用于向浏览器发送事件

  • URL重写插件的条件和永久的URL重写重写URL插件的条件和永久的URL重写

  • ServeStatic插件,支持缓存,Last-Modified,默认索引以及更多ServeStatic插件,支持缓存,最后修改,替代索引和更多

  • 用于处理模板的简单模板处理引擎

需要注意的事情:

  • 这是完全异步的服务器,因此不会在循环线程上运行。

  • 您不能在回调中使用yield或delay或使用它们的任何函数。

  • 服务器足够智能,可以知道何时关闭连接并释放资源。

  • 您不能对一个请求发送多个回复。

详细介绍可查看下面的github介绍。

异步 Web Server需要安装以下两个库:

要下载这两个库,只需单击GitHub页面顶部的“ Clone or download ”按钮。

image-20210421185340915

然后,选择“ Download ZIP ”选项,文件应被下载到您的计算机上。只需打开**.zip**文件并将文件夹解压缩到Arduino库文件夹即可。

通常,用于Arduino安装的库文件夹位于 C:\ Users \ UserName \ Documents \ Arduino \ libraries文件夹中。

请注意,提取的文件夹名称末尾带有**-master**。只需删除此附加的**-master**并保留其余名称即可。

重新打开Arduino IDE ,这些库应该可以在Arduino环境中使用。

例程:simple_server

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->ESPAsyncWebServer\examples\simple_server)

//
// A simple server implementation showing how to:
//  * serve static messages
//  * read GET and POST parameters
//  * handle missing pages / 404s
//

#include <Arduino.h>
#ifdef ESP32
#include <WiFi.h>
#include <AsyncTCP.h>
#elif defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESPAsyncTCP.h>
#endif
#include <ESPAsyncWebServer.h>

AsyncWebServer server(80);

const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";

const char* PARAM_MESSAGE = "message";

void notFound(AsyncWebServerRequest *request) {
    request->send(404, "text/plain", "Not found");
}

void setup() {

    Serial.begin(115200);
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, password);
    if (WiFi.waitForConnectResult() != WL_CONNECTED) {
        Serial.printf("WiFi Failed!\n");
        return;
    }

    Serial.print("IP Address: ");
    Serial.println(WiFi.localIP());

    server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
        request->send(200, "text/plain", "Hello, world");
    });

    // Send a GET request to <IP>/get?message=<message>
    server.on("/get", HTTP_GET, [] (AsyncWebServerRequest *request) {
        String message;
        if (request->hasParam(PARAM_MESSAGE)) {
            message = request->getParam(PARAM_MESSAGE)->value();
        } else {
            message = "No message sent";
        }
        request->send(200, "text/plain", "Hello, GET: " + message);
    });

    // Send a POST request to <IP>/post with a form field message set to <message>
    server.on("/post", HTTP_POST, [](AsyncWebServerRequest *request){
        String message;
        if (request->hasParam(PARAM_MESSAGE, true)) {
            message = request->getParam(PARAM_MESSAGE, true)->value();
        } else {
            message = "No message sent";
        }
        request->send(200, "text/plain", "Hello, POST: " + message);
    });

    server.onNotFound(notFound);

    server.begin();
}

void loop() {
}

程序将从串口打印出Web Server的IP。使用网页浏览器访问 这个IP。可显示网络主页的信息。也可访问 /get 或 /post 。

例程:通过滑条来控制LED亮度

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

/*********
  Rui Santos
  Complete project details at https://RandomNerdTutorials.com/esp32-web-server-slider-pwm/
  
  Permission is hereby granted, free of charge, to any person obtaining a copy
  of this software and associated documentation files.
  
  The above copyright notice and this permission notice shall be included in all
  copies or substantial portions of the Software.
*********/

// Import required libraries
#include <WiFi.h>
#include <AsyncTCP.h>
#include <ESPAsyncWebServer.h>

// Replace with your network credentials
const char *ssid = "your_ssid";
const char *password = "your_password";

const int output = 15;

String sliderValue = "0";

// setting PWM properties
const int freq = 5000;
const int ledChannel = 0;
const int resolution = 8;

const char* PARAM_INPUT = "value";

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

const char index_html[] PROGMEM = R"rawliteral(
<!DOCTYPE HTML><html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Edge101WE Web Server</title>
  <style>
    html {font-family: Arial; display: inline-block; text-align: center;}
    h2 {font-size: 2.3rem;}
    p {font-size: 1.9rem;}
    body {max-width: 400px; margin:0px auto; padding-bottom: 25px;}
    .slider { -webkit-appearance: none; margin: 14px; width: 360px; height: 25px; background: #FFD65C;
      outline: none; -webkit-transition: .2s; transition: opacity .2s;}
    .slider::-webkit-slider-thumb {-webkit-appearance: none; appearance: none; width: 35px; height: 35px; background: #003249; cursor: pointer;}
    .slider::-moz-range-thumb { width: 35px; height: 35px; background: #003249; cursor: pointer; } 
  </style>
</head>
<body>
  <h2>Edge101WE Web Server</h2>
  <p><span id="textSliderValue">%SLIDERVALUE%</span></p>
  <p><input type="range" onchange="updateSliderPWM(this)" id="pwmSlider" min="0" max="255" value="%SLIDERVALUE%" step="1" class="slider"></p>
<script>
function updateSliderPWM(element) {
  var sliderValue = document.getElementById("pwmSlider").value;
  document.getElementById("textSliderValue").innerHTML = sliderValue;
  console.log(sliderValue);
  var xhr = new XMLHttpRequest();
  xhr.open("GET", "/slider?value="+sliderValue, true);
  xhr.send();
}
</script>
</body>
</html>
)rawliteral";

// Replaces placeholder with button section in your web page
String processor(const String& var){
  //Serial.println(var);
  if (var == "SLIDERVALUE"){
    return sliderValue;
  }
  return String();
}

void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);
  
  // configure LED PWM functionalitites
  ledcSetup(ledChannel, freq, resolution);
  
  // attach the channel to the GPIO to be controlled
  ledcAttachPin(output, ledChannel);
  
  ledcWrite(ledChannel, sliderValue.toInt());

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  // Print ESP Local IP Address
  Serial.println(WiFi.localIP());

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send_P(200, "text/html", index_html, processor);
  });

  // Send a GET request to <ESP_IP>/slider?value=<inputMessage>
  server.on("/slider", HTTP_GET, [] (AsyncWebServerRequest *request) {
    String inputMessage;
    // GET input1 value on <ESP_IP>/slider?value=<inputMessage>
    if (request->hasParam(PARAM_INPUT)) {
      inputMessage = request->getParam(PARAM_INPUT)->value();
      sliderValue = inputMessage;
      ledcWrite(ledChannel, sliderValue.toInt());
    }
    else {
      inputMessage = "No message sent";
    }
    Serial.println(inputMessage);
    request->send(200, "text/plain", "OK");
  });
  
  // Start server
  server.begin();
}
  
void loop() {
  
}

访问 串口打印出来的 Web Server IP 可拖动滚动条控制LED灯亮度。

image-20211221141642818

其他异步Web Server例程

例程: 网络串口 Web Serial

此例程可生成基于 Web 的串行监视器,可用于远程的调试。

例程:客户端显示自动更新

本教程展示了如何在使用 Arduino IDE 编程的 ESP32 Web 服务器中使用服务器发送事件 (SSE)。SSE 允许浏览器通过 HTTP 连接从服务器接收自动更新。例如,这对于将更新的传感器读数发送到浏览器很有用。每当有新的读数可用时,ESP32 就会将其发送给客户端,网页可以自动更新,而无需发出额外请求。

例程:ESP32 Web Server using SPIFFS

在本教程中,展示如何构建一个 Web 服务器,为存储在 ESP32 文件系统上的 HTML 和 CSS 文件提供服务。不必将 HTML 和 CSS 文本写入 Arduino 草图,我们将创建分离的 HTML 和 CSS 文件。

9.5 DNSServer

9.5.1 DNSServer

前面讲 WebServer 的时候都是通过 IP 地址去访问的,如果想像一般上网那样输入域名(www.baidu.com、www.taobao.com等)访问的话就需要用到DNSServer了。本文对 Arduino core for the ESP32中DNSServer 使用进行简单介绍。

使用 DNSServer 必须使设备处于AP 模式下,在非 AP 模式下想实现同样功能的话请参考 mDNS。 mDNS 可以在非 A P模式下使用但也有局限,局域网中其它设备也必须开启 mDNS 服务互相间才能通过域名访问。

DNSServer使用步骤如下:

  1. 引入相应库 #include <DNSServer.h>;

  2. 声明 DNSServer 对象;

  3. 使用 start() 方法启动DNS服务器;

  4. 使用 processNextRequest() 方法处理来自客户端的请求;

API 参考

start() - 启动DNSServer
bool start(const uint16_t &port, const String &domainName, const IPAddress &resolvedIP);

启动DNSServer,分别需要填入端口号、域名、IP,域名可以填写 * 表示所有域名都会被跳转至这里。

语法

const byte DNS_PORT = 53; // 默认设置端口为53 
IPAddress apIP(192, 168, 1, 1);

dnsServer.start(DNS_PORT, "www.example.com", apIP);

参数

传入值 说明 值范围
port DNS服务端口号。默认情况下设置为53。
domainName 映射的域名,也就是开启服务后可以直接访问的用于代替IP地址的域名。
resolvedIP 映射的IP地址

返回

返回值 说明 值范围
bool true:启动DNS服务成功
false:启动DNS服务失败
stop() - 停止DNSServer
void stop()

语法

dnsServer.stop();

参数

返回

返回值 说明 值范围
bool true:关闭DNS服务成功
false:关闭DNS服务失败
processNextRequest() - 处理来自客户端的请求
void processNextRequest()

语法

void loop() {
  dnsServer.processNextRequest(); // 处理DNS请求服务
  webServer.handleClient();
}

参数

返回

setErrorReplyCode() - 设置错误响应码
void setErrorReplyCode(const DNSReplyCode &replyCode);

当客户端发送的查询域名不在主板建立的DNS服务器检索中,则返回setErrorReplyCode函数设置的错误代码。

语法

dnsServer.setErrorReplyCode(DNSReplyCode::ServerFailure);

参数

传入值 说明 值范围
DNSReplyCode DNS响应错误码 NoError = 0, //DNS查询成功完成
FormError = 1, //DNS查询格式错误
ServerFailure = 2, //服务器无法完成DNS请求
NonExistentDomain = 3, //域名不存在
NotImplemented = 4, //未定义
Refused = 5, //服务器拒绝回答查询
YXDomain = 6, //名称不应该存在,但该名称确实存在
YXRRSet = 7, //资源记录集不存在
NXRRSet = 8 //服务器对该区域无权

返回

setTTL() - 设置TTL
void setTTL(const uint32_t &ttl);

通过增大TTL值,可以减少DNS递归查询过程,达到提升域名解析速度的效果。反之,通过缩小 TTL 值,以减少更换空间IP地址时造成的不可访问时间,减小空间IP地址造成的访问空窗期。根据不同应用场景和网络需求可以选择合适的TTL值达成更好的访问体验。

语法

dnsServer.setTTL(300);

参数

传入值 说明 值范围
ttl 设置TTL数值(单位:秒)

返回

例程:CaptivePortal

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->DNSServer\examples\CaptivePortal)

#include <WiFi.h>
#include <DNSServer.h>

const byte DNS_PORT = 53;
IPAddress apIP(192,168,4,1); // The default android DNS
DNSServer dnsServer;
WiFiServer server(80);

String responseHTML = ""
  "<!DOCTYPE html><html><head><title>CaptivePortal</title></head><body>"
  "<h1>Hello World!</h1><p>This is a captive portal example. All requests will "
  "be redirected here.</p></body></html>";

void setup() { 
  WiFi.disconnect();
  WiFi.mode(WIFI_AP);
  WiFi.softAP("Edge101WE_DNSServer");
  WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0));

  // if DNSServer is started with "*" for domain name, it will reply with
  // provided IP to all DNS request
  dnsServer.start(DNS_PORT, "edge.com", apIP);

  server.begin();
}

void loop() {
  dnsServer.processNextRequest();
  WiFiClient client = server.available();   // listen for incoming clients

  if (client) {
    String currentLine = "";
    while (client.connected()) {
      if (client.available()) {
        char c = client.read();
        if (c == '\n') {
          if (currentLine.length() == 0) {
            client.println("HTTP/1.1 200 OK");
            client.println("Content-type:text/html");
            client.println();
            client.print(responseHTML);
            break;
          } else {
            currentLine = "";
          }
        } else if (c != '\r') {
          currentLine += c;
        }
      }
    }
    client.stop();
  }
}

例程首先建立一个名字为 “Edge101WE_DNSServer” 的 WiFi AP ,然后启动DNSServer,设置一个域名 “edge.com” 。当电脑连接到这个 AP后,输入域名即可访问。

image-20211221144250481

9.5.2 mDNS 通过STA方式

mDNS 即多播DNS(Multicast DNS),mDNS主要实现了在没有传统 DNS 服务器的情况下使局域网内的主机实现相互发现和通信,使用的端口为 5353,遵从DNS协议,使用现有的 DNS 信息结构、名语法和资源记录类型。并且没有指定新的操作代码或响应代码。在局域网中,设备和设备之前相互通信需要知道对方的ip地址的,大多数情况,设备的 IP 不是静态IP地址,而是通过 DHCP 协议动态分配的 IP 地址,如何设备发现呢,就是要 mDNS 大显身手,例如:现在物联网设备和APP之间的通信,要么APP通过广播,要么通过组播,发一些特定信息,感兴趣设备应答,实现局域网设备的发现,当然 mDNS 比这强大。

mDNS 的工作原理 首先,在 IP 协议里规定了一些保留地址,其中有一个是 224.0.0.251,对应的 IPv6 地址是 [FF02::FB]。

mDNS 协议规定了一个端口,5353。mDNS 基于 UDP 协议。

每个进入局域网的主机,如果开启了mDNS服务的话,都会向局域网内的所有主机组播一个消息,我是谁,和我的IP地址是多少。然后其他也有该服务的主机就会响应,也会告诉你,它是谁,它的IP地址是多少。

比如,A主机进入局域网,开启了 mDNS 服务,并向 mDNS 服务注册以下信息:我提供 FTP 服务,我的IP是 192.168.1.101,端口是 21。当B主机进入局域网,并向 B 主机的 mDNS 服务请求,我要找局域网内 FTP 服务器,B主机的 mDNS 就会去局域网内向其他的 mDNS 询问,并且最终告诉你,有一个IP地址为 192.168.1.101,端口号是 21 的主机,也就是 A 主机提供 FTP 服务,所以 B 主机就知道了 A 主机的 IP 地址和端口号了。

在Apple 的设备上(电脑,笔记本,iphone,ipad等设备)都提供了这个服务。很多Linux设备也提供这个服务。Windows的设备可能没有提供,但是如果安装了iTunes之类的软件的话,也提供了这个服务。**注意安卓系统不支持 mDNS协议 **。

这样就可以利用这个服务开发一些局域网内的自动发现,然后提供一些局域网内交互的应用了。

在 Windows PC 需要安装 Apple 的 Bonjour Print Services for Windows

安装好以后可以在 命令提示符界面输入dns-sd,如果安装成功会输出功能列表。

image-20210407110034549

API参考

begin() - 开启mDNS服务
bool begin(const char* hostName);

语法

#include <ESPmDNS.h>
const char* host = "esp32";

if (!MDNS.begin(host)) { //http://esp32.local
    Serial.println("Error setting up MDNS responder!");
    while (1) {
        delay(1000);
    }
}

参数

传入值 说明 值范围
hostName 要注册的主机名

返回

返回值 说明 值范围
bool true:注册成功。
false:注册不成功
end() - 关闭mDNS服务
void end();

语法

MDNS.end();

参数

返回

addService() - 注册服务
bool addService(char *service, char *proto, uint16_t port);
bool addService(const char *service, const char *proto, uint16_t port){
	return addService((char *)service, (char *)proto, port);
}
bool addService(String service, String proto, uint16_t port){
	return addService(service.c_str(), proto.c_str(), port);
}

bool addServiceTxt(char *name, char *proto, char * key, char * value);
void addServiceTxt(const char *name, const char *proto, const char *key,const char * value){
	addServiceTxt((char *)name, (char *)proto, (char *)key, (char *)value);
}
void addServiceTxt(String name, String proto, String key, String value){
	addServiceTxt(name.c_str(), proto.c_str(), key.c_str(), value.c_str());
}

语法

MDNS.addService("http", "tcp", 80);

参数

传入值 说明 值范围
service 服务名字
proto 服务协议
port 服务端口

返回

返回值 说明 值范围
bool true:注册成功
false:注册失败
queryService() - 查询服务
int queryService(char *service, char *proto);int queryService(const char *service, const char *proto){    return queryService((char *)service, (char *)proto);}int queryService(String service, String proto){    return queryService(service.c_str(), proto.c_str());}

语法

int n = MDNS.queryService(service, proto);

参数

传入值 说明 值范围
service 服务名字
proto 服务协议

返回

返回值 说明 值范围
int 符合条件的服务个数
hostname() - 获取查询服务的主机名
String hostname(int idx);

语法

Serial.print(MDNS.hostname(i));

参数

传入值 说明 值范围
idx 服务索引

返回

返回值 说明 值范围
String 服务域名
IP() - 获取查询服务的IP地址
IPAddress IP(int idx);

语法

Serial.print(MDNS.IP(i));

参数

传入值 说明 值范围
idx 服务索引

返回

返回值 说明 值范围
IPAddress 服务IP地址
port() - 获取查询服务的端口号
uint16_t port(int idx);

语法

Serial.print(MDNS.port(i));

参数

传入值 说明 值范围
idx 服务索引

返回

返回值 说明 值范围
uint16_t 服务端口号

例程:mDNS_Web_Server

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->ESPmDNS\examples\mDNS_Web_Server)

/*
  ESP32 mDNS responder sample

  This is an example of an HTTP server that is accessible
  via http://esp32.local URL thanks to mDNS responder.

  Instructions:
  - Update WiFi SSID and password as necessary.
  - Flash the sketch to the ESP32 board
  - Install host software:
    - For Linux, install Avahi (http://avahi.org/).
    - For Windows, install Bonjour (http://www.apple.com/support/bonjour/).
    - For Mac OSX and iOS support is built in through Bonjour already.
  - Point your browser to http://esp32.local, you should see a response.

 */


#include <WiFi.h>
#include <ESPmDNS.h>
#include <WiFiClient.h>

const char* ssid = "............";
const char* password = "..............";

// TCP server at port 80 will respond to HTTP requests
WiFiServer server(80);

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

    // Connect to WiFi network
    WiFi.begin(ssid, password);
    Serial.println("");

    // Wait for connection
    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
        Serial.print(".");
    }
    Serial.println("");
    Serial.print("Connected to ");
    Serial.println(ssid);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    // Set up mDNS responder:
    // - first argument is the domain name, in this example
    //   the fully-qualified domain name is "esp32.local"
    // - second argument is the IP address to advertise
    //   we send our IP address on the WiFi network
    if (!MDNS.begin("esp32")) {
        Serial.println("Error setting up MDNS responder!");
        while(1) {
            delay(1000);
        }
    }
    Serial.println("mDNS responder started");

    // Start TCP (HTTP) server
    server.begin();
    Serial.println("TCP server started");

    // Add service to MDNS-SD
    MDNS.addService("http", "tcp", 80);
}

void loop(void)
{
    // Check if a client has connected
    WiFiClient client = server.available();
    if (!client) {
        return;
    }
    Serial.println("");
    Serial.println("New client");

    // Wait for data from client to become available
    while(client.connected() && !client.available()){
        delay(1);
    }

    // Read the first line of HTTP request
    String req = client.readStringUntil('\r');

    // First line of HTTP request looks like "GET /path HTTP/1.1"
    // Retrieve the "/path" part by finding the spaces
    int addr_start = req.indexOf(' ');
    int addr_end = req.indexOf(' ', addr_start + 1);
    if (addr_start == -1 || addr_end == -1) {
        Serial.print("Invalid request: ");
        Serial.println(req);
        return;
    }
    req = req.substring(addr_start + 1, addr_end);
    Serial.print("Request: ");
    Serial.println(req);

    String s;
    if (req == "/")
    {
        IPAddress ip = WiFi.localIP();
        String ipStr = String(ip[0]) + '.' + String(ip[1]) + '.' + String(ip[2]) + '.' + String(ip[3]);
        s = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<!DOCTYPE HTML>\r\n<html>Hello from ESP32 at ";
        s += ipStr;
        s += "</html>\r\n\r\n";
        Serial.println("Sending 200");
    }
    else
    {
        s = "HTTP/1.1 404 Not Found\r\n\r\n";
        Serial.println("Sending 404");
    }
    client.print(s);

    client.stop();
    Serial.println("Done with client");
}

访问 http://esp32.local 可显示如下界面

image-20210407110354306

例程:mDNS-SD_Extended

mDNS-SD (DNS Service Discovery)Extended,xmDNS-SD,即为扩展多播DNS服务发现,可用于在本地网络上执行对等发现,这在设计时不知道目标终结点的 IP 地址和主机名时特别有用。如果要建立mDNS,可先通过mDNS-SD服务发现寻找网络内的各个服务的名称,然后在建立mDNS,以避免名字冲突,或者通过服务发现去寻找需要连接的服务,然后建立连接。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->ESPmDNS\examples\mDNS-SD_Extended)

/*
  ESP8266 mDNS-SD responder and query sample

  This is an example of announcing and finding services.
  
  Instructions:
  - Update WiFi SSID and password as necessary.
  - Flash the sketch to two ESP8266 boards
  - The last one powered on should now find the other.
 */

#include <WiFi.h>
#include <ESPmDNS.h>

const char* ssid = "your_ssid";
const char* password = "your_password";

void setup() {
    Serial.begin(115200);
    WiFi.begin(ssid, password);
    while (WiFi.status() != WL_CONNECTED) {
        delay(250);
        Serial.print(".");
    }
    Serial.println("");
    Serial.print("Connected to ");
    Serial.println(ssid);
    Serial.print("IP address: ");
    Serial.println(WiFi.localIP());

    if (!MDNS.begin("ESP32_Browser")) {
        Serial.println("Error setting up MDNS responder!");
        while(1){
            delay(1000);
        }
    }
}

void loop() {
    browseService("http", "tcp");
    delay(1000);
    browseService("arduino", "tcp");
    delay(1000);
    browseService("workstation", "tcp");
    delay(1000);
    browseService("smb", "tcp");
    delay(1000);
    browseService("afpovertcp", "tcp");
    delay(1000);
    browseService("ftp", "tcp");
    delay(1000);
    browseService("ipp", "tcp");
    delay(1000);
    browseService("printer", "tcp");
    delay(10000);
}

void browseService(const char * service, const char * proto){
    Serial.printf("Browsing for service _%s._%s.local. ... ", service, proto);
    int n = MDNS.queryService(service, proto);
    if (n == 0) {
        Serial.println("no services found");
    } else {
        Serial.print(n);
        Serial.println(" service(s) found");
        for (int i = 0; i < n; ++i) {
            // Print details for each service found
            Serial.print("  ");
            Serial.print(i + 1);
            Serial.print(": ");
            Serial.print(MDNS.hostname(i));
            Serial.print(" (");
            Serial.print(MDNS.IP(i));
            Serial.print(":");
            Serial.print(MDNS.port(i));
            Serial.println(")");
        }
    }
    Serial.println();
}

从串口可以打印出扫描到的一些复位,以下显示的是扫描到了8个打印服务。

image-20210407113212051

在扫描到的服务名字后增加 .local 后缀,例如 http://deve4e4xx.local/ 在浏览器进行访问,即可访问到打印机的服务。

image-20210407113444873

9.6 NetBIOS

NetBIOS 为 网上基本输入输出系统(Network Basic Input/Output System)的缩写,它提供了OSI模型中的会话层服务,让在不同计算机上运行的不同程序,可以在局域网中,互相连线,以及分享数据。该协议的主要用途之一就是把计算机名称解析为相应IP地址。如果每个设备有一个固定名字,在实现了NetBIOS 的前提下,用户在浏览器里输入该设备的名字,然后通过 NetBIOS 解析,便可实现访问该设备网页的这个功能了。

在 Windows 操作系统中,默认情况下在安装 TCP/IP 协议后会自动安装 NetBIOS 协议,其他系统需要安装协议。

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->NetBIOS\examples\ESP_NBNST)

#include <WiFi.h>
#include <NetBIOS.h>
#include <WebServer.h> 

const char* ssid = "your_ssid";
const char* password = "your_password";

WebServer server(80); //声明WebServer对象

void handleRoot() //回调函数
{
  server.send(200, "text/plain", "This is the root directory");
}

void handleP1() //回调函数
{
  server.send(200, "text/plain", "This is the Page 1");
}

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

  // Connect to WiFi network
  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid, password);
  Serial.println("");

  // Wait for connection
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("");
  Serial.print("Connected to ");
  Serial.println(ssid);
  Serial.print("IP address: ");
  Serial.println(WiFi.localIP());

  NBNS.begin("ESP");

  server.on("/", handleRoot); //注册链接"/"与对应回调函数

  server.on("/p1", handleP1); //注册链接"/p1"与对应回调函数

  server.on("/p2", []() { //注册链接"/p2",对应回调函数通过内联函数声明
    server.send(200, "text/plain", "This is the Page 2");
  });

  server.begin(); //启动服务器
  Serial.println("Web server started");

}

void loop() {
  server.handleClient(); //处理来自客户端的请求
  delay(2);// 允许cpu切换到其他任务 allow the cpu to switch to other tasks
}

通过DOS命令行发送 ping esp 命令,可获取 FireBeetle MESH 主板的IP地址

C:\Users\DFRobot-DFTV>ping esp

正在 Ping esp [192.168.0.242] 具有 32 字节的数据:
来自 192.168.0.242 的回复: 字节=32 时间=86ms TTL=255
来自 192.168.0.242 的回复: 字节=32 时间=87ms TTL=255
来自 192.168.0.242 的回复: 字节=32 时间=102ms TTL=255
来自 192.168.0.242 的回复: 字节=32 时间=114ms TTL=255

访问http://esp/ 可登录web server

image-20210511184556660

9.7 NTP TIME

网络授时协议,主板读取授时服务器的时间信息,转换为当地时间后,通过串口打印出来。程序在初始状态获取一次网络时间,然后关闭 WiFi。实时时间将在主板中继续计时。

除了 pool.ntp.org 这个NTP服务器还可以使用其他的服务器,例如 ntp1.aliyun.com、1.cn.pool.ntp.org。

API参考

configTime() - 设置NTP服务器和本地时区

设置NTP服务器和本地时区(偏移量规范)。最后,设置环境变量TZ并调用tzset()。

语法

void configTime(long gmtOffset_sec, int daylightOffset_sec, const char* server1, const char* server2, const char* server3);

参数

传入值 说明 值范围
gmtOffset_sec GMT与本地时间之间的时差(以秒为单位)。
daylightOffset_sec 夏令时提前的时间(单位为秒)。
server1, server2, server3 NTP服务器。设置至少一个。

返回

configTzTime() - 设置NTP服务器和本地时区

设置NTP服务器和本地时区(时区规范)。最后,设置环境变量TZ并调用tzset()。

语法

void configTzTime(const char* tz, const char* server1, const char* server2, const char* server3);

参数

传入值 说明 值范围
tz 时区
server1, server2, server3 NTP服务器。设置至少一个。

返回

getLocalTime() - 获取当地时间

获取当地时间。

语法

bool getLocalTime(struct tm * info, uint32_t ms);

参数

传入值 说明 值范围
info 用于存储要获取的时间信息的区域。呼叫者需要分配空间。
ms 超时时间。如果省略,则设置为5000。

返回

返回值 说明 值范围
bool 取得结果。如果成功,则为true。如果不成功则为false。当获取的时间信息的“年份”大于116(代表2016)时,返回True。

注意: 当获取的时间信息的“年”不大于116时,请等待最大以毫秒为单位的超时时间。由于它会尝试每10毫秒获取一次时间信息,因此请注意,如果未使用configTime()等预先设置时间,请谨慎操作。

例程:SimpleTime

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

#include <WiFi.h>
#include "time.h"

const char* ssid       = "YOUR_SSID";
const char* password   = "YOUR_PASS";

const char* ntpServer = "pool.ntp.org";
//GMT为格林尼治标准时间,北京时间使用8*60*60
const long  gmtOffset_sec = 8*3600;
//与具有夏令时的国家/地区有关。在其他国家/地区可以将其设置为0。中国当前没有实行夏令时。
const int   daylightOffset_sec = 0;
void printLocalTime()
{
  struct tm timeinfo;
  if(!getLocalTime(&timeinfo)){
    Serial.println("Failed to obtain time");
    return;
  }
  Serial.println(&timeinfo, "%A, %B %d %Y %H:%M:%S");
}

void setup()
{
  Serial.begin(115200);
  
  //connect to WiFi
  Serial.printf("Connecting to %s ", ssid);
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
      delay(500);
      Serial.print(".");
  }
  Serial.println(" CONNECTED");
  
  //init and get the time
  configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
  printLocalTime();

  //disconnect WiFi as it's no longer needed
  WiFi.disconnect(true);
  WiFi.mode(WIFI_OFF);
}

void loop()
{
  delay(1000);
  printLocalTime();
}

串口每隔1秒钟打印出实时时间

Connecting to dfrobotOffice ....... CONNECTED
Tuesday, December 21 2021 15:15:28
Tuesday, December 21 2021 15:15:29

9.8 HTTP协议

9.8.1 HTTP 简介

HTTP协议是Hyper Text Transfer Protocol(超文本传输协议)的缩写,是用于从万维网(WWW:World Wide Web )服务器传输超文本到本地浏览器的传送协议。

HTTP基于TCP/IP通信协议来传递HTML 文件、图片文件、 查询结果等数据。

HTTP 工作原理

HTTP协议工作于客户端-服务端架构上。浏览器作为HTTP客户端通过URL向HTTP服务端即WEB服务器发送所有请求。

Web服务器根据接收到的请求后,向客户端发送响应信息。

HTTP默认端口号为80,但是你也可以改为8080或者其他端口。

HTTP三点注意事项:

  • HTTP是无连接:无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。

  • HTTP是媒体独立的:这意味着,只要客户端和服务器知道如何处理的数据内容,任何类型的数据都可以通过HTTP发送。客户端以及服务器指定使用适合的MIME-type内容类型。

  • HTTP是无状态:HTTP协议是无状态协议。无状态是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快。

以下图表展示了HTTP协议通信流程:

image-20210422183415270

HTTP 消息结构

HTTP是基于客户端/服务端(C/S)的架构模型,通过一个可靠的链接来交换信息,是一个无状态的请求/响应协议。

一个HTTP”客户端”是一个应用程序(Web浏览器或其他任何客户端),通过连接到服务器达到向服务器发送一个或多个HTTP的请求的目的。

一个HTTP”服务器”同样也是一个应用程序(通常是一个Web服务,如Apache Web服务器或IIS服务器等),通过接收客户端的请求并向客户端发送HTTP响应数据。

HTTP使用统一资源标识符(Uniform Resource Identifiers, URI)来传输数据和建立连接。

一旦建立连接后,数据消息就通过类似Internet邮件所使用的格式[RFC5322]和多用途Internet邮件扩展(MIME)[RFC2045]来传送。

客户端请求消息

客户端发送一个HTTP请求到服务器的请求消息包括以下格式:请求行(request line)、请求头部(header)、空行和请求数据四个部分组成,下图给出了请求报文的一般格式。

客户端请求报文

服务器响应消息

HTTP响应也由四个部分组成,分别是:状态行、消息报头、空行和响应正文。

服务器响应


实例

下面实例是一点典型的使用GET来传递数据的实例:

客户端请求:

GET /hello.txt HTTP/1.1
User-Agent: curl/7.16.3 libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
Host: www.example.com
Accept-Language: en, mi

服务端响应:

HTTP/1.1 200 OK
Date: Mon, 27 Jul 2009 12:28:53 GMT
Server: Apache
Last-Modified: Wed, 22 Jul 2009 19:15:56 GMT
ETag: "34aa387-d-1568eb00"
Accept-Ranges: bytes
Content-Length: 51
Vary: Accept-Encoding
Content-Type: text/plain

输出结果:

Hello World! My payload includes a trailing CRLF.

HTTP请求方法

根据HTTP标准,HTTP请求可以使用多种请求方法。

HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法。

HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT 方法。

序号 方法 描述
1 GET 请求指定的页面信息,并返回实体主体。
2 HEAD 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头
3 POST 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。
4 PUT 从客户端向服务器传送的数据取代指定的文档的内容。
5 DELETE 请求服务器删除指定的页面。
6 CONNECT HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器。
7 OPTIONS 允许客户端查看服务器的性能。
8 TRACE 回显服务器收到的请求,主要用于测试或诊断。

HTTP 响应头信息

HTTP请求头提供了关于请求,响应或者其他的发送实体的信息。在本章节中我们将具体来介绍HTTP响应头信息。

应答头 说明
Allow 服务器支持哪些请求方法(如GET、POST等)。
Content-Encoding 文档的编码(Encode)方法。只有在解码之后才可以得到Content-Type头指定的内容类型。利用gzip压 缩文档能够显著地减少HTML文档的下载时间。Java的GZIPOutputStream可以很方便地进行gzip压缩,但只有Unix上的 Netscape和Windows上的IE 4、IE 5才支持它。因此,Servlet应该通过查看Accept-Encoding头(即request.getHeader("Accept- Encoding"))检查浏览器是否支持gzip,为支持gzip的浏览器返回经gzip压缩的HTML页面,为其他浏览器返回普通页面。
Content-Length 表示内容长度。只有当浏览器使用持久HTTP连接时才需要这个数据。如果你想要利用持久连接的优势,可以把输出文档写入 ByteArrayOutputStram,完成后查看其大小,然后把该值放入Content-Length头,最后通过 byteArrayStream.writeTo(response.getOutputStream()发送内容。
Content-Type 表示后面的文档属于什么MIME类型。Servlet默认为text/plain,但通常需要显式地指定为text/html。由于经常要设置Content-Type,因此HttpServletResponse提供了一个专用的方法setContentType。
Date 当前的GMT时间。你可以用setDateHeader来设置这个头以避免转换时间格式的麻烦。
Expires 应该在什么时候认为文档已经过期,从而不再缓存它?
Last-Modified 文档的最后改动时间。客户可以通过If-Modified-Since请求头提供一个日期,该请求将被视为一个条件 GET,只有改动时间迟于指定时间的文档才会返回,否则返回一个304(Not Modified)状态。Last-Modified也可用setDateHeader方法来设置。
Location 表示客户应当到哪里去提取文档。Location通常不是直接设置的,而是通过HttpServletResponse的sendRedirect方法,该方法同时设置状态代码为302。
Refresh 表示浏览器应该在多少时间之后刷新文档,以秒计。除了刷新当前文档之外,你还可以通过setHeader("Refresh", "5; URL=http://host/path")让浏览器读取指定的页面。 注 意这种功能通常是通过设置HTML页面HEAD区的<META HTTP-EQUIV="Refresh" CONTENT="5;URL=http://host/path">实现,这是因为,自动刷新或重定向对于那些不能使用CGI或Servlet的 HTML编写者十分重要。但是,对于Servlet来说,直接设置Refresh头更加方便。 注意Refresh的意义是"N秒之后刷 新本页面或访问指定页面",而不是"每隔N秒刷新本页面或访问指定页面"。因此,连续刷新要求每次都发送一个Refresh头,而发送204状态代码则可 以阻止浏览器继续刷新,不管是使用Refresh头还是<META HTTP-EQUIV="Refresh" ...>。 注意Refresh头不属于HTTP 1.1正式规范的一部分,而是一个扩展,但Netscape和IE都支持它。
Server 服务器名字。Servlet一般不设置这个值,而是由Web服务器自己设置。
Set-Cookie 设置和页面关联的Cookie。Servlet不应使用response.setHeader("Set-Cookie", ...),而是应使用HttpServletResponse提供的专用方法addCookie。参见下文有关Cookie设置的讨论。
WWW-Authenticate 客户应该在Authorization头中提供什么类型的授权信息?在包含401(Unauthorized)状态行的 应答中这个头是必需的。例如,response.setHeader("WWW-Authenticate", "BASIC realm=\"executives\"")。 注意Servlet一般不进行这方面的处理,而是让Web服务器的专门机制来控制受密码保护页面的访问(例如.htaccess)。

HTTP状态码

当浏览者访问一个网页时,浏览者的浏览器会向网页所在服务器发出请求。当浏览器接收并显示网页前,此网页所在的服务器会返回一个包含HTTP状态码的信息头(server header)用以响应浏览器的请求。

HTTP状态码的英文为HTTP Status Code。

下面是常见的HTTP状态码:

  • 200 - 请求成功

  • 301 - 资源(网页等)被永久转移到其它URL

  • 404 - 请求的资源(网页等)不存在

  • 500 - 内部服务器错误

HTTP状态码分类

HTTP状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,后两个数字没有分类的作用。HTTP状态码共分为5种类型:

分类 分类描述
1** 信息,服务器收到请求,需要请求者继续执行操作
2** 成功,操作被成功接收并处理
3** 重定向,需要进一步的操作以完成请求
4** 客户端错误,请求包含语法错误或无法完成请求
5** 服务器错误,服务器在处理请求的过程中发生了错误

HTTP状态码列表:

状态码 状态码英文名称 中文描述
100 Continue 继续。客户端应继续其请求
101 Switching Protocols 切换协议。服务器根据客户端的请求切换协议。只能切换到更高级的协议,例如,切换到HTTP的新版本协议
200 OK 请求成功。一般用于GET与POST请求
201 Created 已创建。成功请求并创建了新的资源
202 Accepted 已接受。已经接受请求,但未处理完成
203 Non-Authoritative Information 非授权信息。请求成功。但返回的meta信息不在原始的服务器,而是一个副本
204 No Content 无内容。服务器成功处理,但未返回内容。在未更新网页的情况下,可确保浏览器继续显示当前文档
205 Reset Content 重置内容。服务器处理成功,用户终端(例如:浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域
206 Partial Content 部分内容。服务器成功处理了部分GET请求
300 Multiple Choices 多种选择。请求的资源可包括多个位置,相应可返回一个资源特征与地址的列表用于用户终端(例如:浏览器)选择
301 Moved Permanently 永久移动。请求的资源已被永久的移动到新URI,返回信息会包括新的URI,浏览器会自动定向到新URI。今后任何新的请求都应使用新的URI代替
302 Found 临时移动。与301类似。但资源只是临时被移动。客户端应继续使用原有URI
303 See Other 查看其它地址。与301类似。使用GET和POST请求查看
304 Not Modified 未修改。所请求的资源未修改,服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供一个头信息指出客户端希望只返回在指定日期之后修改的资源
305 Use Proxy 使用代理。所请求的资源必须通过代理访问
306 Unused 已经被废弃的HTTP状态码
307 Temporary Redirect 临时重定向。与302类似。使用GET请求重定向
400 Bad Request 客户端请求的语法错误,服务器无法理解
401 Unauthorized 请求要求用户的身份认证
402 Payment Required 保留,将来使用
403 Forbidden 服务器理解请求客户端的请求,但是拒绝执行此请求
404 Not Found 服务器无法根据客户端的请求找到资源(网页)。通过此代码,网站设计人员可设置"您所请求的资源无法找到"的个性页面
405 Method Not Allowed 客户端请求中的方法被禁止
406 Not Acceptable 服务器无法根据客户端请求的内容特性完成请求
407 Proxy Authentication Required 请求要求代理的身份认证,与401类似,但请求者应当使用代理进行授权
408 Request Time-out 服务器等待客户端发送的请求时间过长,超时
409 Conflict 服务器完成客户端的PUT请求是可能返回此代码,服务器处理请求时发生了冲突
410 Gone 客户端请求的资源已经不存在。410不同于404,如果资源以前有现在被永久删除了可使用410代码,网站设计人员可通过301代码指定资源的新位置
411 Length Required 服务器无法处理客户端发送的不带Content-Length的请求信息
412 Precondition Failed 客户端请求信息的先决条件错误
413 Request Entity Too Large 由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果只是服务器暂时无法处理,则会包含一个Retry-After的响应信息
414 Request-URI Too Large 请求的URI过长(URI通常为网址),服务器无法处理
415 Unsupported Media Type 服务器无法处理请求附带的媒体格式
416 Requested range not satisfiable 客户端请求的范围无效
417 Expectation Failed 服务器无法满足Expect的请求头信息
500 Internal Server Error 服务器内部错误,无法完成请求
501 Not Implemented 服务器不支持请求的功能,无法完成请求
502 Bad Gateway 充当网关或代理的服务器,从远端服务器接收到了一个无效的请求
503 Service Unavailable 由于超载或系统维护,服务器暂时的无法处理客户端的请求。延时的长度可包含在服务器的Retry-After头信息中
504 Gateway Time-out 充当网关或代理的服务器,未及时从远端服务器获取请求
505 HTTP Version not supported 服务器不支持请求的HTTP协议的版本,无法完成处理

HTTP content-type

Content-Type,内容类型,一般是指网页中存在的Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件,这就是经常看到一些Asp网页点击的结果却是下载到的一个文件或一张图片的原因。

HTTP content-type 对照表

文件扩展名 Content-Type(Mime-Type) 文件扩展名 Content-Type(Mime-Type)
.*( 二进制流,不知道下载文件类型) application/octet-stream .tif image/tiff
.001 application/x-001 .301 application/x-301
.323 text/h323 .906 application/x-906
.907 drawing/907 .a11 application/x-a11
.acp audio/x-mei-aac .ai application/postscript
.aif audio/aiff .aifc audio/aiff
.aiff audio/aiff .anv application/x-anv
.asa text/asa .asf video/x-ms-asf
.asp text/asp .asx video/x-ms-asf
.au audio/basic .avi video/avi
.awf application/vnd.adobe.workflow .biz text/xml
.bmp application/x-bmp .bot application/x-bot
.c4t application/x-c4t .c90 application/x-c90
.cal application/x-cals .cat application/vnd.ms-pki.seccat
.cdf application/x-netcdf .cdr application/x-cdr
.cel application/x-cel .cer application/x-x509-ca-cert
.cg4 application/x-g4 .cgm application/x-cgm
.cit application/x-cit .class java/*
.cml text/xml .cmp application/x-cmp
.cmx application/x-cmx .cot application/x-cot
.crl application/pkix-crl .crt application/x-x509-ca-cert
.csi application/x-csi .css text/css
.cut application/x-cut .dbf application/x-dbf
.dbm application/x-dbm .dbx application/x-dbx
.dcd text/xml .dcx application/x-dcx
.der application/x-x509-ca-cert .dgn application/x-dgn
.dib application/x-dib .dll application/x-msdownload
.doc application/msword .dot application/msword
.drw application/x-drw .dtd text/xml
.dwf Model/vnd.dwf .dwf application/x-dwf
.dwg application/x-dwg .dxb application/x-dxb
.dxf application/x-dxf .edn application/vnd.adobe.edn
.emf application/x-emf .eml message/rfc822
.ent text/xml .epi application/x-epi
.eps application/x-ps .eps application/postscript
.etd application/x-ebx .exe application/x-msdownload
.fax image/fax .fdf application/vnd.fdf
.fif application/fractals .fo text/xml
.frm application/x-frm .g4 application/x-g4
.gbr application/x-gbr . application/x-
.gif image/gif .gl2 application/x-gl2
.gp4 application/x-gp4 .hgl application/x-hgl
.hmr application/x-hmr .hpg application/x-hpgl
.hpl application/x-hpl .hqx application/mac-binhex40
.hrf application/x-hrf .hta application/hta
.htc text/x-component .htm text/html
.html text/html .htt text/webviewhtml
.htx text/html .icb application/x-icb
.ico image/x-icon .ico application/x-ico
.iff application/x-iff .ig4 application/x-g4
.igs application/x-igs .iii application/x-iphone
.img application/x-img .ins application/x-internet-signup
.isp application/x-internet-signup .IVF video/x-ivf
.java java/* .jfif image/jpeg
.jpe image/jpeg .jpe application/x-jpe
.jpeg image/jpeg .jpg image/jpeg
.jpg application/x-jpg .js application/x-javascript
.jsp text/html .la1 audio/x-liquid-file
.lar application/x-laplayer-reg .latex application/x-latex
.lavs audio/x-liquid-secure .lbm application/x-lbm
.lmsff audio/x-la-lms .ls application/x-javascript
.ltr application/x-ltr .m1v video/x-mpeg
.m2v video/x-mpeg .m3u audio/mpegurl
.m4e video/mpeg4 .mac application/x-mac
.man application/x-troff-man .math text/xml
.mdb application/msaccess .mdb application/x-mdb
.mfp application/x-shockwave-flash .mht message/rfc822
.mhtml message/rfc822 .mi application/x-mi
.mid audio/mid .midi audio/mid
.mil application/x-mil .mml text/xml
.mnd audio/x-musicnet-download .mns audio/x-musicnet-stream
.mocha application/x-javascript .movie video/x-sgi-movie
.mp1 audio/mp1 .mp2 audio/mp2
.mp2v video/mpeg .mp3 audio/mp3
.mp4 video/mpeg4 .mpa video/x-mpg
.mpd application/vnd.ms-project .mpe video/x-mpeg
.mpeg video/mpg .mpg video/mpg
.mpga audio/rn-mpeg .mpp application/vnd.ms-project
.mps video/x-mpeg .mpt application/vnd.ms-project
.mpv video/mpg .mpv2 video/mpeg
.mpw application/vnd.ms-project .mpx application/vnd.ms-project
.mtx text/xml .mxp application/x-mmxp
.net image/pnetvue .nrf application/x-nrf
.nws message/rfc822 .odc text/x-ms-odc
.out application/x-out .p10 application/pkcs10
.p12 application/x-pkcs12 .p7b application/x-pkcs7-certificates
.p7c application/pkcs7-mime .p7m application/pkcs7-mime
.p7r application/x-pkcs7-certreqresp .p7s application/pkcs7-signature
.pc5 application/x-pc5 .pci application/x-pci
.pcl application/x-pcl .pcx application/x-pcx
.pdf application/pdf .pdf application/pdf
.pdx application/vnd.adobe.pdx .pfx application/x-pkcs12
.pgl application/x-pgl .pic application/x-pic
.pko application/vnd.ms-pki.pko .pl application/x-perl
.plg text/html .pls audio/scpls
.plt application/x-plt .png image/png
.png application/x-png .pot application/vnd.ms-powerpoint
.ppa application/vnd.ms-powerpoint .ppm application/x-ppm
.pps application/vnd.ms-powerpoint .ppt application/vnd.ms-powerpoint
.ppt application/x-ppt .pr application/x-pr
.prf application/pics-rules .prn application/x-prn
.prt application/x-prt .ps application/x-ps
.ps application/postscript .ptn application/x-ptn
.pwz application/vnd.ms-powerpoint .r3t text/vnd.rn-realtext3d
.ra audio/vnd.rn-realaudio .ram audio/x-pn-realaudio
.ras application/x-ras .rat application/rat-file
.rdf text/xml .rec application/vnd.rn-recording
.red application/x-red .rgb application/x-rgb
.rjs application/vnd.rn-realsystem-rjs .rjt application/vnd.rn-realsystem-rjt
.rlc application/x-rlc .rle application/x-rle
.rm application/vnd.rn-realmedia .rmf application/vnd.adobe.rmf
.rmi audio/mid .rmj application/vnd.rn-realsystem-rmj
.rmm audio/x-pn-realaudio .rmp application/vnd.rn-rn_music_package
.rms application/vnd.rn-realmedia-secure .rmvb application/vnd.rn-realmedia-vbr
.rmx application/vnd.rn-realsystem-rmx .rnx application/vnd.rn-realplayer
.rp image/vnd.rn-realpix .rpm audio/x-pn-realaudio-plugin
.rsml application/vnd.rn-rsml .rt text/vnd.rn-realtext
.rtf application/msword .rtf application/x-rtf
.rv video/vnd.rn-realvideo .sam application/x-sam
.sat application/x-sat .sdp application/sdp
.sdw application/x-sdw .sit application/x-stuffit
.slb application/x-slb .sld application/x-sld
.slk drawing/x-slk .smi application/smil
.smil application/smil .smk application/x-smk
.snd audio/basic .sol text/plain
.sor text/plain .spc application/x-pkcs7-certificates
.spl application/futuresplash .spp text/xml
.ssm application/streamingmedia .sst application/vnd.ms-pki.certstore
.stl application/vnd.ms-pki.stl .stm text/html
.sty application/x-sty .svg text/xml
.swf application/x-shockwave-flash .tdf application/x-tdf
.tg4 application/x-tg4 .tga application/x-tga
.tif image/tiff .tif application/x-tif
.tiff image/tiff .tld text/xml
.top drawing/x-top .torrent application/x-bittorrent
.tsd text/xml .txt text/plain
.uin application/x-icq .uls text/iuls
.vcf text/x-vcard .vda application/x-vda
.vdx application/vnd.visio .vml text/xml
.vpg application/x-vpeg005 .vsd application/vnd.visio
.vsd application/x-vsd .vss application/vnd.visio
.vst application/vnd.visio .vst application/x-vst
.vsw application/vnd.visio .vsx application/vnd.visio
.vtx application/vnd.visio .vxml text/xml
.wav audio/wav .wax audio/x-ms-wax
.wb1 application/x-wb1 .wb2 application/x-wb2
.wb3 application/x-wb3 .wbmp image/vnd.wap.wbmp
.wiz application/msword .wk3 application/x-wk3
.wk4 application/x-wk4 .wkq application/x-wkq
.wks application/x-wks .wm video/x-ms-wm
.wma audio/x-ms-wma .wmd application/x-ms-wmd
.wmf application/x-wmf .wml text/vnd.wap.wml
.wmv video/x-ms-wmv .wmx video/x-ms-wmx
.wmz application/x-ms-wmz .wp6 application/x-wp6
.wpd application/x-wpd .wpg application/x-wpg
.wpl application/vnd.ms-wpl .wq1 application/x-wq1
.wr1 application/x-wr1 .wri application/x-wri
.wrk application/x-wrk .ws application/x-ws
.ws2 application/x-ws .wsc text/scriptlet
.wsdl text/xml .wvx video/x-ms-wvx
.xdp application/vnd.adobe.xdp .xdr text/xml
.xfd application/vnd.adobe.xfd .xfdf application/vnd.adobe.xfdf
.xhtml text/html .xls application/vnd.ms-excel
.xls application/x-xls .xlw application/x-xlw
.xml text/xml .xpl audio/scpls
.xq text/xml .xql text/xml
.xquery text/xml .xsd text/xml
.xsl text/xml .xslt text/xml
.xwd application/x-xwd .x_b application/x-x_b
.sis application/vnd.symbian.install .sisx application/vnd.symbian.install
.x_t application/x-x_t .ipa application/vnd.iphone
.apk application/vnd.android.package-archive .xap application/x-silverlight-app

9.8.2 HTTPClient API 参考

begin() - 注册要访问的URL

注册要访问的URL。如果使用HTTPS,则还注册根证书。

使用此API注册URL后,实际上是通过调用GET() / PATCH() / POST() / PUT来发送和接收数据的。

语法

#include <HTTPClient.h>
bool HTTPClient::begin(WiFiClient &client, String url);
bool HTTPClient::begin(WiFiClient &client, String host, uint16_t port, String uri, bool https);

如果已经定义 HTTPCLIENT_1_1_COMPATIBLE 以下方式有效

bool HTTPClient::begin(String url);
bool HTTPClient::begin(String url, const char* CAcert);
bool HTTPClient::begin(String host, uint16_t port, String uri = "/");
bool HTTPClient::begin(String host, uint16_t port, String uri, const char* CAcert);

参数

传入值 说明 值范围
client WiFiClient对象
url 访问的URL
uri 访问的URI
host 访问的主机
port 访问的主机的端口
https 协议。如果为true,则为https;如果为false,则为http。
CAcert 访问的URL的根证书。以Base64格式指定。

返回

返回值 说明 值范围
bool 注册结果。如果成功,则为true。如果不成功则为false。 true、false

end() - 断开TCP连接并终止HTTP通信

断开TCP连接并终止HTTP通信。如果使用HTTPClient :: setReuse()重用连接,则不会断开TCP连接。

语法

#include <HTTPClient.h>
void HTTPClient::end(void);

参数

返回

GET() - 将GET请求发送到在HTTPClient :: begin()中注册的URL

语法

#include <HTTPClient.h>
int HTTPClient::GET();

参数

返回

返回值 说明 值范围
int HTTP返回码。出现错误时为负数

PATCH() - 将PATCH请求发送到在HTTPClient :: begin()中注册的URL

语法

#include <HTTPClient.h>
int HTTPClient::PATCH(uint8_t * payload, size_t size);
int HTTPClient::PATCH(String payload);

参数

传入值 说明 值范围
payload 要发送的数据
size 要发送的数据大小

如果使用 HTTPClient::PATCH(String payload),数据长度为payload.length()

返回

返回值 说明 值范围
int HTTP返回码。出现错误时为负数。

POST() - 向通过HTTPClient :: begin()注册的URL发送POST请求

语法

#include <HTTPClient.h>
int HTTPClient::POST(uint8_t * payload, size_t size);
int HTTPClient::POST(String payload);

参数

传入值 说明 值范围
payload 要发送的数据
size 要发送的数据大小

如果使用 HTTPClient::POST(String payload),数据长度为payload.length()

返回

返回值 说明 值范围
int HTTP返回码。出现错误时为负数。

PUT() - 将PUT请求发送到在HTTPClient :: begin()中注册的URL

语法

#include <HTTPClient.h>
int HTTPClient::PUT(uint8_t * payload, size_t size);
int HTTPClient::PUT(String payload);

参数

传入值 说明 值范围
payload 要发送的数据
size 要发送的数据大小

如果使用 HTTPClient::PUT(String payload),数据长度为payload.length()

返回

返回值 说明 值范围
int HTTP返回码。出现错误时为负数。

setAuthorization() - 设置用于BASIC身份验证的身份验证信息

语法

#include <HTTPClient.h>
void HTTPClient::setAuthorization(const char * user, const char * password);
void HTTPClient::setAuthorization(const char * auth);

参数

传入值 说明 值范围
user 用于BASIC身份验证的用户名
password 用于BASIC身份验证的密码
auth 用于BASIC身份验证的身份验证信息(由BASE64通过将用户名和密码连接为“:”来编码的字符串)

返回

setReuse() - 指示重用HTTP连接

语法

#include <HTTPClient.h>
void HTTPClient::setReuse(bool reuse);

参数

传入值 说明 值范围
reuse 如果重新使用HTTP连接,则为true;如果未重新使用,则为false。

返回

getSize() - 返回通过HTTP请求获得的响应消息的正文部分的长度

语法

#include <HTTPClient.h>
int HTTPClient::getSize(void);

参数

返回

返回值 说明 值范围
int 通过HTTP请求获得的响应消息的正文部分的长度。

getString() - 返回通过HTTP请求获得的响应消息的正文部分

语法

#include <HTTPClient.h>
int HTTPClient::getString(void);

参数

返回

返回值 说明 值范围
int HTTP请求获得的响应消息的正文部分。如果无法保护内存,则返回一个空字符串。

getStreamPtr() - 返回指向HTTP连接的数据流的指针

语法

#include <HTTPClient.h>
WiFiClient* HTTPClient::getStreamPtr(void);

参数

返回

返回值 说明 值范围
WiFiClient* 指向HTTP会话的数据流的指针。

connected() - 检查是否连接了HTTP连接

语法

#include <HTTPClient.h>
bool HTTPClient::connected();

参数

返回

返回值 说明 值范围
bool 如果连接了HTTP会话,则为true;如果未连接,则为false。

errorToString() - 将HTTP错误代码转换为字符串

语法

#include <HTTPClient.h>
String HTTPClient::errorToString(int error);

参数

传入值 说明 值范围
error HTTP错误代码

返回

返回值 说明 值范围
String 与HTTP错误代码相对应的字符串(String对象)

writeToStream() - 将消息正文写入流中

语法

#include <HTTPClient.h>
int HTTPClient::writeToStream(Stream * stream);

参数

传入值 说明 值范围
stream 用于写出消息正文的流。

返回

返回值 说明 值范围
int 导出的字符数。出现错误时为负值。

例程:BasicHttpClient

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->HTTPClient\examples\BasicHttpClient)

/**
 * BasicHTTPClient.ino
 *
 *  Created on: 24.05.2015
 *
 */

#include <Arduino.h>

#include <WiFi.h>
#include <WiFiMulti.h>

#include <HTTPClient.h>

#define USE_SERIAL Serial  // USE_SERIAL被定义为Serial,因此前半部分是字符串到控制台的输出。

WiFiMulti wifiMulti; //定义了WiFiMulti类型的变量wifiMulti。WiFiMulti类是用于管理多个访问点的类。

void setup() {

    USE_SERIAL.begin(115200);

    USE_SERIAL.println();
    USE_SERIAL.println();
    USE_SERIAL.println();

    for(uint8_t t = 4; t > 0; t--) {
        USE_SERIAL.printf("[SETUP] WAIT %d...\n", t);
        USE_SERIAL.flush();
        delay(1000);
    }

    wifiMulti.addAP("your_ssid", "your_password"); // WiFi接入点注册。您需要更改SSID和PASSWORD以适合您的环境。

}

void loop() {
    // wait for WiFi connection
    // 如果连接成功,将返回WL_CONNECTED
    if((wifiMulti.run() == WL_CONNECTED)) {

        HTTPClient http; // HTTPClient类是实现HTTP客户端的类

        USE_SERIAL.print("[HTTP] begin...\n");
        // configure traged server and url
        // 注册您要访问的URL
        http.begin("http://example.com/index.html"); //HTTP

        USE_SERIAL.print("[HTTP] GET...\n");
        // start connection and send HTTP header
        // 使用GET请求获取在http.begin()中注册的URL。如果出现错误(例如无法连接到服务器),将返回负值。
        // 如果HTTP连接成功,则将返回HTTP状态代码。
        int httpCode = http.GET();

        // httpCode will be negative on error
        if(httpCode > 0) {
            // HTTP header has been send and Server response header has been handled
            USE_SERIAL.printf("[HTTP] GET... code: %d\n", httpCode);

            // file found at server
            if(httpCode == HTTP_CODE_OK) {
                // http.getString()是Http.GET()返回以字符串形式获取的数据。
                // 随着HTTP响应主体部分的大小增加,它将占用更多内存。
                // 根据您的目的,StreamHttpClient有一个以小增量读取数据的示例。
                String payload = http.getString();
                USE_SERIAL.println(payload);
            }
        } else {
            // 如果http.GET()调用失败,则调用http.errorToString()将错误代码转换为与错误代码对应的字符串,然后显示它。
            USE_SERIAL.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
        }
		// 断开TCP连接。也可以使用http.setReuse()重用该连接。此时,如果服务器返回保持活动状态,则连接似乎保持不变。
        http.end();
    }

    delay(5000);
}

获取example.com的网页从串口打印,

image-20210422191110538

可将串口打印出来的html文本保存为 .html 文件,用浏览器打开即可看到网页界面。

image-20210422191302095

例程:BasicHttpsClient 带认证客户端

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板

(参考Arduino IDE例程 Examples -> Examples for Edge101WE ->HTTPClient\examples\BasicHttpsClient)

/**
   BasicHTTPSClient.ino
    Created on: 14.10.2018
*/

#include <Arduino.h>

#include <WiFi.h>
#include <WiFiMulti.h>

#include <HTTPClient.h>

#include <WiFiClientSecure.h>

// This is GandiStandardSSLCA2.pem, the root Certificate Authority that signed 
// the server certifcate for the demo server https://jigsaw.w3.org in this
// example. This certificate is valid until Sep 11 23:59:59 2024 GMT
const char* rootCACertificate = \
"-----BEGIN CERTIFICATE-----\n" \
"MIIF6TCCA9GgAwIBAgIQBeTcO5Q4qzuFl8umoZhQ4zANBgkqhkiG9w0BAQwFADCB\n" \
"iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl\n" \
"cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV\n" \
"BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTQw\n" \
"OTEyMDAwMDAwWhcNMjQwOTExMjM1OTU5WjBfMQswCQYDVQQGEwJGUjEOMAwGA1UE\n" \
"CBMFUGFyaXMxDjAMBgNVBAcTBVBhcmlzMQ4wDAYDVQQKEwVHYW5kaTEgMB4GA1UE\n" \
"AxMXR2FuZGkgU3RhbmRhcmQgU1NMIENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IB\n" \
"DwAwggEKAoIBAQCUBC2meZV0/9UAPPWu2JSxKXzAjwsLibmCg5duNyj1ohrP0pIL\n" \
"m6jTh5RzhBCf3DXLwi2SrCG5yzv8QMHBgyHwv/j2nPqcghDA0I5O5Q1MsJFckLSk\n" \
"QFEW2uSEEi0FXKEfFxkkUap66uEHG4aNAXLy59SDIzme4OFMH2sio7QQZrDtgpbX\n" \
"bmq08j+1QvzdirWrui0dOnWbMdw+naxb00ENbLAb9Tr1eeohovj0M1JLJC0epJmx\n" \
"bUi8uBL+cnB89/sCdfSN3tbawKAyGlLfOGsuRTg/PwSWAP2h9KK71RfWJ3wbWFmV\n" \
"XooS/ZyrgT5SKEhRhWvzkbKGPym1bgNi7tYFAgMBAAGjggF1MIIBcTAfBgNVHSME\n" \
"GDAWgBRTeb9aqitKz1SA4dibwJ3ysgNmyzAdBgNVHQ4EFgQUs5Cn2MmvTs1hPJ98\n" \
"rV1/Qf1pMOowDgYDVR0PAQH/BAQDAgGGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYD\n" \
"VR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMCIGA1UdIAQbMBkwDQYLKwYBBAGy\n" \
"MQECAhowCAYGZ4EMAQIBMFAGA1UdHwRJMEcwRaBDoEGGP2h0dHA6Ly9jcmwudXNl\n" \
"cnRydXN0LmNvbS9VU0VSVHJ1c3RSU0FDZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNy\n" \
"bDB2BggrBgEFBQcBAQRqMGgwPwYIKwYBBQUHMAKGM2h0dHA6Ly9jcnQudXNlcnRy\n" \
"dXN0LmNvbS9VU0VSVHJ1c3RSU0FBZGRUcnVzdENBLmNydDAlBggrBgEFBQcwAYYZ\n" \
"aHR0cDovL29jc3AudXNlcnRydXN0LmNvbTANBgkqhkiG9w0BAQwFAAOCAgEAWGf9\n" \
"crJq13xhlhl+2UNG0SZ9yFP6ZrBrLafTqlb3OojQO3LJUP33WbKqaPWMcwO7lWUX\n" \
"zi8c3ZgTopHJ7qFAbjyY1lzzsiI8Le4bpOHeICQW8owRc5E69vrOJAKHypPstLbI\n" \
"FhfFcvwnQPYT/pOmnVHvPCvYd1ebjGU6NSU2t7WKY28HJ5OxYI2A25bUeo8tqxyI\n" \
"yW5+1mUfr13KFj8oRtygNeX56eXVlogMT8a3d2dIhCe2H7Bo26y/d7CQuKLJHDJd\n" \
"ArolQ4FCR7vY4Y8MDEZf7kYzawMUgtN+zY+vkNaOJH1AQrRqahfGlZfh8jjNp+20\n" \
"J0CT33KpuMZmYzc4ZCIwojvxuch7yPspOqsactIGEk72gtQjbz7Dk+XYtsDe3CMW\n" \
"1hMwt6CaDixVBgBwAc/qOR2A24j3pSC4W/0xJmmPLQphgzpHphNULB7j7UTKvGof\n" \
"KA5R2d4On3XNDgOVyvnFqSot/kGkoUeuDcL5OWYzSlvhhChZbH2UF3bkRYKtcCD9\n" \
"0m9jqNf6oDP6N8v3smWe2lBvP+Sn845dWDKXcCMu5/3EFZucJ48y7RetWIExKREa\n" \
"m9T8bJUox04FB6b9HbwZ4ui3uRGKLXASUoWNjDNKD/yZkuBjcNqllEdjB+dYxzFf\n" \
"BT02Vf6Dsuimrdfp5gJ0iHRc2jTbkNJtUQoj1iM=\n" \
"-----END CERTIFICATE-----\n";

// Not sure if WiFiClientSecure checks the validity date of the certificate. 
// Setting clock just to be sure...
void setClock() {
  configTime(0, 0, "pool.ntp.org", "time.nist.gov");

  Serial.print(F("Waiting for NTP time sync: "));
  time_t nowSecs = time(nullptr);
  while (nowSecs < 8 * 3600 * 2) {
    delay(500);
    Serial.print(F("."));
    yield();
    nowSecs = time(nullptr);
  }

  Serial.println();
  struct tm timeinfo;
  gmtime_r(&nowSecs, &timeinfo);
  Serial.print(F("Current time: "));
  Serial.print(asctime(&timeinfo));
}


WiFiMulti WiFiMulti;

void setup() {

  Serial.begin(115200);
  // Serial.setDebugOutput(true);

  Serial.println();
  Serial.println();
  Serial.println();

  WiFi.mode(WIFI_STA);
  WiFiMulti.addAP("SSID", "PASSWORD");

  // wait for WiFi connection
  Serial.print("Waiting for WiFi to connect...");
  while ((WiFiMulti.run() != WL_CONNECTED)) {
    Serial.print(".");
  }
  Serial.println(" connected");

  setClock();  
}

void loop() {
  WiFiClientSecure *client = new WiFiClientSecure;
  if(client) {
    client -> setCACert(rootCACertificate);

    {
      // Add a scoping block for HTTPClient https to make sure it is destroyed before WiFiClientSecure *client is 
      HTTPClient https;
  
      Serial.print("[HTTPS] begin...\n");
      if (https.begin(*client, "https://jigsaw.w3.org/HTTP/connection.html")) {  // HTTPS
        Serial.print("[HTTPS] GET...\n");
        // start connection and send HTTP header
        int httpCode = https.GET();
  
        // httpCode will be negative on error
        if (httpCode > 0) {
          // HTTP header has been send and Server response header has been handled
          Serial.printf("[HTTPS] GET... code: %d\n", httpCode);
  
          // file found at server
          if (httpCode == HTTP_CODE_OK || httpCode == HTTP_CODE_MOVED_PERMANENTLY) {
            String payload = https.getString();
            Serial.println(payload);
          }
        } else {
          Serial.printf("[HTTPS] GET... failed, error: %s\n", https.errorToString(httpCode).c_str());
        }
  
        https.end();
      } else {
        Serial.printf("[HTTPS] Unable to connect\n");
      }

      // End extra scoping block
    }
  
    delete client;
  } else {
    Serial.println("Unable to create client");
  }

  Serial.println();
  Serial.println("Waiting 10s before the next round...");
  delay(10000);
}

例程通过增加WiFiClientSecure库来实现了https网站的访问。

image-20210429194242796

例程:HTTP GET 获取温度数据

基于 BasicHttpClient 代码我们可以修改为去读取openweathermap网站发布的天气信息。

首先需要到免费的 openWeatherMap 气象数据网站 注册一个账户,注册完毕后会将API key 发送到注册的邮箱。

在这里可以找到API的介绍

获取当前成都天气的API,使用网页浏览器访问下面的网址

http://api.openweathermap.org/data/2.5/weather?q=Chengdu&appid={your API key}

返回json格式的数据如下:

image-20210506163947096

{"coord":{"lon":104.0667,"lat":30.6667},"weather":[{"id":802,"main":"Clouds","description":"scattered clouds","icon":"03d"}],"base":"stations","main":{"temp":296.15,"feels_like":296.07,"temp_min":296.15,"temp_max":296.15,"pressure":1014,"humidity":60},"visibility":10000,"wind":{"speed":2,"deg":0},"clouds":{"all":40},"dt":1620289984,"sys":{"type":1,"id":9674,"country":"CN","sunrise":1620252944,"sunset":1620301496},"timezone":28800,"id":1815286,"name":"Chengdu","cod":200}

要解析json格式数据,我们需要在Arduino IDE 安装 ArduinoJson 库

Arduino IDE库管理器中安装该库。只需转到Sketch > Include Library > Manage Libraries ,搜索库的名字 ArduinoJson,点击安装

image-20210506170659912

ArduinoJson 库详细教程

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将代码下载到 Edge101WE 主板,注意修改WiFi和openWeatherMapApiKey等信息。

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

#define USE_SERIAL Serial

WiFiMulti wifiMulti;

// Your openWeatherMapApiKey
String openWeatherMapApiKey = "Your_openWeatherMapApiKey";

// your country code and city
String city = "Chengdu";

void setup() {

  USE_SERIAL.begin(115200);

  USE_SERIAL.println();
  USE_SERIAL.println();
  USE_SERIAL.println();

  for (uint8_t t = 4; t > 0; t--) {
    USE_SERIAL.printf("[SETUP] WAIT %d...\n", t);
    USE_SERIAL.flush();
    delay(1000);
  }

  wifiMulti.addAP("your_wifi_ssid", "your_wifi_password");

}

void loop() {
  // Allocate the JSON document
  //
  // Inside the brackets, 1000 is the capacity of the memory pool in bytes.
  // Don't forget to change this value to match your JSON document.
  // Use arduinojson.org/v6/assistant to compute the capacity.
  StaticJsonDocument<1000> doc;
  
  // wait for WiFi connection
  if ((wifiMulti.run() == WL_CONNECTED)) {
    String serverPath = "http://api.openweathermap.org/data/2.5/weather?q=" + city + "&APPID=" + openWeatherMapApiKey;
    HTTPClient http;

    USE_SERIAL.print("[HTTP] begin...\n");
    // configure traged server and url
    http.begin(serverPath); //HTTP

    USE_SERIAL.print("[HTTP] GET...\n");
    // start connection and send HTTP header
    int httpCode = http.GET();

    // httpCode will be negative on error
    if (httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      USE_SERIAL.printf("[HTTP] GET... code: %d\n", httpCode);

      // file found at server
      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        USE_SERIAL.println(payload);
        DeserializationError error = deserializeJson(doc, payload);
        // Test if parsing succeeds.
        if (error) {
          Serial.print(F("deserializeJson() failed: "));
          Serial.println(error.f_str());
          return;
        }
        double  temp = doc["main"]["temp"];
        double  Pressure = doc["main"]["pressure"];
        double  humidity = doc["main"]["humidity"];
        int windSpeed = doc["wind"]["speed"];
        Serial.print("Temperature: ");        
        Serial.println(temp);
        Serial.print("Pressure: ");        
        Serial.println(Pressure);
        Serial.print("Humidity: ");
        Serial.println(humidity);
        Serial.print("Wind Speed: ");
        Serial.println(windSpeed);
      }
    } else {
      USE_SERIAL.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    }

    http.end();
  }

  delay(5000);
}

串口将会打印出指定城市的天气情况,这些数据也可以直接用于计算。

image-20210506182553621

例程: HTTP GET 更新值

HTTP GET除了可以获取服务器数据,还可以更新服务器的数据。

在此示例中, Edge101WE 主板发出 HTTP GET 请求以更新 ThingSpeak 中的读数。

ThingSpeak 具有免费的 API,可让您使用HTTP存储和检索数据。在本教程中,您将使用 ThingSpeak API 从任何地方发布和可视化图表中的数据。例如,我们将发布随机值,但在实际应用中,您将使用真实传感器读数。

要将 ThingSpeak 与 Edge101WE 主板一起使用,您需要一个API密钥。请执行以下步骤:

  1. 转到ThingSpeak.com并创建一个免费帐户。

  2. 然后,打开“频道”标签。

  3. 创建一个新频道。名字为test,选中Field1。

    image-20210506184402589

获取到Write API Key

image-20210506185241119

在右边可看到 Write a Channel Feed API

GET https://api.thingspeak.com/update?api_key={your_APIKey}&field1=0

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将代码下载到 Edge101WE 主板

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiMulti.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

#define USE_SERIAL Serial

WiFiMulti wifiMulti;

// your thingspeak.com Write API Key
String serverName = "http://api.thingspeak.com/update?api_key={your_API_Key}";

void setup() {

  USE_SERIAL.begin(115200);

  USE_SERIAL.println();
  USE_SERIAL.println();
  USE_SERIAL.println();

  for (uint8_t t = 4; t > 0; t--) {
    USE_SERIAL.printf("[SETUP] WAIT %d...\n", t);
    USE_SERIAL.flush();
    delay(1000);
  }

  wifiMulti.addAP("your_wifi_ssid", "your_wifi_password");
  
  // if analog input pin 37 is unconnected, random analog
  // noise will cause the call to randomSeed() to generate
  // different seed numbers each time the sketch runs.
  // randomSeed() will then shuffle the random function.  
  randomSeed(analogRead(37));
}

void loop() {
  
  // wait for WiFi connection
  if ((wifiMulti.run() == WL_CONNECTED)) {
    HTTPClient http;
    String randomData = String(random(100));
    USE_SERIAL.println("random = " + randomData);
    
    String serverPath = serverName + "&field1=" + randomData;
    
    USE_SERIAL.print("[HTTP] begin...\n");
    // configure traged server and url
    http.begin(serverPath); //HTTP

    USE_SERIAL.print("[HTTP] GET...\n");
    // start connection and send HTTP header
    int httpCode = http.GET();

    // httpCode will be negative on error
    if (httpCode > 0) {
      // HTTP header has been send and Server response header has been handled
      USE_SERIAL.printf("[HTTP] GET... code: %d\n", httpCode);

      // file found at server
      if (httpCode == HTTP_CODE_OK) {
        String payload = http.getString();
        USE_SERIAL.println(payload);
      }
    } else {
      USE_SERIAL.printf("[HTTP] GET... failed, error: %s\n", http.errorToString(httpCode).c_str());
    }

    http.end();
  }

  delay(1000);
}

将实时显示上传的随机数的数据

image-20210506192122556

例程: HTTP POST 提交数据

在此例程中我们将通过 IFTTT.com Web API发送电子邮件通知。

IFTTT代表“ If This Than That”,它是一项免费的基于Web的服务,用于创建称为 applet 的简单条件语句链。

这意味着您可以在发生某些事情时触发事件。在此示例中,applet 在 Edge101WE 主板用户按钮按下时向您的电子邮件发送按键按下次数。

访问IFTTT网站

ifttt.com 并输入电子邮件以创建一个帐户。

创建一个 applet

点击主界面右上角 Create 按钮进入 Create 界面,点击If This add按钮。

image-20210510103635362

搜索 Webhooks,然后选择 Webhooks 图标。

image-20210507113014506

3.选择“Receive a web request 接收网络请求”触发器,然后为事件命名。在这种情况下,我输入了“ button_event ”。然后,点击“创建触发器”按钮。

image-20210507113707750

image-20210507114017506

4.单击“Then That”字样继续。现在,定义触发您定义的事件时发生的情况。搜索“email”服务并选择它。您可以保留默认选项。

image-20210507114329391

image-20210507114528407

5.按“完成”按钮创建小程序。

image-20210507114944496

设置 Email 发送内容,点击 Create action ,点击 Finish。

image-20210510105153877

测试小程序

在继续进行该项目之前,首先测试您的Applet。请按照以下步骤进行测试:

1.搜索Webhooks服务或打开此链接:https: //ifttt.com/maker_webhooks

2.单击“Documentation”按钮。

image-20210507115903170

修改 event 字段为 刚才定义的 button_event,Value1 值修改为 12 。点击 Test It 。

image-20210510110040394

此时收到一封邮件,按钮按次数为 Value1 填写的 12 。

image-20210510110239710

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将代码下载到 Edge101WE 主板。

#include <Arduino.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>

const char *ssid = "your_ssdi"; //你的网络名称
const char *password = "your_password"; //你的网络密码

#define USE_SERIAL Serial
#define userButton 38 //定义用户按钮为GPIO38
volatile bool pressed = false;  //按钮按下标志
volatile int pressCounter = 0; //按钮按下计数器


// your thingspeak.com event and Write API Key
// thingspeak 定义的事件和 写API Key
String serverName = "http://maker.ifttt.com/trigger/{your_event}/with/key/{your_key}";
//类似如下格式 
//String serverName = "http://maker.ifttt.com/trigger/button_event/with/key/dAxLm420YlSYWGsaEE-wIG";

// IRAM_ATTR定义callBack中断服务函数在IRAM(指令RAM)中运行,以便在SPI Flash写、OTA更新等禁用高速缓存的同时ISR仍可以运行。
void IRAM_ATTR callBack(void)
{
  pressed = true;
  pressCounter ++;
}

void setup() {

  pinMode(userButton, INPUT_PULLUP);
  attachInterrupt(userButton, callBack, FALLING); //使能外部输入中断,按钮按下下降沿触发

  USE_SERIAL.begin(115200);
  USE_SERIAL.println();
  USE_SERIAL.println();
  USE_SERIAL.println();

  WiFi.begin(ssid, password); //连接网络

  while (WiFi.status() != WL_CONNECTED) //等待网络连接成功
  {
    delay(500);
    USE_SERIAL.print(".");
  }
  USE_SERIAL.println("Connected");
  USE_SERIAL.print("IP Address:");
  USE_SERIAL.println(WiFi.localIP());
}

void loop() {

  // wait for WiFi connection
  if ((WiFi.status() == WL_CONNECTED) && (pressed == true)) {
    USE_SERIAL.print("Button pressed!\n");
    pressed = false;

    HTTPClient http;

    USE_SERIAL.print("[HTTP] begin...\n");

    http.begin(serverName);

    // Specify content-type header
    http.addHeader("Content-Type", "application/x-www-form-urlencoded");
    // Data to send with HTTP POST
    String httpRequestData = "value1=" + String(pressCounter);

    USE_SERIAL.print("[HTTP] POST...\n");
    // Send HTTP POST request
    int httpResponseCode = http.POST(httpRequestData);
    /*
      // If you need an HTTP request with a content type: application/json, use the following:
      http.addHeader("Content-Type", "application/json");
      // JSON data to send with HTTP POST
      String httpRequestData = "{\"value1\":\"" + String(random(40)) + "\",\"value2\":\"" + String(random(40)) + "\",\"value3\":\"" + String(random(40)) + "\"}";
      // Send HTTP POST request
      int httpResponseCode = http.POST(httpRequestData);
    */

    // httpResponseCode will be negative on error
    if (httpResponseCode > 0) {
      // HTTP header has been send and Server response header has been handled
      USE_SERIAL.printf("[HTTP] POST... code: %d\n", httpResponseCode);

      // file found at server
      if (httpResponseCode == HTTP_CODE_OK) {
        String payload = http.getString();
        USE_SERIAL.println(payload);
      }
    } else {
      USE_SERIAL.printf("[HTTP] POST... failed, error: %s\n", http.errorToString(httpResponseCode).c_str());
    }

    http.end();
  }
  delay(100);
}

当主板上的用户按钮按下,主板将发送 HTTP POST 请求给 IFTTT 服务器。服务器的 webhooks 收到请求后将触发 Email 发送用户按钮按下多少次的通知邮件。

image-20210510115628195

image-20210510113108187

9.9 Websocket

由于在 HTTP 协议中,服务器不能主动向设备推送信息。设备使用轮询的方式向服务器请求数据时会消耗大量的设备运行资源与网络资源,因此 WebSocket 协议诞生。 WebSocket 协议是建立在传输层协议 TCP 上进行全双工通信的协议,可以实现设备与物联网协议之间的平等传输,即客户端可以主动向服务器发送请求,服务器也可以向客户端推送信息。WebSocket 只是借用HTTP协议完成一部分握手功能。

sequenceDiagram
    WS客户端->>WS服务端: HTTP请求建立连接
    WS服务端-->WS客户端:切换成WS协议
    WS客户端->>WS服务端:客户端发送数据
    WS服务端-->>WS客户端:服务端发送数据
    WS服务端-->>WS客户端:服务端发送数据
    WS服务端->WS客户端:关闭连接    

连接过程 —— 握手过程

  • 浏览器、服务器建立 TCP 连接,三次握手。这是通信的基础,传输控制层,若失败后续都不执行。

  • TCP 连接成功后,浏览器通过 HTTP 协议向服务器传送 WebSocket 支持的版本号等信息。(开始前的 HTTP 握手)

  • 服务器收到客户端的握手请求后,同样采用 HTTP 协议回馈数据。

  • 当收到了连接成功的消息后,通过 TCP 通道进行传输通信。

首先需要引入 WebSocket 库,这里以 Links2004/arduinoWebSockets 的 WebSocket 库为例。

库作者GitHub:arduinoWebSockets

首先打开 Ardunio IDE,点击标题栏项目 –> 加载库 –> 管理库打开库管理器

搜索 WebSockets,找到 Markus Sattler 版本的 WebSockets 库,点击安装。

WebSockets 事件类型

关键字 描述
WStype_ERROR WebSocket 连接发生错误时触发
WStype_DISCONNECTED 客户端或服务端关闭连接时触发
WStype_CONNECTED 客户端连接到服务端时触发
WStype_TEXT 接收到文本数据时触发
WStype_BIN 接收到二进制数据时触发
WStype_PING Ping 和 Pong 是 websocket 里的心跳,用来保证客户端是在线的,服务端给客户端发送 PING,客户端发送 PONG来回应
WStype_PONG

例程:使用 WebSocket 控制 LED

例程中使用两块 Edge101WE 主板 A 与 B,实现使用主板 A 上的用户按钮控制主板 B 的用户 LED 灯,同时使用主板 B 上的用户按钮控制主板 A 的用户 LED 灯。两块主板连接同一 WiFi 网络,并使用 WebSocket 协议进行网络通信。

当主板 A 用户按钮按下,主板 B 上的用户 LED 灯亮,当主板 B 用户按钮按下,主板 A 上的用户 LED 灯亮。

主板A:服务端开发板代码

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板A

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <WebSocketsServer.h> // 引入WebSocketsServer库

#define userButton 38 // 定义主板用户按钮为GPIO38
#define userLED 15  // 定义主板用户LED为GPIO15

// 在这里填入WiFi名称
const char *ssid = "your_ssid";
// 在这里填入WiFi密码
const char *password = "your_password";

// 在这里指定WebSocket服务端IP地址
IPAddress host(192, 168, 43, 8);
// 在这里填入WiFi网关地址
IPAddress gateway(192, 168, 43, 1);
// 在这里填入WiFi子网掩码,网关地址与子网掩码可用ifconfig(mac os)或ipconfig(windows)命令获取
IPAddress netmask(255, 255, 255, 0);

// 初始化WebSocketsServer对象,并设置端口号为81
WebSocketsServer server(81);

int pressState;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(userButton, INPUT_PULLUP); // 
  pinMode(userLED, OUTPUT);
  
  // 设置WiFi运行模式为无线终端模式
  WiFi.mode(WIFI_STA);
  // 为当前设备配置固定IP
  if (!WiFi.config(host, gateway, netmask)) {
    Serial.println("Can't config wifi.");
  }
  Serial.println("WiFi Connecting");
  // 连接WiFi 
  WiFi.begin(ssid, password);
  // 判断是否连接成功
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println("");
  Serial.println("The server is connected to WiFi");
  Serial.print("Current ip address is ");
  Serial.println(WiFi.localIP());

  // 启动WebSocket服务器
  server.begin();
  // 指定事件处理函数
  /* param:
		num: 客户端编号,可以判断接收的消息来自哪个连接的客户端
		type: 事件类型
		payload: 事件携带数据
		length: 事件携带数据长度
  */    
  server.onEvent([](uint8_t num, WStype_t type, uint8_t * payload, size_t length){

    if (type == WStype_CONNECTED) {
      // 若为客户端连接事件,显示提示信息
      Serial.println("New connection!");
    } else if (type == WStype_DISCONNECTED) {
      // 若为连接断开事件,显示提示信息
      Serial.println("Close the connection.");
    } else if (type == WStype_TEXT) {
      // 接收来自客户端的信息(客户端FLASH按键状态),并控制LED的工作
      String data = (char*)payload;
      if(data.substring(data.indexOf("=") + 1) == "on") {
        digitalWrite(userLED, HIGH);
        Serial.println("server on board LED ON");
      } else if (data.substring(data.indexOf("=") + 1) == "off") {
        digitalWrite(userLED, LOW);
        Serial.println("server on board LED OFF");
      }
    }
  });
}

// 遍历当前连接服务端的所有客户端地址,并与需要获取编号的客户端IP地址进行比较,若找到IP地址,返回IP地址索引,否则返回-1
int8_t get_num_from_ip(IPAddress ip_needed) {
    for(uint8_t i = 0; i < WEBSOCKETS_SERVER_CLIENT_MAX; i++) {
        IPAddress ip = server.remoteIP(i);
        if(ip == ip_needed) {
           // ip found
           return i;
        }
    }
    return -1;
}

void loop() {
  // put your main code here, to run repeatedly:
  server.loop();
  // 判断当前主板用户按键状态是否与之前状态一致,若不一致则按键状态发生改变
  if (digitalRead(userButton) != pressState) {
    // 若按键状态改变,记录当前按键状态,并向客户端发送按键状态
    pressState = digitalRead(userButton);
    String data = String("ledStatus=") + (digitalRead(userButton) == HIGH ? "off": "on");
    Serial.println("set client " + data);
     /* param:
		num: int8_t类型,客户端编号
		str: String类型,要发送的数据
	*/
    server.sendTXT(get_num_from_ip(IPAddress(192, 168, 43, 9)), data);
  }
}

主板B:客户端开发板代码

将下方的代码里WiFi的SSID和密码修改为你自己的WiFi SSID和密码,然后将程序上传到主板B

#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <WebSocketsClient.h> // 引入WebSocketsClient库

#define userButton 38 // 定义主板用户按钮为GPIO38
#define userLED 15  // 定义主板用户LED为GPIO15

// 在这里填入WiFi名称
const char *ssid = "your_ssid";
// 在这里填入WiFi密码
const char *password = "your_password";

// 在这里指定WebSocket客户端IP地址
IPAddress host(192, 168, 43, 9);
// 在这里填入WiFi网关地址
IPAddress gateway(192, 168, 43, 1);
// 在这里填入WiFi子网掩码,网关地址与子网掩码可用ifconfig(mac os)或ipconfig(windows)命令获取
IPAddress netmask(255, 255, 255, 0);

// WebSocket服务器IP地址
String remoteHost = "192.168.43.8";
// 初始化WebSocketsClient对象
WebSocketsClient wsClient;

int pressState;

void setup() {
  // put your setup code here, to run once:
  Serial.begin(115200);
  pinMode(userButton, INPUT_PULLUP);
  pinMode(userLED, OUTPUT);
  
  // 设置WiFi运行模式为无线终端模式
  WiFi.mode(WIFI_STA);
  // 为当前设备配置固定IP
  if (!WiFi.config(host, gateway, netmask)) {
    Serial.println("Can't config WiFi.");
  }
  Serial.println("WiFi Connecting");
  // 连接WiFi
  WiFi.begin(ssid, password);
  // 判断是否连接成功
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(500);
  }
  Serial.println("");
  Serial.println("The client is connected to WiFi");
  Serial.print("Current ip address is ");
  Serial.println(WiFi.localIP());

  // 启动WebSocketsClient,设定WebSocket服务器地址为remoteHost,端口号为81
  wsClient.begin(remoteHost, 81);
    
  // 指定事件处理函数
  /* param:
		type: 事件类型
		payload: 事件携带数据
		length: 事件携带数据长度
  */
  wsClient.onEvent([](WStype_t type, uint8_t * payload, size_t length) {

    if (type == WStype_TEXT) {
      // 接收来自服务端的信息(服务端用户按键状态),并控制LED的工作
      String data = (char*)payload;
      if(data.substring(data.indexOf("=") + 1) == "on") {
        digitalWrite(userLED, HIGH);
        Serial.println("client on board LED ON");
      } else if (data.substring(data.indexOf("=") + 1) == "off") {
        digitalWrite(userLED, LOW);
        Serial.println("client on board LED OFF");
      }
    }
  });
}

void loop() {
  // put your main code here, to run repeatedly:
  wsClient.loop();
  // 判断当前主板用户按键状态是否与之前状态一致,若不一致则按键状态发生改变
  if (digitalRead(userButton) != pressState) {
    // 若按键状态改变,记录当前按键状态,并向服务端发送按键状态
    pressState = digitalRead(userButton);
    String data = String("ledStatus=") + (digitalRead(userButton) == HIGH ? "off": "on");
    Serial.println("set server "+ data);
    /* param:
		str: String类型,要发送的数据
	*/
    wsClient.sendTXT(data);
  }
}

服务端串口打印信息

WiFi Connecting
.....
The server is connected to WiFi
Current ip address is 192.168.43.8
New connection!
server on board LED OFF
server on board LED ON
set client ledStatus=on
set client ledStatus=off

客户端串口打印信息

WiFi Connecting
.....
The client is connected to WiFi
Current ip address is 192.168.43.9
set server ledStatus=off
set server ledStatus=on
client on board LED ON
client on board LED OFF

9.10 ESPNOW

概述

ESP-NOW 是一种由乐鑫公司定义的无连接 Wi-Fi 通信协议。在 ESP-NOW 中,应用程序数据被封装在各个供应商的动作帧中,然后在无连接的情况下,从一个 Wi-Fi 设备传输到另一个 Wi-Fi 设备。 CTR 与 CBC-MAC 协议 (CCMP) 可用来保护动作帧的安全。ESP-NOW 广泛应用于智能照明、远程控制、传感器等领域。

使用了 ESP-NOW 通信之后,如果某一个设备突然断电,只要一旦重启,就可自动连接到对应的节点中重新进行通信。

ESP-NOW 支持特性:

  • 单播包加密或单播包不加密通信

  • 加密配对设备和非加密配对设备混合使用

  • 可携带最长为 250 字节的有效 payload 数据

  • 支持设置发送回调函数以通知应用层帧发送失败或成功

ESP-NOW 存在限制:

  • 暂时不支持广播包

  • 加密配对设备有限制,Station 模式下最多支持10 个加密配对设备;SoftAP 或 SoftAP + Station 混合模式下最多支持 6 个加密配对设备。非加密配对设备支持若干,与加密设备总数和不超过 20 个

  • 有效 payload 限制为 250 字节

帧格式

ESP-NOW 使用各个供应商的动作帧传输数据,默认比特率为 1 Mbps。各个供应商的动作帧格式为:

-----------------------------------------------------------------------------------------
|   MAC 报头   |  分类代码  |  组织标识符  |  随机值  |  供应商特定内容  |   FCS   |
-----------------------------------------------------------------------------------------
   24 字节        1 字节        3 字节      4 字节      7~255 字节       4 字节
  • 分类代码:分类代码字段可用于指示各个供应商的类别(比如 127)。

  • 组织标识符:组织标识符包含一个唯一标识符 (比如 0x18fe34),为乐鑫指定的 MAC 地址的前三个字节。

  • 随机值:防止重放攻击。

  • 供应商特定内容:供应商特定内容包含供应商特定字段,如下所示:

----------------------------------------------------------------------------------------
|  元素 ID  |  长度  |  组织标识符  |  类型  |  版本  |     正文     |
----------------------------------------------------------------------------------------
   1 字节     1 字节     3 字节      1 字节   1 字节    0~250 字节
  • 元素 ID:元素 ID 字段可用于指示特定于供应商的元素。

  • 长度:长度是组织标识符、类型、版本和正文的总长度。

  • 组织标识符:组织标识符包含一个唯一标识符 (比如 0x18fe34),为乐鑫指定的 MAC 地址的前三个字节。

  • 类型:类型字段设置为 4,代表 ESP-NOW

  • 版本:版本字段设置为 ESP-NOW 的版本。

  • 正文:正文包含 ESP-NOW 数据。

由于 ESP-NOW 是无连接的,因此 MAC 报头与标准帧略有不同。FrameControl 字段的 FromDS 和 ToDS 位均为 0。第一个地址字段用于配置目标地址。第二个地址字段用于配置源地址。第三个地址字段用于配置广播地址 (0xff:0xff:0xff:0xff:0xff:0xff)。

安全

  • ESP-NOW 采用 CCMP 方法保护供应商特定动作帧的安全,具体可参考 IEEE Std. 802.11-2012。Wi-Fi 设备维护一个初始主密钥 (PMK) 和若干本地主密钥 (LMK),长度均为 16 个字节。PMK 可使用 AES-128 算法加密 LMK。请调用 esp_now_set_pmk() 设置 PMK。如果未设置 PMK,将使用默认 PMK。

  • LMK 可通过 CCMP 方法对供应商特定的动作帧进行加密,最多拥有 6 个不同的 LMK。如果未设置配对设备的 LMK,则动作帧不进行加密。

目前,不支持加密组播供应商特定的动作帧。

初始化和反初始化

调用 esp_now_init() 初始化 ESP-NOW,调用 esp_now_deinit() 反初始化 ESP-NOW。ESP-NOW 数据必须在 Wi-Fi 启动后传输,因此建议在初始化 ESP-NOW 之前启动 Wi-Fi,并在反初始化 ESP-NOW 之后停止 Wi-Fi。 当调用 esp_now_deinit() 时,配对设备的所有信息都将被删除。

添加配对设备

在将数据发送到其他设备之前,请先调用 esp_now_add_peer() 将其添加到配对设备列表中。配对设备的最大数量是 20。如果启用了加密,则必须设置 LMK。ESP-NOW 数据可以从 Station 或 Softap 接口发送。 确保在发送 ESP-NOW 数据之前已启用该接口。在发送广播数据之前必须添加具有广播 MAC 地址的设备。配对设备的信道范围是从 0 ~14。如果信道设置为 0,数据将在当前信道上发送。否则,必须使用本地设备所在的通道。

发送 ESP-NOW 数据

调用 esp_now_send() 发送 ESP-NOW 数据,调用 esp_now_register_send_cb 注册发送回调函数。如果 MAC 层成功接收到数据,则该函数将返回 ESP_NOW_SEND_SUCCESS 事件。否则,它将返回 ESP_NOW_SEND_FAIL。ESP-NOW 数据发送失败可能有几种原因,比如目标设备不存在、设备的信道不相同、动作帧在传输过程中丢失等。应用层并不一定可以总能接收到数据。如果需要,应用层可在接收 ESP-NOW 数据时发回一个应答 (ACK) 数据。如果接收 ACK 数据超时,则将重新传输 ESP-NOW 数据。可以为 ESP-NOW 数据设置序列号,从而删除重复的数据。

如果有大量 ESP-NOW 数据要发送,则调用 esp_now_send() 一次性发送不大于 250 字节的数据。 请注意,两个 ESP-NOW 数据包的发送间隔太短可能导致回调函数返回混乱。因此,建议在等到上一次回调函数返回 ACK 后再发送下一个 ESP-NOW 数据。发送回调函数从高优先级的 Wi-Fi 任务中运行。因此,不要在回调函数中执行冗长的操作。相反,将必要的数据发布到队列,并交给优先级较低的任务处理。

接收 ESP-NOW 数据

调用 esp_now_register_recv_cb 注册接收回调函数。当接收 ESP-NOW 数据时,需要调用接收回调函数。接收回调函数也在 Wi-Fi 任务任务中运行。因此,不要在回调函数中执行冗长的操作。 相反,将必要的数据发布到队列,并交给优先级较低的任务处理。

API参考

esp_now_init(void) - 初始化ESPNOW功能

初始化ESPNOW功能。

参数

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_INTERNAL:内部错误

esp_now_deinit(void) - 取消初始化ESPNOW函数

取消初始化ESPNOW函数。

Return ESP_OK : succeed

参数

返回 说明 值范围
esp_err_t ESP_OK:成功

esp_now_get_version(uint32_t *version) - 获取ESPNOW的版本

获取ESPNOW的版本。

参数

传入值 说明 值范围
version ESPNOW版本

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_ARG:无效的参数

esp_now_register_recv_cb(esp_now_recv_cb_t cb) - 接收ESPNOW数据的回调

参数

传入值 说明 值范围
cb 接收ESPNOW数据的回调函数

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化ESP_ERR_ESPNOW_INTERNAL:内部错误

esp_now_unregister_recv_cb(void) - 注销接收ESPNOW数据的回调函数

注销接收ESPNOW数据的回调函数。

参数

返回

返回 说明 值范围
esp_err_t ESP_OK:成功
ESP_ERR_ESPNOW_NOT_INIT:ESPNOW未初始化

esp_now_register_send_cb(esp_now_send_cb_t cb) - 注册发送ESPNOW数据的回调函数

注册发送ESPNOW数据的回调函数。

参数

传入值 说明 值范围
cb 发送ESPNOW数据的回调函数

返回

返回 说明 值范围