websocket协议详解与代码实现
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
更多推荐



所有评论(0)