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

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Puppeteer学习

一步一步学Vue(七)

17130
来自专栏程序员互动联盟

【专业技术】linux下socket编程

1. 网络中进程之间如何通信 进程通信的概念最初来源于单机系统。由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进程之间既互不干扰又协调一致工作,操作...

34360
来自专栏cloudskyme

jbpm5.1介绍(3)

在您好的应用程序中使用一个新的流程 流程处理  (1)你需要建立一个知识库,其中包含过程定义 KnowledgeBuilder kbuilder = Knowl...

38040
来自专栏点滴积累

geotrellis使用(二)geotrellis-chatta-demo以及geotrellis框架数据读取方式初探

在上篇博客(geotrellis使用初探)中简单介绍了geotrellis-chatta-demo的大致工作流程,但是有一个重要的问题就是此demo如何调取数据...

44460
来自专栏林德熙的博客

dot net core 使用 IPC 进程通信 原理例子序列化

一般都是使用 WCF 或 remoting 做远程通信,但是 dot net core 不支持 WCF 所以暂时我就只能使用 管道通信。

11920
来自专栏腾讯云Elasticsearch Service

Elasticsearch 底层系列之分片恢复解析

我们是基础架构部,腾讯云 CES/CTSDB 产品后台服务的支持团队,我们拥有专业的ES开发运维能力,为大家提供稳定、高性能的服务,欢迎有需求的童鞋接入,同时也...

8.5K00
来自专栏SDNLAB

OpenDaylight Lithium-SR2 Cluster集群搭建

目的 希望大家能够通过本教程对OpenDaylight集群的基本概念如shard/基本配置有所了解,感受OpenDaylight的High Availabili...

37150
来自专栏企鹅号快讯

GoAhead服务器 远程命令执行漏洞 分析报告

安全通告 1 GoAhead Web Server是为嵌入式实时操作系统(RTOS)量身定制的开源Web服务器。很多国际一线大厂商,包括IBM、HP、Oracl...

265100
来自专栏Create Sun

基础拾遗------webservice详解

前言   工作当中常用的服务接口有三个wcf,webservice和webapi.首先第一个接触的就是webservice,今天大致总结一下。 1.webser...

416110
来自专栏腾讯Bugly的专栏

美女程序媛发福利,读懂ANR的trace文件So easy

想要分析ANR问题,读懂trace文件是关键。Trace文件到底是什么鬼?如何才能破解深藏其中的奥义? App的进程发生ANR时,系统让活跃的Top进程都进行了...

44250

扫码关注云+社区

领取腾讯云代金券