Node.js下基於Express + Socket.io 搭建一個基本的在線聊天室

 

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

相關文章
相關標籤/搜索