0. 简介
本文主要分析 https://github.com/mortzdk/websocket中解析帧相关函数
1. predict.h
#ifndef wss_predict_h
#define wss_predict_h
#if defined(__GNUC__ ) || defined(__INTEL_COMPILER)
/*__builtin_expect 是 GCC 提供的一个内建函数,用于向编译器提示某个条件在大多数情况下是否为真*/
#define likely(x) __builtin_expect(!!(x), 1) /*!!(x) 将 x 转换为布尔值(0 或 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
/*解析帧的第一个字节,WebSocket 帧的控制位(FIN、RSV1、RSV2、RSV3)和操作码(opcode)*/
frame->fin = 0x80 & payload[*offset]; //10000000 & payload,只对字节的最高位
frame->rsv1 = 0x40 & payload[*offset];
frame->rsv2 = 0x20 & payload[*offset];
frame->rsv3 = 0x10 & payload[*offset];
frame->opcode = 0x0F & payload[*offset]; //最低4位,即第0到第3位
*offset += 1; //偏移量增加1,解析帧的下一个字节
/*解析第二个字节*/
if ( likely(*offset < length) )
{
frame->mask = 0x80 & payload[*offset];
frame->payloadLength = 0x7F & payload[*offset];
}
*offset += 1;
/*
*payload length,masking-key,payload数据的提取(对照着websocket数据格式)
*......
*/
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
/*根据 payload 的长度决定是否需要额外的 2 字节或 8 字节来表示长度(对照websocket数据格式)*/
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;
/*设置Payload length 字段*/
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++) {
/*遍历每个帧并调用 WSS_stringify_frame 函数将其转换为字节数组。*/
n = WSS_stringify_frame(frames[i], &f);
/*如果接收到的字节数小于 2,说明帧无效,记录错误日志,释放已分配的内存并返回 0*/
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;
}
/*将当前帧的字节数组复制到消息数组中,并更新message_length*/
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
/*
*处理关闭帧。如果操作码是关闭帧,则创建关闭帧,并返回 1。
*......
*/
/*根据消息长度和每帧最大大小,循环创建帧*/
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; //将最后一个帧的 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
/**
*根据关闭原因枚举值设置对应的默认消息。
*......
*/
/*计算应用数据长度,应用数据长度等于关闭原因字符串的长度加上 2个字节(用于存储关闭状态码)。*/
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; //设置为 1,表示这是一个完整的帧
frame->opcode = PING_FRAME; //设置为 PING_FRAME,表示这是一个 PING 帧
frame->mask = 0; //设置为 0,表示不使用掩码
/*设置 PING 帧的负载数据*/
frame->applicationDataLength = 120; //长度,设置为 120 字节
if ( unlikely(NULL == (frame->payload = random_bytes(frame->applicationDataLength))) ) { //生成 120 字节的随机数据作为负载数据
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;
/**
*rsv1,rsv2和rsv3位在没有扩展时应该保持为0,适用于所有帧类型
*在这里显示的设置保留位rsv,主要是为了确保在处理 PING 帧转换为 PONG 帧时,保留位保持为 0,确保符合协议规范
*/
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 帧 |