概述
参考链接:
链接1
链接2
链接3
WebSocket简介
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。
WebSocket 与 HTTP/2 一样,其实都是为了解决 HTTP/1.1 的一些缺陷而诞生的,而 WebSocket 针对的就是「请求-应答」这种"半双工"的模式的通信缺陷。
「请求-应答」是"半双工"的通信模式,数据的传输必须经过一次请求应答,这个完整的通信过程,通信的同一时刻数据只能在一个方向上传递。它最大的问题在于,HTTP 是一种被动的通信模式,服务端必须等待客户端请求才可以返回数据,无法主动向客户端发送数据。
这也导致在 WebSocket 出现之前,一些对实时性有要求的服务,通常是基于轮询(Polling)这种简单的模式来实现。轮询就是由客户端定时发起请求,如果服务端有需要传递的数据,可以借助这个请求去响应数据。轮询的缺点也非常明显,大量空闲的时间,其实是在反复发送无效的请求,这显然是一种资源的损耗。
创建WebSocket服务器的一般步骤
- 创建一个服务器监听
- 开始接收数据,此时开始接收的数据主要是客户端发出的协议握手报文,报文内容大概如下:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
其中
- | Sec-WebSocket-Key |, 必传, 由客户端随机生成的 16 字节值, 然后做 base64 编码, 客户端需要保证该值是足够随机, 不可被预测的 (换句话说, 客户端应使用熵足够大的随机数发生器), 在 WebSocket 协议中, 该头部字段必传, 若客户端发起握手时缺失该字段, 则无法完成握手
- | Sec-WebSocket-Version |, 必传, 指示 WebSocket 协议的版本, RFC 6455 的协议版本为 13, 在 RFC 6455 的 Draft 阶段已经有针对相应的 WebSocket 实现, 它们当时使用更低的版本号, 若客户端同时支持多个 WebSocket 协议版本, 可以在该字段中以逗号分隔传递支持的版本列表 (按期望使用的程序降序排列), 服务端可从中选取一个支持的协议版本
- | Sec-WebSocket-Protocol |, 可选, 客户端发起握手的时候可以在头部设置该字段, 该字段的值是一系列客户端希望在于服务端交互时使用的子协议 (subprotocol), 多个子协议之间用逗号分隔, 按客户端期望的顺序降序排列, 服务端可以根据客户端提供的子协议列表选择一个或多个子协议
- | Sec-WebSocket-Extensions |, 可选, 客户端在 WebSocket 握手阶段可以在头部设置该字段指示自己希望使用的 WebSocket 协议拓展
- 根据接收到的数据(就是上面的报文),可以先判断一下是不是握手协议(这里可以直接判断一下recv从缓冲区读到的数据是否包含“GET”),对这个报文进行解析,获取其中的Sec-WebSocket-Key(也就是这里的dGhlIHNhbXBsZSBub25jZQ==)。然后服务端要对这个key(包含24个字符)进行解析,解析完成后会得到一个密码,如果服务器发给客户端的报文密码一致的话,此时就完成了协议握手,然后现在就已经成功创建了一个基于webosket协议的连接了。
当握手成功后传入的报文信息如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Version: 13
- | Sec-WebSocket-Accept |, 必传, 客户端发起握手时通过 | Sec-WebSocket-Key | 字段传递了一个将随机生成的 16 字节做 base64 编码后的字符串, 服务端若接收握手, 则应将该值与 WebSocket 魔数 (Magic Number) “258EAFA5-E914-47DA- 95CA-C5AB0DC85B11” 进行字符串连接, 将得到的字符串做 SHA-1 哈希, 将得到的哈希值再做 base64 编码, 最终的值便是该字段的值, 举例来说, 假设客户端传递的 Sec-WebSocket-Key 为 “dGhlIHNhbXBsZSBub25jZQ==”, 服务端应首先将该字符串与 WebSocket 魔数进行字符串拼接, 得到 “dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA- C5AB0DC85B11”, 然后对该字符串做 SHA-1 哈希运算得到哈希值 0xb3 0x7a 0x4f 0x2c 0xc0 0x62 0x4f 0x16 0x90 0xf6 0x46 0x06 0xcf 0x38 0x59 0x45 0xb2 0xbe 0xc4 0xea, 然后对该哈希值做 base64 编码, 最终得到 Sec-WebSocket-Accept 的值为 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=, 当客户端收到服务端的握手响应后, 会做同样的运算来校验该值是否符合预期, 以便于判断服务端是否真的支持 WebSocket 协议, 设置这个环节的目的就是为了最终校验服务端对 WebSocket 协议的支持性, 因为单纯使用 Upgrade 机制, 对于一些没有正确实现 HTTP Upgrade 机制的 Web Server, 可能也会返回预期的 Upgrade, 但实际上它并不支持 WebSocket, 而引入 WebSocket 魔数并进行这一系列操作后可以很大程度上确定服务端确实支持 WebSocket 协议
- 最后,如果握手完成,服务端和客户端之间就可以传输数据了。此时传入的数据就是用WebSocket协议封装好的数据。
WebSocket协议解析
下面就来讲下WebSocket协议,协议格式如下:
第一个字节:
FIN:1位,用于描述消息是否结束,如果为1则该消息为消息尾部,如果为零则还有后续数据包;
RSV1,RSV2,RSV3:各1位,用于扩展定义的,如果没有扩展约定的情况则必须为0
OPCODE:4位,用于表示消息接收类型,如果接收到未知的opcode,接收端必须关闭连接。长连接探活包就是这里标识的。
OPCODE定义的范围:
0x0表示附加数据帧
0x1表示文本数据帧
0x2表示二进制数据帧
0x3-7暂时无定义,为以后的非控制帧保留
0x8表示连接关闭
0x9表示ping
0xA表示pong
0xB-F暂时无定义,为以后的控制帧保留
第二个字节和以后的字节:
第二个字节:
其他字节:
MASK:1位,用于标识PayloadData是否经过掩码处理,客户端发出的数据帧需要进行掩码处理,所以此位是1,数据需要解码。当是服务端发给客户端的代码时,此位为0,数据不需解码。
Payload length === x,
-
如果 x值在0-125,则是payload的真实长度,即数据的真实长度为x。那么当MASK位为0时,从第3个字节开始全是数据信息。当MASK位为1时,从第7个字节开始全是数据信息(Maskinf-key占4个字节)。
-
如果 x值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。即当MASK位为0时,第2个字节数为0x7e(0111 1110(126)),第3到4字节为实际的数据长度,第5个字节后就为数据信息了。当MASK位为1时,第2个字节数为0xfe(第一个比特为1,后7位为111 1110(126))。
-
如果 x值是127,则后面8个字节形成的64位无符号整型数的值是payload的真实长度。
此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)。
数据解码
此处主要处理接收客户端传入的信息,数据已经经过掩码处理,代码如下:
int Websocket_Codetool::wsDecodeFrame(const char* frameData, int len, char* outMessage)
{
int ret = WS_OPENING_FRAME;
const int frameLength = len;
if (frameLength < 2)
{
ret = WS_ERROR_FRAME;
}
// 检查扩展位并忽略
if ((frameData[0] & 0x70) != 0x0)
{
ret = WS_ERROR_FRAME;
}
// fin位: 为1表示已接收完整报文, 为0表示继续监听后续报文
ret = (frameData[0] & 0x80);
if ((frameData[0] & 0x80) != 0x80)
{
ret = WS_ERROR_FRAME;
}
// mask位, 为1表示数据被加密
if ((frameData[1] & 0x80) != 0x80)
{
ret = WS_ERROR_FRAME;
}
// 操作码
uint16_t payloadLength = 0;
uint8_t payloadFieldExtraBytes = 0;
uint8_t opcode = static_cast<uint8_t>(frameData[0] & 0x0f);
//std::cout << "mask:" << ((frameData[1] & 0x80) != 0x80) << std::endl;
//std::cout << "frameLength: " << frameLength << std::endl;
//std::cout << "payloadLength: " << payloadLength << std::endl;
//std::cout << "payloadFieldExtraBytes: " << payloadFieldExtraBytes << std::endl;
//std::cout << "opcode: " << opcode << std::endl;
if (opcode == WS_TEXT_FRAME)
{
// 处理utf-8编码的文本帧
ret = WS_TEXT_FRAME;
payloadLength = static_cast<uint16_t>(frameData[1] & 0x7f);
if (payloadLength == 0x7e)
{
uint16_t payloadLength16b = 0;
payloadFieldExtraBytes = 2;
memcpy(&payloadLength16b, &frameData[2], payloadFieldExtraBytes);
payloadLength = ntohs(payloadLength16b);
}
else if (payloadLength == 0x7f)
{
// 数据过长,暂不支持
ret = WS_ERROR_FRAME;
}
}
else if (opcode == WS_BINARY_FRAME || opcode == WS_PING_FRAME || opcode == WS_PONG_FRAME)
{
// 二进制/ping/pong帧暂不处理
}
else if (opcode == WS_CLOSING_FRAME)
{
ret = WS_CLOSING_FRAME;
}
else
{
ret = WS_ERROR_FRAME;
}
// 数据解码
if ((ret != WS_ERROR_FRAME) && (payloadLength > 0))
{
// header: 2字节, masking key: 4字节
const char *maskingKey = &frameData[2 + payloadFieldExtraBytes];
char *payloadData = new char[payloadLength + 1];
memset(payloadData, 0, payloadLength + 1);
memcpy(payloadData, &frameData[2 + payloadFieldExtraBytes + 4], payloadLength);
for (int i = 0; i < payloadLength; i++)
{
payloadData[i] = payloadData[i] ^ maskingKey[i % 4];
}
//outMessage = payloadData;
int totLen = payloadLength;
memcpy(outMessage, payloadData, totLen);
outMessage[totLen] = 0x00;
delete[] payloadData;
}
return ret;
}
数据编码
主要是对服务器传给客户端的数据加入协议头,不涉及掩码处理。
int Websocket_Codetool::wsEncodeFrame(const char * inMessage, int messageLen, char* outFrame, enum WS_FrameType frameType)
{
int ret = WS_EMPTY_FRAME;
const uint32_t messageLength = messageLen;
if (messageLength > 32767)
{
// 暂不支持这么长的数据
return WS_ERROR_FRAME;
}
uint8_t payloadFieldExtraBytes = (messageLength <= 0x7d) ? 0 : 2;
// header: 2字节, mask位设置为0(不加密), 则后面的masking key无须填写, 省略4字节
uint8_t frameHeaderSize = 2 + payloadFieldExtraBytes;
uint8_t *frameHeader = new uint8_t[frameHeaderSize];
memset(frameHeader, 0, frameHeaderSize);
// fin位为1, 扩展位为0, 操作位为frameType
frameHeader[0] = static_cast<uint8_t>(0x80 | frameType);
// 填充数据长度
if (messageLength <= 0x7d)
{
//头1个字节+ 1个字节长度 + 0x0000
frameHeader[1] = static_cast<uint8_t>(messageLength);
}
else
{
//头1个字节+ 0x7e +2个字节长度
frameHeader[1] = 0x7e;
uint16_t len = htons(messageLength);
memcpy(&frameHeader[2], &len, payloadFieldExtraBytes);
}
// 填充数据
uint32_t frameSize = frameHeaderSize + messageLength;
char *frame = new char[frameSize + 1];
memcpy(frame, frameHeader, frameHeaderSize);
memcpy(frame + frameHeaderSize, inMessage, messageLength);
frame[frameSize] = '