i007.cc

i007.cc

优先队列-降维打击

05.价值资料

服务端使用c++实现websocket协议解析及通信

服务端使用c++实现websocket协议解析及通信

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013085897/article/details/54234518

       WebSocket 设计出来的目的就是要使客户端浏览器具备像 C/S 架构下桌面系统的实时通讯能力。 浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器端就可以通过 TCP 连接直接交换数据。因为 WebSocket 连接本质上就是一个 TCP 连接,所以在数据传输的稳定性和数据传输量的大小方面,和轮询以及 Comet 技术比较,具有很大的性能优势。下面是一个简单 Web 应用分别用轮询方式和 WebSocket 方式来实现,下面是测试结果图:

       通过这张图可以清楚的看出,在流量和负载增大的情况下,WebSocket 方案相比传统的 Ajax 轮询方案有极大的性能优势。
       好了不过多介绍 WebSocket 了,更多介绍大家可以点击参考资料引用的链接查看,还是回到解析协议及通信上来。解析协议这种事,就得耐着性子,一个字节一个字节解析,按步骤一点一点写程序。不过读懂了文档,知道了每个字节的属性意义后,解析起来还是挺简单的。按照协议说明,一旦完成数据解码,那么编码就稍微容易一些,差不多就是解码的逆向操作了。服务端使用c++完成 WebSocket 通信,主要需要完成以下三方面编程:

1. 服务端与h5客户端发起的 WebSocket 连接握手:

  1. int wsHandshake(string &request, string &response)
  2. {
  3. // 解析http请求头信息
  4. int ret = WS_STATUS_UNCONNECT;
  5. std::istringstream stream(request.c_str());
  6. std::string reqType;
  7. std::getline(stream, reqType);
  8. if (reqType.substr(0, 4) != “GET “)
  9. {
  10. return ret;
  11. }
  12. std::string header;
  13. std::string::size_type pos = 0;
  14. std::string websocketKey;
  15. while (std::getline(stream, header) && header != “\r”)
  16. {
  17. header.erase(header.end() – 1);
  18. pos = header.find(“: “, 0);
  19. if (pos != std::string::npos)
  20. {
  21. std::string key = header.substr(0, pos);
  22. std::string value = header.substr(pos + 2);
  23. if (key == “Sec-WebSocket-Key”)
  24. {
  25. ret = WS_STATUS_CONNECT;
  26. websocketKey = value;
  27. break;
  28. }
  29. }
  30. }
  31. if (ret != WS_STATUS_CONNECT)
  32. {
  33. return ret;
  34. }
  35. // 填充http响应头信息
  36. response = “HTTP/1.1 101 Switching Protocols\r\n”;
  37. response += “Upgrade: websocket\r\n”;
  38. response += “Connection: upgrade\r\n”;
  39. response += “Sec-WebSocket-Accept: “;
  40. const std::string magicKey(“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”);
  41. std::string serverKey = websocketKey + magicKey;
  42. char shaHash[32];
  43. memset(shaHash, 0, sizeof(shaHash));
  44. sha1::calc(serverKey.c_str(), serverKey.size(), (unsigned char *) shaHash);
  45. serverKey = base64::base64_encode(std::string(shaHash)) + “\r\n\r\n”;
  46. string strtmp(serverKey.c_str());
  47. response += strtmp;
  48. return ret;
  49. }

2. 完成握手后连接就建立了。然后就是接收h5客户端通过 WebSocket 发过来的数据帧并解码:

  1. int wsDecodeFrame(string inFrame, string &outMessage)
  2. {
  3. int ret = WS_OPENING_FRAME;
  4. const char *frameData = inFrame.c_str();
  5. const int frameLength = inFrame.size();
  6. if (frameLength < 2)
  7. {
  8. ret = WS_ERROR_FRAME;
  9. }
  10. // 检查扩展位并忽略
  11. if ((frameData[0] & 0x70) != 0x0)
  12. {
  13. ret = WS_ERROR_FRAME;
  14. }
  15. // fin位: 为1表示已接收完整报文, 为0表示继续监听后续报文
  16. ret = (frameData[0] & 0x80);
  17. if ((frameData[0] & 0x80) != 0x80)
  18. {
  19. ret = WS_ERROR_FRAME;
  20. }
  21. // mask位, 为1表示数据被加密
  22. if ((frameData[1] & 0x80) != 0x80)
  23. {
  24. ret = WS_ERROR_FRAME;
  25. }
  26. // 操作码
  27. uint16_t payloadLength = 0;
  28. uint8_t payloadFieldExtraBytes = 0;
  29. uint8_t opcode = static_cast<uint8_t >(frameData[0] & 0x0f);
  30. if (opcode == WS_TEXT_FRAME)
  31. {
  32. // 处理utf-8编码的文本帧
  33. payloadLength = static_cast<uint16_t >(frameData[1] & 0x7f);
  34. if (payloadLength == 0x7e)
  35. {
  36. uint16_t payloadLength16b = 0;
  37. payloadFieldExtraBytes = 2;
  38. memcpy(&payloadLength16b, &frameData[2], payloadFieldExtraBytes);
  39. payloadLength = ntohs(payloadLength16b);
  40. }
  41. else if (payloadLength == 0x7f)
  42. {
  43. // 数据过长,暂不支持
  44. ret = WS_ERROR_FRAME;
  45. }
  46. }
  47. else if (opcode == WS_BINARY_FRAME || opcode == WS_PING_FRAME || opcode == WS_PONG_FRAME)
  48. {
  49. // 二进制/ping/pong帧暂不处理
  50. }
  51. else if (opcode == WS_CLOSING_FRAME)
  52. {
  53. ret = WS_CLOSING_FRAME;
  54. }
  55. else
  56. {
  57. ret = WS_ERROR_FRAME;
  58. }
  59. // 数据解码
  60. if ((ret != WS_ERROR_FRAME) && (payloadLength > 0))
  61. {
  62. // header: 2字节, masking key: 4字节
  63. const char *maskingKey = &frameData[2 + payloadFieldExtraBytes];
  64. char *payloadData = new char[payloadLength + 1];
  65. memset(payloadData, 0, payloadLength + 1);
  66. memcpy(payloadData, &frameData[2 + payloadFieldExtraBytes + 4], payloadLength);
  67. for (int i = 0; i < payloadLength; i++)
  68. {
  69. payloadData[i] = payloadData[i] ^ maskingKey[i % 4];
  70. }
  71. outMessage = payloadData;
  72. delete[] payloadData;
  73. }
  74. return ret;
  75. }

3. 解码完数据帧,服务端做出相应处理后将结果按照 WebSocket 协议编码,然后发给h5客户端:

  1. int wsEncodeFrame(string inMessage, string &outFrame, enum WS_FrameType frameType)
  2. {
  3. int ret = WS_EMPTY_FRAME;
  4. const uint32_t messageLength = inMessage.size();
  5. if (messageLength > 32767)
  6. {
  7. // 暂不支持这么长的数据
  8. return WS_ERROR_FRAME;
  9. }
  10. uint8_t payloadFieldExtraBytes = (messageLength <= 0x7d) ? 0 : 2;
  11. // header: 2字节, mask位设置为0(不加密), 则后面的masking key无须填写, 省略4字节
  12. uint8_t frameHeaderSize = 2 + payloadFieldExtraBytes;
  13. uint8_t *frameHeader = new uint8_t[frameHeaderSize];
  14. memset(frameHeader, 0, frameHeaderSize);
  15. // fin位为1, 扩展位为0, 操作位为frameType
  16. frameHeader[0] = static_cast<uint8_t>(0x80 | frameType);
  17. // 填充数据长度
  18. if (messageLength <= 0x7d)
  19. {
  20. frameHeader[1] = static_cast<uint8_t>(messageLength);
  21. }
  22. else
  23. {
  24. frameHeader[1] = 0x7e;
  25. uint16_t len = htons(messageLength);
  26. memcpy(&frameHeader[2], &len, payloadFieldExtraBytes);
  27. }
  28. // 填充数据
  29. uint32_t frameSize = frameHeaderSize + messageLength;
  30. char *frame = new char[frameSize + 1];
  31. memcpy(frame, frameHeader, frameHeaderSize);
  32. memcpy(frame + frameHeaderSize, inMessage.c_str(), messageLength);
  33. frame[frameSize] = ‘\0’;
  34. outFrame = frame;
  35. delete[] frame;
  36. delete[] frameHeader;
  37. return ret;
  38. }

4. 握手只需一次,随后反复执行第2步及第3步,就完成了服务端与h5客户端通信。这个只是c++版本的,可以很容易改成java版本的。下面是上述方法用到的一些枚举:

  1. enum WS_Status
  2. {
  3. WS_STATUS_CONNECT = 0,
  4. WS_STATUS_UNCONNECT = 1,
  5. };
  6. enum WS_FrameType
  7. {
  8. WS_EMPTY_FRAME = 0xF0,
  9. WS_ERROR_FRAME = 0xF1,
  10. WS_TEXT_FRAME = 0x01,
  11. WS_BINARY_FRAME = 0x02,
  12. WS_PING_FRAME = 0x09,
  13. WS_PONG_FRAME = 0x0A,
  14. WS_OPENING_FRAME = 0xF3,
  15. WS_CLOSING_FRAME = 0x08
  16. };

参考资料:

发表回复