前言,終于要到網路模型的最后一層,第四層,應用層,http、websocket的實踐了,
文章目錄
- ESP32 單片機學習筆記 - 08 - WebSocket客戶端
- 一、應用層協議 科普概念
- 二、編程指南 翻譯
- 1. 概述
- 2. 特點
- 3. 配置
- 1)URI
- 2)TLS
- 3)子協議
- 4. 事件
- 5. 限制和已知問題
- 6. 應用舉例
- 三、例程決議
- 四、試驗總結
- 1. 查看握手協議
- 2. 連接 Websocket服務器
- 【小插曲】使用 node.js 撰寫簡易 Websocket服務器
- 3. 實驗現象
- 4. 總結的總結
ESP32 單片機學習筆記 - 08 - WebSocket客戶端
一、應用層協議 科普概念
在看例程之前先補補概念,我現在還是一臉懵逼,不知道這個是什么的狀態,明明上一層的tcp已經能夠通訊了,怎么又加多了一層,
- 各個知識點講解:90分鐘搞定Web基礎:網路協議,HTTP,Web服務器(只挑想看的),
- WebSocket大概介紹:WebSocket打造在線聊天室【完結】(只看了第一個介紹視頻),
- 知乎白話文解釋:WebSocket 是什么原理?為什么可以實作持久連接?(文字),
- 幾分鐘快速入門:前后端互動之 HTTP 協議(AE短視頻?懷念),
- WebSocket的使用教程:WebSocket 教程,
- 快速看完上面的資料后,對
Http和WebSocket都有了初步的認知,總結以下幾點:
http有1.0和1.1版本,現在基本1.1版本,http屬于無狀態、無連接、單向的應用層協議,即通訊請求只能有客戶端發起,服務端只負責回應請求,不能主動發起,在應用中表現為 通過頻繁的請求實作長輪詢 ,WebSocket對比http,最大的特點就是可以主動向客戶端發起請求,兩者屬于交集關系,有相同的地方,但不一樣(所以應該是并列關系?),

- 二者的握手協議也很相識,下圖中,左圖是
WebSocket右圖是http,可發現格式差不多,

- 總結回顧之前三層內容:Ethernet,TCP,IP協議簡介(鏈路層、網路層、傳輸層),梳理一下之前學到的知識(按我個人的理解):
- wifi和Ethernet協議,本身有通訊協議,類比uart底層發送,定義電信號0/1的層面,有自身的校驗位、起始位結束位等,屬于鏈路層(也稱網路介面層),主要是物理層面,保證了位元組資料的正確性,
- IP協議,本身有通訊協議,規定了資料要發送給網路中的哪個設備,類比片選等功能,屬于網路層,主要是指定設備,保證傳輸物件的正確性,
- TCP/UDP協議,也是有協議,用鏈路層得到的資訊都是0/1的位元組資訊,打包了一些通訊資料,保證這些通訊資料的正確性,類比我用uart通訊時也寫了一個幀頭幀尾校驗位的協議,如果一個資料包不符合設定要求,我就認為錯誤不可用,這時已經能初步得到想要的通訊資料了,不是八位的0/1或是單個字符,而是按我自己設定的16位、32位資料讀取,屬于傳輸層,主要是資料包的傳輸,保證設備間資料包傳輸的正確性,

- 通過上圖就能清晰明了的理解,資料是如何被層層打包的,再上一層,就是應用層 —— HTTP/WebSocket協議,用傳輸層得到的資料包為基礎,進一步規定兩個設備的通訊(對話)方式,是一問一答還是多問多答,還是只答不問、只問不答……這樣的規定是很重要的,因為網路通訊中要同時訪問多個服務器/設備,應用層的協議有很多,適用不同的應用場景,
二、編程指南 翻譯
編程指南:ESP WebSocket Client,
系列教程:第十八章 ESP32的WebSocket服務器,
官方例程:examples/protocols/websocket,
1. 概述
ESP WebSocket客戶端是用于ESP32的WebSocket協議客戶端實作,
2. 特點
- 支持基于
TCP的WebSocket,帶有mbedtls的TLS, - 易于設定
URI, - 多個實體(一個應用程式中的多個客戶機),
3. 配置
1)URI
- 支持
ws,wss方案, - WebSocket樣本:①
ws://echo.websocket.org:WebSocket通過TCP,默認埠80,②wss://echo.websocket.org:WebSocket通過SSL,默認埠443,
// 最小的配置:
const esp_websocket_client_config_t ws_cfg = {
.uri = "ws://echo.websocket.org",
};
// WebSocket客戶端支持在URI中同時使用路徑和查詢,示例:
const esp_websocket_client_config_t ws_cfg = {
.uri = "ws://echo.websocket.org/connectionhandler?id=104",
};
// 如果在 esp_websocket_client_config_t 中有任何與URI相關的選項,則URI定義的選項將被覆寫,示例:
const esp_websocket_client_config_t ws_cfg = {
.uri = "ws://echo.websocket.org:123",
.port = 4567, //WebSocket客戶端將使用埠4567連接到websocket.org
};
2)TLS
- 如果需要驗證服務器端,需要提供
PEM格式的證書,并在“websocket_client_config_t”中提供cert_pem,如果沒有提供證書,那么TLS連接將默認不需要驗證,
// 配置
const esp_websocket_client_config_t ws_cfg = {
.uri = "wss://echo.websocket.org",
.cert_pem = (const char *)websocket_org_pem_start,
};
3)子協議
- 客戶端對服務器回應中的子協議欄位無關,并且無論服務器回應什么都將接受連接,
// 配置結構中的子協議欄位可用于請求子協議
const esp_websocket_client_config_t ws_cfg = {
.uri = "ws://websocket.org",
.subprotocol = "soap",
};
4. 事件
WEBSOCKET_EVENT_CONNECTED:客戶端與服務器成功建立連接,客戶機現在可以發送和接收資料了,不包含事件資料,WEBSOCKET_EVENT_DISCONNECTED:由于傳輸層讀取資料失敗(例如服務器不可用),客戶端已經終止連接,不包含事件資料,WEBSOCKET_EVENT_DATA:客戶端已經成功接收并決議了一個WebSocket幀,事件資料包含一個指向有效載荷資料的指標,有效載荷資料的長度以及接收幀的操作碼,如果長度超過緩沖區大小,則訊息可能被分割成多個事件,此事件也將被發布為非有效載荷幀,例如pong或連接關閉幀,WEBSOCKET_EVENT_ERROR:在客戶端的當前實作中未使用,
// 如果客戶端句柄需要在事件處理程式中,它可以通過傳遞給事件處理程式的指標訪問:
esp_websocket_client_handle_t client = (esp_websocket_client_handle_t)handler_args;
5. 限制和已知問題
- 客戶端可以在握手期間請求服務器使用子協議,但是不會對來自服務器的回應進行任何與子協議相關的檢查,
6. 應用舉例
- 一個簡單的
WebSocket示例,使用esp_websocket_client建立一個WebSocket連接,并通過websocket.org服務器發送/接收資料,可以在這里找到:protocols/ WebSocket,
// WebSocket客戶端支持以文本資料幀的形式發送資料,這告知應用層有效載荷資料是編碼為UTF-8的文本資料,例子:
esp_websocket_client_send_text(client, data, len, portMAX_DELAY);
三、例程決議
- 一堆列印和老三件初始化,
ESP_LOGI(TAG, "[APP] Startup..");
ESP_LOGI(TAG, "[APP] Free memory: %d bytes", esp_get_free_heap_size());
ESP_LOGI(TAG, "[APP] IDF version: %s", esp_get_idf_version());
esp_log_level_set("*", ESP_LOG_INFO);
esp_log_level_set("WEBSOCKET_CLIENT", ESP_LOG_DEBUG);
esp_log_level_set("TRANS_TCP", ESP_LOG_DEBUG);
ESP_ERROR_CHECK(nvs_flash_init());
ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
- 初始化聯網,開啟聯網,記得打開專案配置選單(
idf.py menuconfig)修改配置,
/* This helper function configures Wi-Fi or Ethernet, as selected in menuconfig.
* Read "Establishing Wi-Fi or Ethernet Connection" section in
* examples/protocols/README.md for more information about this function.
*/
ESP_ERROR_CHECK(example_connect());
- 例程創建了一個定時器和信號用于除錯,如果定時器超時,就表示已經10s沒有收到資訊,同時釋放信號量,如果有收到資訊,會觸發事件重置定時器,
// 定時器超時函式
static void shutdown_signaler(TimerHandle_t xTimer)
{
ESP_LOGI(TAG, "No data received for %d seconds, signaling shutdown", NO_DATA_TIMEOUT_SEC);
// 宏定義 釋放信號量
xSemaphoreGive(shutdown_sema);
}
// 創建一個新的軟體計時器實體,并回傳一個句柄,通過這個句柄可以參考創建的軟體計時器,
shutdown_signal_timer = xTimerCreate("Websocket shutdown timer", // 只是一個文本名稱,不被內核使用,
NO_DATA_TIMEOUT_SEC * 1000 / portTICK_PERIOD_MS, // 計時器周期(單位是tick),
pdFALSE, // 計時器將在到期時自動重新加載,(不會)
NULL, // 為每個計時器分配一個唯一的id等于它的陣列索引,
shutdown_signaler); // 每個計時器在到期時呼叫同一個回呼,
// 創建一個新的二進制信號量實體,并回傳一個句柄,通過這個句柄可以參考新的信號量,
shutdown_sema = xSemaphoreCreateBinary();
- 除了聯網的內容需要配置,還有
WebSocket客戶端的URI需要配置,如果第一個選項設定了From stdin,例程就會開啟WEBSOCKET_URI_FROM_STDIN宏定義,在聯網成功后,連接服務器的URI需要手動輸入(在監視器中),如果設定為From string,會開啟CONFIG_WEBSOCKET_URI宏定義,直接配置URI設定,

// 打包函式,用于獲取uri字串
#if CONFIG_WEBSOCKET_URI_FROM_STDIN
static void get_string(char *line, size_t size)
{
int count = 0;
while (count < size) {
int c = fgetc(stdin);
if (c == '\n') {
line[count] = '\0';
break;
} else if (c > 0 && c < 127) {
line[count] = c;
++count;
}
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
#endif /* CONFIG_WEBSOCKET_URI_FROM_STDIN */
// 是否需要手動輸入uri地址,若配置中不存在則需要
#if CONFIG_WEBSOCKET_URI_FROM_STDIN
char line[128];
ESP_LOGI(TAG, "Please enter uri of websocket endpoint");
get_string(line, sizeof(line));
websocket_cfg.uri = line;
ESP_LOGI(TAG, "Endpoint uri: %s\n", line);
#else
// 直接獲取uri地址
websocket_cfg.uri = CONFIG_WEBSOCKET_URI;
#endif /* CONFIG_WEBSOCKET_URI_FROM_STDIN */
/*
這個函式必須是第一個呼叫的函式,它回傳一個 esp_websocket_client_handle_t ,
你必須把它作為介面中其他函式的輸入,
當操作完成時,這個呼叫必須有一個對應的 esp_websocket_client_destroy 呼叫,
*/
esp_websocket_client_handle_t client = esp_websocket_client_init(&websocket_cfg);
// 注冊Websocket事件,
esp_websocket_register_events(client, WEBSOCKET_EVENT_ANY, websocket_event_handler, (void *)client);
// 打開WebSocket連接,
esp_websocket_client_start(client);
- 在上一步中注冊了
Websocket事件,以下是處理事件的函式,
static void websocket_event_handler(void *handler_args, esp_event_base_t base, int32_t event_id, void *event_data)
{
esp_websocket_event_data_t *data = (esp_websocket_event_data_t *)event_data;
switch (event_id) {
// 客戶端與服務器成功建立連接,客戶機現在可以發送和接收資料了,不包含事件資料,
case WEBSOCKET_EVENT_CONNECTED:
ESP_LOGI(TAG, "WEBSOCKET_EVENT_CONNECTED");
break;
// 由于傳輸層讀取資料失敗(例如服務器不可用),客戶端已經終止連接,不包含事件資料,
case WEBSOCKET_EVENT_DISCONNECTED:
ESP_LOGI(TAG, "WEBSOCKET_EVENT_DISCONNECTED");
break;
// 客戶端已經成功接收并決議了一個`WebSocket`幀,
// 事件資料包含一個指向有效載荷資料的指標,有效載荷資料的長度以及接收幀的操作碼,
// 如果長度超過緩沖區大小,則訊息可能被分割成多個事件,
// 此事件也將被發布為非有效載荷幀,例如pong或連接關閉幀,
case WEBSOCKET_EVENT_DATA:
ESP_LOGI(TAG, "WEBSOCKET_EVENT_DATA");
ESP_LOGI(TAG, "Received opcode=%d", data->op_code);
ESP_LOGW(TAG, "Received=%.*s", data->data_len, (char *)data->data_ptr);
ESP_LOGW(TAG, "Total payload length=%d, data_len=%d, current payload offset=%d\r\n", data->payload_len, data->data_len, data->payload_offset);
xTimerReset(shutdown_signal_timer, portMAX_DELAY);
break;
// 在客戶端的當前實作中未使用,
case WEBSOCKET_EVENT_ERROR:
ESP_LOGI(TAG, "WEBSOCKET_EVENT_ERROR");
break;
}
}
- 啟動定時器,回圈查詢接收資料,
- 表現結果:一直回圈(1s間隔)查詢連接狀態,如果連接上,就連續發送10次資料,然后退出,
- 不然就一直查詢連接狀態,如果沒有發送滿10次,也沒有收到資料,定時器就會每10s列印一次報錯,且釋放一次信號量,
- 如果一直沒連接上,試驗現象就是每隔10秒列印,
// 啟動定時器
xTimerStart(shutdown_signal_timer, portMAX_DELAY);
char data[32];
int i = 0;
while (i < 10)
{
// 檢查WebSocket客戶端連接狀態,
if (esp_websocket_client_is_connected(client))
{
int len = sprintf(data, "hello %04d", i++);
ESP_LOGI(TAG, "Sending %s", data);
// 將文本資料寫入WebSocket連接
esp_websocket_client_send_text(client, data, len, portMAX_DELAY);
}
vTaskDelay(1000 / portTICK_RATE_MS);
}
- 已經發送完資料,等待是否沒有資料接收,然后關閉程式,退出,沒有收到資料的判斷依據是信號量,只要進入一次定時器超時就會滿足,
// 宏獲取信號量
xSemaphoreTake(shutdown_sema, portMAX_DELAY);
/*
停止WebSocket連接而沒有WebSocket關閉握手,
此API停止ws客戶端并直接關閉TCP連接,而不發送關閉幀,
使用 esp_websocket_client_close() 以一種干凈的方式關閉連接是一個很好的實踐,
*/
esp_websocket_client_stop(client);
ESP_LOGI(TAG, "Websocket Stopped");
/*
銷毀 WebSocket 連接并釋放所有資源,
這個函式必須是會話呼叫的最后一個函式,
它與 esp_websocket_client_init 函式相反,
呼叫時必須使用與 esp_websocket_client_init 呼叫回傳的輸入相同的句柄,
這可能會關閉該句柄使用過的所有連接,
*/
esp_websocket_client_destroy(client);
四、試驗總結
1. 查看握手協議
- 我使用網路除錯助手上位機,連接esp32,注意,該上位機沒有
Websocket服務端功能,我只是用來看esp32發送的tcp資料到底是什么, - esp32配置中的URI設定為
ws://192.168.1.101:8080,連接到我的上位機,然后上位機就接收到了Websocket客戶端的握手資料,

- 但是這個上位機不具有
Websocket服務端的功能,所以不會回應握手資料,esp32也就連接不上,發送不了10次資料,也接收不到資料,就會每10s進入一次超時函式列印錯誤,從接收資訊上看,客戶端好像在重復發送握手,嘗試鏈接,我在例程里沒看到具體寫出來的步驟,應該是打包在api中實作的功能,

- 對比上一個筆記,tcp例程的實驗,加深理解: 傳輸層就是發送應用層的資料 ,不同層之間的資料是層層打包的關系,
2. 連接 Websocket服務器
- 例程中自帶的python腳本運行不了,搜索不到
Websocket服務器的小工具,基本都是Websocket客戶端的小工具,真的是奇了怪了, - 在看Websocket概念時明白到可以用
node.js寫一個服務器,恰巧之前為了學上位機安裝了node.js,可以用上了,
【小插曲】使用 node.js 撰寫簡易 Websocket服務器
-
第一步應該先安裝nodejs,然后安裝npm,設定npm軟體包的安裝目錄:Nodejs+npm詳細安裝,
-
第二步要注意是否匯入了npm安裝包的路徑,以免在撰寫程式匯入包時報錯找不到:nodejs require模塊找不到的兩種解決辦法,
-
在撰寫程式之前,先安裝模塊
npm install nodejs-websocket (-g),要注意安裝模塊的方法:【nodejs】使用 npm安裝模塊方法,本地安裝就是指直接安裝在讀取目錄的node_modules檔案夾下,如果沒有就會新建,直接到全域目錄下使用本地安裝,得到的效果和全域安裝一樣, -
撰寫參考教程,nodejs-websocket創建websocket服務器,注意教程中用的是本地安裝,包不算大,所以本地安裝也沒關系,不過已經全域安裝就不用再安裝了,
-
然后我直接拷貝了教程中的代碼,僅把埠改成了
8080,運行,成功(這個程式好像有點bug,無論如何都會列印“連接成功”),好了,剩下的之后學node.js時再深究,
3. 實驗現象
- 半完整的實驗現象如下圖,還有一半是列印接收內容,因為服務器沒有寫發送內容,所以就沒有,
注意事項:連接前記得查看自己電腦的IP地址,然后輸入正確的
URI,不然連接不上,我剛剛重啟了一下路由器,我的ip地址就變了……要重新修改,

- 客戶端在連續發送10次資料,且進入了一次定時器超時函式后,就關閉了
Websocket客戶端功能, - 另外,不知道為什么看同步效果好差,懷疑是不是服務器小工具的問題,居然肉眼可見的不同步,達到1s以上(即客戶端發送2次資料,服務器才列印一次),暫時留下疑問,日后解答,
4. 總結的總結
以上沒了,下一步繼續按著教程系列學習,話說這節的教程系列的內容的
Websocket服務器,而且用到的api好像都是再次宏定義后的介面,在官方api里找不到(我去沒看原始碼證實猜想),
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/287684.html
標籤:其他
