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會出現偽連線現象。