websocket协议详解与代码实现

1 前言

websocket是基于http的长链接协议。最近OpenAI的Realtime Api比较火(人工智能实时语音互动),并且与AI做语音交互的协议就是用的websocket。

websocket本身具有的特点,比较适合做可靠,和相对实时的语音传输:

  • 兼容性好,本身采用http协议,且浏览器均支持

  • 长连接稳定的保障:

  • 双向互动

本文的内容:

  • websocket协议详解

    Http部分

    websocket frame部分

  • websocket协议的C++实现

    基于libuv高性能异步网络,跨平台支持,供大家学习和使用。

代码地址:

https://github.com/runner365/cpp_websocket

2 websocket协议详解

websocket协议是基于http的长连接协议,可以理解成协议分两部分:

图片

  • http的协商部分

  • 长连接报文的文本或数据传输部分

2.1 http协商部分

客户端发送的http Get:

GET / HTTP/1.1Host: localhost:8080Origin: http://127.0.0.1:3000Connection: UpgradeUpgrade: websocketSec-WebSocket-Version: 13Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==

相比普通的http get的消息,websocket的http get主要有以下的特点:

  • Connection: Upgrade

    Connection的value字段必须是Upgrade

  • Upgrade: websocket

    必须有Upgrate的项目,且value字段必须是websocket

  • Sec-WebSocket-Version: 13

    websocket的版本号,通常是13

  • Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==

    websocket的加密key,其实加密的方式非常简单,后面的http response会详细描述,如何通过这个key来加密对应的字段。

服务端的http response:

HTTP/1.1 101 Switching ProtocolsConnection:UpgradeUpgrade: websocketSec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=

response的关键点:

  • http response的code:101

    必须是101,而不是平时的200

  • Connection: Upgrade

    Connection的value字段必须是Upgrade

  • Upgrade: websocket

    必须有Upgrate的项目,且value字段必须是websocket

  • Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=

    这里的回复验证字符串,是通过request部分的Sec-WebSocket-Key对应字符串来进行加密

Sec-WebSocket-Accept的加密方式如下:

std::string WebSocketSession::GenHashcode() {
    //ses_key: 为http get中header中的Sec-WebSocket-Key对应字段
    std::string sec_key = sec_ws_key_;
    //加上规定的字符串"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
    sec_key += "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
    unsigned char hash[20];
    
    //做SHA1的加密,得到字符串
    SHA_CTX sha1;
    SHA1_Init(&sha1);
    SHA1_Update(&sha1, sec_key.data(), sec_key.size());
    SHA1_Final(hash, &sha1);
  
    //加密的字符串在做base64加密
    hash_code_ = Base64Encode(hash, sizeof(hash));
    return hash_code_;
}

主要采用:

Sec-WebSocket-Key跟258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接。通过 SHA1 计算出摘要,并转成 base64 字符串。

协商完成后,基于这个tcp的长连接就建立起来了。

2.2 数据传输

websocket的数据传输部分,是按照frame为单位来进行传输的,是基于tcp流式来传输。

frame的具体格式如下:​​​​​​​

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 ...                |
+---------------------------------------------------------------+
  • FIN:该frame是否为最后一个报文,是否为最后一个报文

  • RSV:保留字段

  • opcode: 操作码

#define WS_OP_CONTINUE_TYPE        0x00 //持续类型
#define WS_OP_TEXT_TYPE            0x01 //内容为text的文字
#define WS_OP_BIN_TYPE             0x02 //内容为二进制的bin数据
#define WS_OP_FURTHER_NO_CTRL_TYPE 0x03 // 
#define WS_OP_CLOSE_TYPE           0x08 // 关闭连接
#define WS_OP_PING_TYPE            0x09 // websocket的ping
#define WS_OP_PONG_TYPE            0x0a // websocket的pong,回复ping
#define WS_OP_FURTHER_CTRL_TYPE    0x0b
  • Payload len:负载长度字段

    如果payloadlen<126,那么payloadlen就是当前的一个字节;

    如果payloadlen==126,那么payloadlen就是后面的两个字节;

    如果payloadlen==127,那么payloadlen就是后面的8个字节;

  • MASK(1bit):是否有mask-key字段

  • Masking-key:掩码加密的key,4个字节

  • Payload data:负载数据部分

如果有masking-key部分,意味着payload data的数据需要加密。加密方式比较简单,就是简单的异或加密。

发送方,也是用mask_key来进行加密:​​​​​​​

    uint8_t masking_key[4];

    masking_key[0] = ByteCrypto::GetRandomUint(1, 0xff);
    masking_key[1] = ByteCrypto::GetRandomUint(1, 0xff);
    masking_key[2] = ByteCrypto::GetRandomUint(1, 0xff);
    masking_key[3] = ByteCrypto::GetRandomUint(1, 0xff);
    
    std::vector<uint8_t> data_vec(len);
    uint8_t* p = &data_vec[0];
    
    memcpy(p, data, len);//copy进原始数据

    size_t temp_len = len & ~3;
    //对原始数据进行异或加密
    for (size_t i = 0; i < temp_len; i += 4) {
        p[i + 0] ^= masking_key[0];
        p[i + 1] ^= masking_key[1];
        p[i + 2] ^= masking_key[2];
        p[i + 3] ^= masking_key[3];
    }
    for (size_t i = temp_len; i < len; ++i) {
        p[i] ^= masking_key[i % 4];
    }    //发送普通的frame header + masking key + 加密异或数据
    client_ptr_->GetTcpClient()->Send((char*)header_start, header_len);
    client_ptr_->GetTcpClient()->Send((char*)masking_key, sizeof(masking_key));
    client_ptr_->GetTcpClient()->Send((char*)p, len);

接收方,如果发现mask(1bit)使能后,就对payload data数据进行异或解密​​​​​​​


    if (mask_enable_) {
        size_t frame_length = payload_len_;
        uint8_t *p = (uint8_t *)buffer_.Data() + payload_start_;

        size_t temp_len = frame_length & ~3;
        for (size_t i = 0; i < temp_len; i += 4) {
            p[i + 0] ^= masking_key_[0];
            p[i + 1] ^= masking_key_[1];
            p[i + 2] ^= masking_key_[2];
            p[i + 3] ^= masking_key_[3];
        }

        for (size_t i = temp_len; i < frame_length; ++i) {
            p[i] ^= masking_key_[i % 4];
        }
    }

如果想要看详细代码和使用,可以前往代码地址:

https://github.com/runner365/cpp_websocket

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐