websocket host 请求头 怎么填写_websocket 协议帧 解析

本文介绍了WebSocket协议的握手过程,包括配置地址、服务端口、协议概况、握手原理和数据帧的解析。通过示例代码展示了如何进行WebSocket客户端的连接、配置、验证以及数据帧的生成与解密,确保客户端与服务端的正确通信。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

众所周知,为了应对人们日益强烈的 RealTime 交互需求,模拟 TCP socket 全双工 的特性,产生了 websocket 这一 双向通信协议,它借用 HTTP协议的 101 switch protocal 协议转换,所以在一定程度跟HTTP协议传输有很大程度的相似,但是请放心,ws是一个独立的协议。本次我们从 websocket client 的角度 实现协议功能 以及 对 ws协议进行解析

环境说明:

  • 完整代码: websocket_client.c
  • RFC6455协议
  • gcc version 4.4.7 x86_64-redhat-linux
  • websocket服务端:Workerman 3.5.3 ( websocket://0.0.0.0:2346 )

websocket client 功能流程

首先我们先对 websocket 客户端的功能实现的流程进行一个将要的说明,后面结合协议进行解析的时候可能仅仅解释一些比较重要的功能模块(有网络编程功底的同学可以先将例子clone下来查看,简单例子,见谅!^-^)

- 配置地址以及服务端口

- 往服务端发送 握手信息的header

- 验证服务端返回的握手信息(验证成功,完成握手)

- 掩码加密 客户端实体数据信息 “aaaaa”

- 掩码解密 服务端返回的数据信息

- close 连接

协议概况

ws是一个基于 TCP 的独立的协议,和HTTP的关系就是握手请求可以作为一个升级请求(Upgrade)经由HTTP服务器进行解释。协议主体主要分为两部分 握手数据帧

握手

私以为握手部分就是为了兼容现有的 基于HTTP的服务端组件(web服务器软件) 或者 代理服务器软件类型的中间件等。这样一个端口既可以接收普通的web请求,又可以对 websocket 进行横向兼容。所以 websocket client 的握手是一个 HTTP升级版请求(HTTP Upgrade request),ws协议规定,握手中的header字段并没有顺序要求。

可以看一下 vpgame 的 websocket

Request header

GET /null HTTP/1.1
Connection: Upgrade
Host: 127.0.0.1:2346
Sec-WebSocket-Key: xh2LW9mb1ZklWVH2gCUlHw==
Sec-WebSocket-Version: 13
Upgrade: websocket
  • GET 请求标识符 Request-URI,用于识别 websocket 链接到不同的服务终结点。
  • Connection 告诉服务端对协议进行升级,具体升级内容取决于 Upgrade部分
  • Host 服务器地址以及service端口
  • Sec-WebSocket-Key 为了保证握手一致性,由客户端生成随机字符串并base64编码,发送给服务端
  • Sec-WebSocket-Version ws协议版本,常用13
  • Upgrade 升级至*协议

Response header

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Sec-WebSocket-Version: 13
Connection: Upgrade
Server: workerman/3.5.3
Sec-WebSocket-Accept: 47N20KYf6c6Qe2+UDp2BQfK0hkw=
  • 状态码101 代表协议升级成功后的状态码
  • ConnectionUpgrade 内容代表协议成功升级为ws
  • Sec-WebSocket-VersionServer 内容代表 ws协议版本 以及 服务端服务软件信息
  • Sec-WebSocket-Accept 重点在此,证明服务端接受了客户端的请求,然后客户端对accept值进行验证,任何 为空或者不符合验证规则的都视为服务器拒绝了请求。
    • 服务端生成 Sec-WebSocket-Accept 规则:服务端将客户端传递的key进行去除首尾空白,然后和一段固定的GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)进行连接,连接后的结果使用 SHA-1(160数位)进行哈希操作,对哈希后的字符串进行base64编码,即为 Accept 内容。
    • 客户端验证规则 仿上述规则对 Sec-WebSocket-Key 进行加密,跟服务端返回 Sec-WebSocket-Accept 值进行对比即可。

上述只是基础的 header 字段,当然还会根据具体需求添加一些 cookie 或者 其他衍生字段。

握手过程的实现代码片段如下(请着重注意代码注释,注意事项过程)(代码中使用的函数均会存在于完整代码中

/**
 * 客户端连接服务端握手处理
 * @param struct *socket_info  服务地址等信息
 * @return uint32_t     调用握手状态的返回
 */
static int w_client_to_server(socket_info *socket_infos)
{
    struct sockaddr_in addr = socket_infos->addr;
    int fd, ret, timeout;
    char buf[512] = {"0"}, *p;
    /*握手协议内容*/
    uint8_t shake_buf[512] = {"0"}, shake_key[128] = {"0"};

    /*建立TCP socket通道*/
    fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0) {
        fprintf(stderr, "socket create faild: %s n", strerror(errno));
        return fd;
    }

    /*非阻塞*/
    ret = fcntl(fd, F_SETFL, 0);
    fcntl(fd, F_SETFL, ret | O_NONBLOCK);

    timeout = 0;
    /*socket connect*/
    while ((ret = connect(fd, (struct sockaddr *)&addr, sizeof(addr))) == -1) {
        if (++timeout > 100) {
            fprintf(stderr, "connect timeout: %s n", strerror(errno));
            close(fd);
            return -1;
        }
        delayms(1);
    }

    /*握手key*/
    memset(shake_key, 0, sizeof(shake_key));
    /*生成Sec-WebSocket-Key,函数实现为 随机生成 16位字符串,然后进行 base64加密*/
    build_shake_key(shake_key);
    /*握手协议包*/
    memset(shake_buf, 0, sizeof(shake_buf));
    /*组装握手协议包,按照之前讲解的 Request header 格式进行组装 */
    build_header(socket_infos->host, socket_infos->port, shake_key, (char *)shake_buf);
    printf("Request header:-------------- n%sn", shake_buf);
    /*发送协议包*/
    ret = send(fd, shake_buf, strlen((const char *)shake_buf), MSG_NOSIGNAL);
    if (ret < 0) {
        fprintf(stderr, "Send to shake info faild: %s n", strerror(errno));
        return ret;
    }

    /*握手*/
    timeout = 0;
    while(true) {
        memset(buf, 0, sizeof(buf));
        /*接收服务端返回数据*/
        ret = recv(fd, buf, sizeof(buf), MSG_NOSIGNAL);
        printf("Response header:-------------- n%sn", buf);
        if (ret > 0) {
            /*判断是不是HTTP协议头*/
            if (strncmp((const char *)buf, (const char *)"HTTP", strlen((const char *)"HTTP")) == 0) {
                /*检验握手信号*/
                if ((p = (uint8_t *)strstr((const char*)buf, (const char *)"Sec-WebSocket-Accept: ")) != NULL) {
                    p += strlen((const char *)"Sec-WebSocket-Accept: "); 
                    sscanf((const char *)p, "%srn", p);
                    printf("accept key: %sn", p);
                    /*握手成功,握手规则如之前所述:服务端将客户端传递的key进行去除首尾空白,*/
                    /*然后和一段固定的 GUID(258EAFA5-E914-47DA-95CA-C5AB0DC85B11)进行连接,*/
                    /*连接后的结果使用 SHA-1(160数位)进行哈希操作,对哈希后的字符串进行base64编码*/
                    if (match_shake_key(shake_key, strlen((const char*)shake_key), p, 
                                strlen((const char *)p)) == 0) {
                        return fd;
                    } else {
                        /*握手不对,重新发送协议包*/
                        ret = send(fd, shake_buf, strlen((const char *)shake_buf), MSG_NOSIGNAL);
                    }
                } else {
                    ret = send(fd, shake_buf, strlen((const char *)shake_buf), MSG_NOSIGNAL);
                }
            }
        }
        if (++timeout > 100) {
            fprintf(stderr, "shake to server timeout: %s n", strerror(errno));
            close(fd);
            return -1;
        }
        delayms(1);
    }
    return -1;
}

“数据帧”

“数据帧” 之所以加引号,是因为它包含 控制帧 + 数据帧 两个基本组成部分,基础帧协议如下【或者点此查看高清协议图片】

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 ...                |
 +---------------------------------------------------------------+

常规套路上我们先解释一下帧协议字段内容,为了更加详细一点儿,我们提前打印demo(发送字符串为 aaaaa)中的控制帧:

// 0x605090 地址为控制帧数据的指针地址
// 这里打印从 0x605090 内存地址开始的20个地址空间 (0x605090/0x605098/0x6050a0 分别开始)
(gdb) x /20tb 0x605090
0x605090:   10000001    10000101    00010011    10110010    11110100    01111000    01110010    11010011
0x605098:   10010101    00011001    01110010    00000000    00000000    00000000    00000000    00000000
0x6050a0:   00000000    00000000    00000000    00000000
  • FIN:1 bit
    表示这是不是消息的最后一帧。第一帧也有可能是最后一帧。 %x0: 还有后续帧 %x1:最后一帧
  • RSV1RSV2RSV3:1 bit
    扩展字段,除非一个扩展经过协商赋予了非零值的某种含义,否则必须为0
  • opcode:4 bit
    解释 payload data 的类型,如果收到识别不了的opcode,直接断开。分类值如下: %x0:连续的帧 %x1:text帧 %x2:binary帧 %x3 - 7:为非控制帧而预留的 %x8:关闭握手帧 %x9:ping帧 %xA:pong帧 %xB - F:为非控制帧而预留的
0x605090:10000001
// 第1部分: 1, FIN数据
// 第2部分: 000, RSV1/RSV2/RSV3数据
// 第3部分: 0001, opcode数据text帧
  • MASK:1 bit
    标识 Payload data 是否经过掩码处理,如果是 1,Masking-key域的数据即为掩码密钥,用于解码Payload data。协议规定客户端数据需要进行掩码处理,所以此位为1
  • Payload len:7 bit | 7+16 bit | 7+64 bit
    表示了 “有效负荷数据 Payload data”,以字节为单位: - 如果是 0~125,那么就直接表示了 payload 长度 - 如果是 126,那么 先存储 0x7E(=126)接下来的两个字节表示的 16位无符号整型数的值就是 payload 长度 - 如果是 127,那么 先存储 0x7E(=126)接下来的八个字节表示的 64位无符号整型数的值就是 payload 长度
0x605091:10000101
// 第1部分: 1, MASK
// 第2部分: 0000101, 发送数据“aaaaa”的长度5
  • Masking-key:0 | 4 bytes 掩码密钥,所有从客户端发送到服务端的帧都包含一个 32bits 的掩码(如果mask被设置成1),否则为0。一旦掩码被设置,所有接收到的 payload data 都必须与该值以一种算法做异或运算来获取真实值。
    ws协议中,数据掩码的作用是增强协议的安全性。但数据掩码并不是为了保护数据本身,因为算法本身是公开的,运算也不复杂。除了加密通道本身,似乎没有太多有效的保护通信安全的办法,那么为什么还要引入掩码计算呢,除了增加计算机器的运算量外似乎并没有太多的收益(这也是不少同学疑惑的点)
    答案还是两个字:安全。但并不是为了防止数据泄密,而是为了防止早期版本的协议中存在的代理缓存污染攻击(proxy cache poisoning attacks)等问题
0x605092:00010011
0x605093:10110010
0x605094:11110100
0x605095:01111000
// 掩码占用 4 bytes = 32 bits,生成的字符为 {19 '023', 178 '262', 244 '364', 120 'x'},转化成二进制即为上述的四个bytes
  • Payload data:(x+y) bytes
    它是 Extension dataApplication data 数据的总和,但是一般扩展数据为空。
  • Extension data:x bytes
    除非扩展被定义,否则就是0
  • Application data:y bytes
    占据 Extension data 后面的所有空间
0x605096:01110010
0x605097:11010011
0x605098:10010101
0x605099:00011001
0x60509a:01110010
// 数据前提:mask_key[4] = {19 '023', 178 '262', 244 '364', 120 'x'}; data[4] = 'aaaaa';
// 第1部分: 01110010,计算来源 mask key 的第一位 和 数据的第一位进行计算,((~mask_key[0])&data[0]) | (mask_key[0]&(~data[0])) = 01110010
// 第2部分: 11010011,((~mask_key[1])&data[1]) | (mask_key[1]&(~data[1])) = 11010011
// 第3部分: 10010101,((~mask_key[2])&data[2]) | (mask_key[2]&(~data[2])) = 10010101
// 第4部分: 00011001,((~mask_key[3])&data[3]) | (mask_key[3]&(~data[3])) = 00011001
// 第5部分: 01110010,mask key是循环利用的,((~mask_key[0])&data[0]) | (mask_key[0]&(~data[0])) = 01110010

数据帧数据的生成 实现代码片段如下(请着重注意代码注释,注意事项过程)(代码中使用的函数均会存在于完整代码中

/**
 * websocket数据收发阶段的数据打包, 通常client发server的数据
 * 都要isMask(掩码)处理, 反之server到client却不用
 * 
 * @param uint8_t       *data               准备发出的数据
 * @param uint32_t      data_len            长度
 * @param uint8_t       *package            打包后存储地址
 * @param uint32_t      package_max_len     存储地址可用长度
 * @param bool          is_mask             是否使用掩码,1要 0不要
 * @param w_com_type    type 数据类型, 由打包后第一个字节决定, 这里默认是数据传输, 即0x81
 * 
 * @return uint32_t     打包后的长度(会比原数据长2~16个字节不等)      <=0 打包失败
 */
static int w_enpackage(uint8_t *data, uint32_t data_len, uint8_t *package, uint32_t package_max_len, bool is_mask, w_com_type type)
{
    /*掩码*/
    uint8_t mask_key[4] = {0};
    uint8_t temp1, temp2;
    int count;
    uint32_t i, len = 0;

    if(package_max_len < 2)
        return -1;

    if(type == WCT_MINDATA)
        *package++ = 0x00;
    else if(type == WCT_TXTDATA)
        *package++ = 0x81;
    else if(type == WCT_BINDATA)
        *package++ = 0x82;
    else if(type == WCT_DISCONN)
        *package++ = 0x88;
    else if(type == WCT_PING)
        *package++ = 0x89;
    else if(type == WCT_PONG)
        *package++ = 0x8A;
    else
        return -1;

    if(is_mask)
        *package = 0x80;
    len += 1;

    if(data_len < 126) {
        *package++ |= (data_len&0x7F);
        len += 1;
    } else if(data_len < 65536) {
        if(package_max_len < 4)
            return -1;
        *package++ |= 0x7E;
        *package++ = (char)((data_len >> 8) & 0xFF);
        *package++ = (uint8_t)((data_len >> 0) & 0xFF);
        len += 3;
    } else if(data_len < 0xFFFFFFFF) {
        if(package_max_len < 10)
            return -1;
        *package++ |= 0x7F;
        /*(char)((data_len >> 56) & 0xFF);   数据长度变量是 uint32_t data_len, 暂时没有那么多数据*/
        *package++ = 0; 
        /*(char)((data_len >> 48) & 0xFF);*/
        *package++ = 0;
        /*(char)((data_len >> 40) & 0xFF);*/
        *package++ = 0;
        /*(char)((data_len >> 32) & 0xFF);*/
        *package++ = 0;
        /*到这里就够传4GB数据了*/
        *package++ = (char)((data_len >> 24) & 0xFF);
        *package++ = (char)((data_len >> 16) & 0xFF);
        *package++ = (char)((data_len >> 8) & 0xFF);
        *package++ = (char)((data_len >> 0) & 0xFF);
        len += 9;
    }
    /*数据使用掩码时, 使用异或解码, maskKey[4]依次和数据异或运算, 逻辑如下*/
    if(is_mask) {
        if(package_max_len < len + data_len + 4)
            return -1;

        /*随机生成掩码*/
        get_random_string(mask_key, sizeof(mask_key));
        *package++ = mask_key[0];
        *package++ = mask_key[1];
        *package++ = mask_key[2];
        *package++ = mask_key[3];
        len += 4;
        for(i = 0, count = 0; i < data_len; i++) {
            temp1 = mask_key[count];
            temp2 = data[i];
            /*异或运算后得到数据*/
            *package++ = (char)(((~temp1)&temp2) | (temp1&(~temp2)));
            count += 1;
            /*maskKey[4]循环使用*/
            if(count >= sizeof(mask_key))
                count = 0;
        }
        len += i;
        *package = '0';
    } else {
        /*数据没使用掩码, 直接复制数据段*/
        if(package_max_len < len + data_len)
            return -1;
        memcpy(package, data, data_len);
        package[data_len] = '0';
        len += data_len;
    }

    return len;
}

解密数据帧

// 实际服务端返回主体文字内容为 "hello aaaaa", 但是数据帧内容就肯定不止主体文字内容
// 服务端返回数据的指针地址为0x605040, 打印0x605040开始的内存数据可以确定服务端返回数据的二进制状态
(gdb) x /20tb 0x605040
0x605040:   10000001    00001011    01101000    01100101    01101100    01101100    01101111    00100000
0x605048:   01100001    01100001    01100001    01100001    01100001    00000000    00000000    00000000
0x605050:   00000000    00000000    00000000    00000000
0x605040:10000001
// 第1部分: 1, FIN数据
// 第2部分: 000, RSV1/RSV2/RSV3数据
// 第3部分: 0001, opcode数据text帧
0x605041:00001011
// 第1部分: 0, NO MASK
// 第2部分: 00001011, 发送数据“hello aaaaa”的长度11
0x605042:01101000
0x605043:01100101
0x605044:01101100
0x605045:01101100
0x605046:01101111
0x605047:00100000
0x605048: 01100001
0x605049:01100001
0x60504a:01100001
0x60504b:01100001
0x60504c:01100001
// 没有mask, 没有 mask key,所以后全部是数据内容,分别对应:
0x605042:h
0x605043:e
0x605044:l
0x605045:l
0x605046:o
0x605047:空格
0x605048:a
0x605049:a
0x60504a:a
0x60504b:a
0x60504c:a

服务端返回的数据帧的解析 实现代码片段如下(请着重注意代码注释,注意事项过程)(代码中使用的函数均会存在于完整代码中

/**
 * websocket数据收发阶段的数据解包, 通常client发server的数据都要isMask(掩码)处理, 
 * 反之server到client却不用
 *
 * @param uint8_t   *data           解包的数据
 * @param uint32_t  data_len        长度
 * @param uint8_t   *package        解包后存储地址
 * @param uint32_t  package_max_len 存储地址可用长度
 * @param uint32_t  *package_len    解包所得长度
 *
 * @return int      解包识别的数据类型 如 : txt数据, bin数据, ping, pong等
 *
 */
static int w_depackage(uint8_t *data, uint32_t data_len, uint8_t *package, uint32_t package_max_len, 
        uint32_t *package_len)
{
    /*掩码*/
    uint8_t mask_key[4] = {0};
    uint8_t temp1, temp2;
    char mask = 0, type;
    int count, ret;
    uint32_t i, len = 0, data_start = 2;
    if(data_len < 2)
        return -1;

    type = data[0]&0x0F;

    if((data[0]&0x80) == 0x80) {
        if(type == 0x01) 
            ret = WCT_TXTDATA;
        else if(type == 0x02) 
            ret = WCT_BINDATA;
        else if(type == 0x08) 
            ret = WCT_DISCONN;
        else if(type == 0x09) 
            ret = WCT_PING;
        else if(type == 0x0A) 
            ret = WCT_PONG;
        else 
            return WCT_ERR;
    } else if(type == 0x00) {
        ret = WCT_MINDATA;
    } else {
        return WCT_ERR;
    }

    if((data[1] & 0x80) == 0x80) {
        mask = 1;
        count = 4;
    } else {
        mask = 0;
        count = 0;
    }

    len = data[1] & 0x7F;

    if(len == 126) {
        if(data_len < 4)
            return WCT_ERR;
        len = data[2];
        len = (len << 8) + data[3];
        if(data_len < len + 4 + count)
            return WCT_ERR;
        if(mask) {
            mask_key[0] = data[4];
            mask_key[1] = data[5];
            mask_key[2] = data[6];
            mask_key[3] = data[7];
            data_start = 8;
        } else {
            data_start = 4;
        }
    } else if(len == 127) {
        if(data_len < 10)
            return WCT_ERR;

        /*使用8个字节存储长度时, 前4位必须为0, 装不下那么多数据...*/
        if(data[2] != 0 || data[3] != 0 || data[4] != 0 || data[5] != 0)
            return WCT_ERR;
        len = data[6];
        len = (len << 8) + data[7];
        len = (len << 8) + data[8];
        len = (len << 8) + data[9];
        if(data_len < len + 10 + count)
            return WCT_ERR;

        if(mask) {
            mask_key[0] = data[10];
            mask_key[1] = data[11];
            mask_key[2] = data[12];
            mask_key[3] = data[13];
            data_start = 14;
        } else {
            data_start = 10;
        }
    } else {
        if(data_len < len + 2 + count)
            return WCT_ERR;

        if(mask) {
            mask_key[0] = data[2];
            mask_key[1] = data[3];
            mask_key[2] = data[4];
            mask_key[3] = data[5];
            data_start = 6;
        } else {
            data_start = 2;
        }
    }

    if(data_len < len + data_start)
        return WCT_ERR;

    if(package_max_len < len + 1)
        return WCT_ERR;

    /*解包数据使用掩码时, 使用异或解码, maskKey[4]依次和数据异或运算, 逻辑如下*/
    if(mask) {
        for(i = 0, count = 0; i < len; i++) {
            temp1 = mask_key[count];
            temp2 = data[i + data_start];
            /*异或运算后得到数据*/
            *package++ =  (char)(((~temp1)&temp2) | (temp1&(~temåp2)));
            count += 1;
            /*mask_key[4]循环使用*/
            if(count >= sizeof(mask_key))
                count = 0;
        }
        *package = '0';
    } else {
        /*解包数据没使用掩码, 直接复制数据段*/
        memcpy(package, &data[data_start], len);
        package[len] = '0';
    }
    *package_len = len;
    return ret;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值