【Websocket】解析帧frame.c源码分析

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 帧
posted @ 2024-07-01 21:38  梨子Li  阅读(12)  评论(0编辑  收藏  举报