議)
WebSocket協(xié)議是什么WebSocket 解決的是服務(wù)端主動推送客戶端和服務(wù)端同時發(fā)送低延遲、低開銷支持文本和二進制連接長期保持典型場景聊天、實時通知、協(xié)作編輯、多人游戲、股票行情、IoT 控制等。連接建立HTTP Upgrade 握手WebSocket 不是直接裸 TCP 開始而是先發(fā)一個 HTTP/1.1 請求要求升級協(xié)議??蛻舳苏埱驡ET /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要求升級到 WebSocket。Connection: Upgrade表示希望升級連接。Sec-WebSocket-Key客戶端生成的 16 字節(jié)隨機數(shù)Base64 編碼。Sec-WebSocket-Version: 13當前標準版本。Origin瀏覽器自動帶上用于服務(wù)端做來源校驗。Sec-WebSocket-Protocol可選表示應(yīng)用層子協(xié)議如chat、mqtt。Sec-WebSocket-Extensions可選表示希望使用的擴展如壓縮。服務(wù)端響應(yīng) 如果同意升級HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbKxOo Sec-WebSocket-Protocol: chatSec-WebSocket-Accept的計算方式accept base64( sha1( Sec-WebSocket-Key 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 ) )這個固定 GUID 是協(xié)議規(guī)定的用來證明服務(wù)端確實理解 WebSocket而不是普通 HTTP 服務(wù)器誤響應(yīng)。握手成功后這條 TCP 連接就不再是 HTTP而是 WebSocket 幀流。WebSocket 幀格式WebSocket 的數(shù)據(jù)以“幀”為單位傳輸。一個消息可以由一個或多個幀組成。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 ... :---------------------------------------------------------------字段說明字段長度說明FIN1 bit是否為消息最后一幀。1 表示結(jié)束0 表示還有后續(xù)幀RSV1/2/3各 1 bit保留位通常為 0擴展可能使用opcode4 bit幀類型MASK1 bit是否使用掩碼??蛻舳税l(fā)給服務(wù)端必須為 1服務(wù)端發(fā)給客戶端必須為 0Payload len7 bit負載長度Extended payload length0/16/64 bit負載長度擴展Masking-key0 或 4 字節(jié)掩碼密鑰MASK1 時存在Payload Data變長實際數(shù)據(jù)Opcode 幀類型Opcode類型說明0x0Continuation延續(xù)幀用于分片消息0x1TextUTF-8 文本消息0x2Binary二進制消息0x3-0x7保留非控制幀保留0x8Close關(guān)閉連接0x9Ping心跳探測0xAPong心跳響應(yīng)0xB-0xF保留控制幀保留文本消息必須是合法 UTF-8。如果接收方發(fā)現(xiàn)不是合法 UTF-8通常關(guān)閉連接并返回1007。消息、分片與控制幀WebSocket 是“消息導(dǎo)向”的不是字節(jié)流。一個消息可以分成多個幀。第一幀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。控制幀不能分片F(xiàn)IN 必須為 1負載不能超過 125 字節(jié)??刂茙琍ing發(fā)送方詢問對方是否在線。Pong收到 Ping 后必須盡快回 Pong且 payload 要和 Ping 相同。Close關(guān)閉握手。關(guān)閉握手與狀態(tài)碼關(guān)閉 WebSocket 不是直接斷開 TCP而是先發(fā) Close 幀。Close 幀格式opcode 0x8 payload 2 字節(jié)狀態(tài)碼 可選 UTF-8 原因流程一方發(fā)送 Close 幀。另一方收到后如果還沒發(fā) Close就回一個 Close 幀。然后關(guān)閉 TCP 連接。發(fā)送 Close 后不應(yīng)再發(fā)送數(shù)據(jù)幀。常見狀態(tài)碼狀態(tài)碼含義1000正常關(guān)閉1001端點離開如服務(wù)端關(guān)閉或瀏覽器跳轉(zhuǎn)1002協(xié)議錯誤1003收到不支持的數(shù)據(jù)類型1007數(shù)據(jù)無效如文本不是合法 UTF-81008策略違規(guī)1009消息太大1010客戶端期望服務(wù)端協(xié)商某個擴展1011服務(wù)端內(nèi)部錯誤1012服務(wù)重啟1013稍后再試1014網(wǎng)關(guān)錯誤1015TLS 握手失敗不能由端點主動發(fā)送3000-3999IANA 注冊使用注意1005表示“沒有狀態(tài)碼”1006表示“異常關(guān)閉”它們不能出現(xiàn)在 Close 幀中。心跳與保活WebSocket 本身有 Ping/Pong但很多中間設(shè)備會斷開長時間無數(shù)據(jù)的連接。常見做法服務(wù)端定期發(fā) Ping客戶端回 Pong。應(yīng)用層定期發(fā) JSON 心跳如{type:ping}。設(shè)置空閑超時超時后主動關(guān)閉??蛻舳藬嗑€后實現(xiàn)指數(shù)退避重連。瀏覽器端不能直接發(fā) Ping所以很多應(yīng)用用業(yè)務(wù)消息做心跳。擴展與子協(xié)議客戶端httpSec-WebSocket-Protocol: chat, superchat服務(wù)端選擇一個Sec-WebSocket-Protocol: chat它只是告訴雙方“我們接下來用 chat 協(xié)議通信”具體格式由應(yīng)用定義。Sec-WebSocket-Extensions用于協(xié)商協(xié)議層擴展。最常見的是permessage-deflate對消息進行壓縮。擴展可能使用 RSV 位例如 RSV1 表示壓縮。使用壓縮時要注意CPU 和內(nèi)存開銷壓縮炸彈壓縮側(cè)信道攻擊風(fēng)險是否允許上下文接管總結(jié):WebSocket 是通過一次 HTTP 握手升級出來的全雙工 TCP 通信協(xié)議之后雙方可以隨時以幀為單位互發(fā)消息。esp32單片機的示例代碼// 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 斷開重連中...); 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 已斷開); 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 時是首片 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 錯誤); break; case WEBSOCKET_EVENT_CLOSED: ESP_LOGW(TAG, WebSocket 連接已關(guān)閉); break; default: break; } } // ---------- 啟動 WebSocket 客戶端 ---------- 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:// 無 TLS不用管下面兩項 // .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)); // 注冊事件 esp_websocket_register_events(s_ws, WEBSOCKET_EVENT_ANY, ws_event_handler, NULL); // 如需自定義請求頭如 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 客戶端啟動); } // ---------- 周期性心跳任務(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); }