1. 程式人生 > >Node.js websocket 使用 socket.io庫實現實時聊天室

Node.js websocket 使用 socket.io庫實現實時聊天室

認識websocket

WebSocket protocol 是HTML5一種新的協議。它實現了瀏覽器與伺服器全雙工通訊(full-duple)。一開始的握手需要藉助HTTP請求完成。

其實websocket 並不是很依賴Http協議,它也擁有自己的一套協議機制,但在這裡我們需要利用的socket.io 需要依賴到http 。
之前用java jsp寫過一個聊天,其實實現邏輯並不難,只是大部分時間都用在UI的設計上,其實現原理就是一個基於websocket的通訊,要想做一個好的聊天室,我覺得大部分精力可能更應該花在與使用者的視覺層互動上。

廢話不閒扯,我們先來看一下websocket 與傳統的ajax 有什麼不同之處。
在之前,如果我們想要獲取到伺服器更新的資訊,我們可以使用ajax 輪詢來完成,然而,這樣做的弊端是增大了我們與伺服器的互動次數,然而極大部分的互動都是無意義的,因為我們只是做一個詢問,如果沒有任何新的資訊,我們幾乎什麼都不用做,因此這樣會極大的浪費伺服器資源和頻寬。
然而使用websocket 會使客戶端與伺服器建立一個長連線,並且,當伺服器有新訊息時可以主動推送到客戶端,所以我們可以不用一次次的去詢問伺服器是否有新訊息,而是直接由伺服器主動推送到客戶端,這樣在無訊息的狀態下,客戶端不會頻繁的去請求伺服器。
使用websocket 的特點在於伺服器可以主動推送訊息到客戶端。

使用socket.io 庫實現實時聊天

這也是這篇博文的主題之處。socket.io釋出到npm 平臺上,我們可以直接用npm 來安裝到**當前**node_modules目錄下。

npm install socket.io --save 

下面我們就可以直接使用require 方法來將這個模組引入

const socket = require("socket.io");

在建立此websocket 伺服器之前,它需要依賴於一個已經建立好的http伺服器。

let socketServer = socket.listen(require("http").createServer((req,resp) => {
    //
返回頁面 resp.end(require("fs").readFileSync("./socketIOTest1.html")); }).listen(9999,"localhost",() => {console.log("listening");}))
;

在上述程式碼中socketIOTest1.html 是在當前目錄下的一個html檔案,在下面我會貼上詳細的程式碼,這裡先稍稍帶過。

在websocket 伺服器物件中有一個connection事件,這個事件在有客戶端連線到socket伺服器時被觸發。下面我們監聽這個事件,列印一句話來表示有使用者連線。

//監聽connection 事件
socketServer.on
("connection",socket => { console.log("有一使用者連線"); }

上述程式碼中,callback有一個引數socket為連線到客戶端的一個socket埠物件,這個物件有一個message 事件,當客戶端有訊息推送到伺服器時,事件迴圈會取出這個事件與之對應的回撥函式並執行。

socket.on("message",msg => {
    console.log(msg);
});

同時,socket物件還可以監聽disconnect 事件,來監聽使用者斷開連線的情況

socket.on("disconnect",() => {
    console.log("有一使用者退出連線");
});

因為我們這次的主題是要建立一個能夠實時聊天的聊天室,因此光有這些是不夠的,我們還需要一個能夠與使用者互動的客戶端。
下面是我的socketIOTest程式碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <textarea name="" id="content" cols="30" rows="10" ></textarea>
    <input id="write" type="text" placeholder="please write content here">
    <input id="send" type="button" value="send" />
    <script src="./socket.io/socket.io.js"></script>
    <script>
        let send = document.getElementById("send");
        let write = document.getElementById("write");
        let content = document.getElementById("content");
        let socket = io.connect();
        //傳送訊息
        send.onclick = () => {
            let msg = write.value;
            // content.innerHTML = content.value + msg + "\n";
            socket.send(msg);
        };
        //接收到訊息
        socket.on("message",msg => {
            console.log("從伺服器接收到的訊息 : " + msg);
            //更新內容
            content.innerHTML = content.value + msg + "\n";
        });
        socket.on("disconnect",() => {
            console.log("與伺服器斷開連線");
        });
    </script>
</body>
</html>

在上述程式碼中,我用script標籤引入了一個socket.io.js檔案,這個檔案不需要另外去下載,而直接引入即可,因為socket.io.js是被包含於socket.io模組中,在上面node的程式程式碼中,我們通過require方法引入了socket.io模組,因此我們可以直接通過相對路徑訪問到它。

<script src="./socket.io/socket.io.js"></script>

接下來我們就可以在script標籤中使用如同服務端的程式碼。

    let socket = io.connect();

使用io.connect()方法連線到websocket伺服器,該方法返回一個與連線的伺服器與之對應的一個socket埠物件。

下面我們同樣監聽message 和 disconnect事件。

//接收到訊息
socket.on("message",msg => {
    console.log("從伺服器接收到的訊息 : " + msg);
    //更新內容
    content.innerHTML = content.value + msg + "\n";
});
socket.on("disconnect",() => {
    console.log("與伺服器斷開連線");
});

為了更能突出websocket的作用,在html程式碼中,我只使用了一個textarea標籤來顯示內容,兩個input標籤用於傳送。
使用socket物件的send方法就能使訊息在伺服器與客戶端進行訊息傳遞。

websocket群聊實現

現在我們假設一個場景,有u1和u2兩個使用者,同時連線到伺服器,那麼我們怎麼使他們互相通訊呢,實現的方法及其簡單。當u1連線到伺服器,在伺服器中,使用一個map鍵值對把與u1對應的socket物件進行儲存。

//建立一個用於放置使用者物件的map
let map = new Map();
//用於記錄使用者數量的變數,並初始化為0
let userCount = 0;
//監聽connection 事件
socketServer.on("connection",socket => {
    console.log("有一使用者連線");
    map.set(++userCount,socket);
    //...
}); 

與此同時,u2也連線上伺服器,也由該map把與u2與之對應的socket物件進行儲存。
現在,u1點選了send按鈕傳送一條訊息至伺服器,伺服器收到訊息後遍歷map,轉發給所有socket物件,實現群聊的實時通訊。

socketServer.on("connection",socket => {
    console.log("有一使用者連線");
    map.set(++userCount,socket);
    //監聽客戶端來的資訊
    socket.on("message",msg => {
        //從客戶端接收的訊息
        //遍歷所有使用者
        map.forEach((value,index,arr) => {
            value.send(msg);
        });
    });
}); 

下面我貼上服務端的完整程式碼,僅供參考

const socket = require("socket.io");
//建立一個websocket伺服器
let socketServer = socket.listen(require("http").createServer((req,resp) => {
    //返回頁面
    resp.end(require("fs").readFileSync("./socketIOTest1.html"));
}).listen(9999,"localhost",() => {console.log("listening");}));

//建立一個用於放置使用者物件的map
let map = new Map();
//用於記錄使用者數量的變數,並初始化為0
let userCount = 0;

//監聽connection 事件
socketServer.on("connection",socket => {
    console.log("有一使用者連線");
    map.set(++userCount,socket);
    //監聽客戶端來的資訊
    socket.on("message",msg => {
        //從客戶端接收的訊息
        //遍歷所有使用者
        map.forEach((value,index,arr) => {
            value.send(msg);
        });
    });
    //監聽客戶端退出情況
    socket.on("disconnect",() => {
        console.log("有一使用者退出連線");
    });
}); 

這裡寫圖片描述
這裡寫圖片描述

websocket私聊實現

在說私聊的實現之前,我們首先要找到對於每一個使用者的唯一標識,在通常的專案開發中,我們都使用使用者的使用者名稱進行標識,每個使用者通過註冊獲得與之對應的使用者名稱。將使用者名稱儲存在資料庫中利用主鍵防止重複。

實現私聊的方法有很多種,這裡我的實現方法是這樣的:
① 當用戶連線時,把使用者的socket埠物件使用map進行儲存,儲存的key 為使用者的socket物件,value為使用者的使用者名稱,寫一個方法用於更新客戶端列表
② 使用者預設使用者名稱為 <未命名>,指定自定義使用者名稱時,使用socket.emit方法觸發服務端的某個事件,遍歷map找到與之對應的key,進行value修改
③ 傳送訊息時,根據選擇列表來指定要傳送的人,在服務端,遍歷map,找到要傳送到的使用者名稱,進行傳送,同時更新到自己的聊天框

以上就是私聊的簡單實現。

下面看一下具體程式碼:
//Node.js

const socket = require("socket.io");
//建立一個websocket伺服器
let socketServer = socket.listen(require("http").createServer((req,resp) => {
    //返回頁面
    resp.end(require("fs").readFileSync("./socketIOTest1.html"));
}).listen(9999,"localhost",() => {console.log("listening");}));

//建立一個用於放置使用者物件的map
let map = new Map();
//用於記錄使用者數量的變數,並初始化為0
let userCount = 0;
//遍歷map 
let scanMap = func => {
    try{
        map.forEach((value,index,arr) => {
            func(value,index,arr);
        });
    }
    catch(e){
        if(e.message == "break"){
            return;
        }
        else{
            throw e;
        }
    }
}

//通知客戶端彈出對話方塊
let showDialog = (socket,msg) => {
    socket.emit("showDialog",msg);
}

//更新使用者列表
let updateList = socket => {
    let userArr = [];
    scanMap((value,index) => {
        if(value != undefined){
            userArr.push(value);
        }
    });
    socket.emit("newUser",userArr);
}

//監聽connection 事件
socketServer.on("connection",socket => {
    console.log("有一使用者連線");
    //初始化儲存當前socket物件
    map.set(socket,"<未命名>");
    //將使用者資訊寫入map
    socket.on("getUser",user => {
        //修改名稱
        map.set(socket,user);
        scanMap((value,index) => {
            updateList(index);
        });
    });
    //通知所有客戶端更新列表
    scanMap((value,index) => {
        updateList(index);
    });
    //監聽客戶端來的資訊
    socket.on("message",msg => {
        //從客戶端接收的訊息
        let sender;
        //遍歷所有使用者
        scanMap((value,index) => {
            if(index == socket){
                sender = value;
            }
        });
        scanMap((value,index) => {
            if(msg.person == "all"){
                index.send(sender + " : " + msg.msg);
            }
            else if(msg.person == value){
                socket.send(sender + " : " +msg.msg);
                index.send(sender + " : " +msg.msg);
                throw new Error("break");
            }
        });
    });
    //監聽客戶端退出情況
    socket.on("disconnect",() => {
        //使用者退出,從map裡刪除該使用者
        map.set(socket,undefined);
        //通知所有使用者更新列表
        scanMap((value,index) => {
            updateList(index);
        });
        console.log("有一使用者退出連線");
    });
}); 

客戶端:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <textarea name="" id="content" cols="30" rows="10" ></textarea>
    <input id="write" type="text" placeholder="please write content here">
    <input id="send" type="button" value="send" />
    <input type="text" id="user" placeholder="user">
    <select style="width: 100px;" size="2" name="" id="userList">
        <option value="all">群聊</option>
    </select>
    <script src="./socket.io/socket.io.js"></script>
    <script>
        let send = document.getElementById("send");
        let write = document.getElementById("write");
        let content = document.getElementById("content");
        let user = document.getElementById("user");
        //使用者列表
        let userList = document.getElementById("userList");
        let socket = io.connect();
        //判斷使用者名稱是否為空
        let isUserEmpty = () => {
            if(user.value == ""){
                alert("請填寫使用者名稱");
                return false;
            }
            else {
                return true;
            }
        }
        //監聽使用者名稱變化
        let oldUser;
        user.onblur = () => {
            if(isUserEmpty()){
                //防止重複發射
                if(oldUser == user.value){return;}
                oldUser = user.value;
                socket.emit("getUser",user.value);
            }
        }       
        //傳送訊息
        send.onclick = () => {
            if(isUserEmpty()){
                let msg = write.value;
                // content.innerHTML = content.value + msg + "\n";
                socket.send({msg:msg,person:userList.value});
            }
            if(select.value == ""){
                alert("請選擇一個聊天物件");
            }
        };
        //接收到訊息
        socket.on("message",msg => {
            console.log("從伺服器接收到的訊息 : " + msg);
            //更新內容
            content.innerHTML = content.value + msg + "\n";
        });
        socket.on("disconnect",() => {
            console.log("與伺服器斷開連線");
        });
        //新使用者加入聊天室
        socket.on("newUser",arr => {
            userList.innerHTML = "";
            let all = document.createElement("option");
            all.innerHTML = "群聊";
            all.setAttribute("value","all");
            userList.appendChild(all);
            //新增新使用者
            arr.forEach((value,index) => {
                console.log("value :" + value + "index :" + index);
                let option = document.createElement("option");
                option.innerHTML = value;
                option.setAttribute("value",value);
                userList.appendChild(option);
                userList.setAttribute("size",userList.children.length);
            });
            //預設選中群聊
            userList.value = "all";
        });
        //接收伺服器需要彈出對話方塊的需求
        socket.on("showDialog",msg => {
            alert(msg);
        });
    </script>
</body>
</html>

這裡寫圖片描述

程式碼的具體我就不在詳細講解,都標有註釋,由於只是用於博文,整體程式碼沒有重構優化,大家看不懂的可以回覆我,或者有什麼地方錯誤請指出,我會及時改正。

另外在這個聊天室中,當用戶重新整理頻率較快時,websocket會出現偽連線現象。