0. 简介
本文主要分析 https://github.com/mortzdk/websocket中解析帧相关函数
1. predict.h
| #ifndef wss_predict_h |
| #define wss_predict_h |
| |
| #if defined(__GNUC__ ) || defined(__INTEL_COMPILER) |
| |
| #define likely(x) __builtin_expect(!!(x), 1) |
| #define unlikely(x) __builtin_expect(!!(x), 0) |
| |
| #else |
| #define likely(x) (x) |
| #define unlikely(x) (x) |
| |
| #endif |
| |
| #endif |
2. frame.c
| 0 1 2 3 |
| 0 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 len==126/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 ... : |
| + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + |
| | Payload Data continued ... | |
| + |
| | Frame Header | Extension Data | Application Data | |
| |--------------|----------------|------------------| |
| | 2 bytes | | | |
帧头部(Frame Header):
FIN
, RSV1
, RSV2
, RSV3
, Opcode
, Mask
, Payload Length
扩展数据(Extension Data)(如果有):
- 根据扩展协议的定义,可以有任意长度。
- 扩展数据的长度必须在帧头信息中进行描述,并且在应用数据之前
应用数据(Application Data):
- 实际需要传输的数据。
- 应用数据的长度由帧头中的 payload length 减去扩展数据的长度来确定
2.1 解析 WebSocket 帧
| wss_frame_t *WSS_parse_frame(char *payload, size_t length, size_t *offset); |
| |
| 功能: |
| 从给定的 payload 数据中解析出一个 WebSocket 帧 |
| |
| 参数: |
| payload:指向数据的指针。 |
| length:是数据的长度。 |
| offset:是当前解析数据的偏移量,初始值应为0,并在函数内更新。 |
| |
| 返回值: |
| 成功:解析后的frame,一个wss_frame_t结构体类型的指针 |
| 失败:NULL |
| |
| frame->fin = 0x80 & payload[*offset]; |
| frame->rsv1 = 0x40 & payload[*offset]; |
| frame->rsv2 = 0x20 & payload[*offset]; |
| frame->rsv3 = 0x10 & payload[*offset]; |
| frame->opcode = 0x0F & payload[*offset]; |
| |
| *offset += 1; |
| |
| |
| if ( likely(*offset < length) ) |
| { |
| frame->mask = 0x80 & payload[*offset]; |
| frame->payloadLength = 0x7F & payload[*offset]; |
| } |
| *offset += 1; |
| |
| |
| |
| |
| |
2.2 转换一个帧
| size_t WSS_stringify_frame(wss_frame_t *frame, char **message); |
| |
| 功能: |
| 将一个 WebSocket 帧(wss_frame_t)转换为一个字节数组(char array) |
| |
| 参数: |
| frame:指向 wss_frame_t 结构的指针,表示需要转换的 WebSocket 帧。 |
| message:指向 char 数组的指针的指针,转换后的帧数据将存储在这里。 |
| |
| 返回值: |
| 成功:帧数据的总长度 |
| 失败:0 |
| |
| if ( likely(frame->payloadLength > 125) ) |
| { |
| if ( likely(frame->payloadLength <= 65535) ) { |
| len += sizeof(uint16_t); |
| } else { |
| len += sizeof(uint64_t); |
| } |
| } |
| |
| len += frame->payloadLength; |
| |
| if (frame->fin) { |
| mes[offset] |= 0x80; |
| } |
| |
| if (frame->rsv1) { |
| mes[offset] |= 0x40; |
| } |
| |
| if ( unlikely(frame->rsv2) ) { |
| mes[offset] |= 0x20; |
| } |
| |
| if ( unlikely(frame->rsv3) ) { |
| mes[offset] |= 0x10; |
| } |
| |
| mes[offset++] |= 0xF & frame->opcode; |
| |
| |
| |
| if ( unlikely(frame->payloadLength <= 125) ) { |
| mes[offset++] = frame->payloadLength; |
| } else if ( likely(frame->payloadLength <= 65535) ) { |
| uint16_t plen; |
| mes[offset++] = 126; |
| plen = htons16(frame->payloadLength); |
| memcpy(mes+offset, &plen, sizeof(plen)); |
| offset += sizeof(plen); |
| } else { |
| uint64_t plen; |
| mes[offset++] = 127; |
| plen = htonl64(frame->payloadLength); |
| memcpy(mes+offset, &plen, sizeof(plen)); |
| offset += sizeof(plen); |
| } |
| |
| |
| |
| if ( unlikely(frame->extensionDataLength > 0) ) { |
| memcpy(mes+offset, frame->payload, frame->extensionDataLength); |
| offset += frame->extensionDataLength; |
| } |
| |
| if ( likely(frame->applicationDataLength > 0) ) { |
| memcpy(mes+offset, frame->payload+frame->extensionDataLength, frame->applicationDataLength); |
| offset += frame->applicationDataLength; |
| } |
2.3 转换多个帧
| size_t WSS_stringify_frames(wss_frame_t **frames, size_t size, char **message); |
| |
| 功能: |
| 将多个 WebSocket 帧转换为一个连续的字节数组 |
| |
| 参数: |
| frames:指向 wss_frame_t 结构数组的指针,表示需要转换的多个 WebSocket 帧。 |
| size:帧的数量 |
| message:指向 char 数组的指针的指针,转换后的帧数据将存储在这里 |
| |
| 返回值: |
| 成功:消息的总长度 |
| 失败:0 |
| for (i = 0; likely(i < size); i++) { |
| |
| n = WSS_stringify_frame(frames[i], &f); |
| |
| |
| if ( unlikely(n < 2) ) { |
| WSS_log_error("Received invalid frame"); |
| *message = NULL; |
| WSS_free((void **)&f); |
| WSS_free((void **)&msg); |
| return 0; |
| } |
| |
| |
| if ( unlikely(NULL == (msg = WSS_realloc((void **) &msg, message_length*sizeof(char),(message_length+n+1)*sizeof(char)))) ) { |
| WSS_log_error("Unable to allocate message string"); |
| *message = NULL; |
| WSS_free((void **)&f); |
| return 0; |
| } |
| |
| |
| memcpy(msg+message_length, f, n); |
| message_length += n; |
| |
| WSS_free((void **) &f); |
| } |
为什么需要处理多个帧?
1、消息分片(Fragmentation):
- WebSocket 协议允许将一条大的消息分成多个较小的帧进行传输。这样做的好处是可以控制每个帧的大小,以避免在传输大消息时一次性占用过多带宽或内存。
- 处理多个帧意味着接收端需要将这些帧重新组装成一条完整的消息。
2、控制帧与数据帧的混合传输:
- WebSocket 协议定义了几种不同类型的帧(例如,文本帧、二进制帧、关闭帧、Ping 帧和 Pong 帧)。在一个 WebSocket 会话中,可能会同时传输多种类型的帧。
- 处理多个帧使得应用程序能够处理控制帧(如 Ping/Pong)和数据帧(如文本和二进制数据)之间的交互。
3、流控制与高效传输:
- 通过分片,可以更有效地实现流控制。如果某个帧丢失,只需要重传丢失的帧,而不是重传整个消息。
- 在实时通信场景中,小帧的传输和处理延迟通常较低,可以提高实时性和响应速度。
4、错误处理和恢复:
- 如果单个大帧在传输过程中出现错误,可能会导致整个消息无法恢复。但如果消息被分成多个小帧传输,即使某个帧出现错误,也可以通过重传该帧来恢复整个消息。
- 多帧处理可以检测和处理错误。
2.4 将消息转换为帧
| size_t WSS_create_frames(wss_config_t *config, wss_opcode_t opcode, char *message, size_t message_length, wss_frame_t ***fs) ; |
| |
| 功能: |
| 将消息转换为多个 WebSocket 帧 |
| |
| 参数: |
| config:服务器配置,包含每帧的最大大小等参数。 |
| opcode:帧的操作码,指示帧的类型(如文本帧、二进制帧、关闭帧等)。 |
| message:要转换为帧的消息。 |
| message_length:消息的长度。 |
| fs:指向 wss_frame_t 结构数组的指针的指针,存储创建的帧。 |
| |
| 返回值: |
| 成功:创建的帧的数量 |
| 失败:0 |
| |
| |
| |
| |
| |
| |
| for (i = 0; i < frames_count; i++) { |
| if ( unlikely(NULL == (frame = WSS_malloc(sizeof(wss_frame_t)))) ) { |
| WSS_log_error("Unable to allocate frame"); |
| for (j = 0; j < i; j++) { |
| WSS_free_frame(frames[j]); |
| } |
| WSS_free((void **)&frames); |
| *fs = NULL; |
| return 0; |
| } |
| |
| frame->fin = 0; |
| frame->opcode = opcode; |
| frame->mask = 0; |
| |
| frame->applicationDataLength = MIN(message_length-(config->size_frame*i), config->size_frame); |
| if ( unlikely(NULL == (frame->payload = WSS_malloc(frame->applicationDataLength+1))) ) { |
| WSS_log_error("Unable to allocate frame application data"); |
| for (j = 0; j < i; j++) { |
| WSS_free_frame(frames[j]); |
| } |
| WSS_free((void **)&frame); |
| WSS_free((void **)&frames); |
| *fs = NULL; |
| return 0; |
| } |
| memcpy(frame->payload, msg+offset, frame->applicationDataLength); |
| frame->payloadLength += frame->extensionDataLength; |
| frame->payloadLength += frame->applicationDataLength; |
| offset += frame->payloadLength; |
| |
| frames[i] = frame; |
| } |
| |
| frames[frames_count-1]->fin = 1; |
2.4 关闭帧
| wss_frame_t *WSS_closing_frame(wss_close_t reason, char *message); |
| |
| 功能: |
| 根据关闭原因创建一个 WebSocket 关闭帧 |
| |
| 参数: |
| reason:关闭的原因,类型为 wss_close_t,枚举值表示不同的关闭原因。 |
| message:关闭帧的附加消息,类型为 char*,可以为空。 |
| |
| 返回值: |
| 成功:创建的 WebSocket 关闭帧 |
| 失败:NULL |
| |
| |
| |
| |
| |
| |
| |
| frame->applicationDataLength = strlen(reason_str)+sizeof(uint16_t); |
| if ( unlikely(NULL == (frame->payload = WSS_malloc(frame->applicationDataLength+1))) ) { |
| WSS_log_error("Unable to allocate closing frame application data"); |
| WSS_free_frame(frame); |
| return NULL; |
| } |
| nbo_reason = htons16(reason); |
| memcpy(frame->payload, &nbo_reason, sizeof(uint16_t)); |
| memcpy(frame->payload+sizeof(uint16_t), reason_str, strlen(reason_str)); |
| |
| frame->payloadLength += frame->extensionDataLength; |
| frame->payloadLength += frame->applicationDataLength; |
| |
| return frame; |
| |
2.5 PING帧
| wss_frame_t *WSS_ping_frame(); |
| |
| 功能: |
| 创建一个PING帧作为心跳消息 |
| |
| 返回值: |
| 成功:创建的 WebSocket PING 帧 |
| 失败:NULL |
| frame->fin = 1; |
| frame->opcode = PING_FRAME; |
| frame->mask = 0; |
| |
| |
| frame->applicationDataLength = 120; |
| |
| if ( unlikely(NULL == (frame->payload = random_bytes(frame->applicationDataLength))) ) { |
| WSS_log_error("Unable to allocate ping frame application data"); |
| WSS_free_frame(frame); |
| return NULL; |
| } |
| |
2.6 PONG帧
| wss_frame_t *WSS_pong_frame(); |
| |
| 功能: |
| 将接收到的 PING 帧转换为 PONG 帧 |
| |
| 返回值: |
| 成功:创建的 WebSocket PONG 帧 |
| 失败:NULL |
| ping->fin = 1; |
| |
| |
| |
| |
| ping->rsv1 = 0; |
| ping->rsv2 = 0; |
| ping->rsv3 = 0; |
| ping->opcode = PONG_FRAME; |
| ping->mask = 0; |
| |
| memset(ping->maskingKey, '\0', sizeof(uint32_t)); |
| |
2.7 PING/PONG
特性 |
PING 帧 |
PONG 帧 |
作用 |
发送以检查连接状态和保持连接活跃 |
响应 PING 帧并保持连接活跃 |
发送方 |
客户端或服务器 |
接收 PING 帧的一方(客户端或服务器) |
负载数据 |
可以为空或随机数据 |
通常匹配 PING 帧的负载数据 |
主要用途 |
连接检查、心跳机制、保持连接活跃 |
确认连接活跃、响应 PING 帧 |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!