Node.js websocket使用socket.io庫(kù)實(shí)現(xiàn)實(shí)時(shí)聊天室
認(rèn)識(shí)websocket
WebSocket protocol 是HTML5一種新的協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信(full-duple)。一開(kāi)始的握手需要借助HTTP請(qǐng)求完成。
其實(shí)websocket 并不是很依賴(lài)Http協(xié)議,它也擁有自己的一套協(xié)議機(jī)制,但在這里我們需要利用的socket.io 需要依賴(lài)到http 。
之前用java jsp寫(xiě)過(guò)一個(gè)聊天,其實(shí)實(shí)現(xiàn)邏輯并不難,只是大部分時(shí)間都用在UI的設(shè)計(jì)上,其實(shí)現(xiàn)原理就是一個(gè)基于websocket的通信,要想做一個(gè)好的聊天室,我覺(jué)得大部分精力可能更應(yīng)該花在與用戶(hù)的視覺(jué)層交互上。
廢話(huà)不閑扯,我們先來(lái)看一下websocket 與傳統(tǒng)的ajax 有什么不同之處。
在之前,如果我們想要獲取到服務(wù)器更新的信息,我們可以使用ajax 輪詢(xún)來(lái)完成,然而,這樣做的弊端是增大了我們與服務(wù)器的交互次數(shù),然而極大部分的交互都是無(wú)意義的,因?yàn)槲覀冎皇亲鲆粋€(gè)詢(xún)問(wèn),如果沒(méi)有任何新的信息,我們幾乎什么都不用做,因此這樣會(huì)極大的浪費(fèi)服務(wù)器資源和帶寬。
然而使用websocket 會(huì)使客戶(hù)端與服務(wù)器建立一個(gè)長(zhǎng)連接,并且,當(dāng)服務(wù)器有新消息時(shí)可以主動(dòng)推送到客戶(hù)端,所以我們可以不用一次次的去詢(xún)問(wèn)服務(wù)器是否有新消息,而是直接由服務(wù)器主動(dòng)推送到客戶(hù)端,這樣在無(wú)消息的狀態(tài)下,客戶(hù)端不會(huì)頻繁的去請(qǐng)求服務(wù)器。
使用websocket 的特點(diǎn)在于服務(wù)器可以主動(dòng)推送消息到客戶(hù)端。
使用socket.io 庫(kù)實(shí)現(xiàn)實(shí)時(shí)聊天
這也是這篇博文的主題之處。socket.io發(fā)布到npm 平臺(tái)上,我們可以直接用npm 來(lái)安裝到**當(dāng)前**node_modules目錄下。
npm install socket.io --save
下面我們就可以直接使用require 方法來(lái)將這個(gè)模塊引入
const socket = require("socket.io");
在創(chuàng)建此websocket 服務(wù)器之前,它需要依賴(lài)于一個(gè)已經(jīng)創(chuàng)建好的http服務(wù)器。
let socketServer = socket.listen(require("http").createServer((req,resp) => { //返回頁(yè)面 resp.end(require("fs").readFileSync("./socketIOTest1.html")); }).listen(9999,"localhost",() => {console.log("listening");}));
在上述代碼中socketIOTest1.html 是在當(dāng)前目錄下的一個(gè)html文件,在下面我會(huì)貼上詳細(xì)的代碼,這里先稍稍帶過(guò)。
在websocket 服務(wù)器對(duì)象中有一個(gè)connection事件,這個(gè)事件在有客戶(hù)端連接到socket服務(wù)器時(shí)被觸發(fā)。下面我們監(jiān)聽(tīng)這個(gè)事件,打印一句話(huà)來(lái)表示有用戶(hù)連接。
//監(jiān)聽(tīng)connection 事件 socketServer.on("connection",socket => { console.log("有一用戶(hù)連接"); }
上述代碼中,callback有一個(gè)參數(shù)socket為連接到客戶(hù)端的一個(gè)socket端口對(duì)象,這個(gè)對(duì)象有一個(gè)message 事件,當(dāng)客戶(hù)端有消息推送到服務(wù)器時(shí),事件循環(huán)會(huì)取出這個(gè)事件與之對(duì)應(yīng)的回調(diào)函數(shù)并執(zhí)行。
socket.on("message",msg => { console.log(msg); });
同時(shí),socket對(duì)象還可以監(jiān)聽(tīng)disconnect 事件,來(lái)監(jiān)聽(tīng)用戶(hù)斷開(kāi)連接的情況
socket.on("disconnect",() => { console.log("有一用戶(hù)退出連接"); });
因?yàn)槲覀冞@次的主題是要?jiǎng)?chuàng)建一個(gè)能夠?qū)崟r(shí)聊天的聊天室,因此光有這些是不夠的,我們還需要一個(gè)能夠與用戶(hù)交互的客戶(hù)端。
下面是我的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ù)器斷開(kāi)連接"); }); </script> </body> </html>
在上述代碼中,我用script標(biāo)簽引入了一個(gè)socket.io.js文件,這個(gè)文件不需要另外去下載,而直接引入即可,因?yàn)閟ocket.io.js是被包含于socket.io模塊中,在上面node的程序代碼中,我們通過(guò)require方法引入了socket.io模塊,因此我們可以直接通過(guò)相對(duì)路徑訪問(wèn)到它。
<script src="./socket.io/socket.io.js"></script>
接下來(lái)我們就可以在script標(biāo)簽中使用如同服務(wù)端的代碼。
let socket = io.connect();
使用io.connect()方法連接到websocket服務(wù)器,該方法返回一個(gè)與連接的服務(wù)器與之對(duì)應(yīng)的一個(gè)socket端口對(duì)象。
下面我們同樣監(jiān)聽(tīng)message 和 disconnect事件。
//接收到消息 socket.on("message",msg => { console.log("從服務(wù)器接收到的消息 : " + msg); //更新內(nèi)容 content.innerHTML = content.value + msg + "\n"; }); socket.on("disconnect",() => { console.log("與服務(wù)器斷開(kāi)連接"); });
為了更能突出websocket的作用,在html代碼中,我只使用了一個(gè)textarea標(biāo)簽來(lái)顯示內(nèi)容,兩個(gè)input標(biāo)簽用于發(fā)送。
使用socket對(duì)象的send方法就能使消息在服務(wù)器與客戶(hù)端進(jìn)行消息傳遞。
websocket群聊實(shí)現(xiàn)
現(xiàn)在我們假設(shè)一個(gè)場(chǎng)景,有u1和u2兩個(gè)用戶(hù),同時(shí)連接到服務(wù)器,那么我們?cè)趺词顾麄兓ハ嗤ㄐ拍?,?shí)現(xiàn)的方法及其簡(jiǎn)單。當(dāng)u1連接到服務(wù)器,在服務(wù)器中,使用一個(gè)map鍵值對(duì)把與u1對(duì)應(yīng)的socket對(duì)象進(jìn)行保存。
//創(chuàng)建一個(gè)用于放置用戶(hù)對(duì)象的map let map = new Map(); //用于記錄用戶(hù)數(shù)量的變量,并初始化為0 let userCount = 0; //監(jiān)聽(tīng)connection 事件 socketServer.on("connection",socket => { console.log("有一用戶(hù)連接"); map.set(++userCount,socket); //... });
與此同時(shí),u2也連接上服務(wù)器,也由該map把與u2與之對(duì)應(yīng)的socket對(duì)象進(jìn)行儲(chǔ)存。
現(xiàn)在,u1點(diǎn)擊了send按鈕發(fā)送一條消息至服務(wù)器,服務(wù)器收到消息后遍歷map,轉(zhuǎn)發(fā)給所有socket對(duì)象,實(shí)現(xiàn)群聊的實(shí)時(shí)通信。
socketServer.on("connection",socket => { console.log("有一用戶(hù)連接"); map.set(++userCount,socket); //監(jiān)聽(tīng)客戶(hù)端來(lái)的信息 socket.on("message",msg => { //從客戶(hù)端接收的消息 //遍歷所有用戶(hù) map.forEach((value,index,arr) => { value.send(msg); }); }); });
下面我貼上服務(wù)端的完整代碼,僅供參考
const socket = require("socket.io"); //創(chuàng)建一個(gè)websocket服務(wù)器 let socketServer = socket.listen(require("http").createServer((req,resp) => { //返回頁(yè)面 resp.end(require("fs").readFileSync("./socketIOTest1.html")); }).listen(9999,"localhost",() => {console.log("listening");})); //創(chuàng)建一個(gè)用于放置用戶(hù)對(duì)象的map let map = new Map(); //用于記錄用戶(hù)數(shù)量的變量,并初始化為0 let userCount = 0; //監(jiān)聽(tīng)connection 事件 socketServer.on("connection",socket => { console.log("有一用戶(hù)連接"); map.set(++userCount,socket); //監(jiān)聽(tīng)客戶(hù)端來(lái)的信息 socket.on("message",msg => { //從客戶(hù)端接收的消息 //遍歷所有用戶(hù) map.forEach((value,index,arr) => { value.send(msg); }); }); //監(jiān)聽(tīng)客戶(hù)端退出情況 socket.on("disconnect",() => { console.log("有一用戶(hù)退出連接"); }); });
websocket私聊實(shí)現(xiàn)
在說(shuō)私聊的實(shí)現(xiàn)之前,我們首先要找到對(duì)于每一個(gè)用戶(hù)的唯一標(biāo)識(shí),在通常的項(xiàng)目開(kāi)發(fā)中,我們都使用用戶(hù)的用戶(hù)名進(jìn)行標(biāo)識(shí),每個(gè)用戶(hù)通過(guò)注冊(cè)獲得與之對(duì)應(yīng)的用戶(hù)名。將用戶(hù)名保存在數(shù)據(jù)庫(kù)中利用主鍵防止重復(fù)。
實(shí)現(xiàn)私聊的方法有很多種,這里我的實(shí)現(xiàn)方法是這樣的:
① 當(dāng)用戶(hù)連接時(shí),把用戶(hù)的socket端口對(duì)象使用map進(jìn)行儲(chǔ)存,儲(chǔ)存的key 為用戶(hù)的socket對(duì)象,value為用戶(hù)的用戶(hù)名,寫(xiě)一個(gè)方法用于更新客戶(hù)端列表
② 用戶(hù)默認(rèn)用戶(hù)名為 <未命名>,指定自定義用戶(hù)名時(shí),使用socket.emit方法觸發(fā)服務(wù)端的某個(gè)事件,遍歷map找到與之對(duì)應(yīng)的key,進(jìn)行value修改
③ 發(fā)送消息時(shí),根據(jù)選擇列表來(lái)指定要發(fā)送的人,在服務(wù)端,遍歷map,找到要發(fā)送到的用戶(hù)名,進(jìn)行發(fā)送,同時(shí)更新到自己的聊天框
以上就是私聊的簡(jiǎn)單實(shí)現(xiàn)。
下面看一下具體代碼:
//Node.js const socket = require("socket.io"); //創(chuàng)建一個(gè)websocket服務(wù)器 let socketServer = socket.listen(require("http").createServer((req,resp) => { //返回頁(yè)面 resp.end(require("fs").readFileSync("./socketIOTest1.html")); }).listen(9999,"localhost",() => {console.log("listening");})); //創(chuàng)建一個(gè)用于放置用戶(hù)對(duì)象的map let map = new Map(); //用于記錄用戶(hù)數(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; } } } //通知客戶(hù)端彈出對(duì)話(huà)框 let showDialog = (socket,msg) => { socket.emit("showDialog",msg); } //更新用戶(hù)列表 let updateList = socket => { let userArr = []; scanMap((value,index) => { if(value != undefined){ userArr.push(value); } }); socket.emit("newUser",userArr); } //監(jiān)聽(tīng)connection 事件 socketServer.on("connection",socket => { console.log("有一用戶(hù)連接"); //初始化存儲(chǔ)當(dāng)前socket對(duì)象 map.set(socket,"<未命名>"); //將用戶(hù)信息寫(xiě)入map socket.on("getUser",user => { //修改名稱(chēng) map.set(socket,user); scanMap((value,index) => { updateList(index); }); }); //通知所有客戶(hù)端更新列表 scanMap((value,index) => { updateList(index); }); //監(jiān)聽(tīng)客戶(hù)端來(lái)的信息 socket.on("message",msg => { //從客戶(hù)端接收的消息 let sender; //遍歷所有用戶(hù) 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)聽(tīng)客戶(hù)端退出情況 socket.on("disconnect",() => { //用戶(hù)退出,從map里刪除該用戶(hù) map.set(socket,undefined); //通知所有用戶(hù)更新列表 scanMap((value,index) => { updateList(index); }); console.log("有一用戶(hù)退出連接"); }); });
客戶(hù)端:
<!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"); //用戶(hù)列表 let userList = document.getElementById("userList"); let socket = io.connect(); //判斷用戶(hù)名是否為空 let isUserEmpty = () => { if(user.value == ""){ alert("請(qǐng)?zhí)顚?xiě)用戶(hù)名"); return false; } else { return true; } } //監(jiān)聽(tīng)用戶(hù)名變化 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("請(qǐng)選擇一個(gè)聊天對(duì)象"); } }; //接收到消息 socket.on("message",msg => { console.log("從服務(wù)器接收到的消息 : " + msg); //更新內(nèi)容 content.innerHTML = content.value + msg + "\n"; }); socket.on("disconnect",() => { console.log("與服務(wù)器斷開(kāi)連接"); }); //新用戶(hù)加入聊天室 socket.on("newUser",arr => { userList.innerHTML = ""; let all = document.createElement("option"); all.innerHTML = "群聊"; all.setAttribute("value","all"); userList.appendChild(all); //添加新用戶(hù) 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ù)器需要彈出對(duì)話(huà)框的需求 socket.on("showDialog",msg => { alert(msg); }); </script> </body> </html>
代碼的具體我就不在詳細(xì)講解,都標(biāo)有注釋?zhuān)捎谥皇怯糜诓┪模w代碼沒(méi)有重構(gòu)優(yōu)化,大家看不懂的可以回復(fù)我,或者有什么地方錯(cuò)誤請(qǐng)指出,我會(huì)及時(shí)改正。
另外在這個(gè)聊天室中,當(dāng)用戶(hù)刷新頻率較快時(shí),websocket會(huì)出現(xiàn)偽連接現(xiàn)象。
下面附上我的github地址,大家可以下載我的源碼進(jìn)行修改學(xué)習(xí),共勉。
https://github.com/HaoDaWang/chat
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(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)境的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-05-05npm install安裝模塊-save和-save-dev命令的區(qū)別
這篇文章介紹了npm install安裝模塊-save和-save-dev命令的區(qū)別,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-06-06使用node.js 制作網(wǎng)站前臺(tái)后臺(tái)
本文給大家介紹實(shí)用node.js 制作網(wǎng)站前臺(tái)和后臺(tái),非常的詳盡,有需要的朋友可以參考下2014-11-11Node.js連接MongoDB數(shù)據(jù)庫(kù)產(chǎn)生的問(wèn)題
Node.js是使用JavaScript 編寫(xiě)的可以運(yùn)行在服務(wù)端的JS語(yǔ)言。node.js和mongodb碰撞會(huì)產(chǎn)生一系列問(wèn)題,下面通過(guò)本文給大家分享Node.js連接MongoDB數(shù)據(jù)庫(kù),需要的的朋友參考下2017-02-02NPM相關(guān)命令之報(bào)錯(cuò)node-gyp...的解決方法
node-gyp就是為node編譯c++擴(kuò)展的時(shí)候使用的編譯工具,下面這篇文章主要給大家介紹了關(guān)于NPM相關(guān)命令之報(bào)錯(cuò)node-gyp...的解決方法,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-09-09