Node.js websocket使用socket.io庫實(shí)現(xiàn)實(shí)時聊天室
認(rèn)識websocket
WebSocket protocol 是HTML5一種新的協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信(full-duple)。一開始的握手需要借助HTTP請求完成。
其實(shí)websocket 并不是很依賴Http協(xié)議,它也擁有自己的一套協(xié)議機(jī)制,但在這里我們需要利用的socket.io 需要依賴到http 。
之前用java jsp寫過一個聊天,其實(shí)實(shí)現(xiàn)邏輯并不難,只是大部分時間都用在UI的設(shè)計上,其實(shí)現(xiàn)原理就是一個基于websocket的通信,要想做一個好的聊天室,我覺得大部分精力可能更應(yīng)該花在與用戶的視覺層交互上。
廢話不閑扯,我們先來看一下websocket 與傳統(tǒng)的ajax 有什么不同之處。
在之前,如果我們想要獲取到服務(wù)器更新的信息,我們可以使用ajax 輪詢來完成,然而,這樣做的弊端是增大了我們與服務(wù)器的交互次數(shù),然而極大部分的交互都是無意義的,因?yàn)槲覀冎皇亲鲆粋€詢問,如果沒有任何新的信息,我們幾乎什么都不用做,因此這樣會極大的浪費(fèi)服務(wù)器資源和帶寬。
然而使用websocket 會使客戶端與服務(wù)器建立一個長連接,并且,當(dāng)服務(wù)器有新消息時可以主動推送到客戶端,所以我們可以不用一次次的去詢問服務(wù)器是否有新消息,而是直接由服務(wù)器主動推送到客戶端,這樣在無消息的狀態(tài)下,客戶端不會頻繁的去請求服務(wù)器。
使用websocket 的特點(diǎn)在于服務(wù)器可以主動推送消息到客戶端。
使用socket.io 庫實(shí)現(xiàn)實(shí)時聊天
這也是這篇博文的主題之處。socket.io發(fā)布到npm 平臺上,我們可以直接用npm 來安裝到**當(dāng)前**node_modules目錄下。
npm install socket.io --save
下面我們就可以直接使用require 方法來將這個模塊引入
const socket = require("socket.io");
在創(chuàng)建此websocket 服務(wù)器之前,它需要依賴于一個已經(jīng)創(chuàng)建好的http服務(wù)器。
let socketServer = socket.listen(require("http").createServer((req,resp) => {
//返回頁面
resp.end(require("fs").readFileSync("./socketIOTest1.html"));
}).listen(9999,"localhost",() => {console.log("listening");}));
在上述代碼中socketIOTest1.html 是在當(dāng)前目錄下的一個html文件,在下面我會貼上詳細(xì)的代碼,這里先稍稍帶過。
在websocket 服務(wù)器對象中有一個connection事件,這個事件在有客戶端連接到socket服務(wù)器時被觸發(fā)。下面我們監(jiān)聽這個事件,打印一句話來表示有用戶連接。
//監(jiān)聽connection 事件
socketServer.on("connection",socket => {
console.log("有一用戶連接");
}
上述代碼中,callback有一個參數(shù)socket為連接到客戶端的一個socket端口對象,這個對象有一個message 事件,當(dāng)客戶端有消息推送到服務(wù)器時,事件循環(huán)會取出這個事件與之對應(yīng)的回調(diào)函數(shù)并執(zhí)行。
socket.on("message",msg => {
console.log(msg);
});
同時,socket對象還可以監(jiān)聽disconnect 事件,來監(jiān)聽用戶斷開連接的情況
socket.on("disconnect",() => {
console.log("有一用戶退出連接");
});
因?yàn)槲覀冞@次的主題是要創(chuàng)建一個能夠?qū)崟r聊天的聊天室,因此光有這些是不夠的,我們還需要一個能夠與用戶交互的客戶端。
下面是我的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();
//發(fā)送消息
send.onclick = () => {
let msg = write.value;
// content.innerHTML = content.value + msg + "\n";
socket.send(msg);
};
//接收到消息
socket.on("message",msg => {
console.log("從服務(wù)器接收到的消息 : " + msg);
//更新內(nèi)容
content.innerHTML = content.value + msg + "\n";
});
socket.on("disconnect",() => {
console.log("與服務(wù)器斷開連接");
});
</script>
</body>
</html>
在上述代碼中,我用script標(biāo)簽引入了一個socket.io.js文件,這個文件不需要另外去下載,而直接引入即可,因?yàn)閟ocket.io.js是被包含于socket.io模塊中,在上面node的程序代碼中,我們通過require方法引入了socket.io模塊,因此我們可以直接通過相對路徑訪問到它。
<script src="./socket.io/socket.io.js"></script>
接下來我們就可以在script標(biāo)簽中使用如同服務(wù)端的代碼。
let socket = io.connect();
使用io.connect()方法連接到websocket服務(wù)器,該方法返回一個與連接的服務(wù)器與之對應(yīng)的一個socket端口對象。
下面我們同樣監(jiān)聽message 和 disconnect事件。
//接收到消息
socket.on("message",msg => {
console.log("從服務(wù)器接收到的消息 : " + msg);
//更新內(nèi)容
content.innerHTML = content.value + msg + "\n";
});
socket.on("disconnect",() => {
console.log("與服務(wù)器斷開連接");
});
為了更能突出websocket的作用,在html代碼中,我只使用了一個textarea標(biāo)簽來顯示內(nèi)容,兩個input標(biāo)簽用于發(fā)送。
使用socket對象的send方法就能使消息在服務(wù)器與客戶端進(jìn)行消息傳遞。
websocket群聊實(shí)現(xiàn)
現(xiàn)在我們假設(shè)一個場景,有u1和u2兩個用戶,同時連接到服務(wù)器,那么我們怎么使他們互相通信呢,實(shí)現(xiàn)的方法及其簡單。當(dāng)u1連接到服務(wù)器,在服務(wù)器中,使用一個map鍵值對把與u1對應(yīng)的socket對象進(jìn)行保存。
//創(chuàng)建一個用于放置用戶對象的map
let map = new Map();
//用于記錄用戶數(shù)量的變量,并初始化為0
let userCount = 0;
//監(jiān)聽connection 事件
socketServer.on("connection",socket => {
console.log("有一用戶連接");
map.set(++userCount,socket);
//...
});
與此同時,u2也連接上服務(wù)器,也由該map把與u2與之對應(yīng)的socket對象進(jìn)行儲存。
現(xiàn)在,u1點(diǎn)擊了send按鈕發(fā)送一條消息至服務(wù)器,服務(wù)器收到消息后遍歷map,轉(zhuǎn)發(fā)給所有socket對象,實(shí)現(xiàn)群聊的實(shí)時通信。
socketServer.on("connection",socket => {
console.log("有一用戶連接");
map.set(++userCount,socket);
//監(jiān)聽客戶端來的信息
socket.on("message",msg => {
//從客戶端接收的消息
//遍歷所有用戶
map.forEach((value,index,arr) => {
value.send(msg);
});
});
});
下面我貼上服務(wù)端的完整代碼,僅供參考
const socket = require("socket.io");
//創(chuàng)建一個websocket服務(wù)器
let socketServer = socket.listen(require("http").createServer((req,resp) => {
//返回頁面
resp.end(require("fs").readFileSync("./socketIOTest1.html"));
}).listen(9999,"localhost",() => {console.log("listening");}));
//創(chuàng)建一個用于放置用戶對象的map
let map = new Map();
//用于記錄用戶數(shù)量的變量,并初始化為0
let userCount = 0;
//監(jiān)聽connection 事件
socketServer.on("connection",socket => {
console.log("有一用戶連接");
map.set(++userCount,socket);
//監(jiān)聽客戶端來的信息
socket.on("message",msg => {
//從客戶端接收的消息
//遍歷所有用戶
map.forEach((value,index,arr) => {
value.send(msg);
});
});
//監(jiān)聽客戶端退出情況
socket.on("disconnect",() => {
console.log("有一用戶退出連接");
});
});

websocket私聊實(shí)現(xiàn)
在說私聊的實(shí)現(xiàn)之前,我們首先要找到對于每一個用戶的唯一標(biāo)識,在通常的項(xiàng)目開發(fā)中,我們都使用用戶的用戶名進(jìn)行標(biāo)識,每個用戶通過注冊獲得與之對應(yīng)的用戶名。將用戶名保存在數(shù)據(jù)庫中利用主鍵防止重復(fù)。
實(shí)現(xiàn)私聊的方法有很多種,這里我的實(shí)現(xiàn)方法是這樣的:
① 當(dāng)用戶連接時,把用戶的socket端口對象使用map進(jìn)行儲存,儲存的key 為用戶的socket對象,value為用戶的用戶名,寫一個方法用于更新客戶端列表
② 用戶默認(rèn)用戶名為 <未命名>,指定自定義用戶名時,使用socket.emit方法觸發(fā)服務(wù)端的某個事件,遍歷map找到與之對應(yīng)的key,進(jìn)行value修改
③ 發(fā)送消息時,根據(jù)選擇列表來指定要發(fā)送的人,在服務(wù)端,遍歷map,找到要發(fā)送到的用戶名,進(jìn)行發(fā)送,同時更新到自己的聊天框
以上就是私聊的簡單實(shí)現(xiàn)。
下面看一下具體代碼:
//Node.js
const socket = require("socket.io");
//創(chuàng)建一個websocket服務(wù)器
let socketServer = socket.listen(require("http").createServer((req,resp) => {
//返回頁面
resp.end(require("fs").readFileSync("./socketIOTest1.html"));
}).listen(9999,"localhost",() => {console.log("listening");}));
//創(chuàng)建一個用于放置用戶對象的map
let map = new Map();
//用于記錄用戶數(shù)量的變量,并初始化為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);
}
//監(jiān)聽connection 事件
socketServer.on("connection",socket => {
console.log("有一用戶連接");
//初始化存儲當(dāng)前socket對象
map.set(socket,"<未命名>");
//將用戶信息寫入map
socket.on("getUser",user => {
//修改名稱
map.set(socket,user);
scanMap((value,index) => {
updateList(index);
});
});
//通知所有客戶端更新列表
scanMap((value,index) => {
updateList(index);
});
//監(jiān)聽客戶端來的信息
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");
}
});
});
//監(jiān)聽客戶端退出情況
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("請?zhí)顚懹脩裘?);
return false;
}
else {
return true;
}
}
//監(jiān)聽用戶名變化
let oldUser;
user.onblur = () => {
if(isUserEmpty()){
//防止重復(fù)發(fā)射
if(oldUser == user.value){return;}
oldUser = user.value;
socket.emit("getUser",user.value);
}
}
//發(fā)送消息
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("從服務(wù)器接收到的消息 : " + msg);
//更新內(nèi)容
content.innerHTML = content.value + msg + "\n";
});
socket.on("disconnect",() => {
console.log("與服務(wù)器斷開連接");
});
//新用戶加入聊天室
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);
});
//默認(rèn)選中群聊
userList.value = "all";
});
//接收服務(wù)器需要彈出對話框的需求
socket.on("showDialog",msg => {
alert(msg);
});
</script>
</body>
</html>

代碼的具體我就不在詳細(xì)講解,都標(biāo)有注釋,由于只是用于博文,整體代碼沒有重構(gòu)優(yōu)化,大家看不懂的可以回復(fù)我,或者有什么地方錯誤請指出,我會及時改正。
另外在這個聊天室中,當(dāng)用戶刷新頻率較快時,websocket會出現(xiàn)偽連接現(xiàn)象。
下面附上我的github地址,大家可以下載我的源碼進(jìn)行修改學(xué)習(xí),共勉。
https://github.com/HaoDaWang/chat
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
Node.js中,在cmd界面,進(jìn)入退出Node.js運(yùn)行環(huán)境的方法
今天小編就為大家分享一篇Node.js中,在cmd界面,進(jìn)入退出Node.js運(yùn)行環(huán)境的方法,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2018-05-05
npm install安裝模塊-save和-save-dev命令的區(qū)別
這篇文章介紹了npm install安裝模塊-save和-save-dev命令的區(qū)別,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06
Node.js連接MongoDB數(shù)據(jù)庫產(chǎn)生的問題
Node.js是使用JavaScript 編寫的可以運(yùn)行在服務(wù)端的JS語言。node.js和mongodb碰撞會產(chǎn)生一系列問題,下面通過本文給大家分享Node.js連接MongoDB數(shù)據(jù)庫,需要的的朋友參考下2017-02-02
NPM相關(guān)命令之報錯node-gyp...的解決方法
node-gyp就是為node編譯c++擴(kuò)展的時候使用的編譯工具,下面這篇文章主要給大家介紹了關(guān)于NPM相關(guān)命令之報錯node-gyp...的解決方法,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09

