如何在C++ 网络库中支持websocket

本文涉及的产品
无影云电脑企业版,4核8GB 120小时 1个月
无影云电脑个人版,1个月黄金款+200核时
资源编排,不限时长
简介: 如何在C++ 网络库中支持websocket

        我们在一些中重度游戏中经常使用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

image.gif

客户端握手连接请求的格式下面几个关键点需要注意:

请求行: 请求方法必须是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;
}

image.gif

服务端收到客户端连接后,回复格式如下:

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

image.gif

这里有几点需要注意的是:

响应行: 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"); 
}

image.gif

至此,一来一回,客户端和服务器端已经完成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;
}

image.gif

修改连接状态为数据传输,这样websocket的连接建立完成,下一步就是传输数据。

websocket数据传输

RFC6455中定义了websocket数据帧的格式,如下:

数据帧的组成结构和其他协议类似,归纳起来:数据头+载荷

WebSocket的数据头长度是可变的,有两个因素影响:

载荷长度的数值大小,Payload length: 占7或7+16或7+64bit,具体看下面详解。

是否有maks key,有的话头部多4个字节

数据帧格式如下:

image.gif编辑

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);
}

image.gif

如下是具体的编码逻辑:

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();
}

image.gif

同样的对于接收端,需要解码数据帧

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;
}

image.gif

关于websocket的心跳:

webSocket长连接的心跳保持需要基于Ping/Pong心跳机制来维持websocket连接时会发生中断,需要查找中断原因。在中断关闭链接时进行错误信息的输出。

在webSocket连接过程中会出现,切换网络,网络连接断开的问题,WebSocket都没有断开,但对上层来说,都没办法正常的收发数据了。因此我们需要一种机制来感知连接是否可用、服务是否可用,以便能够快速恢复。一旦感知到了连接不可用,弃用并断开旧连接,然后发起一次新连接。定时发送心跳包,来感知网络是可用的。

相关源码我已经更新至gitee上  https://gitee.com/gaoke_it/baselib.git

相关文章
|
2月前
|
NoSQL 网络协议 Linux
Redis的实现一:c、c++的网络通信编程技术,先实现server和client的通信
本文介绍了使用C/C++进行网络通信编程的基础知识,包括创建socket、设置套接字选项、绑定地址、监听连接以及循环接受和处理客户端请求的基本步骤。
54 6
|
7月前
|
存储 网络协议 Ubuntu
【C++网络编程】Socket基础:网络通讯程序入门级教程
【C++网络编程】Socket基础:网络通讯程序入门级教程
157 7
|
7月前
|
消息中间件 网络协议 C++
C/C++网络编程基础知识超详细讲解第三部分(系统性学习day13)
C/C++网络编程基础知识超详细讲解第三部分(系统性学习day13)
|
7月前
|
存储 算法 Linux
【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现
【实战项目】网络编程:在Linux环境下基于opencv和socket的人脸识别系统--C++实现
238 7
|
2月前
|
存储 监控 NoSQL
Redis的实现二: c、c++的网络通信编程技术,让服务器处理多个client
本文讨论了在C/C++中实现服务器处理多个客户端的技术,重点介绍了事件循环和非阻塞IO的概念,以及如何在Linux上使用epoll来高效地监控和管理多个文件描述符。
29 0
|
3月前
|
网络协议 Linux C++
超级好用的C++实用库之网络
超级好用的C++实用库之网络
49 0
|
4月前
|
C++
C++ Qt开发:QUdpSocket网络通信组件
QUdpSocket是Qt网络编程中一个非常有用的组件,它提供了在UDP协议下进行数据发送和接收的能力。通过简单的方法和信号,可以轻松实现基于UDP的网络通信。不过,需要注意的是,UDP协议本身不保证数据的可靠传输,因此在使用QUdpSocket时,可能需要在应用层实现一些机制来保证数据的完整性和顺序,或者选择在适用的场景下使用UDP协议。
173 2
|
5月前
|
存储 安全 Linux
网络请求的高效处理:C++ libmicrohttpd库详解
网络请求的高效处理:C++ libmicrohttpd库详解
|
7月前
|
Serverless C++
C++多态性、虚函数、纯虚函数和抽象类知识网络构造
C++多态性、虚函数、纯虚函数和抽象类知识网络构造
|
7月前
|
数据采集 API 数据安全/隐私保护
畅游网络:构建C++网络爬虫的指南
本文介绍如何使用C++和cpprestsdk库构建高效网络爬虫,以抓取知乎热点信息。通过亿牛云爬虫代理服务解决IP限制问题,利用多线程提升数据采集速度。示例代码展示如何配置代理、发送HTTP请求及处理响应,实现多线程抓取。注意替换有效代理服务器参数,并处理异常。
181 0
畅游网络:构建C++网络爬虫的指南