这个实验的功能演示 ESP32WebSocket 的使用方法。 这个实验的代码为工程“4_8_wifi_WebSocket”目录。

4.8.1. 实验内容

(1) 学习 Websocket 原理和工作过程

4.8.2. WebSocket 简介

WebSocket 是一种网络通信协议,是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的 协议。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。 在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,

并进行双向数据传输。
WebSocket 协议的优点:
(1) 连接在 80(ws)或者 443(wss)上创建,与 Http 使用的端口相同,几乎所有防火墙不会阻塞 WebSocket
的连接。
(2) 使用 Http 进行握手,该协议很自然地集成到网络浏览器和 http 服务器中。
(3) 心跳消息(ping pong)反复地被发送,保持 WebSocket 连接几乎一致处于活跃状态(一个节点周期性 的发送一个小数据包到另一个节点(ping),另一个节点使用相同的数据包作为相应(pang),将使者两 个节点都处于连接状态。)
(4) 该协议构建消息不需要额外的代码,消息启动和内容到达时,服务器好客户端都会知晓。
(5) WebSocket 连接关闭时发送一个特殊的关闭消息,其中包含原因代码和用于解释连接被关闭原因 的文本。
(6) WebSocket 协议可以安全地支持跨域连接。避免 Ajax 和 XMLHttpRequest 上的限制。
(7) Http 规范要求浏览器将并发连接数限制为每个主机名两个连接,但是握手之后该限制就不存在 了,因为此时的连接已经不再是 HTTP 连接了。

HTTP 和 websocket 数据流程图对比如下:

4.8.3. ESP32 函数介绍

 连接函数:netconn_new();
 绑定函数:netconn_bind();
 监听函数:netconn_listen();
 获取连接函数:netconn_accept();
 接收数据函数:netconn_recv();
 发送数据函数:netconn_write();
 关闭连接函数:netconn_close();
 删除连接函数:netconn_delete();

4.8.4. 代码讲解

使用 vs code 展开本实验的工程目录,如下图:

我们的这个实验,主要代码有 main 目录下,文件夹 components 下是之前讲过的 LCD 和 LED 驱动文件。 下面按照程序启动的流程讲解。
(1) 开机读取 smartconfig 配置
和 4.7.4 第一步一样。在 app_smartConfig.c 里,有函数 read_Smartconfig()用于开机读取 smartconfig 配 置的 wifi 信息,如果读取不到,就需要进行 smartconfig 配置。

//读 smartconfig 配置 void read_Smartconfig()
{
uint32_t len=0;
//初始化 NVS
esp_err_t  err  = nvs_flash_init();
if  (err  ==  ESP_ERR_NVS_NO_FREE_PAGES  ||  err  ==  ESP_ERR_NVS_NEW_VERSION_FOUND) {
//发现新版本
//擦除 ESP_ERROR_CHECK(nvs_flash_erase()); err  = nvs_flash_init();
}//打开,类似数据库的表
err =  nvs_open(SMARTCONFIG_LIST,  NVS_READWRITE,  &my_handle); if  (err  !=  ESP_OK) {
ESP_LOGE(TAG, "read_Smartconfig NVS Error (%s)!\n", esp_err_to_name(err)); wifi_isConfig=0;
}  else {
//读取,类似数据读字段对应的值
err = nvs_get_i8(my_handle, SMARTCONFIG_ISCONFIG, &wifi_isConfig); if(err==ESP_OK){
ESP_LOGI(TAG,  "wifi_isConfig  =  %d\n", wifi_isConfig);//名称 len=32;
err = nvs_get_str (my_handle, SMARTCONFIG_SSID, wifi_ssid, &len); if(err==ESP_OK)  ESP_LOGI(TAG,  "wifi_ssid  =  %s\n", wifi_ssid);//密码
}else{
wifi_isConfig=0;
}//关闭 nvs_close(my_handle);
}
}

读取的数据保存在 app_smartConfig.c 里的全局变量里,变量定义如下:

(2) 程序启动
和 4.7.4 第二步一样。程序启动后,先是读取了 smartconfig 配置,然后初始化了 LCD 和 LED,接着根 据 wifi_isConfig 决定是否需要启动 smartconfig。如果是需要启动 smartconfig,那么就打开红灯,进 入 smartconfig 流程;如果不需要启动 smartconfig,就进入 WiFi 的 STA 连接流程。

//用户函数入口,相当于 main 函数
void app_main(void)
{
ESP_ERROR_CHECK(nvs_flash_init()); tcpip_adapter_init(); read_Smartconfig();//读 smartconfig 配置 initLed();//LED  IO 口初始化//显示屏初始化以及显示相关的提示 Lcd_Init();//根据 NVS 数据决定是否要进入 Smartconfig if(1!=wifi_isConfig){
led_red(LED_ON);//打开红灯,表示正在配置中 lcd_display(0);
startSmartconfig();//配置 wifi 进入 smartconfig
}else{
//启动 STA,连接 AP wifi_init_sta(); lcd_display(1);
}
}

(3) 启动 websocket 任务
不管是否启用了 smartconfig,当网络连接上之后,都会进入文件 app_smartConfig.c 里的 wifi 回调函数, 当 wifi 取得 IP 后,就启动 websocket 任务,关键代码如下:

//wifi 连接事件回调函数
static esp_err_t smartconfig_event_handler(void *ctx, system_event_t *event)
{
......
case SYSTEM_EVENT_STA_GOT_IP://获取 IP ESP_LOGI(TAG1, "SYSTEM_EVENT_STA_GOT_IP");......//启动 WebSocketTask
extern void start_WebSocketTask(); start_WebSoc
break;
......
}void start_WebSocketTask()
{
//接收 websocket 数据任务:数据接收处理
xTaskCreate(&task_process_WebSocket,  "ws_process_rx",  2048,  NULL,  5, NULL);//websocket server 任务:建立 server、等待连接、连接、数据接收打包 xTaskCreate(&ws_server,  "ws_server",  2048,  NULL,  5, NULL);
}

(4) Websocket 服务创建
在文件 app_WebSocket_Task.c 的最前面,定义了 websocket 的端口:

#define WS_PORT  9998    /*server  tcp 端口*/

在文件 app_WebSocket_Task.c 里,通过 6 步完成 websocket 创建,其中第 4 步等待客户端的连接,连 接成功后,进入第 5 步接收发送处理数据,代码如下:

//websocket  server 建立服务
//conn:websocket  connect 句柄
//void:无
void  ws_server(void *pvParameters)
{
struct  netconn  *conn, *newconn;
//第一步:获取 tcp socket connect conn  = netconn_new(NETCONN_TCP);
//第二步:绑定 port netconn_bind(conn,  NULL, WS_PORT);
//第三步:监听 netconn_listen(conn);
//第四步:等待 client 连接
while  (netconn_accept(conn,  &newconn)  == ERR_OK)
{
//第五步:新连接:等待连接、连接过程、数据读取 ws_server_netconn_serve(newconn);
}
//第六步:关闭 websocket server connect netconn_close(conn); netconn_delete(conn);
}

Websocket 连接过程代码:

//websocket server 连接、握手、数据读取
//conn  :websocket connect 句柄
static void ws_server_netconn_serve(struct netconn *conn) {
......
//Check if malloc suceeded
if ((p_SHA1_Inp != NULL) && (p_SHA1_result != NULL)) {//接收“连接”过程的数据
if (netconn_recv(conn, &inbuf) == ERR_OK) {
//读取“连接”过程的数据到 buf netbuf_data(inbuf, (void**) &buf, &i);//把 server 的 key 传给 SHA1
for (i = 0; i < sizeof(WS_sec_conKey); i++)
{
//放在后 24 字节
p_SHA1_Inp[i + WS_CLIENT_KEY_L] = WS_sec_conKey[i];
}//搜索 client 的 key
p_buf = strstr(buf, WS_sec_WS_keys);
//找到 key
if (p_buf != NULL) {
//get Client Key
for (i = 0; i < WS_CLIENT_KEY_L; i++)
{
//放在前 24 字节
p_SHA1_Inp[i] = *(p_buf + sizeof(WS_sec_WS_keys) + i);
}// 计算 hash
esp_sha(SHA1, (unsigned char*) p_SHA1_Inp, strlen(p_SHA1_Inp), (unsigned char*) p_SHA1_result);
//转 base64
p_buf = (char*) base64_encode((unsigned char*) p_SHA1_result, SHA1_RES_L, (size_t*) &i);
//free SHA1 input free(p_SHA1_Inp);
//free SHA1 result free(p_SHA1_result);
//申请“握手”内存
p_payload = pvPortMallocCaps( sizeof(WS_srv_hs) + i - WS_SPRINTF_ARG_L, MALLOC_CAP_8BIT);if (p_payload != NULL) {
//准备“握手”帧
sprintf(p_payload, WS_srv_hs, i - 1, p_buf);
//发送“握手”帧
netconn_write(conn, p_payload, strlen(p_payload),NETCONN_COPY);
//free base64 free(p_buf);
//free “握手”内存 free(p_payload);
//websocket 连接成功 WS_conn = conn;//“接收数据”
while (netconn_recv(conn, &inbuf) == ERR_OK) {
//读取数据到 buf
netbuf_data(inbuf, (void**) &buf, &i);//扔到 p_frame_hdr
p_frame_hdr = (WS_frame_header_t*) buf;//此帧是“连接关闭”帧,直接退出
if (p_frame_hdr->opcode == WS_OP_CLS) break;//有效数据帧长度判断
if (p_frame_hdr->payload_length <= WS_STD_LEN) {
//数据扔到 p_buf
p_buf = (char*) &buf[sizeof(WS_frame_header_t)];//check if content is masked if (p_frame_hdr->mask) {
//申请内存
p_payload = pvPortMallocCaps( p_frame_hdr->payload_length + 1, MALLOC_CAP_8BIT);//申请内存成功
if (p_payload != NULL) {
//解码
for (i = 0; i < p_frame_hdr->payload_length; i++) p_payload[i] = (p_buf + WS_MASK_L)[i]^ p_buf[i % WS_MASK_L];//加个尾巴
p_payload[p_frame_hdr->payload_length] = 0;
}
} else{
//content is not masked p_payload = p_buf;
}//有效数据
if ((p_payload != NULL) && (p_frame_hdr->opcode == WS_OP_TXT)) {
//组包
WebSocket_frame_t  ws_frame;ws_frame.conenction=conn;ws_frame.frame_header=*p_frame_hdr;ws_frame.payload_length=p_frame_hdr->payload_length;ws_frame.payload=p_payload;//发送给另一个任务解析 xQueueSendFromISR(WebSocket_rx_queue,& ws_frame,0);
}} //数据中超长//清空 buf netbuf_delete(inbuf);
} //有效数据读取失败
} //握手内存申请失败
} //连接过程无 key
} //连接数据读取失败
} //p_SHA1_Inp!=NULL&p_SHA1_result!=NULL

Websocket 收到数据后,通过消息队列发送到另一个单独处理接收消息的任务处理,如果收到的消息 是“ON”打开绿灯,如果是“OFF”关闭绿灯,其他的消息通过函数 WS_write_data()返回发送端。

//websocket  server 数据解析
void  task_process_WebSocket(  void  *pvParameters )
{
{
//接收到 WebSocket 数据包
if(xQueueReceive(WebSocket_rx_queue,&  RX_frame, 3*portTICK_PERIOD_MS)==pdTRUE)
{
//打印下
printf("Websocket Data Length %d,  Data:  %.*s  \r\n",  RX_frame.payload_l ength,    RX_frame.payload_length,   RX_frame.payload);
if(memcmp(   RX_frame.payload,"ON",2)==0)
{
led_green(LED_ON);
}
else  if(memcmp(   RX_frame.payload,"OFF",3)==0)
{
led_green(LED_OFF);
}else {
//把接收到的数据回发
WS_write_data(  RX_frame.payload,   RX_frame.payload_length);
}//free memory
if ( RX_frame.payload != NULL) free( RX_frame.payload);
}
}......
while (1)

(5) Websocket 数据发送

在这里插入代码片
```//  websocket 发送数据
// p_data:数据指针
// length:数据长度
err_t  WS_write_data(char*  p_data,  size_t  length) {
//websocket 未连接,直接退出 if  (WS_conn  == NULL)
return ERR_CONN;//数据帧长度溢出,直接退出 if  (length  > WS_STD_LEN)
return ERR_VAL;err_t result;
//报头 WS_frame_header_t hdr; hdr.FIN  = 0x1;
hdr.payload_length = length; hdr.mask  = 0;
hdr.reserved = 0; hdr.opcode  = WS_OP_TXT;//发送报头
result =  netconn_write(WS_conn,  &hdr,  sizeof(WS_frame_header_t),  NETCONN_COPY); if  (result  != ERR_OK)
return result;//发送数据
return  netconn_write(WS_conn,  p_data,  length, NETCONN_COPY);4.8.5. 实验过程配置下载串口、波特率、编绎和程序下载的详细过程请往回看 3.1.4,在这个实验里都是一笔带过。 (1) 把开发板通过 USB 线接到电脑上,通过设备管理器查看生成的串口。开发板在我们演示电脑上生成的是 COM3。
(2) 在 menuconfig 菜单里配置下载程序串口。提供的例程配置的串口是 COM3,波特率为 921600。
(3) 通过 make all 编绎工程。
(4) 当编绎通过之后,使用命令 make flash 把程序下载到开发板上。或者参考 2.3.2 节,使用工具 下载。
(5) 使用串口工具打开开发板生成的串口,默认的波特率是 115200。 串口工具在目录:.\开发软件\串口工具-sscom32.rar。
(6) 打开按下开发板的复位键,让程序运行起来。第一次启动开发板上应该是亮起红灯,此时需要按 照 4.3.5 里的手机一键配置 smartconfig,先配置 ESP32 的 wifi 名字和密码。如果开发板的蓝灯 亮起表示 wifi 已经正确连接。(7) 使用 IE 打开 WebSocket 在线测试:http://www.websocket-test.com/
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201215163457670.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2J5dGVjaGlw,size_16,color_FFFFFF,t_70)(8) 可输入“ON”或者“OFF”用于控制板上的绿色灯。最后推荐一款开发套件,可以手淘扫码查看。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20201215163646586.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2J5dGVjaGlw,size_16,color_FFFFFF,t_70#pic_center)

最新文章

  1. 《C语言及程序设计》实践参考——当年第几天
  2. 图像特征检测描述(一):SIFT、SURF、ORB、HOG、LBP特征的原理概述及OpenCV代码实现
  3. 深度学习:背景建模高斯混合模型
  4. 信道分配 以太网
  5. 流量专家为114搜索提供权威流量访问统计
  6. JavaScript内存那点事
  7. window10设置文件夹备注
  8. 【TP】TP如何向模板中的js传变量
  9. 第三节 UNIX文件系统结构
  10. 详解K均值聚类算法(K-means Clustering)简易实例:从空调温度判别使用者
  11. 学习Axure RP原型设计
  12. rand函数和srand函数详解
  13. APP性能测试——启动时间
  14. android 程序运行,Android如何保持程序一直运行
  15. ToDesk软件安装教程(远程办公必备软件)
  16. 古人教你怎样识人不走眼
  17. Python 安装PyQt5失败:Permission denied:d3dcompiler_47.dll
  18. 云边端一体化技术白皮书
  19. 2019团体程序设计天梯赛L1 L1-1 PTA使我精神焕发L1-2 6翻了L1-3 敲笨钟L1-4 心理阴影面积L1-5 新胖子公式L1-6 幸运彩票L1-7 吃鱼还是吃肉
  20. Rapid IO接口测试工装研究

热门文章

  1. 仓库进销存管理软件系统如何更换电脑使用
  2. 4k显卡视频测试软件,4K分辨率下体验测试
  3. 代码制作数字流星雨_C语言实现流星雨
  4. 毕设过程小记—同步带传动选型计算+张紧机构
  5. Tomcat源码解析(一):开坑!手把手教你读Tomcat源码。
  6. 网页转PDF 在线工具 输入网址获取PDF
  7. 基于java jsp的铁路售票系统(火车票预订)ssh框架
  8. java系列视频教程下载
  9. 计算机端口详解(总结)
  10. 学完计算机技术的感受,计算机培训心得体会