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