早點時候翻譯了篇實現一個websocket伺服器-理論篇 ,簡單介紹了下理論基礎,本來打算放在一起,但是感覺太長了大家可能都看不下去。不過發現如果拆開的話,還是不可避免的要提及理論部分。用到的地方就簡要回顧一下好了。
Websockt 基本通訊流程
在具體程式碼實現之前,我們需要大概理一下思路。回顧一下websocket的理論部分。簡單的websocket流程如下(這裡就不談詳細的過程了,大概描述一下)
- 客戶端傳送握手請求
- 伺服器響應、處理握手並返回
- 客戶端驗證通過後,傳送資料
- 伺服器接收、處理資料,然後返回給客戶端
- 客戶端接收伺服器的推送
作為一個伺服器而言,我們主要的精力需要放在2,4這兩個步驟。
響應並處理握手
雖然websocket可以實現伺服器推送,前提在於該連線已經建立。客戶端仍然需要發起一個Websocket握手請求。 既然要響應該握手請求,我們需要了解一下該請求。
客戶端握手請求
客戶端的握手請求是一個標準的HTTP請求,大概像下面的例子。
GET / HTTP/1.1 //HTTP版本必須1.1及以上,請求方式為GET
Host: localhost:8081 //本地專案
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket //指定websocket協議
Origin: http://192.168.132.170:8000
Sec-WebSocket-Version: 13 //版本
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: optimizelyEndUserId=oeu1505722530441r0.5993643212774391; _ga=GA1.1.557695983.1505722531
Sec-WebSocket-Key: /2R6uuzPqLT/6z8fnZfN3w== //握手返回基於該金鑰
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
複製程式碼
上面列出了實際例子中的請求頭,內容由瀏覽器生成,需要注意的部分如下。
- HTTP版本必須1.1及以上,請求方式為GET
- Connection: Upgrade
- Upgrade: websocket //指定websocket
- Sec-WebSocket-Key 金鑰 伺服器處理握手的依據
我們伺服器處理握手時需要關注的就是上面四點。
響應握手請求
伺服器根據是否websocket的必須請求頭,分下面兩種情況:
- 不滿足,作為http請求來響應。
- 滿足,解析處理按照websocket規定的資料格式來響應
返回格式
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
複製程式碼
請注意每一個header以\r\n結尾並且在最後一個後面加入額外的\r\n。
這裡的Sec-WebSocket-Accept 就是基於請求頭中Sec-WebSocket-Key來生成。規則如下:
Sec-WebSocket-Key 和"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"連結,通過SHA-1 hash獲得結果,然後返回該結果的base64編碼。
程式碼如下:
// 指定拼接字元
var ws_key = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
// 生成相應key
function getAccpectKey(rSWKey) {
return crypto.createHash('sha1').update(rSWKey + ws_key).digest('base64')
}
function handShake(socket, headers) {
var reqSWKey = headers['Sec-WebSocket-Key'],
resSWKey = getAccpectKey(reqSWKey)
socket.write('HTTP/1.1 101 Switching Protocols\r\n');
socket.write('Upgrade: websocket\r\n');
socket.write('Connection: Upgrade\r\n');
socket.write('Sec-WebSocket-Accept: ' + resSWKey + '\r\n');
socket.write('\r\n');
}
複製程式碼
這樣我們的握手協議就算完成了,此時會觸發客戶端websocket的onopen事件,即websocket開啟,可以進行通訊
解析資料
客戶端傳送幀格式
握手協議完成之後,我們就該解析資料了,還是要把這張幀格式拿出來。
幀格式:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
複製程式碼
每個從客戶端傳送到伺服器的資料幀都遵循上面的格式。
-
MASK位:只表明資訊是否已進行掩碼處理。來自客戶端的訊息必須經過處理,因此我們應該將其置為1
-
opcode欄位定義如何解析有效的資料:
- 0x0 繼續處理
- 0x1 text(必須是UTF-8編碼)
- 0x2 二進位制 和其他叫做控制程式碼的資料。
- 0x3-0x7 0xB-0xF 該版本的WebSockets無意義
-
FIN 表明是否是資料集合的最後一段訊息,如果為0,伺服器繼續監聽訊息,以待訊息剩餘的部分。否則伺服器認為訊息已經完全傳送。
-
Payload len:有效資料長度
- Payload len<126, 即為真實長度
- 126,說明真實長度大於125,後面2個位元組的值為真實長度
- 127,真實長度大於65535,後面8位元組值為真實長度
解析資料
所謂解析資料,肯定是基於上面的格式按照一定規則來進行處理。下面就是處理的規則。
- 獲取有效資料長度
- 獲取掩碼並依據規則進行反序列化資料
直接看程式碼應該更加清晰。
// 解析接受的資料幀
function decodeFrame(buffer) {
/**
* >>> 7 右移操作,即位元組右移7位,目的是為了即只取第一位的值
* 10010030 ====> 00000001
* & 按位與 同1為1
* 15二進位制表示為:00001111 ,運算之後前四位即為0,得到後四位的值
* 11011000 & 00001111 ===》 00001000
*
*/
var fBite = buffer[0],
/**
* 獲取Fin的值,
* 1傳輸結束
* 0 繼續監聽
*/
Fin = fBite >>> 7,
/**
* 獲取opcode的值,opcode為fBite的4-7位
* & 按位與 同1為1
* 15二進位制表示為:00001111 ,運算之後前四位即為0,得到後四位的值
*/
opcode = buffer[0] & 15,
/**
* 獲取有效資料長度
*/
len = buffer[1] & 127,
// 是否進行掩碼處理,客戶端請求必須為1
Mask = buffer[1] >>> 7,
maskKey = null
// 獲取資料長度
//真實長度大於125,讀取後面2位元組
if (len == 126) {
len = buffer.readUInt16BE(2)
} else if (len == 127) {
// 真實長度大於65535,讀取後面8位元組
len = buffer.readUInt64BE(2)
}
// 判斷是否進行掩碼處理
Mask && (maskKey = buffer.slice(2,5))
/**
* 反掩碼處理
* 迴圈遍歷加密的位元組(octets,text資料的單位)並且將其與第(i%4)位掩碼位元組(即i除以4取餘)進行異或運算
*/
if(Mask){
for (var i = 2;i<len ;i++){
buffer[i] = maskKey[(i - 2) % 4] ^ buffer[i];
}
}
var data = buffer.slice(2)
return {
Fin:Fin,
opcode:opcode,
data:data
}
}
複製程式碼
傳送資料
處理完接收到的資料之後,下面就是傳送響應了。 響應資料不需要進行掩碼運算,只需要根據幀的格式(即上面的幀),將資料進行組裝就好
// 加密傳送資料
function encodeFrame(data){
var len = Buffer.byteLength(data),
// 2的64位
payload_len = len > 65535 ?10:(len > 125 ? 4 : 2),
buf = new Buffer(len+payload_len)
/**
* 首個位元組,0x81 = 10000001
*對應的Fin 為1 opcode為001 mask 為0
* 即表明 返回資料為txt文字已經結束並未使用掩碼處理
*/
buf[0] = 0x81
/**
* 根據真實資料長度設定payload_len位
*/
if(payload_len == 2){
buf[1] = len
}else if(payload_len == 4){
buf[1] = 126;
buf.writeUInt16BE(payload_len, 2);
}else {
buf[1] = 127;
buf.writeUInt32BE(payload_len >>> 32, 2);
buf.writeUInt32BE(payload_len & 0xFFFFFFFF, 6);
}
buf.write(data, payload_len);
return buf;
}
複製程式碼
心跳響應
當收到opcode 為 9時即ping請求,直接返回具有完全相同有效資料的pong即可。 Pings的opcode為0x9,pong是0xA,所以可以直接如下
// ping請求
if(opcode == 9){
console.log("ping相應");
/**
* ping pong最大長度為125,所以可以直接拼接
* 前兩位資料為10001010+資料長度
* 即傳輸完畢的pong響應,資料肯定小於125
*/
socke.write(Buffer.concat([new Buffer([0x8A, data.length]), data]))
}
複製程式碼
結束語
至此,一個websocket伺服器的簡單實現就完成了更多細節請檢視。當然成熟的websocket庫處理各種情況是比較完善的,更推薦大家使用,這裡只是簡單實踐,更多的是滿足一下自己的好奇心,知其然,也要知其所以然,希望大家共同學習和進步