ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

WebSocket协议

WebSocket协议 WebSocket协议是什么WebSocket 解决的是服务端主动推送客户端和服务端同时发送低延迟、低开销支持文本和二进制连接长期保持典型场景聊天、实时通知、协作编辑、多人游戏、股票行情、IoT 控制等。连接建立HTTP Upgrade 握手WebSocket 不是直接裸 TCP 开始而是先发一个 HTTP/1.1 请求要求升级协议。客户端请求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关键头Upgrade: websocket要求升级到 WebSocket。Connection: Upgrade表示希望升级连接。Sec-WebSocket-Key客户端生成的 16 字节随机数Base64 编码。Sec-WebSocket-Version: 13当前标准版本。Origin浏览器自动带上用于服务端做来源校验。Sec-WebSocket-Protocol可选表示应用层子协议如chat、mqtt。Sec-WebSocket-Extensions可选表示希望使用的扩展如压缩。服务端响应 如果同意升级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 是协议规定的用来证明服务端确实理解 WebSocket而不是普通 HTTP 服务器误响应。握手成功后这条 TCP 连接就不再是 HTTP而是 WebSocket 帧流。WebSocket 帧格式WebSocket 的数据以“帧”为单位传输。一个消息可以由一个或多个帧组成。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 表示结束0 表示还有后续帧RSV1/2/3各 1 bit保留位通常为 0扩展可能使用opcode4 bit帧类型MASK1 bit是否使用掩码。客户端发给服务端必须为 1服务端发给客户端必须为 0Payload len7 bit负载长度Extended payload length0/16/64 bit负载长度扩展Masking-key0 或 4 字节掩码密钥MASK1 时存在Payload Data变长实际数据Opcode 帧类型Opcode类型说明0x0Continuation延续帧用于分片消息0x1TextUTF-8 文本消息0x2Binary二进制消息0x3-0x7保留非控制帧保留0x8Close关闭连接0x9Ping心跳探测0xAPong心跳响应0xB-0xF保留控制帧保留文本消息必须是合法 UTF-8。如果接收方发现不是合法 UTF-8通常关闭连接并返回1007。消息、分片与控制帧WebSocket 是“消息导向”的不是字节流。一个消息可以分成多个帧。第一帧FIN0, opcode0x1, payloadHel 第二帧FIN0, opcode0x0, payloadlo 第三帧FIN1, opcode0x0, payloadWorld接收方按顺序重组得到Hello World。规则第一条分片帧的 opcode 是0x1或0x2。后续分片帧 opcode 必须是0x0。最后一帧 FIN1。控制帧可以穿插在分片消息中间。控制帧不能分片FIN 必须为 1负载不能超过 125 字节。控制帧Ping发送方询问对方是否在线。Pong收到 Ping 后必须尽快回 Pong且 payload 要和 Ping 相同。Close关闭握手。关闭握手与状态码关闭 WebSocket 不是直接断开 TCP而是先发 Close 帧。Close 帧格式opcode 0x8 payload 2 字节状态码 可选 UTF-8 原因流程一方发送 Close 帧。另一方收到后如果还没发 Close就回一个 Close 帧。然后关闭 TCP 连接。发送 Close 后不应再发送数据帧。常见状态码状态码含义1000正常关闭1001端点离开如服务端关闭或浏览器跳转1002协议错误1003收到不支持的数据类型1007数据无效如文本不是合法 UTF-81008策略违规1009消息太大1010客户端期望服务端协商某个扩展1011服务端内部错误1012服务重启1013稍后再试1014网关错误1015TLS 握手失败不能由端点主动发送3000-3999IANA 注册使用注意1005表示“没有状态码”1006表示“异常关闭”它们不能出现在 Close 帧中。心跳与保活WebSocket 本身有 Ping/Pong但很多中间设备会断开长时间无数据的连接。常见做法服务端定期发 Ping客户端回 Pong。应用层定期发 JSON 心跳如{type:ping}。设置空闲超时超时后主动关闭。客户端断线后实现指数退避重连。浏览器端不能直接发 Ping所以很多应用用业务消息做心跳。扩展与子协议客户端httpSec-WebSocket-Protocol: chat, superchat服务端选择一个Sec-WebSocket-Protocol: chat它只是告诉双方“我们接下来用 chat 协议通信”具体格式由应用定义。Sec-WebSocket-Extensions用于协商协议层扩展。最常见的是permessage-deflate对消息进行压缩。扩展可能使用 RSV 位例如 RSV1 表示压缩。使用压缩时要注意CPU 和内存开销压缩炸弹压缩侧信道攻击风险是否允许上下文接管总结:WebSocket 是通过一次 HTTP 握手升级出来的全双工 TCP 通信协议之后双方可以随时以帧为单位互发消息。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 服务器 #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 已连接); } // ---------- 发送 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, 发送: %s, out); esp_websocket_client_send_text(s_ws, out, strlen(out), portMAX_DELAY); cJSON_free(out); cJSON_Delete(root); } // ---------- WebSocket 事件回调 ---------- 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 连接已关闭); 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, // 若服务器要求子协议 // .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 客户端启动); } // ---------- 周期性心跳任务 ---------- 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); }
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表