分享

C++实现websocket协议通讯

 猎狐肥 2019-12-02

一、websocket协议原理

(一)websocket 协议的官方文档 :

https://tools./html/draft-ietf-hybi-thewebsocketprotocol-13#section-5

Linux 下c语言 实现 websocket  包含客户端 和服务器测试代码 :

http://blog.csdn.net/sguniver_22/article/details/74273839

c语言实现websocket服务器:

http://blog.csdn.net/lell3538/article/details/60470558

细说webosokcet-php篇

https://www.cnblogs.com/hustskyking/p/websocket-with-php.html

(二)需要学习哪些东西?

1. 如何建立连接

2. 如何交换数据

3. 数据帧格式

4. 如何维持连接

websocket连接建立过程:

      websocket 复用了HTTP的握手通道。具体指的是,客户端HTTP请求与websocket 服务端协商升级协议。

1. client -> server  发送Sec-WebSocket-Key

2. server-> client   加密返回Sec-WebScoket-Accept

3  client -> server  本地校验

1.  客户端发起协议升级请求。 采用标准的HTTP报文格式,只支持 GET

GET / HTTP/1.1
Host: localhost:8080
Origin: http://127.0.0.1:3000
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: w4v7O6xFTi36lq3RNcgctw==
2. 服务端相应协议升级

HTTP/1.1 101 Switching Protocols
Connection:Upgrade
Upgrade: websocket
Sec-WebSocket-Accept: Oy4NRAQ13jhfONC7bP8dTKb4PTU=
注意每个header都以\r\n结尾,并且最后一行加上一个额外的空行\r\n

Sec-WebSocket-Accept的计算

伪代码如下:

>toBase64( sha1( Sec-WebSocket-Key + 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 )  )


(三) 数据帧格式

WebSocket客户端,服务端通信的最小单位是帧(frame),由一个或多个帧组成一条完整的消息(message)

  1. 发送端: 将消息切割成多个帧,并发送给服务端;

  2. 接收端: 接受消息帧,并将关联的帧重新组装成完整的消息;

    

数据帧格式详解:

第一个字节

FIN:1位,用于描述消息是否结束,如果为1则该消息为消息尾部,如果为零则还有后续数据包;

uint8_t fin = (uint8_t)msg[pos] >> 7;

RSV1,RSV2,RSV3,各1位,用于扩展定义的,如果没有扩展约定的情况则必须为0

OPCODE:4位,用于表示消息接收类型,如果接收到未知的opcode,接收端必须关闭连接。

uint8_t opcode = msg[pos] & 0x0f;

0x0表示附加数据帧
0x1表示文本数据帧
0x2表示二进制数据帧
0x3-7暂时无定义,为以后的非控制帧保留
0x8表示连接关闭
0x9表示ping
0xA表示pong
0xB-F暂时无定义,为以后的控制帧保留

第二个字节

MASK:1位,用于标识PayloadData是否经过掩码处理,客户端发出的数据帧需要进行掩码处理,所以此位是1。数据需要解码

uint8_t mask = (uint8_t)msg[pos] >> 7;

Mask: 1个比特。

            不需要对数据进行掩码操作。 从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,     如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。

掩码算法:

 首先,假设:

original-octet-i:为原始数据的第i字节。
transformed-octet-i:为转换后的数据的第i字节。
j:为i mod 4的结果。
masking-key-octet-j:为mask key第j字节。
    算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。

    j = i MOD 4

    transformed-octet-i = original-octet-i XOR masking-key-octet-j

PayloadData的长度:7位,7+16位,7+64位
如果其值在0-125,则是payload的真实长度。
如果值是126,则后面2个字节形成的16位无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
如果值是127,则后面8个字节形成的64位无符号整型数的值是payload的真实长度。注意,网络字节序,需要转换。
长度表示遵循一个原则,用最少的字节表示长度(我理解是尽量减少不必要的传输)。举例说,payload真实长度是124,在0-125之间,必须用前7位表示;不允许长度1是126或127,然后长度2是124,这样违反原则。

  uint64_t payload_length_ = msg[pos] & 0x7f;
   pos++;
   if(payload_length_ == 126){
       uint16_t length = 0;
       memcpy(&length, msg + pos, 2);
       pos += 2;
       payload_length_ = ntohs(length);
   }
   else if(payload_length_ == 127){
       uint32_t length = 0;
       memcpy(&length, msg + pos, 4);
       pos += 4;
       payload_length_ = ntohl(length);
   }


后面的字节就是消息体,获取消息体内如如下:

char payload_[2048];

    memset(payload_, 0, sizeof(payload_));
    if(mask_ != 1){
        memcpy(payload_, msg + pos, payload_length_);
    }
    else {
        for(uint i = 0; i < payload_length_; i++){
            int j = i % 4;
            payload_[i] = msg[pos + i] ^ masking_key_[j];
        }
    }
    pos += payload_length_;

(四)、数据传递

     一旦WebSocket 客户端、服务端连接后,后续的操作都是基于数据帧的传递。

    1、数据分片

第一条消息

FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。

第二条消息

FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧。
FIN=0,opcode=0x0,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。
FIN=1,opcode=0x0,表示消息已经发送完成,没有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。服务端可以将关联的数据帧组装成完整的消息。
Client: FIN=1, opcode=0x1, msg="hello"
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg="and a"
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg="happy new"
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg="year!"
Server: (process complete message) Happy new year to you too!
(五)、 连接保持+心跳

WebSocket为了保持客户端,服务端的实时双向通信,需要确保客户端和服务端之间的TCP通道长期连接不断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费连接资源。 但是不排除有些场景,客户端和服务端虽然长时间没有数据往来,但仍需保持连接。这个时候可以采用心跳 实现。

发送方-> 接收方 :ping

接收方-> 发送方 : pong

(六)、Sec-WebSocket-Key/Accept的作用?

       2. 确保服务端理解websocket连接,因为握手阶段采用的是http协议,因此ws连接可能是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并没有实现ws协议。。。)

       5. Sec-WebSocket-Key主要目的并不是保证数据的安全,因为Sec-WebSocket-Key, Sec-WebSocket-Accept的转换计算公式是公开的,而且很简单,主要作用是防止一些常见的以外情况(非故意的)

(七),数据掩码的作用

       安全。但并不是为了防止数据泄密,而是为了防止早期版本协议存在的代理缓存污染攻击(proxy cache poisoning attacks) 等问题。 (不甚理解)

下面给出一段 c++实现 websocket 服务端 ,作为研究包解析,数据解码,链接建立过程等,不能作为生产环境使用!

二、c++实现部分源码

下面是主要代码。

  1. #ifndef __WebSocketProtocol_H__
  2. #define __WebSocketProtocol_H__
  3. #include <string>
  4. using std::string;

  5. class CWebSocketProtocol
  6. {
  7. public:
  8. enum WS_Status
  9. {
  10. WS_STATUS_CONNECT = 0,
  11. WS_STATUS_UNCONNECT = 1,
  12. };

  13. enum WS_FrameType
  14. {
  15. WS_EMPTY_FRAME = 0xF0,
  16. WS_ERROR_FRAME = 0xF1,
  17. WS_TEXT_FRAME = 0x01,
  18. WS_BINARY_FRAME = 0x02,
  19. WS_PING_FRAME = 0x09,
  20. WS_PONG_FRAME = 0x0A,
  21. WS_OPENING_FRAME = 0xF3,
  22. WS_CLOSING_FRAME = 0x08
  23. };

  24. static CWebSocketProtocol * getInstance();

  25. int getResponseHttp(string &request, string &response);
  26. int wsDecodeFrame(string inFrame, string &outMessage); //解码帧
  27. int wsEncodeFrame(string inMessage, string &outFrame, enum WS_FrameType frameType); //编码帧打包

  28. private:
  29. CWebSocketProtocol();
  30. ~CWebSocketProtocol();

  31. class CGrabo
  32. {
  33. public:
  34. ~CGrabo()
  35. {
  36. if (m_inst != 0)
  37. {
  38. delete m_inst;
  39. m_inst = 0;
  40. }
  41. }
  42. };

  43. static CGrabo m_grabo;
  44. static CWebSocketProtocol * m_inst;
  45. };

  46. #endif
  1. #include "WebSocketProtocol.h"
  2. #include <iostream>
  3. #include <sstream>
  4. #include <string.h>
  5. #include <arpa/inet.h>
  6. #include "sha1.h"
  7. #include "base64.h"

  8. const char * MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

  9. CWebSocketProtocol::CGrabo CWebSocketProtocol::m_grabo;
  10. CWebSocketProtocol * CWebSocketProtocol::m_inst = 0;

  11. CWebSocketProtocol::CWebSocketProtocol()
  12. {
  13. }

  14. CWebSocketProtocol::~CWebSocketProtocol()
  15. {
  16. }

  17. CWebSocketProtocol * CWebSocketProtocol::getInstance()
  18. {
  19. if (m_inst != 0)
  20. {
  21. m_inst = new CWebSocketProtocol;
  22. }
  23. return m_inst;
  24. }

  25. int CWebSocketProtocol::getResponseHttp(string &request, string &response)
  26. {
  27. // 解析http请求头信息
  28. int ret = WS_STATUS_UNCONNECT;
  29. std::istringstream stream(request.c_str());
  30. std::string reqType;
  31. std::getline(stream, reqType);
  32. if (reqType.substr(0, 4) != "GET ")
  33. {
  34. return ret;
  35. }

  36. std::string header;
  37. std::string::size_type pos = 0;
  38. std::string websocketKey;
  39. while (std::getline(stream, header) && header != "\r")
  40. {
  41. header.erase(header.end() - 1);
  42. pos = header.find(": ", 0);
  43. if (pos != std::string::npos)
  44. {
  45. std::string key = header.substr(0, pos);
  46. std::string value = header.substr(pos + 2);
  47. if (key == "Sec-WebSocket-Key")
  48. {
  49. ret = WS_STATUS_CONNECT;
  50. websocketKey = value;
  51. break;
  52. }
  53. }
  54. }

  55. if (ret != WS_STATUS_CONNECT)
  56. {
  57. return ret;
  58. }

  59. // 填充http响应头信息
  60. response = "HTTP/1.1 101 Switching Protocols\r\n";
  61. response += "Connection: upgrade\r\n";
  62. response += "Sec-WebSocket-Accept: ";

  63. std::string serverKey = websocketKey + MAGIC_KEY;

  64. SHA1 sha;
  65. unsigned int message_digest[5];
  66. sha.Reset();
  67. sha << serverKey.c_str();

  68. sha.Result(message_digest);
  69. for (int i = 0; i < 5; i++) {
  70. message_digest[i] = htonl(message_digest[i]);
  71. }
  72. serverKey = base64_encode(reinterpret_cast<const unsigned char*>(message_digest), 20);
  73. response += serverKey;
  74. response += "\r\n";
  75. response += "Upgrade: websocket\r\n\r\n";

  76. return ret;
  77. }

  78. int CWebSocketProtocol::wsDecodeFrame(string inFrame, string &outMessage)
  79. {
  80. int ret = WS_OPENING_FRAME;
  81. const char *frameData = inFrame.c_str();
  82. const int frameLength = inFrame.size();
  83. if (frameLength < 2)
  84. {
  85. ret = WS_ERROR_FRAME;
  86. }

  87. // 检查扩展位并忽略
  88. if ((frameData[0] & 0x70) != 0x0)
  89. {
  90. ret = WS_ERROR_FRAME;
  91. }

  92. // fin位: 为1表示已接收完整报文, 为0表示继续监听后续报文
  93. ret = (frameData[0] & 0x80);
  94. if ((frameData[0] & 0x80) != 0x80)
  95. {
  96. ret = WS_ERROR_FRAME;
  97. }

  98. // mask位, 为1表示数据被加密
  99. if ((frameData[1] & 0x80) != 0x80)
  100. {
  101. ret = WS_ERROR_FRAME;
  102. }

  103. // 操作码
  104. uint16_t payloadLength = 0;
  105. uint8_t payloadFieldExtraBytes = 0;
  106. uint8_t opcode = static_cast<uint8_t>(frameData[0] & 0x0f);
  107. if (opcode == WS_TEXT_FRAME)
  108. {
  109. // 处理utf-8编码的文本帧
  110. payloadLength = static_cast<uint16_t>(frameData[1] & 0x7f);
  111. if (payloadLength == 0x7e)
  112. {
  113. uint16_t payloadLength16b = 0;
  114. payloadFieldExtraBytes = 2;
  115. memcpy(&payloadLength16b, &frameData[2], payloadFieldExtraBytes);
  116. payloadLength = ntohs(payloadLength16b);
  117. }
  118. else if (payloadLength == 0x7f)
  119. {
  120. // 数据过长,暂不支持
  121. ret = WS_ERROR_FRAME;
  122. 144 }
  123. 145 }
  124. 146 else if (opcode == WS_BINARY_FRAME || opcode == WS_PING_FRAME || opcode == WS_PONG_FRAME)
  125. 147 {
  126. 148 // 二进制/ping/pong帧暂不处理
  127. 149 }
  128. 150 else if (opcode == WS_CLOSING_FRAME)
  129. 151 {
  130. 152 ret = WS_CLOSING_FRAME;
  131. 153 }
  132. 154 else
  133. 155 {
  134. 156 ret = WS_ERROR_FRAME;
  135. 157 }
  136. 158
  137. 159 // 数据解码
  138. 160 if ((ret != WS_ERROR_FRAME) && (payloadLength > 0))
  139. 161 {
  140. 162 // header: 2字节, masking key: 4字节
  141. 163 const char *maskingKey = &frameData[2 + payloadFieldExtraBytes];
  142. 164 char *payloadData = new char[payloadLength + 1];
  143. 165 memset(payloadData, 0, payloadLength + 1);
  144. 166 memcpy(payloadData, &frameData[2 + payloadFieldExtraBytes + 4], payloadLength);
  145. 167 for (int i = 0; i < payloadLength; i++)
  146. 168 {
  147. 169 payloadData[i] = payloadData[i] ^ maskingKey[i % 4];
  148. 170 }
  149. 171
  150. 172 outMessage = payloadData;
  151. 173 delete[] payloadData;
  152. 174 }
  153. 175
  154. 176 return ret;
  155. 177 }
  156. 178
  157. 179 int CWebSocketProtocol::wsEncodeFrame(string inMessage, string &outFrame, enum WS_FrameType frameType)
  158. {
  159. int ret = WS_EMPTY_FRAME;
  160. const uint32_t messageLength = inMessage.size();
  161. if (messageLength > 32767)
  162. {
  163. // 暂不支持这么长的数据
  164. std::cout << "暂不支持这么长的数据" << std::endl;

  165. return WS_ERROR_FRAME;
  166. }

  167. uint8_t payloadFieldExtraBytes = (messageLength <= 0x7d) ? 0 : 2;
  168. // header: 2字节, mask位设置为0(不加密), 则后面的masking key无须填写, 省略4字节
  169. uint8_t frameHeaderSize = 2 + payloadFieldExtraBytes;
  170. uint8_t *frameHeader = new uint8_t[frameHeaderSize];
  171. memset(frameHeader, 0, frameHeaderSize);
  172. // fin位为1, 扩展位为0, 操作位为frameType
  173. frameHeader[0] = static_cast<uint8_t>(0x80 | frameType);

  174. // 填充数据长度
  175. if (messageLength <= 0x7d)
  176. {
  177. frameHeader[1] = static_cast<uint8_t>(messageLength);
  178. }
  179. else
  180. {
  181. frameHeader[1] = 0x7e;
  182. uint16_t len = htons(messageLength);
  183. memcpy(&frameHeader[2], &len, payloadFieldExtraBytes);
  184. }

  185. // 填充数据
  186. uint32_t frameSize = frameHeaderSize + messageLength;
  187. char *frame = new char[frameSize + 1];
  188. memcpy(frame, frameHeader, frameHeaderSize);
  189. memcpy(frame + frameHeaderSize, inMessage.c_str(), messageLength);
  190. frame[frameSize] = '\0';
  191. outFrame = frame;

  192. delete[] frame;
  193. delete[] frameHeader;
  194. return ret;
  195. }

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多