一、聊天室简单介绍

  采用nodeJS设计,基于express框架,使用WebSocket编程之 socket.io机制。聊天室增加了 注册登录模块 ,并将用户个人信息和聊天记录存入数据库.

数据库采用的是mongodb , 并使用其相应mongoose对象工具来处理数据的存取。

  功能主要涉及:群聊、私聊、设置个人信息、查看聊天记录、查看在线用户等

  效果图:

  你也可以直接来这里  查看演示

二、聊天室基本设计思路

  除去上次的注册登录模块不说,本次主要就是增加了socket.io模块的设计 以及  整合全部代码的过程..太艰难了奋战了几天...

  首先,数据库中存储了用户信息(user)和聊天内容(content), mongoose版的Schema如下:

  

  1. module.exports = {
  2. user:{
  3. name:{type:String,required:true},
  4. password:{type:String,required:true},
  5. sex:{type:String,default:"boy"},
  6. status:{type:String,default: "down"}
  7. },
  8. content:{
  9. name:{type:String,require:true},
  10. data:{type:String,require:true},
  11. time:{type:String,required:true}
  12. }
  13. };

然后通过对其的模型拉取就可以获取相应的Model, 然后传递一下

  1. var mongoose = require('mongoose');
  2. var Schema = mongoose.Schema;
  3. var models = require("./models");
  4.  
  5. for(var m in models){
  6. mongoose.model(m,new Schema(models[m]));
  7. }
  8.  
  9. module.exports = {
  10. getModel: function(type){
  11. return _getModel(type);
  12. }
  13. };
  14.  
  15. var _getModel = function(type){
  16. return mongoose.model(type);
  17. };

app.js 中

  1. global.dbHandel = require('./database/dbHandel'); // 全局handel获取数据库Model
  2. global.db = mongoose.connect("mongodb://127.0.0.1:27017/nodedb");

这样一来就可以直接操作数据库数据了,比如与app.js在同目录下的  chat_server.js 中的某部分(获取上线用户)

  1. // 获取上线的用户
  2. function getUserUp(ssocket){
  3. var User = global.dbHandel.getModel('user');
  4. User.find({status: "up"},function(err,docs){
  5. if(err){
  6. console.log(err);
  7. }else{
  8. console.log('users list --default: '+docs);
  9. // 因为是回调函数 socket.emit放在这里可以防止 用户更新列表滞后
  10. ssocket.broadcast.emit('user_list',docs); //更新用户列表
  11. ssocket.emit('user_list',docs); //更新用户列表
  12.  
  13. }
  14. });
  15. }

如此之类,数据库数据的存取就使用这种方式

正式介绍聊天室的核心 --- socket.io

这里不是介绍socket.io的基本知识,只是大概讲解一下这个聊天室如何通过socket.io 构建  即思路

1.上面说到了,每位用户都把数据置入数据库中,其中有status这一属性,其实"down"表示下线,“up"表示上线,在线用户就是这么处理

在index.js(路由配置文件)看看这小段代码,登录成功后就马上 statusSetUp() 将其上线,

  1. if(req.body.upwd != doc.password){ //查询到匹配用户名的信息,但相应的password属性不匹配
  2. req.session.error = "密码错误";
  3. res.send(404);
  4. // res.redirect("/login");
  5. }else{ //信息匹配成功,则将此对象(匹配到的user) 赋给session.user 并返回成功
  6. req.session.user = doc;
  7. statusSetUp(uname); // 上线
  8. res.send(200);
  9. // res.redirect("/home");
  10. }

看看statusSetUp()的内容:将状态改成 up 之后,看上边的代码,下面是 res.send(200); 就是说执行完statusSetUp()之后才返回给原 "login',然后正式进入‘home'之后

  1. function statusSetUp(oName){ //登录 上线处理
  2. var User = global.dbHandel.getModel('user');
  3. User.update({name:oName},{$set: {status: 'up'}},function(err,doc){
  4. if(err){
  5. console.log(err);
  6. }else{
  7. console.log(oName+ " is up");
  8. }
  9. });
  10. }

在home.html文件中有引用

  1. <script type="text/javascript" src="javascripts/jquery.min.js"></script>
  2. <script type="text/javascript" src="javascripts/bootstrap.min.js"></script>
  3. <script type="text/javascript" src="/socket.io/socket.io.js"></script>
  4. <script type="text/javascript" src="javascripts/chat_client.js"></script>

说明1:进入home路径之后便开始渲染home.html页面,此时将加载chat_client.js文件信息并处理,此时,开始连接

说明2:连接成功后会自动创建socket.io.js 路径引用一般就使用上述的方法

下面是chat_client.js里头开始连接服务端的部分,

  1. socket.on("connect",function(){ // 进入聊天室
  2. var userName = $("#nickname span").html();
  3. socket.send(userName); // 向服务器发送自己的昵称
  4. console.log("send userName to server completed");
  5. });

以及服务端chat_server.js处理的初始部分

  1. server.on('connection',function(socket){ // server listening
  2. console.log('socket.id '+socket.id+ ': connecting'); // console-- message
  3. getUserUp(socket); //获取在线用户
  4.  
  5. // 构造用户对象client
  6. var client = {
  7. Socket: socket,
  8. name: '----'
  9. };
  10. socket.on("message",function(name){
  11. client.name = name; // 接收user name
  12. clients.push(client); //保存此client
  13. console.log("client-name: "+client.name);
  14. socket.broadcast.emit("userIn","system@: 【"+client.name+"】-- a newer ! Let's welcome him ~");
  15. });
  16. socket.emit("system","system@: Welcome ! Now chat with others");
  17. ...

由上可知(send和message是默认一对)客户端连接成功就马上把自己的name提交,服务器检测到新连接后马上监听客户端的name提交。

当然,在此之前要先马上更新用户列表,并构造客户端对象(socket和name属性),收到name后即处理好(保存至全局clients存储所有客户)并返回

2.这里的更新用户列表的安排很重要

  1. // 获取上线的用户
  2. function getUserUp(ssocket){
  3. var User = global.dbHandel.getModel('user');
  4. User.find({status: "up"},function(err,docs){
  5. if(err){
  6. console.log(err);
  7. }else{
  8. console.log('users list --default: '+docs);
  9. // 因为是回调函数 socket.emit放在这里可以防止 用户更新列表滞后
  10. ssocket.broadcast.emit('user_list',docs); //更新用户列表
  11. ssocket.emit('user_list',docs); //更新用户列表
  12.  
  13. }
  14. });
  15. }

上段代码显示:把返回给客户端用户列表的操作是放到了函数里头。这样做是为了避免一个问题:

函数里头function(err,docs)是属于回调函数的,也就是说getUserUp()函数的处理完与回调函数中搜索在线用户的处理完 是两个概念。

如果用成这样就会出错:

实际测试的时候就会发现,比如你刚上线,这种方法就不会获得任何用户列表信息

因为console.log("user list --default:",docs) 会输出你这个新上线的用户

但下边的console.log("user list",users) 输出值为空

所以回调函数会后执行,所以返回给你自己或者其他在线用户的用户列表得不到更新...

  1. function getUserUp(ssocket){
  2. var User = global.dbHandel.getModel('user');
  3. User.find({status: "up"},function(err,docs){
  4. if(err){
  5. console.log(err);
  6. }else{
  7. console.log('users list --default: '+docs);
  8. for(var n in docs){
  9. users[n] = docs[n];
  10. }
  11. // 因为是回调函数 socket.emit放在这里可以防止 用户更新列表滞后
  12. //ssocket.broadcast.emit('user_list',docs); //更新用户列表
  13. //ssocket.emit('user_list',docs); //更新用户列表
  14.  
  15. }
  16. });
  17. }
  18.  
  19. server.on('connection',function(socket){ // server listening
  20. console.log('socket.id '+socket.id+ ': connecting'); // console-- message
  21. getUserUp(socket); //获取在线用户
  22. console.log("user_list",users);
  23. ssocket.broadcast.emit('user_list',users); //更新用户列表
  24. ssocket.emit('user_list',users); //更新用户列表
  25. // 构造用户对象client
  26. var client = {
  27. Socket: socket,
  28. name: '----'
  29. };

所以还是用回上一种方式,把socket.emit放到回调函数里边确保执行顺序

3.私聊的实现

socket.emit 是返回给socket

所以假如某user的socket是socket[n], 那么想只发送给他当然就是  socket[n].emit

所以实现方式就是全局存储所以clients信息(当然了也会随用户更新个人信息随着更新),然后收到客户端私聊(可以自定义私聊的格式)的请求时:

  1. socket.on("say_private",function(fromuser,touser,content){ //私聊阶段
  2. var toSocket = "";
  3. for(var n in clients){
  4. if(clients[n].name === touser){ // get touser -- socket
  5. toSocket = clients[n].Socket;
  6. }
  7. }
  8. console.log("toSocket: "+toSocket.id);
  9. if(toSocket != ""){
  10. socket.emit("say_private_done",touser,content); //数据返回给fromuser
  11. toSocket.emit("sayToYou",fromuser,content); // 数据返回给 touser
  12. console.log(fromuser+" 给 "+touser+"发了份私信: "+content);
  13. }
  14. });

4.一般的消息发送接收就涉及  socket.emit  和 socket.on 这两中方式,想好事件的处理过程就行了

5.用户更新个人信息的时候也要注意,因为更新信息就涉及数据库的更新以及用户列表的更新,要顺序放好,就想第二点提到的一样

  1. function updateInfo(User,oldName,uname,usex){ // 更新用户信息
  2. User.update({name:oldName},{$set: {name: uname, sex: usex}},function(err,doc){ //更新用户名
  3. if(err){
  4. console.log(err);
  5. }else{
  6. for(var n in clients){ //更新全局数组中client.name
  7. if(clients[n].Socket === socket){ // get socket match
  8. clients[n].name = uname;
  9. }
  10. }
  11. socket.emit("setInfoDone",oldName,uname,usex); // 向客户端返回信息已更新成功
  12. socket.broadcast.emit("userChangeInfo",oldName,uname,usex);
  13. console.log("【"+oldName+"】changes name to "+uname);
  14. global.userName = uname;
  15. getUserUp(socket); // 更新用户列表
  16. }
  17. });
  18. }

6.用户下线的处理,当然了就是设置他 status='down'

  曾思考过用户亲自点击注销(在客户端实现下线处理)才将其下线,其他因素(已经出发的 disconnect事件)不考虑下线

这种形式有个好处:比如用户直接关闭浏览器之后,再开启进入,就无需再次验证个人信息

但有两个不妥:    session值的处理更新和用户上下线status的处理会很麻烦,很乱

        用户列表的显示会有严重错误,其根源还是数据库中status处理不当

所以后面通过在服务端实现下线处理的操作,disconnect之后:

  1. socket.on('disconnect',function(){ // Event: disconnect
  2. var Name = "";
  3. for(var n in clients){
  4. if(clients[n].Socket === socket){ // get socket match
  5. Name = clients[n].name;
  6. }
  7. }
  8. statusSetDown(Name,socket); // status --> set down
  9.  
  10. socket.broadcast.emit('userOut',"system@: 【"+client.name+"】 leave ~");
  11. console.log(client.name + ': disconnect');
  12.  
  13. });
  14. });
  15. function statusSetDown(oName,ssocket){ //注销 下线处理
  16. var User = global.dbHandel.getModel('user');
  17. User.update({name:oName},{$set: {status: 'down'}},function(err,doc){
  18. if(err){
  19. console.log(err);
  20. }else{
  21. console.log(oName+ " is down");
  22. getUserUp(ssocket); // 放在内部保证顺序
  23. }
  24. });
  25. }

7.另外有两个小效果的使用:

按住Ctrl+Enter就发送的话-->

  1. document.getElementById("msgIn").onkeydown = function keySend(event){ // ctrl + enter sendMessage
  2. if(event.ctrlKey && event.keyCode == 13){
  3. sendMyMessage();
  4. }
  5. }

发送消息之后让滚动条保持在最底部-->

  1. <div id="msg_list"> </div>
  2.  
  3. //如果是原生 JS
  4. var div = document.getElementById("msg_list");
  5. div.scrollTop = div.scrollHeight;
  6.  
  7. //如果是jquery
  8. var div = $("#msg_list");
  9. var hei = div.height();
  10. div.scrollTop(hei);

小小聊天室实现了基本的几个功能,当然也有很多不足之处

                  IF YOU WANT THE SOURCE CODE ,WELCOME TO FORK ME IN Github

Node.js下基于Express + Socket.io 搭建一个基本的在线聊天室的更多相关文章

  1. 使用React、Node.js、MongoDB、Socket.IO开发一个角色投票应用的学习过程(三)

    这几篇都是我原来首发在 segmentfault 上的地址:https://segmentfault.com/a/1190000005040834 突然想起来我这个博客冷落了好多年了,也该更新一下,呵 ...

  2. node.js 下依赖Express 实现post 4种方式提交参数

    上面这个图好有意思啊,哈哈, v8威武啊.... 在2014年的最后一天和大家分享关于node.js 如何提交4种格式的post数据. 上上一篇说到了关于http协议里定义的4种常见数据的post方法 ...

  3. node+express+socket.io制作一个聊天室功能

    首先是下载包: npm install express npm install socket.io 建立文件: 服务器端代码:server.js var http=require("http ...

  4. NodeJS+Express+Socket.io的一个简单例子

    关键字:NodeJS,Express,Socket.io. OS:Windows 8.1 with update pro. 1.安装NodeJS:http://nodejs.org/. 2.初始化一个 ...

  5. AgileEAS.NET SOA 中间件平台.Net Socket通信框架-完整应用例子-在线聊天室系统-下载配置

    一.AgileEAS.NET SOA中间件Socket/Tcp框架介绍 在文章AgileEAS.NET SOA 中间件平台Socket/Tcp通信框架介绍一文之中我们对AgileEAS.NET SOA ...

  6. 基于Express+Socket.io+MongoDB的即时聊天系统的设计与实现

    记得从高中上课时经常偷偷的和同学们使用qq进行聊天,那时候经常需要进行下载qq,但是当时又没有那么多的流量进行下载,这就是一个很尴尬的事情了,当时就多想要有一个可以进行线上聊天的网站呀,不用每次痛苦的 ...

  7. [Node.js]29. Level 6: Socket.io: Setting up Socket.io server-side & Client socket.io setup

    Below we've already created an express server, but we want to start building a real-time Q&A mod ...

  8. 使用socket.io+redis来实现基本的聊天室应用场景

    本文根据socket.io与Redis来实现基本的聊天室应用场景,主要表现于多个浏览器之间的信息同步和实时更新. 只是简单记录了一下, 更详细的内容可以参考后续的一篇补充文章: 使用node.js + ...

  9. [NodeJS]使用Node.js写一个简单的在线聊天室

    声明:教程来自<Node即学即用>.源代码案例均出自此书.博文仅为个人学习笔记. 第一步:创建一个聊天server. 首先,我们先来写一个Server: var net = require ...

随机推荐

  1. Atlas+Keepalived系列二:管理Atlas

    1:登录代理端口1234 [root@localhost bin]# mysql -uroot -p -P1234 -h127.0.0.1 proxy-address项配置,例如proxy-addre ...

  2. 使用sqlserver的游标功能来导数据的常见写法

    一定要自己试过才知道么? 你也没试过吃屎,你怎么知道屎不能吃,难道你试过啊...(没有愤怒的意思) ),),) declare cursor_data CURSOR FOR SELECT [UserN ...

  3. Swift中对C语言接口缓存的使用以及数组、字符串转为指针类型的方法

    由于Swift编程语言属于上层编程语言,而Swift中由于为了低层的高性能计算接口,所以往往需要C语言中的指针类型,由此,在Swift编程语言刚诞生的时候就有了UnsafePointer与Unsafe ...

  4. cmd实用命令

    1.netstat 查看电脑端口状况 实际应用举例:查看某软件坚监听的电脑端口. 在任务管理器中选择列...,打开PID的显示.在这里查看某个应用程序的线程ID是多少.例如QQ:4904. 运行,cm ...

  5. Spring源码追踪4——SpringMVC View解析

    这次的议题是返回json和返回普通view经过的路线差异. ---------------------------------------------------------------------- ...

  6. mysql隔离级别

    MySQL/InnoDB定义的4种隔离级别: Read Uncommited 可以读取未提交记录. Read Committed (RC) 针对当前读,RC隔离级别保证对读取到的记录加锁 (记录锁), ...

  7. 谈"自驱力"

    最新说明: 1.标题是为了博眼球取的,请不大家不要纠结具体薪资数字,我瞎取的 2.请注意素质,不要满口喷粪,不要搞人身攻击,尊重别人,就是尊重你自己 3.请大家就事论事,不要胡乱臆想,请站在全局的角度 ...

  8. HTTP 头部详细解释

    HTTP 头部解释 ================================================   Accept 告诉WEB服务器自己接受什么介质类型,*/* 表示任何类型,ty ...

  9. Shooting Algorithm

    Shooting算法是Wenjiang提出的一种优化Lasso(L1 Regularization)和Bridge Regression的算法, 本文以Lasso为例. 对于线性回归问题$\mathb ...

  10. PHP 常见语法 集合

    1.die()与exit()的真正区别 die 为 exit 的别名, 执行过程 将释放内存,停止代码执行 echo "begin exec <br/>"; show( ...