議)
WebSocket協(xié)議是什么WebSocket 解決的是服務(wù)端主動(dòng)推送客戶端和服務(wù)端同時(shí)發(fā)送低延遲、低開銷支持文本和二進(jìn)制連接長(zhǎng)期保持典型場(chǎng)景聊天、實(shí)時(shí)通知、協(xié)作編輯、多人游戲、股票行情、IoT 控制等。連接建立HTTP Upgrade 握手WebSocket 不是直接裸 TCP 開始而是先發(fā)一個(gè) HTTP/1.1 請(qǐng)求要求升級(jí)協(xié)議??蛻舳苏?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客戶端生成的 16 字節(jié)隨機(jī)數(shù)Base64 編碼。Sec-WebSocket-Version: 13當(dāng)前標(biāo)準(zhǔn)版本。Origin瀏覽器自動(dòng)帶上用于服務(wù)端做來源校驗(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ī)定的用來證明服務(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 ... :---------------------------------------------------------------字段說明字段長(zhǎng)度說明FIN1 bit是否為消息最后一幀。1 表示結(jié)束0 表示還有后續(xù)幀RSV1/2/3各 1 bit保留位通常為 0擴(kuò)展可能使用opcode4 bit幀類型MASK1 bit是否使用掩碼??蛻舳税l(fā)給服務(wù)端必須為 1服務(wù)端發(fā)給客戶端必須為 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 幀類型Opcode類型說明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。控制幀可以穿插在分片消息中間??刂茙荒芊制現(xiàn)IN 必須為 1負(fù)載不能超過 125 字節(jié)??刂茙琍ing發(fā)送方詢問對(duì)方是否在線。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就回一個(gè) Close 幀。然后關(guān)閉 TCP 連接。發(fā)送 Close 后不應(yīng)再發(fā)送數(shù)據(jù)幀。常見狀態(tài)碼狀態(tài)碼含義1000正常關(guān)閉1001端點(diǎn)離開如服務(wù)端關(guān)閉或?yàn)g覽器跳轉(zhuǎn)1002協(xié)議錯(cuò)誤1003收到不支持的數(shù)據(jù)類型1007數(shù)據(jù)無效如文本不是合法 UTF-81008策略違規(guī)1009消息太大1010客戶端期望服務(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表示“沒有狀態(tài)碼”1006表示“異常關(guān)閉”它們不能出現(xiàn)在 Close 幀中。心跳與?;頦ebSocket 本身有 Ping/Pong但很多中間設(shè)備會(huì)斷開長(zhǎng)時(shí)間無數(shù)據(jù)的連接。常見做法服務(wù)端定期發(fā) Ping客戶端回 Pong。應(yīng)用層定期發(fā) JSON 心跳如{type:ping}。設(shè)置空閑超時(shí)超時(shí)后主動(dòng)關(guān)閉??蛻舳藬嗑€后實(shí)現(xiàn)指數(shù)退避重連。瀏覽器端不能直接發(fā) Ping所以很多應(yīng)用用業(yè)務(wù)消息做心跳。擴(kuò)展與子協(xié)議客戶端httpSec-WebSocket-Protocol: chat, superchat服務(wù)端選擇一個(gè)Sec-WebSocket-Protocol: chat它只是告訴雙方“我們接下來用 chat 協(xié)議通信”具體格式由應(yīng)用定義。Sec-WebSocket-Extensions用于協(xié)商協(xié)議層擴(kuò)展。最常見的是permessage-deflate對(duì)消息進(jìn)行壓縮。擴(kuò)展可能使用 RSV 位例如 RSV1 表示壓縮。使用壓縮時(shí)要注意CPU 和內(nèi)存開銷壓縮炸彈壓縮側(cè)信道攻擊風(fēng)險(xiǎn)是否允許上下文接管總結(jié):WebSocket 是通過一次 HTTP 握手升級(jí)出來的全雙工 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 斷開重連中...); 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 時(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 客戶端 ---------- 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不用管下面兩項(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 客戶端啟動(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); }