赞
踩
这个实验的功能演示 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)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。