利用express+socket.io實現一個簡易版聊天室

寫在前面

最近因爲利用node重構某個項目,項目中有一個實時聊天的功能,因而就研究了一下聊天室,在線demo|源碼,歡迎你們反饋。這個聊天室的主要利用到了socket.ioexpress。這個聊天室支持羣聊,私聊,支持發送圖片(PS:你們在體驗時最好開啓兩個瀏覽器,自問自答)。下面就來和你們分享下實現過程:php

WebSocket

HTML5一種新的協議。它實現了瀏覽器與服務器全雙工通訊。css

爲了更好的理解WebSocket,須要瞭解一下在沒有WebSocket階段是如何寫聊天室這種實時系統的:
基於http協議瀏覽器能夠實現單向通訊,只能由瀏覽器發起請求(Request),服務器進行響應(Response),一個請求對應一個響應。因爲服務器不能主動向客戶端推送消息,因而廣泛採用的方式就是輪詢(polling),輪詢實現起來很是簡單,就是定時的利用ajax向服務器端進行請求。若是服務器有新的數據就返回新的數據,若是沒有數據就返回空響應。用代碼來模擬下就是這個樣子的:前端

// 前端請求代碼
function update (fn) {
    var xhr = new XMLHttpRequest();
    xhr.open("get", "./update.php");
    xhr.onreadystatechange = function(){    
    if(xhr.readyState === 4){
      if(xhr.status == 200){    
        const res = JSON.parse(xhr.response);
          if (res.flag) {
              // 進行相應操做
              
              // fn爲接到響應後的處理函數
              fn && fn(fn);
          }
      }
    }
    };
    xhr.send();
}
function polling () {
    update();
}
setInterval(polling, 2000);
// 後臺響應代碼
<?php
    // 利用隨機數的大小來模擬是否有新數據
    if (rand(1, 100) < 35) {
        echo json_encode(array( 
            "flag" => true, 
            "data" => '有新數據來了'
        ));
    } else {
        echo json_encode(array(
            "flag" => false
        ));
    }
?>

這種定時請求的方式的關鍵在於間隔時間的選取,依據我在上面代碼作的模擬,不多機率能拿到下真正的數據,多半的ajax請求是無效的,因而又有前輩基於輪詢提出來了Comet(服務器推),這種技術能夠經過長輪詢(long polling)實現(還能夠利用iframe),長輪詢也是靠ajax實現客戶端的請求,其流程爲:客戶端發起請求,服務器掛起請求,倘若有新的數據返回,服務器響應客戶端剛纔的請求,客戶端獲得響應後繼續請求服務器。用僞代碼來模擬下長輪詢的過程:node

// 前端利用下面函數進行請求
function longPolling () {
    update(update);
}
longpolling();
// 後端代碼作以下更改
<?php
    // 利用隨機數的大小來模擬是否有新數據
    while (true) {
        if (rand(1, 100) < 5) {
            echo json_encode(array( 
                "flag" => true, 
                "data" => '有新數據來了'
            ));
            break;
        }
    }
?>

長輪詢的確減小了請求的次數,可是它也有着很大的問題,那就是耗費服務器的資源
不管是輪詢仍是長輪詢,還有着一個問題就是http並非支持長鏈接不少人會說keep-alive不就是作到了長鏈接嗎?然而並不是如此,keep-alive是重用一個TCP鏈接,就是說http 1.1作到了一個TCP鏈接能夠發送多個http請求,然而每一個http請求還須要發送Request Header,每一個請求的響應還會帶着Response Header。對於輪詢和長輪詢來講伴隨着真實數據的交換,還有進行的就是大量的http header的交換。
基於這些問題,WebSocket被提出,WebSocket能夠理解爲對http的一個補丁包,WebSocket使http變成了一個真正的長鏈接,握手階段利用http協議,以後就不會再發起http請求了。下面來看下WebSocket握手的過程:git

clipboard.png
客戶端的請求頭比通常的http請求多出來幾個字段:github

  • Upgrade: websocket,Connection: Upgrade,利用這兩個字段來告訴服務器,我要將協議升級爲websocketweb

  • Sec-WebSocket-Version: 13,來告訴服務器我想要使用的WebSocket的版本。ajax

  • Sec-WebSocket-Key,其值採用base64編碼的隨機16字節長的字符序列,這個值會在響應頭中迴應。express

  • Sec-WebSocket-Extensions,提供了一個客戶端支持的協議擴展列表來供服務器選擇,服務器只能選擇一個,而且會將選擇的擴展寫入響應頭的Sec-WebSocket-Extensionsjson

  • Sec-WebSocket-Protocol,與Sec-WebSocket-Extensions原理類似,用於協商應用子協議。

再來看看響應頭:

  • Status Code,值爲101,表示已經升級到WebSocket協議

  • Sec-WebSocket-Extensions告訴客戶端服務器選擇的協議擴展

  • Sec-WebSocket-Protocol告訴客戶端服務器選擇的子協議

  • Sec-WebSocket-Accept經服務器確認而且加密後的Sec-WebSocket-Key

還有一點值得關注的就是協議頭由http/https換成了ws/wss,也標識真http完成了其使命,接下來的事情由WebSocket來負責啦!

socket.io

因爲寫原生的WebSocket在處理低版本瀏覽器的兼容性上的困難,因此通常在寫實時交互的這種項目時通常會利用到socket.iosocket.io並不只僅是WebSocket,還包含着AJAX long pollingAJAX multipart streamingJSONP Polling等。socket.io能夠看作是基於engine.io的二次開發。經過emiton能夠輕鬆地實現服務器與客戶端之間的雙向通訊,emit來發布事件,on來訂閱事件。

用戶登陸/登出

下面開始來寫代碼,我利用的構建工具是gulp,模板語言是jade,css預處理語言是less,倘若也須要使用到這些,能夠關注下我所在團隊搭建的一個小的腳手架,先從app.js開始:

const users = {}, 
    app = express(),
    server = require("http").createServer(app),
    io = require("socket.io").listen(server); 
// 將socket.io綁定到服務器上,使得任何鏈接到服務器的客戶端都具備實時通訊的功能

// 服務器來監聽客戶端
io.on("connection", (socket) => {
    // socket是返回的鏈接對象,兩端的交互就是經過這個對象
});

須要建立一個對象(users)來存儲在線用戶,鍵值爲用戶暱稱,爲用戶登陸來訂閱個事件:

socket.on("login", (nickname) => {
        if (users[nickname] || nickname === "system") {
            socket.emit("repeat");            
        } else {
            socket.nickname = nickname;
            users[nickname] = {
                name: nickname,
                socket: socket,
                lastSpeakTime: nowSecond()
            };
            socket.emit("loginSuccess");            
            UsersChange(nickname, true);
        }
});
socket.on("disconnect", () => {
    if (socket.nickname && users[socket.nickname]) {
        delete users[socket.nickname];
        UsersChange(socket.nickname, false);
    }
});
function UsersChange (nickname, flag) {
    io.sockets.emit("system", {
        nickname: nickname,
        size: Object.keys(users).length,
        flag: flag
    });
}
function nowSecond () {
    return Math.floor(new Date() / 1000);
}

用戶登陸時須要驗證其暱稱是否含有,倘若函數,則觸發在客戶端的js代碼中註冊的repeat事件,反之觸發loginSuccess事件而且登陸成功後須要向全部的客戶端來廣播,因此利用了io.sockets.emitrepeatloginSuccesssystem,在src/js/index.js中進行註冊,主要用於頁面的顯示,也就是一些dom操做,因此在這裏沒有什麼好講的。用戶退出,直接調用默認事件disconnect就好,並將該用戶從用戶對象中移除。

心跳檢測

在用戶的狀態上的坑仍是很多的,由於WebSocket中間過程比較複雜,常常會出現一些異常的狀況,因此須要進行心跳檢測,我採用的方式是服務端定時遍歷用戶列表,倘若用戶最後的發言時間與如今相比超過了5分鐘,就將其視爲掉線,從而避免了"用戶undefined退出羣聊"的這種狀況。

function pong () {
    const now = nowSecond();
    for (let k in users) {
        if (users[k].lastSpeakTime + MAX_LEAVE_TIME < now) {
            var socket = users[k].socket;
            users[k].socket.emit("disconnect");
            socket.emit("nouser", "因爲長時間未說話,您已經掉線,請從新刷新頁面");
            socket = null;
        } 
    }
}
// 心跳檢測
setInterval(pong, PONG_TIME);
function UsersChange (nickname, flag) {
    io.sockets.emit("system", {
        nickname: nickname,
        size: Object.keys(users).length,
        flag: flag
    });
}

寫在最後

其實socket.io的使用真的很是簡單,很容易就會上手,因此其他功能再也不一一演示,你們能夠看代碼的實現(寫的比較差,還請見諒),客戶端代碼中大量用到了L,至關於zepto$,特別須要處理的是在私信和發送圖片的處理上,私信須要處理不一樣消息框,到底把消息添加到那個消息框中,我利用了一個對象來存儲這些信息(cache),cache的鍵名爲用戶的暱稱(由於在註冊時判斷了其是否惟一,因此能夠將其視爲惟一的);鍵值爲對象,對象屬性以下圖所示:

clipboard.png
具體實現你們仍是到源碼中去看吧!

感謝王哇勇大神的HiChat小鬍子哥的blogChat
因爲本人水平有限,若有錯誤,歡迎你們指出!

相關文章
相關標籤/搜索