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


 

一、聊天室簡單介紹

  采用nodeJS設計,基於express框架,使用WebSocket編程之 socket.io機制。聊天室增加了 注冊登錄 模塊 ,並將用戶個人信息和聊天記錄存入數據庫.

數據庫采用的是mongodb , 並使用其相應mongoose對象工具來處理數據的存取。

  功能主要涉及:群聊、私聊、設置個人信息、查看聊天記錄、查看在線用戶等

 

  效果圖:

 

  你也可以直接來這里  查看演示

 

二、聊天室基本設計思路

  除去上次的注冊登錄模塊不說,本次主要就是增加了socket.io模塊的設計 以及  整合全部代碼的過程..太艱難了奮戰了幾天...

  首先,數據庫中存儲了用戶信息(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, 然后傳遞一下

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM