早點時候翻譯了篇實現一個websocket服務器-理論篇 ,簡單介紹了下理論基礎,原本打算放在一塊兒,可是感受太長了你們可能都看不下去。不過發現若是拆開的話,仍是不可避免的要說起理論部分。用到的地方就簡要回顧一下好了。git
在具體代碼實現以前,咱們須要大概理一下思路。回顧一下websocket的理論部分。簡單的websocket流程以下(這裏就不談詳細的過程了,大概描述一下)github
做爲一個服務器而言,咱們主要的精力須要放在2,4這兩個步驟。web
雖然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
上面列出了實際例子中的請求頭,內容由瀏覽器生成,須要注意的部分以下。websocket
咱們服務器處理握手時須要關注的就是上面四點。socket
服務器根據是否websocket的必須請求頭,分下面兩種狀況:學習
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
請注意每個header以rn結尾而且在最後一個後面加入額外的rn。 編碼
這裏的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 ... | +---------------------------------------------------------------+
每一個從客戶端發送到服務器的數據幀都遵循上面的格式。
opcode字段定義如何解析有效的數據:
Payload len:有效數據長度
所謂解析數據,確定是基於上面的格式按照必定規則來進行處理。下面就是處理的規則。
直接看代碼應該更加清晰。
// 解析接受的數據幀 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庫處理各類狀況是比較完善的,更推薦你們使用,這裏只是簡單實踐,更多的是知足一下本身的好奇心,知其然,也要知其因此然,但願你們共同窗習和進步