我们在一些中重度游戏中经常使用TCP,可靠UDP(参考之前的剖析KCP以及KCP在游戏中是如何使用的)来进行网络传输,不过随着一些小游戏和超轻休闲类游戏的逐步崛起,越来越多的开发者逐渐使用websocket来进行网络传输数据,像node.js/ ts , go, java等语言都有造好的轮子,直接引用现成的库并能很方便的调用,但是如果你使用的是C++,你会发现,websocket的库
倒是挺多的,但是找到真正适合到自己的项目中的却寥寥无几。
相信很多读者参考过: websocketcpp,uWebSockets,libuv等等,不过即便有这些代码参考,也难以快速移植代码到你的C++服务器端程序里,先说websocketcpp, uWebSockets 这两个相对比较重型,代码量较大,快速裁剪并移植到现有C++工程里比较耗时。大家都懂,一般开发时间是比较紧张的,花精力配置运行起来、再对比搞懂这些库没时间啊。 最后libuv虽然更轻量一些,但是由于他返回给业务层的仍然是在多个子线程里,因此需要开发者自己加锁,这无疑是给后边的业务开发埋下隐患。
其他WebSocket开源库等与底层网络库耦合的较多,相信各位的服务端都有自己定制的网络库,切换网络库,再考虑线程安全等因素,给修改移植工
作带来不少工作量。
因此,我在我原来的net_manager网络库中开始支持websocket了,目前网络库已支持TCP,可靠UDP(KCP方式),Websocket,reactor模型多路复用,在网络层线程专门做网络事件的触发和处理,而业务逻辑在主线程,因此开发者不需要在业务逻辑层关心锁的问题。
所以开发者如果要在他的网络库中支持websocket ,实际上你知道websocket的请求头和握手原理,发送和接收做了哪些事,相信码代码只是体力活了。
websocket请求头和握手
websocket本质是前端 与服务器端建立一条TCP长连接,服务端可以随时向前端推送数据,前端也可以随时向服务端发送数据,实现了两者间双向数据实时传输。 websocket是基于http协议的,为什么这么说?因为从协议来说,websocket是借用了一部分为http请求头信息来进行验证和请求的的。
让我们来看一个标准的websocket请求头:
--- request header --- GET /chat HTTP/1.1 Upgrade: websocket Connection: Upgrade Host: 127.0.0.1:6000Origin: http://127.0.0.1:6000 Sec-WebSocket-Key: hj0eNqbhE/A0GkBXDRrYYw== Sec-WebSocket-Version: 13
客户端握手连接请求的格式下面几个关键点需要注意:
请求行: 请求方法必须是GET, HTTP版本至少是1.1
请求必须含有Host 如果请求来自浏览器客户端, 必须包含Origin
请求必须含有Connection, 其值必须含有"Upgrade"记号,用于实现协议升级.
请求必须含有Upgrade, 其值必须含有"websocket"关键字
请求必须含有Sec-Websocket-Version, 其值必须是13
请求必须含有Sec-Websocket-Key, Sec-WebSocket-Key是客户端也就是浏览器或者其他终端随机生成一组16位的随机base64编码的串用于提供基本的防护, 比如无意的连接。
如下是握手的cpp代码逻辑:
void SOCK_Websocket::pack_shake_request(std::string& shakeMsg) { // // 生成要发给服务端的WS握手数据 // char msg[1024] = ""; strcat(msg, "GET / HTTP/1.1\r\n"); strcat(msg, "Host: 127.0.0.1:5010\r\n"); strcat(msg, "Connection: Upgrade\r\n"); strcat(msg, "Pragma: no-cache\r\n"); strcat(msg, "Cache-Control: no-cache\r\n"); strcat(msg, "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.111 Safari/537.36\r\n"); strcat(msg, "Upgrade: websocket\r\n"); strcat(msg, "Origin: file://\r\n"); strcat(msg, "Sec-WebSocket-Version: 13\r\n"); strcat(msg, "Accept-Encoding: gzip, deflate, br\r\n"); strcat(msg, "Accept-Language: zh-CN,zh-TW;q=0.9,zh;q=0.8,en-US;q=0.7,en;q=0.6\r\n"); // 以当前时间作为随机字符串(Sec-WebSocket-Key: inYDATs71e8Y81vfK+Wc6Q==) char clientkey[128] = { 0 }; #ifdef WIN32 SYSTEMTIME time; GetLocalTime(&time); // get current time sprintf(clientkey, "%04d%02d%02d%02d%02d%02dvfK+Wc6Q==", time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, time.wSecond); #else struct timeval time; gettimeofday(&time, NULL); sprintf(clientkey, "%llu%%lluvfK+Wc6Q==", time.tv_sec, time.tv_usec); #endif strcat(msg, "Sec-WebSocket-Key: "); strcat(msg, clientkey); strcat(msg, "\r\n"); strcat(msg, "Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n"); strcat(msg, "\r\n"); strcat(msg, "\r\n"); // // 计算服务端要返回的key值,作为握手暗号 // std::string server_key = clientkey; server_key += MAGIC_KEY; utils::SHA1 sha; unsigned int message_digest[5]; sha.Reset(); sha << server_key.c_str(); sha.Result(message_digest); for (int i = 0; i < 5; i++) { message_digest[i] = htonl(message_digest[i]); } server_key = utils::Base64::base64_encode(reinterpret_cast<const unsigned char*>(message_digest), 20); m_expectKey.assign(server_key); shakeMsg.assign(msg); LOG(INFO)("send handshake (len=%d)\n%s\n", shakeMsg.length(), shakeMsg.c_str()); LOG(INFO)("hope to server key:%s\n", m_expectKey.c_str()); return; }
服务端收到客户端连接后,回复格式如下:
HTTP/1.1 101 Switching Protocols Content-Length: 0 Upgrade: websocket Sec-Websocket-Accept: ZEs+c+VBk8Aj01+wJGN7Y15796g= Server: TornadoServer/4.5.1 Connection: Upgrade Date: Tue, 29 Nov 2022 11:29:14 GMT
这里有几点需要注意的是:
响应行: HTTP/1.1 101 Switching Protocols
响应必须含有Upgrade, 其值为"weboscket"
响应必须含有Connection, 其值为"Upgrade"
响应必须含有Sec-Websocket-Accept, 根据请求首部的Sec-Websocket-key计算出来
服务端回复中关键点在于Sec-Websocket-Accept值的计算,具体计算方式如下:
将客户端送来的Sec-Websocket-Key的值和258EAFA5-E914-47DA-95CA-C5AB0DC85B11拼接
258EAFA5-E914-47DA-95CA-C5AB0DC85B11是一个magic key,是RFC6455 Page24页中定义的一个固定值,直接用即可。通过SHA1计算出摘要, 并转成base64字符串
下面是服务器端收到握手信息后的逻辑处理:
int SOCK_Websocket::hand_shake(const char* buffer, const int len) { int rc = fetch_http_info(buffer, len); if (rc != 0) { return -1; } char request[1024] = {}; pack_shake_response(request); Net_Packet* net_packet = new Net_Packet(); net_packet->write_string(request); net_packet->movetobegin(); int ret = post_packet(net_packet); m_status = WS_DATATRANSFORM; LOG(INFO)("websocket handshake post packet to client, ret:%d", ret); return 0; }int SOCK_Websocket::fetch_http_info(const char* buffer, int len) { std::istringstream s(std::string(buffer,len)); std::string request; std::getline(s, request); if (request.size() == 0) { return -1; } if (request[request.size() - 1] == '\r') { request.erase(request.end() - 1); } else { return -1; } std::string header; std::string::size_type end; while (std::getline(s, header) && header != "\r") { if (header[header.size() - 1] != '\r') { continue; //end } else { header.erase(header.end() - 1); //remove last char } end = header.find(": ", 0); if (end != std::string::npos) { std::string key = header.substr(0, end); std::string value = header.substr(end + 2); m_headers[key] = value; } } return 0; } void SOCK_Websocket::pack_shake_response(char* encodeKey) { strcat(encodeKey, "HTTP/1.1 101 Switching Protocols\r\n"); strcat(encodeKey, "Connection: upgrade\r\n"); strcat(encodeKey, "Sec-WebSocket-Accept: "); std::string server_key = m_headers["Sec-WebSocket-Key"]; server_key += MAGIC_KEY; utils::SHA1 sha; unsigned int message_digest[5]; sha.Reset(); sha << server_key.c_str(); sha.Result(message_digest); for (int i = 0; i < 5; i++) { message_digest[i] = htonl(message_digest[i]); } server_key = utils::Base64::base64_encode(reinterpret_cast<const unsigned char*>(message_digest), 20); server_key += "\r\n"; strcat(encodeKey, server_key.c_str()); strcat(encodeKey, "Upgrade: websocket\r\n\r\n"); }
至此,一来一回,客户端和服务器端已经完成WebSocket 握手,那么客户端还需要对服务器返回的握手信息进行验证:
int SOCK_Websocket::hand_shake_resp(const char* buffer, const int len) { int rc = fetch_http_info(buffer, len); if (rc != 0) { return -1; } if (m_headers["Sec-WebSocket-Accept"] != m_expectKey) { LOG(ERROR)("invalid websoket-handshake response expect :%s, but current: %s", m_expectKey.c_str(), m_headers["Sec-WebSocket-Accept"].c_str()); return -1; } m_status = WS_DATATRANSFORM; return 0; }
修改连接状态为数据传输,这样websocket的连接建立完成,下一步就是传输数据。
websocket数据传输
RFC6455中定义了websocket数据帧的格式,如下:
数据帧的组成结构和其他协议类似,归纳起来:数据头+载荷
WebSocket的数据头长度是可变的,有两个因素影响:
载荷长度的数值大小,Payload length: 占7或7+16或7+64bit,具体看下面详解。
是否有maks key,有的话头部多4个字节
数据帧格式如下:
编辑
FIN: 占1bit 0表示不是消息的最后一个分片 1表示是消息的最后一个分片
RSV1, RSV2, RSV3: 各占1bit, 一般情况下全为0, 与Websocket拓展有关, 如果出现非零的值且没有采用WebSocket拓展, 连接出错
Opcode
: 占4bit
%x0: 表示本次数据传输采用了数据分片, 当前数据帧为其中一个数据分片
%x1: 表示这是一个文本帧
%x2: 表示这是一个二进制帧
%x3-7: 保留的操作代码, 用于后续定义的非控制帧
%x8: 表示连接断开
%x9: 表示这是一个心跳请求(ping)
%xA: 表示这是一个心跳响应(pong)
%xB-F: 保留的操作代码, 用于后续定义的非控制帧
Mask
: 占1bit 0表示不对数据载荷进行掩码异或操作 1表示对数据载荷进行掩码异或操作
Payload length
: 占7或7+16或7+64bit
0~125: 数据长度等于该值
126: 后续的2个字节代表一个16位的无符号整数, 值为数据的长度
127: 后续的8个字节代表一个64位的无符号整数, 值为数据的长度
Masking-key: 占0或4bytes 1: 携带了4字节的Masking-key 0: 没有Masking-key
payload data: 数据段
这里有几个关键点需要注意:
Fin为0,表示一个完整的消息被分片成多个数据帧中传输的,需要一直等待接到Fin为1的数据帧之后,才算收到一个完整的消息。
只有客户端给服务器端发送数据时才会有masking key,服务器端给客户端发送数据不需要masking key
ok,这里我给大家演示下数据发送的时候应该做的事情,首先我们要按照websocket的数据帧格式进行编码:
int Net_Manager::send_packet(uint32_t id, Websocket_Opcode opcode, Net_Packet* packet) { //websocket的需要编码 SOCK_Websocket::encode_websocket_buffer((uint8_t*)packet->getData().c_str(), packet->getdatalen(), opcode, packet); return send_packet(id, packet); }
如下是具体的编码逻辑:
void SOCK_Websocket::encode_websocket_buffer(const uint8_t* buffer,uint32_t buffer_len, uint8_t opcode, Net_Packet* net_packet) { string bf((const char*)buffer,buffer_len); net_packet->clear(); uint8_t onebyte = 0; onebyte |= (1 << 7); onebyte |= (0 << 6); onebyte |= (0 << 5); onebyte |= (0 << 4); onebyte |= (opcode & 0x0F); net_packet->write_uint8(onebyte); uint8_t mask = 0; onebyte = 0; //set mask flag onebyte = onebyte | (mask << 7); if (buffer_len < 126) { onebyte |= buffer_len; net_packet->write_uint8(onebyte); } else if (buffer_len >= 126 && buffer_len <= 0xFFFF) { onebyte |= 126; net_packet->write_uint8(onebyte); // also can use htons onebyte = (buffer_len >> 8) & 0xFF; net_packet->write_uint8(onebyte); onebyte = buffer_len & 0xFF; net_packet->write_uint8(onebyte); } else { net_packet->write_uint8(127); for (int i = 3;i >= 0;i--) { net_packet->write_uint8(0); } for (int i = 3;i >= 0;i--) { net_packet->write_uint8((buffer_len >> 8*i) &0xFF); } } { net_packet->write_data((const char*)bf.c_str(), buffer_len); } net_packet->movetobegin(); }
同样的对于接收端,需要解码数据帧
int SOCK_Websocket::decode_websocket_buffer(uint8_t* buf, uint32_t len, uint32_t& packet_len) { m_fin = (buf[0] & 0x80) == 0x80;//结束标志 m_opcode = (buf[0] & 0x0f); m_mask = (buf[1] & 0x80) == 0x80; int type = (buf[1] & 0x7f); int header_size = 2 + (type == 126 ? 2 : 0) + (type == 127 ? 8 : 0) + (m_mask ? 4 : 0); if (len < header_size) { return -1; /* Need: ws.header_size - rxbuf.size() */ } int i = 0; if (type < 126) { m_payloadLen = type; i = 2; } else if (type == 126) { m_payloadLen = 0; m_payloadLen |= ((uint64_t)buf[2]) << 8; m_payloadLen |= ((uint64_t)buf[3]) << 0; i = 4; } else if (type == 127) { m_payloadLen = 0; m_payloadLen |= ((uint64_t)buf[2]) << 56; m_payloadLen |= ((uint64_t)buf[3]) << 48; m_payloadLen |= ((uint64_t)buf[4]) << 40; m_payloadLen |= ((uint64_t)buf[5]) << 32; m_payloadLen |= ((uint64_t)buf[6]) << 24; m_payloadLen |= ((uint64_t)buf[7]) << 16; m_payloadLen |= ((uint64_t)buf[8]) << 8; m_payloadLen |= ((uint64_t)buf[9]) << 0; i = 10; if (m_payloadLen > 0xFFFFFFFF) { //std::cout >> } } if (!m_fin) { packet_len = m_payloadLen + header_size; return 0;//继续接收 } //包么有接收完,需要继续接收 if (len < m_payloadLen + header_size) { packet_len = m_payloadLen + header_size; return 0; } if (m_payloadMaxLen < m_payloadLen) { if (m_payload) { delete m_payload; m_payload = NULL; } m_payload = new char[m_payloadLen]; m_payloadMaxLen = m_payloadLen; } if (m_mask) { m_masking_key[0] = ((uint8_t)buf[i + 0]) << 0; m_masking_key[1] = ((uint8_t)buf[i + 1]) << 0; m_masking_key[2] = ((uint8_t)buf[i + 2]) << 0; m_masking_key[3] = ((uint8_t)buf[i + 3]) << 0; } else { m_masking_key[0] = 0; m_masking_key[1] = 0; m_masking_key[2] = 0; m_masking_key[3] = 0; } memset(m_payload, 0, m_payloadLen); if (m_mask != 1) { memcpy(m_payload, buf + header_size, m_payloadLen); } else { for (auto i = 0; i < m_payloadLen; i++) { int j = i % 4; m_payload[i] = buf[header_size + i] ^ m_masking_key[j]; } } switch (m_opcode) { case OPCODE_CONTINUATION: case OPCODE_TEXT: case OPCODE_BINARY: break; case OPCODE_CLOSE: return -1; case OPCODE_PING://客户端ping消息 { Net_Packet* pongPacket = new Net_Packet; pongPacket->movetobegin(); encode_websocket_buffer((uint8_t*)pongPacket->getData().c_str(), pongPacket->getdatalen(), OPCODE_PONG, pongPacket); post_packet(pongPacket); }break; case OPCODE_PONG: /*Net_Packet* pingPacket = new Net_Packet; pingPacket->movetobegin(); encode_websocket_buffer((uint8_t*)pingPacket->getData().c_str(), pingPacket->getdatalen(), OPCODE_PING, pingPacket); post_packet(pingPacket);*/ break; } if (len > m_payloadLen + header_size) { //粘包了 packet_len = m_payloadLen + header_size;//获取实际包的大小,剩下的包等下次接收缓存 return 1; } packet_len = len; return 1; }
关于websocket的心跳:
webSocket长连接的心跳保持需要基于Ping/Pong心跳机制来维持websocket连接时会发生中断,需要查找中断原因。在中断关闭链接时进行错误信息的输出。
在webSocket连接过程中会出现,切换网络,网络连接断开的问题,WebSocket都没有断开,但对上层来说,都没办法正常的收发数据了。因此我们需要一种机制来感知连接是否可用、服务是否可用,以便能够快速恢复。一旦感知到了连接不可用,弃用并断开旧连接,然后发起一次新连接。定时发送心跳包,来感知网络是可用的。
相关源码我已经更新至gitee上 https://gitee.com/gaoke_it/baselib.git