1、聊天室簡單介紹javascript
採用nodeJS設計,基於express框架,使用WebSocket編程之 socket.io機制。聊天室增長了 註冊登陸 模塊 ,並將用戶我的信息和聊天記錄存入數據庫.html
數據庫採用的是mongodb , 並使用其相應mongoose對象工具來處理數據的存取。java
功能主要涉及:羣聊、私聊、設置我的信息、查看聊天記錄、查看在線用戶等node
效果圖:jquery
你也能夠直接來這裏 查看演示git
2、聊天室基本設計思路github
除去上次的註冊登陸模塊不說,本次主要就是增長了socket.io模塊的設計 以及 整合所有代碼的過程..太艱難了奮戰了幾天...mongodb
首先,數據庫中存儲了用戶信息(user)和聊天內容(content), mongoose版的Schema以下:數據庫
module.exports = { user:{ name:{type:String,required:true}, password:{type:String,required:true}, sex:{type:String,default:"boy"}, status:{type:String,default: "down"} }, content:{ name:{type:String,require:true}, data:{type:String,require:true}, time:{type:String,required:true} } };
而後經過對其的模型拉取就能夠獲取相應的Model, 而後傳遞一下express
var mongoose = require('mongoose'); var Schema = mongoose.Schema; var models = require("./models"); for(var m in models){ mongoose.model(m,new Schema(models[m])); } module.exports = { getModel: function(type){ return _getModel(type); } }; var _getModel = function(type){ return mongoose.model(type); };
app.js 中
global.dbHandel = require('./database/dbHandel'); // 全局handel獲取數據庫Model global.db = mongoose.connect("mongodb://127.0.0.1:27017/nodedb");
這樣一來就能夠直接操做數據庫數據了,好比與app.js在同目錄下的 chat_server.js 中的某部分(獲取上線用戶)
// 獲取上線的用戶 function getUserUp(ssocket){ var User = global.dbHandel.getModel('user'); User.find({status: "up"},function(err,docs){ if(err){ console.log(err); }else{ console.log('users list --default: '+docs); // 由於是回調函數 socket.emit放在這裏能夠防止 用戶更新列表滯後 ssocket.broadcast.emit('user_list',docs); //更新用戶列表 ssocket.emit('user_list',docs); //更新用戶列表 } }); }
如此之類,數據庫數據的存取就使用這種方式
正式介紹聊天室的核心 --- socket.io
這裏不是介紹socket.io的基本知識,只是大概講解一下這個聊天室如何經過socket.io 構建 即思路
1.上面說到了,每位用戶都把數據置入數據庫中,其中有status這一屬性,其實"down"表示下線,「up"表示上線,在線用戶就是這麼處理
在index.js(路由配置文件)看看這小段代碼,登陸成功後就立刻 statusSetUp() 將其上線,
if(req.body.upwd != doc.password){ //查詢到匹配用戶名的信息,但相應的password屬性不匹配 req.session.error = "密碼錯誤"; res.send(404); // res.redirect("/login"); }else{ //信息匹配成功,則將此對象(匹配到的user) 賦給session.user 並返回成功 req.session.user = doc; statusSetUp(uname); // 上線 res.send(200); // res.redirect("/home"); }
看看statusSetUp()的內容:將狀態改爲 up 以後,看上邊的代碼,下面是 res.send(200); 就是說執行完statusSetUp()以後才返回給原 "login',而後正式進入‘home'以後
function statusSetUp(oName){ //登陸 上線處理 var User = global.dbHandel.getModel('user'); User.update({name:oName},{$set: {status: 'up'}},function(err,doc){ if(err){ console.log(err); }else{ console.log(oName+ " is up"); } }); }
在home.html文件中有引用
<script type="text/javascript" src="javascripts/jquery.min.js"></script> <script type="text/javascript" src="javascripts/bootstrap.min.js"></script> <script type="text/javascript" src="/socket.io/socket.io.js"></script> <script type="text/javascript" src="javascripts/chat_client.js"></script>
說明1:進入home路徑以後便開始渲染home.html頁面,此時將加載chat_client.js文件信息並處理,此時,開始鏈接
說明2:鏈接成功後會自動建立socket.io.js 路徑引用通常就使用上述的方法
下面是chat_client.js裏頭開始鏈接服務端的部分,
socket.on("connect",function(){ // 進入聊天室 var userName = $("#nickname span").html(); socket.send(userName); // 向服務器發送本身的暱稱 console.log("send userName to server completed"); });
以及服務端chat_server.js處理的初始部分
server.on('connection',function(socket){ // server listening console.log('socket.id '+socket.id+ ': connecting'); // console-- message getUserUp(socket); //獲取在線用戶 // 構造用戶對象client var client = { Socket: socket, name: '----' }; socket.on("message",function(name){ client.name = name; // 接收user name clients.push(client); //保存此client console.log("client-name: "+client.name); socket.broadcast.emit("userIn","system@: 【"+client.name+"】-- a newer ! Let's welcome him ~"); }); socket.emit("system","system@: Welcome ! Now chat with others"); ...
由上可知(send和message是默認一對)客戶端鏈接成功就立刻把本身的name提交,服務器檢測到新鏈接後立刻監聽客戶端的name提交。
固然,在此以前要先立刻更新用戶列表,並構造客戶端對象(socket和name屬性),收到name後即處理好(保存至全局clients存儲全部客戶)並返回
2.這裏的更新用戶列表的安排很重要
// 獲取上線的用戶 function getUserUp(ssocket){ var User = global.dbHandel.getModel('user'); User.find({status: "up"},function(err,docs){ if(err){ console.log(err); }else{ console.log('users list --default: '+docs); // 由於是回調函數 socket.emit放在這裏能夠防止 用戶更新列表滯後 ssocket.broadcast.emit('user_list',docs); //更新用戶列表 ssocket.emit('user_list',docs); //更新用戶列表 } }); }
上段代碼顯示:把返回給客戶端用戶列表的操做是放到了函數裏頭。這樣作是爲了不一個問題:
函數裏頭function(err,docs)是屬於回調函數的,也就是說getUserUp()函數的處理完與回調函數中搜索在線用戶的處理完 是兩個概念。
若是用成這樣就會出錯:
實際測試的時候就會發現,好比你剛上線,這種方法就不會得到任何用戶列表信息
由於console.log("user list --default:",docs) 會輸出你這個新上線的用戶
但下邊的console.log("user list",users) 輸出值爲空
因此回調函數會後執行,因此返回給你本身或者其餘在線用戶的用戶列表得不到更新...
function getUserUp(ssocket){ var User = global.dbHandel.getModel('user'); User.find({status: "up"},function(err,docs){ if(err){ console.log(err); }else{ console.log('users list --default: '+docs); for(var n in docs){ users[n] = docs[n]; } // 由於是回調函數 socket.emit放在這裏能夠防止 用戶更新列表滯後 //ssocket.broadcast.emit('user_list',docs); //更新用戶列表 //ssocket.emit('user_list',docs); //更新用戶列表 } }); } server.on('connection',function(socket){ // server listening console.log('socket.id '+socket.id+ ': connecting'); // console-- message getUserUp(socket); //獲取在線用戶 console.log("user_list",users); ssocket.broadcast.emit('user_list',users); //更新用戶列表 ssocket.emit('user_list',users); //更新用戶列表 // 構造用戶對象client var client = { Socket: socket, name: '----' };
因此仍是用回上一種方式,把socket.emit放到回調函數裏邊確保執行順序
3.私聊的實現
socket.emit 是返回給socket
因此假如某user的socket是socket[n], 那麼想只發送給他固然就是 socket[n].emit
因此實現方式就是全局存儲因此clients信息(固然了也會隨用戶更新我的信息隨着更新),而後收到客戶端私聊(能夠自定義私聊的格式)的請求時:
socket.on("say_private",function(fromuser,touser,content){ //私聊階段 var toSocket = ""; for(var n in clients){ if(clients[n].name === touser){ // get touser -- socket toSocket = clients[n].Socket; } } console.log("toSocket: "+toSocket.id); if(toSocket != ""){ socket.emit("say_private_done",touser,content); //數據返回給fromuser toSocket.emit("sayToYou",fromuser,content); // 數據返回給 touser console.log(fromuser+" 給 "+touser+"發了份私信: "+content); } });
4.通常的消息發送接收就涉及 socket.emit 和 socket.on 這兩中方式,想好事件的處理過程就好了
5.用戶更新我的信息的時候也要注意,由於更新信息就涉及數據庫的更新以及用戶列表的更新,要順序放好,就想第二點提到的同樣
function updateInfo(User,oldName,uname,usex){ // 更新用戶信息 User.update({name:oldName},{$set: {name: uname, sex: usex}},function(err,doc){ //更新用戶名 if(err){ console.log(err); }else{ for(var n in clients){ //更新全局數組中client.name if(clients[n].Socket === socket){ // get socket match clients[n].name = uname; } } socket.emit("setInfoDone",oldName,uname,usex); // 向客戶端返回信息已更新成功 socket.broadcast.emit("userChangeInfo",oldName,uname,usex); console.log("【"+oldName+"】changes name to "+uname); global.userName = uname; getUserUp(socket); // 更新用戶列表 } }); }
6.用戶下線的處理,固然了就是設置他 status='down'
曾思考過用戶親自點擊註銷(在客戶端實現下線處理)纔將其下線,其餘因素(已經出發的 disconnect事件)不考慮下線
這種形式有個好處:好比用戶直接關閉瀏覽器以後,再開啓進入,就無需再次驗證我的信息
但有兩個不妥: session值的處理更新和用戶上下線status的處理會很麻煩,很亂
用戶列表的顯示會有嚴重錯誤,其根源仍是數據庫中status處理不當
因此後面經過在服務端實現下線處理的操做,disconnect以後:
socket.on('disconnect',function(){ // Event: disconnect var Name = ""; for(var n in clients){ if(clients[n].Socket === socket){ // get socket match Name = clients[n].name; } } statusSetDown(Name,socket); // status --> set down socket.broadcast.emit('userOut',"system@: 【"+client.name+"】 leave ~"); console.log(client.name + ': disconnect'); }); }); function statusSetDown(oName,ssocket){ //註銷 下線處理 var User = global.dbHandel.getModel('user'); User.update({name:oName},{$set: {status: 'down'}},function(err,doc){ if(err){ console.log(err); }else{ console.log(oName+ " is down"); getUserUp(ssocket); // 放在內部保證順序 } }); }
7.另外有兩個小效果的使用:
按住Ctrl+Enter就發送的話-->
document.getElementById("msgIn").onkeydown = function keySend(event){ // ctrl + enter sendMessage if(event.ctrlKey && event.keyCode == 13){ sendMyMessage(); } }
發送消息以後讓滾動條保持在最底部-->
<div id="msg_list"> </div> //若是是原生 JS var div = document.getElementById("msg_list"); div.scrollTop = div.scrollHeight; //若是是jquery var div = $("#msg_list"); var hei = div.height(); div.scrollTop(hei);
小小聊天室實現了基本的幾個功能,固然也有不少不足之處
IF YOU WANT THE SOURCE CODE , WELCOME TO FORK ME IN Github