ESP32WebSocket

这个实验的功能演示 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)

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

bytechip

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值