node實現一個websocket服務器

早點時候翻譯了篇實現一個websocket服務器-理論篇 ,簡單介紹了下理論基礎,原本打算放在一塊兒,可是感受太長了你們可能都看不下去。不過發現若是拆開的話,仍是不可避免的要說起理論部分。用到的地方就簡要回顧一下好了。git

Websockt 基本通訊流程

在具體代碼實現以前,咱們須要大概理一下思路。回顧一下websocket的理論部分。簡單的websocket流程以下(這裏就不談詳細的過程了,大概描述一下)github

  1. 客戶端發送握手請求
  2. 服務器響應、處理握手並返回
  3. 客戶端驗證經過後,發送數據
  4. 服務器接收、處理數據,而後返回給客戶端
  5. 客戶端接收服務器的推送

做爲一個服務器而言,咱們主要的精力須要放在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

  • HTTP版本必須1.1及以上,請求方式爲GET
  • Connection: Upgrade
  • Upgrade: websocket //指定websocket
  • Sec-WebSocket-Key 密鑰 服務器處理握手的依據

咱們服務器處理握手時須要關注的就是上面四點。socket

響應握手請求

服務器根據是否websocket的必須請求頭,分下面兩種狀況:學習

  1. 不知足,做爲http請求來響應。
  2. 知足,解析處理按照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 ...                |
     +---------------------------------------------------------------+

每一個從客戶端發送到服務器的數據幀都遵循上面的格式。

  1. MASK位:只代表信息是否已進行掩碼處理。來自客戶端的消息必須通過處理,所以咱們應該將其置爲1
  2. opcode字段定義如何解析有效的數據:

    • 0x0 繼續處理
    • 0x1 text(必須是UTF-8編碼)
    • 0x2 二進制 和其餘叫作控制代碼的數據。
    • 0x3-0x7 0xB-0xF 該版本的WebSockets無心義
  3. FIN 代表是不是數據集合的最後一段消息,若是爲0,服務器繼續監聽消息,以待消息剩餘的部分。不然服務器認爲消息已經徹底發送。
  4. Payload len:有效數據長度

    • Payload len<126, 即爲真實長度
    • 126,說明真實長度大於125,後面2個字節的值爲真實長度
    • 127,真實長度大於65535,後面8字節值爲真實長度

解析數據

所謂解析數據,確定是基於上面的格式按照必定規則來進行處理。下面就是處理的規則。

  1. 獲取有效數據長度
  2. 獲取掩碼並依據規則進行反序列化數據

直接看代碼應該更加清晰。

// 解析接受的數據幀
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庫處理各類狀況是比較完善的,更推薦你們使用,這裏只是簡單實踐,更多的是知足一下本身的好奇心,知其然,也要知其因此然,但願你們共同窗習和進步

相關文章
相關標籤/搜索