C++實現websocket協議通訊
一、websocket協議原理
(一)websocket 協議的官方文件 :
https://tools.ietf.org/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
4 #include <string>
5
6 using std::string;
7
8 class CWebSocketProtocol
9 {
10 public:
11 enum WS_Status
12 {
13 WS_STATUS_CONNECT = 0,
14 WS_STATUS_UNCONNECT = 1,
15 };
16
17 enum WS_FrameType
18 {
19 WS_EMPTY_FRAME = 0xF0,
20 WS_ERROR_FRAME = 0xF1,
21 WS_TEXT_FRAME = 0x01,
22 WS_BINARY_FRAME = 0x02,
23 WS_PING_FRAME = 0x09,
24 WS_PONG_FRAME = 0x0A,
25 WS_OPENING_FRAME = 0xF3,
26 WS_CLOSING_FRAME = 0x08
27 };
28
29 static CWebSocketProtocol * getInstance();
30
31 int getResponseHttp(string &request, string &response);
32 int wsDecodeFrame(string inFrame, string &outMessage); //解碼幀
33 int wsEncodeFrame(string inMessage, string &outFrame, enum WS_FrameType frameType); //編碼幀打包
34
35 private:
36 CWebSocketProtocol();
37 ~CWebSocketProtocol();
38
39 class CGrabo
40 {
41 public:
42 ~CGrabo()
43 {
44 if (m_inst != 0)
45 {
46 delete m_inst;
47 m_inst = 0;
48 }
49 }
50 };
51
52 static CGrabo m_grabo;
53 static CWebSocketProtocol * m_inst;
54 };
55
56 #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
9 const char * MAGIC_KEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
10
11 CWebSocketProtocol::CGrabo CWebSocketProtocol::m_grabo;
12 CWebSocketProtocol * CWebSocketProtocol::m_inst = 0;
13
14 CWebSocketProtocol::CWebSocketProtocol()
15 {
16 }
17
18
19 CWebSocketProtocol::~CWebSocketProtocol()
20 {
21 }
22
23
24
25 CWebSocketProtocol * CWebSocketProtocol::getInstance()
26 {
27 if (m_inst != 0)
28 {
29 m_inst = new CWebSocketProtocol;
30 }
31
32 return m_inst;
33 }
34
35 int CWebSocketProtocol::getResponseHttp(string &request, string &response)
36 {
37 // 解析http請求頭資訊
38 int ret = WS_STATUS_UNCONNECT;
39 std::istringstream stream(request.c_str());
40 std::string reqType;
41 std::getline(stream, reqType);
42 if (reqType.substr(0, 4) != "GET ")
43 {
44 return ret;
45 }
46
47 std::string header;
48 std::string::size_type pos = 0;
49 std::string websocketKey;
50 while (std::getline(stream, header) && header != "\r")
51 {
52 header.erase(header.end() - 1);
53 pos = header.find(": ", 0);
54 if (pos != std::string::npos)
55 {
56 std::string key = header.substr(0, pos);
57 std::string value = header.substr(pos + 2);
58 if (key == "Sec-WebSocket-Key")
59 {
60 ret = WS_STATUS_CONNECT;
61 websocketKey = value;
62 break;
63 }
64 }
65 }
66
67 if (ret != WS_STATUS_CONNECT)
68 {
69 return ret;
70 }
71
72 // 填充http響應頭資訊
73 response = "HTTP/1.1 101 Switching Protocols\r\n";
74 response += "Connection: upgrade\r\n";
75 response += "Sec-WebSocket-Accept: ";
76
77 std::string serverKey = websocketKey + MAGIC_KEY;
78
79 SHA1 sha;
80 unsigned int message_digest[5];
81 sha.Reset();
82 sha << serverKey.c_str();
83
84 sha.Result(message_digest);
85 for (int i = 0; i < 5; i++) {
86 message_digest[i] = htonl(message_digest[i]);
87 }
88 serverKey = base64_encode(reinterpret_cast<const unsigned char*>(message_digest), 20);
89 response += serverKey;
90 response += "\r\n";
91 response += "Upgrade: websocket\r\n\r\n";
92
93 return ret;
94 }
95
96 int CWebSocketProtocol::wsDecodeFrame(string inFrame, string &outMessage)
97 {
98 int ret = WS_OPENING_FRAME;
99 const char *frameData = inFrame.c_str();
100 const int frameLength = inFrame.size();
101 if (frameLength < 2)
102 {
103 ret = WS_ERROR_FRAME;
104 }
105
106 // 檢查擴充套件位並忽略
107 if ((frameData[0] & 0x70) != 0x0)
108 {
109 ret = WS_ERROR_FRAME;
110 }
111
112 // fin位: 為1表示已接收完整報文, 為0表示繼續監聽後續報文
113 ret = (frameData[0] & 0x80);
114 if ((frameData[0] & 0x80) != 0x80)
115 {
116 ret = WS_ERROR_FRAME;
117 }
118
119 // mask位, 為1表示資料被加密
120 if ((frameData[1] & 0x80) != 0x80)
121 {
122 ret = WS_ERROR_FRAME;
123 }
124
125 // 操作碼
126 uint16_t payloadLength = 0;
127 uint8_t payloadFieldExtraBytes = 0;
128 uint8_t opcode = static_cast<uint8_t>(frameData[0] & 0x0f);
129 if (opcode == WS_TEXT_FRAME)
130 {
131 // 處理utf-8編碼的文字幀
132 payloadLength = static_cast<uint16_t>(frameData[1] & 0x7f);
133 if (payloadLength == 0x7e)
134 {
135 uint16_t payloadLength16b = 0;
136 payloadFieldExtraBytes = 2;
137 memcpy(&payloadLength16b, &frameData[2], payloadFieldExtraBytes);
138 payloadLength = ntohs(payloadLength16b);
139 }
140 else if (payloadLength == 0x7f)
141 {
142 // 資料過長,暫不支援
143 ret = WS_ERROR_FRAME;
144 }
145 }
146 else if (opcode == WS_BINARY_FRAME || opcode == WS_PING_FRAME || opcode == WS_PONG_FRAME)
147 {
148 // 二進位制/ping/pong幀暫不處理
149 }
150 else if (opcode == WS_CLOSING_FRAME)
151 {
152 ret = WS_CLOSING_FRAME;
153 }
154 else
155 {
156 ret = WS_ERROR_FRAME;
157 }
158
159 // 資料解碼
160 if ((ret != WS_ERROR_FRAME) && (payloadLength > 0))
161 {
162 // header: 2位元組, masking key: 4位元組
163 const char *maskingKey = &frameData[2 + payloadFieldExtraBytes];
164 char *payloadData = new char[payloadLength + 1];
165 memset(payloadData, 0, payloadLength + 1);
166 memcpy(payloadData, &frameData[2 + payloadFieldExtraBytes + 4], payloadLength);
167 for (int i = 0; i < payloadLength; i++)
168 {
169 payloadData[i] = payloadData[i] ^ maskingKey[i % 4];
170 }
171
172 outMessage = payloadData;
173 delete[] payloadData;
174 }
175
176 return ret;
177 }
178
179 int CWebSocketProtocol::wsEncodeFrame(string inMessage, string &outFrame, enum WS_FrameType frameType)
180 {
181 int ret = WS_EMPTY_FRAME;
182 const uint32_t messageLength = inMessage.size();
183 if (messageLength > 32767)
184 {
185 // 暫不支援這麼長的資料
186 std::cout << "暫不支援這麼長的資料" << std::endl;
187
188 return WS_ERROR_FRAME;
189 }
190
191 uint8_t payloadFieldExtraBytes = (messageLength <= 0x7d) ? 0 : 2;
192 // header: 2位元組, mask位設定為0(不加密), 則後面的masking key無須填寫, 省略4位元組
193 uint8_t frameHeaderSize = 2 + payloadFieldExtraBytes;
194 uint8_t *frameHeader = new uint8_t[frameHeaderSize];
195 memset(frameHeader, 0, frameHeaderSize);
196 // fin位為1, 擴充套件位為0, 操作位為frameType
197 frameHeader[0] = static_cast<uint8_t>(0x80 | frameType);
198
199 // 填充資料長度
200 if (messageLength <= 0x7d)
201 {
202 frameHeader[1] = static_cast<uint8_t>(messageLength);
203 }
204 else
205 {
206 frameHeader[1] = 0x7e;
207 uint16_t len = htons(messageLength);
208 memcpy(&frameHeader[2], &len, payloadFieldExtraBytes);
209 }
210
211 // 填充資料
212 uint32_t frameSize = frameHeaderSize + messageLength;
213 char *frame = new char[frameSize + 1];
214 memcpy(frame, frameHeader, frameHeaderSize);
215 memcpy(frame + frameHeaderSize, inMessage.c_str(), messageLength);
216 frame[frameSize] = ‘\0‘;
217 outFrame = frame;
218
219 delete[] frame;
220 delete[] frameHeader;
221 return ret;
222 }