議)
WebSocket協(xié)議是什么WebSocket 解決的是服務(wù)端主動(dòng)推送客戶(hù)端和服務(wù)端同時(shí)發(fā)送低延遲、低開(kāi)銷(xiāo)支持文本和二進(jìn)制連接長(zhǎng)期保持典型場(chǎng)景聊天、實(shí)時(shí)通知、協(xié)作編輯、多人游戲、股票行情、IoT 控制等。連接建立HTTP Upgrade 握手WebSocket 不是直接裸 TCP 開(kāi)始而是先發(fā)一個(gè) HTTP/1.1 請(qǐng)求要求升級(jí)協(xié)議。客戶(hù)端請(qǐng)求GET /chat HTTP/1.1 Host: example.com Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ Sec-WebSocket-Version: 13 Origin: https://example.com Sec-WebSocket-Protocol: chat, superchat關(guān)鍵頭Upgrade: websocket要求升級(jí)到 WebSocket。Connection: Upgrade表示希望升級(jí)連接。Sec-WebSocket-Key客戶(hù)端生成的 16 字節(jié)隨機(jī)數(shù)Base64 編碼。Sec-WebSocket-Version: 13當(dāng)前標(biāo)準(zhǔn)版本。Origin瀏覽器自動(dòng)帶上用于服務(wù)端做來(lái)源校驗(yàn)。Sec-WebSocket-Protocol可選表示應(yīng)用層子協(xié)議如chat、mqtt。Sec-WebSocket-Extensions可選表示希望使用的擴(kuò)展如壓縮。服務(wù)端響應(yīng) 如果同意升級(jí)HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOo Sec-WebSocket-Protocol: chatSec-WebSocket-Accept的計(jì)算方式accept base64( sha1( Sec-WebSocket-Key 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )這個(gè)固定 GUID 是協(xié)議規(guī)定的用來(lái)證明服務(wù)端確實(shí)理解 WebSocket而不是普通 HTTP 服務(wù)器誤響應(yīng)。握手成功后這條 TCP 連接就不再是 HTTP而是 WebSocket 幀流。WebSocket 幀格式WebSocket 的數(shù)據(jù)以“幀”為單位傳輸。一個(gè)消息可以由一個(gè)或多個(gè)幀組成。0 1 2 30 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1--------------------------------------------------------|F|R|R|R| opcode|M| Payload len | Extended payload length ||I|S|S|S| (4) |A| (7) | (16/64) ||N|V|V|V| |S| | (if payload len126/127) || |1|2|3| |K| | |------------------------- - - - - - - - - - - - - - - - | Extended payload length continued, if payload len 127 | - - - - - - - - - - - - - - - -------------------------------| |Masking-key, if MASK set to 1 |--------------------------------------------------------------| Masking-key (continued) | Payload Data |-------------------------------- - - - - - - - - - - - - - - - : Payload Data continued ... :---------------------------------------------------------------字段說(shuō)明字段長(zhǎng)度說(shuō)明FIN1 bit是否為消息最后一幀。1 表示結(jié)束0 表示還有后續(xù)幀RSV1/2/3各 1 bit保留位通常為 0擴(kuò)展可能使用opcode4 bit幀類(lèi)型MASK1 bit是否使用掩碼??蛻?hù)端發(fā)給服務(wù)端必須為 1服務(wù)端發(fā)給客戶(hù)端必須為 0Payload len7 bit負(fù)載長(zhǎng)度Extended payload length0/16/64 bit負(fù)載長(zhǎng)度擴(kuò)展Masking-key0 或 4 字節(jié)掩碼密鑰MASK1 時(shí)存在Payload Data變長(zhǎng)實(shí)際數(shù)據(jù)Opcode 幀類(lèi)型Opcode類(lèi)型說(shuō)明0x0Continuation延續(xù)幀用于分片消息0x1TextUTF-8 文本消息0x2Binary二進(jìn)制消息0x3-0x7保留非控制幀保留0x8Close關(guān)閉連接0x9Ping心跳探測(cè)0xAPong心跳響應(yīng)0xB-0xF保留控制幀保留文本消息必須是合法 UTF-8。如果接收方發(fā)現(xiàn)不是合法 UTF-8通常關(guān)閉連接并返回1007。消息、分片與控制幀WebSocket 是“消息導(dǎo)向”的不是字節(jié)流。一個(gè)消息可以分成多個(gè)幀。第一幀F(xiàn)IN0, opcode0x1, payloadHel 第二幀F(xiàn)IN0, opcode0x0, payloadlo 第三幀F(xiàn)IN1, opcode0x0, payloadWorld接收方按順序重組得到Hello World。規(guī)則第一條分片幀的 opcode 是0x1或0x2。后續(xù)分片幀 opcode 必須是0x0。最后一幀 FIN1??刂茙梢源┎逶诜制⒅虚g??刂茙荒芊制現(xiàn)IN 必須為 1負(fù)載不能超過(guò) 125 字節(jié)??刂茙琍ing發(fā)送方詢(xún)問(wèn)對(duì)方是否在線。Pong收到 Ping 后必須盡快回 Pong且 payload 要和 Ping 相同。Close關(guān)閉握手。關(guān)閉握手與狀態(tài)碼關(guān)閉 WebSocket 不是直接斷開(kāi) TCP而是先發(fā) Close 幀。Close 幀格式opcode 0x8 payload 2 字節(jié)狀態(tài)碼 可選 UTF-8 原因流程一方發(fā)送 Close 幀。另一方收到后如果還沒(méi)發(fā) Close就回一個(gè) Close 幀。然后關(guān)閉 TCP 連接。發(fā)送 Close 后不應(yīng)再發(fā)送數(shù)據(jù)幀。常見(jiàn)狀態(tài)碼狀態(tài)碼含義1000正常關(guān)閉1001端點(diǎn)離開(kāi)如服務(wù)端關(guān)閉或?yàn)g覽器跳轉(zhuǎn)1002協(xié)議錯(cuò)誤1003收到不支持的數(shù)據(jù)類(lèi)型1007數(shù)據(jù)無(wú)效如文本不是合法 UTF-81008策略違規(guī)1009消息太大1010客戶(hù)端期望服務(wù)端協(xié)商某個(gè)擴(kuò)展1011服務(wù)端內(nèi)部錯(cuò)誤1012服務(wù)重啟1013稍后再試1014網(wǎng)關(guān)錯(cuò)誤1015TLS 握手失敗不能由端點(diǎn)主動(dòng)發(fā)送3000-3999IANA 注冊(cè)使用注意1005表示“沒(méi)有狀態(tài)碼”1006表示“異常關(guān)閉”它們不能出現(xiàn)在 Close 幀中。心跳與?;頦ebSocket 本身有 Ping/Pong但很多中間設(shè)備會(huì)斷開(kāi)長(zhǎng)時(shí)間無(wú)數(shù)據(jù)的連接。常見(jiàn)做法服務(wù)端定期發(fā) Ping客戶(hù)端回 Pong。應(yīng)用層定期發(fā) JSON 心跳如{type:ping}。設(shè)置空閑超時(shí)超時(shí)后主動(dòng)關(guān)閉。客戶(hù)端斷線后實(shí)現(xiàn)指數(shù)退避重連。瀏覽器端不能直接發(fā) Ping所以很多應(yīng)用用業(yè)務(wù)消息做心跳。擴(kuò)展與子協(xié)議客戶(hù)端httpSec-WebSocket-Protocol: chat, superchat服務(wù)端選擇一個(gè)Sec-WebSocket-Protocol: chat它只是告訴雙方“我們接下來(lái)用 chat 協(xié)議通信”具體格式由應(yīng)用定義。Sec-WebSocket-Extensions用于協(xié)商協(xié)議層擴(kuò)展。最常見(jiàn)的是permessage-deflate對(duì)消息進(jìn)行壓縮。擴(kuò)展可能使用 RSV 位例如 RSV1 表示壓縮。使用壓縮時(shí)要注意CPU 和內(nèi)存開(kāi)銷(xiāo)壓縮炸彈壓縮側(cè)信道攻擊風(fēng)險(xiǎn)是否允許上下文接管總結(jié):WebSocket 是通過(guò)一次 HTTP 握手升級(jí)出來(lái)的全雙工 TCP 通信協(xié)議之后雙方可以隨時(shí)以幀為單位互發(fā)消息。esp32單片機(jī)的示例代碼// main/main.c #include stdio.h #include string.h #include freertos/FreeRTOS.h #include freertos/task.h #include freertos/event_groups.h #include esp_system.h #include esp_wifi.h #include esp_event.h #include esp_log.h #include nvs_flash.h #include esp_websocket_client.h #include cJSON.h static const char *TAG WS_CLIENT; // WiFi 配置 #define WIFI_SSID 你的WiFi名 #define WIFI_PASS 你的WiFi密碼 // WebSocket 服務(wù)器 #define WS_URI wss://echo.websocket.events // 若用普通 ws ws://192.168.1.100:81/ws static EventGroupHandle_t s_wifi_eg; #define WIFI_CONNECTED_BIT BIT0 static esp_websocket_client_handle_t s_ws; // ---------- WiFi 事件 ---------- static void wifi_event_handler(void *arg, esp_event_base_t base, int32_t id, void *data) { if (base WIFI_EVENT id WIFI_EVENT_STA_START) { esp_wifi_connect(); } else if (base WIFI_EVENT id WIFI_EVENT_STA_DISCONNECTED) { ESP_LOGW(TAG, WiFi 斷開(kāi)重連中...); esp_wifi_connect(); xEventGroupClearBits(s_wifi_eg, WIFI_CONNECTED_BIT); } else if (base IP_EVENT id IP_EVENT_STA_GOT_IP) { ip_event_got_ip_t *e (ip_event_got_ip_t *)data; ESP_LOGI(TAG, 獲取 IP: IPSTR, IP2STR(e-ip_info.ip)); xEventGroupSetBits(s_wifi_eg, WIFI_CONNECTED_BIT); } } static void wifi_init_sta(void) { s_wifi_eg xEventGroupCreate(); ESP_ERROR_CHECK(nvs_flash_init()); ESP_ERROR_CHECK(esp_netif_init()); ESP_ERROR_CHECK(esp_event_loop_create_default()); esp_netif_create_default_wifi_sta(); wifi_init_config_t cfg WIFI_INIT_CONFIG_DEFAULT(); ESP_ERROR_CHECK(esp_wifi_init(cfg)); ESP_ERROR_CHECK(esp_event_handler_instance_register( WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, NULL)); ESP_ERROR_CHECK(esp_event_handler_instance_register( IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, NULL)); wifi_config_t wc { .sta { .ssid WIFI_SSID, .password WIFI_PASS, }, }; ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, wc)); ESP_ERROR_CHECK(esp_wifi_start()); ESP_LOGI(TAG, 等待 WiFi 連接...); xEventGroupWaitBits(s_wifi_eg, WIFI_CONNECTED_BIT, pdFALSE, pdTRUE, portMAX_DELAY); ESP_LOGI(TAG, WiFi 已連接); } // ---------- 發(fā)送 JSON 消息 ---------- static void send_json(const char *type, const char *extra_key, const char *extra_val) { if (!esp_websocket_client_is_connected(s_ws)) return; cJSON *root cJSON_CreateObject(); cJSON_AddStringToObject(root, type, type); cJSON_AddNumberToObject(root, ts, esp_log_timestamp()); if (extra_key) cJSON_AddStringToObject(root, extra_key, extra_val); char *out cJSON_PrintUnformatted(root); ESP_LOGI(TAG, 發(fā)送: %s, out); esp_websocket_client_send_text(s_ws, out, strlen(out), portMAX_DELAY); cJSON_free(out); cJSON_Delete(root); } // ---------- WebSocket 事件回調(diào) ---------- static void ws_event_handler(void *arg, 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 已連接); send_json(hello, device, esp32-idf); break; case WEBSOCKET_EVENT_DISCONNECTED: ESP_LOGW(TAG, WebSocket 已斷開(kāi)); break; case WEBSOCKET_EVENT_DATA: ESP_LOGI(TAG, 收到 opcode%d, len%d, payload_offset%d, data-op_code, data-data_len, data-payload_offset); if (data-op_code 0x08) { ESP_LOGW(TAG, 收到 Close 幀: %.*s, data-data_len, (char *)data-data_ptr); } else if (data-op_code 0x09) { ESP_LOGI(TAG, 收到 Ping); } else if (data-op_code 0x0A) { ESP_LOGI(TAG, 收到 Pong); } else if (data-op_code 0x01 data-data_len 0) { // 文本消息注意分片payload_offset0 時(shí)是首片 if (data-payload_offset 0) { ESP_LOGI(TAG, 文本: %.*s, data-data_len, data-data_ptr); // 嘗試解析 JSON cJSON *root cJSON_ParseWithLength(data-data_ptr, data-data_len); if (root) { cJSON *type cJSON_GetObjectItem(root, type); if (cJSON_IsString(type)) { ESP_LOGI(TAG, 消息 type%s, type-valuestring); } cJSON_Delete(root); } } } break; case WEBSOCKET_EVENT_ERROR: ESP_LOGE(TAG, WebSocket 錯(cuò)誤); break; case WEBSOCKET_EVENT_CLOSED: ESP_LOGW(TAG, WebSocket 連接已關(guān)閉); break; default: break; } } // ---------- 啟動(dòng) WebSocket 客戶(hù)端 ---------- static void ws_start(void) { esp_websocket_client_config_t cfg { .uri WS_URI, .reconnect_timeout_ms 3000, .network_timeout_ms 10000, // 若服務(wù)器要求子協(xié)議 // .subprotocol chat, // 若是 ws:// 無(wú) TLS不用管下面兩項(xiàng) // .cert_pem NULL, // 若不用 CA bundle可指定 PEM // .use_global_ca_store false, }; s_ws esp_websocket_client_init(cfg); ESP_ERROR_CHECK(esp_websocket_client_start(s_ws)); // 注冊(cè)事件 esp_websocket_register_events(s_ws, WEBSOCKET_EVENT_ANY, ws_event_handler, NULL); // 如需自定義請(qǐng)求頭如 Token // esp_websocket_client_append_header(s_ws, Authorization, Bearer xxx); // esp_websocket_client_append_header(s_ws, Origin, http://esp32.local); ESP_LOGI(TAG, WebSocket 客戶(hù)端啟動(dòng)); } // ---------- 周期性心跳任務(wù) ---------- static void heartbeat_task(void *arg) { while (1) { vTaskDelay(pdMS_TO_TICKS(5000)); if (esp_websocket_client_is_connected(s_ws)) { send_json(heartbeat, NULL, NULL); } } } void app_main(void) { wifi_init_sta(); ws_start(); xTaskCreate(heartbeat_task, hb, 4096, NULL, 5, NULL); }