用Express + Socket.io + MongoDB实现简易聊天室

2016-05-01阅读量

最近做大作业需要研究一下Node.js,需要了解node与mongoDB的链接,前后端的通信,后端的逻辑结构等,怎么快速上手呢?那就做个聊天室吧。

安装Node.JS和MongoDB

Node.js就不多说了,MongoDB可以看我上一篇博客

构建Express项目

找一个合适的地方:

mkdir chatroom

cd chatroom

npm install express

express -e  //-e 是用ejs作为模板引擎

npm install //安装依赖,目录在package.json中

这样就创建好了,结构如下:

- chatroom

    - bin
        - www           //配置端口启动文件
    - node_modules        //下载的模块
        - express
    - public            //静态资源
        - images
        - javascripts
        - stylesheets
    - routes            //后端逻辑、路由
        - index.js
        - users.js
    - views                //视图
        - error.ejs
        - index.ejs
    - app.js             //入口文件,相当于main();
    - package.json         //配置信息

我按照个人习惯做一些调整

- chatroom

    - node_modules        //下载的模块
        - express
    - public            //静态资源
        - img
        - js
        - css
    - routes            //后端逻辑、路由
        - index.js
        - users.js
    - views                //视图
        - error.ejs
        - index.ejs
    - app.js             //入口文件,相当于main();
    - package.json         //配置信息

我把public目录下目录改一下名称,www文件删了,用app.js作为启动文件,就需要修改一下app.js:

var debug = require('debug')('chat');
//var users = require('./routes/users');    //单页面不需要这个

app.set('port', process.env.PORT || 3000);

var server = app.listen(app.get('port'), function(){
  debug('Express server listening on port ' + server.address().port);
})

//app.use('/users', users);

增加,注释这些后,运行 DEBUG=chatroom & node app.jsnode app.js ,然后浏览器打开127.0.0.1:3000,如下图所示,就说明配置好了

实现前端页面

这个没什么好说的,修改views目录下的index.ejs文件,效果如下:

数据库设计

注意使用数据库前一定要先开启mongodb服务!

安装 mongodb 和 mongoose 模块:

npm install mongodb mongoose

在主目录下新建chat_server.js :

var mongoose = require('mongoose');

//连接数据库
var db = mongoose.createConnection('localhost','chatroom');
db.on('error',function(err) {
    console.error(err);
});
var Schema = mongoose.Schema;

//聊天记录表
var ChatSchema = new Schema({
    nickname: String,
    time: String,
    content: String
});
var ChatModel = db.model('chats',ChatSchema);


// 这里的listen函数在 app.js 文件中被调用
exports.listen = function(_server){
    return io.listen(_server);
}

在app.js中增加:

require('./chat\_server').listen(server);

前后端通信Socket.io

借用这篇博客里讲的介绍一下socket.io:

首先先简单讲解下Socket.io的原理. 操作系统有一个非常伟大的设计就是轮询机制,而Node.js中的callback机制正是基于此机制:

JS的异步编程就是这么来的.但是对于类似聊天这种应用,使用轮询机制明显不合理.轮询机制在于你触发了一个事件后异步处理,但这里异步本身就是硬伤,毕竟聊天要实时的.

而Node.js中有另外一种伟大的模型: 观察者模式. 即我就一直监听,监听到的某个事件后,执行相应的处理函数.

举个栗子

在chat_server.js中添加:

var io = require('socket.io')();
var xssEscape = require('xss-escape');

var nickname_list = [];

// 检查是昵称是否已经存在
function HasNickname(_nickname){
    for(var i=0; i<nickname_list.length; i++){
        if(nickname_list[i] == _nickname){
            return true;
        }
    }
};

// 删除昵称
function RemoveNickname(_nickname){
    for(var i=0; i< nickname_list.length; i++){
        if(nickname_list[i] == _nickname){
            nickname_list.splice(i, 1);
        }
    }
}

io.on('connection', function(_socket){
    console.log(_socket.id + ':connection');

    // 向当前用户发送命令和消息
    _socket.emit('user_list', nickname_list);
    _socket.emit('need_nickname');
    _socket.emit('server_message','欢迎来到聊天室 :)');

    // 监听当前用户的请求和数据

    // 离开
    _socket.on('disconnect', function(){
        console.log(_socket.id + ':disconnect');
        if(_socket.nickname != null && _socket.nickname != ""){
            // 广播 用户退出
            _socket.broadcast.emit('user_quit', _socket.nickname);
            RemoveNickname(_socket.nickname);
        }
    });

    // 添加 和 修改 昵称
    _socket.on('change_nickname', function(_nickname, clr){
        console.log(_socket.id + ': change_nickname('+_nickname+')');

        _nickname = xssEscape(_nickname.trim());

        // 半角替换为tt,模拟为全角字符判断长度
        var name_len = _nickname.replace(/[^\u0000-\u00ff]/g, "tt").length;

        // 字符长度必须在4到16个字符之间
        if(name_len < 4 || name_len > 16){
            return _socket.emit('change_nickname_error', '请填写正确的用户昵称,应在416个字符之间。')
        }

        // 昵称重复
        if(_socket.nickname == _nickname){
            return _socket.emit('change_nickname_error', '你本来就叫这个名字。')
        }

        // 昵称已经被占用
        if(HasNickname(_nickname)){
            return _socket.emit('change_nickname_error', '此昵称已经被占用。')
        }

        var old_name = '';
        if(_socket.nickname != '' && _socket.nickname != null){
            old_name = _socket.nickname;
            RemoveNickname(old_name);
        }

        nickname_list.push(_nickname);
        _socket.nickname = _nickname;
        _socket.color = clr;

        console.log(nickname_list);

        _socket.emit('change_nickname_done', old_name, _nickname, clr);

        if(old_name == ''){
            // 广播 用户加入
            return _socket.broadcast.emit('user_join', _nickname);
        }else{
            // 广播 用户改名
            return _socket.broadcast.emit('user_change_nickname', old_name, _nickname);
        }
    });

    // 说话
    _socket.on('say', function(_time, _content){
        if('' == _socket.nickname || null == _socket.nickname){
            return _socket.emit('need_nickname');
        }

        _content = _content.trim();
        var chatinfo = new ChatModel();
        chatinfo.nickname = _socket.nickname;
        chatinfo.time = _time;
        chatinfo.content = _content;
        chatinfo.save(function(err) {
            if (err) throw err;
        });
        ChatModel.find({nickname: _socket.nickname},function(err,data) {
            if (err) {
             console.log('存储失败' + err);
             return;
             } else {
             console.log('存储成功:' + data);
             }
        });
        console.log(_socket.nickname + ': say('+_content+')');
        // 广播 用户新消息
        _socket.broadcast.emit('user_say', _socket.nickname, xssEscape(_content), _socket.color);
        return _socket.emit('say_done', _socket.nickname, xssEscape(_content), _socket.color);
    });

    //显示历史记录
    _socket.on('show_history',function(clr){
        console.log('ok');
        ChatModel.find({},function(err,data) {
            if (err) {
             console.error(err);
             return;
             } else {
                 console.log('data = ' + data);
                 console.log(data[0].nickname);
                 for(var i = 0;i < data.length;i++){
                    console.log(data[i].nickname, data[i].time, xssEscape(data[i].content), clr);
                    _socket.emit('return_history', data[i].nickname, data[i].time, xssEscape(data[i].content), clr);
                }
             }
        });
    });
})

这是后端的响应机制,前端逻辑在public目录下js中的index.js中,这里就简单举个例子,显示历史消息:

var chat_Utils,     //聊天室 工具类
    chat_UI,         //聊天室 界面逻辑
    chat_Socket;     //聊天室 数据逻辑

// 与后台服务器建立websocket连接
var chat_server = "http://" + location.hostname + ':3000';
var socket = io.connect(chat_server);


chat_UI = {
    init: function(){
        this.historyShow();        //点击显示历史消息事件
    },
    historyShow:function(){
            var self = this;
            $("#showHistory").on('click',function() {
                if($('#history-modal').css('display') == 'none') {
                    $('.history-list-body').empty();
                }
                $("#history-modal").modal('show');
                chat_Socket.showHistory(chat_Utils.getUserColor());
            })
    },
    chatBodyToBottom: function(){
        var chat_body = $('.chat-body');
        var height = chat_body.prop('scrollHeight');
        chat_body.prop('scrollTop', height);
    },
    addHistoryMessage: function(_time, _content, _name, clr){
        var history_list = $('.history-list-body');
        _content = QxEmotion.Parse(_content);
        var msgAlignCls = _name ==$('#my-nickname').text() ? 'msg-right':'msg-left';
        history_list.append(
            '<div class="msg-item clearfix '+msgAlignCls+'">\
                    <div class="msg-avatar" style="background-color:'+clr+';"><i class="glyphicon glyphicon-user"></i></div>\
                    <div class="msg-con-box" style="background-color:'+clr+';">\
                        <p class="con">'+_content+'</p>\
                        <time class="time">'+_time+'</time>\
                    </div>\
                </div>'
            );
        this.chatBodyToBottom();

    },
};

chat_Socket = {
    init:function(){
        this.chatHistoryEv();//监听后端 获取历史消息
    },
    showHistory:function(clr){
        socket.emit('show_history',clr);
    },
    chatHistoryEv:function(){
        socket.on('return_history',function(_nickname, _time, _content, clr) {
            console.log(_nickname, _time, _content, clr);
            chat_UI.addHistoryMessage(_time, _content, _nickname, clr);
        });
    },
}

chat_UI.init();
chat_Socket.init();

我们过一下思路,首先当用户点击显示历史消息时,调用chat_UI.historyShow函数,先清空一下历史记录列表,然后显示历史记录弹窗,调用chat_Socket.showHistory函数:

historyShow:function(){
            var self = this;
            $("#showHistory").on('click',function() {
                if($('#history-modal').css('display') == 'none') {
                    $('.history-list-body').empty();
                }
                $("#history-modal").modal('show');
                chat_Socket.showHistory(chat_Utils.getUserColor());
            })
    },

chat_Socket.showHistory这个函数调用socket.emit发射show_history事件:

showHistory:function(clr){
        socket.emit('show_history',clr);
    },

后端chat_server.js 中用socket.on(‘show_history’)捕获了这一事件,从数据库中获取数据发送return_history事件到前端:

//显示历史记录
    _socket.on('show_history',function(clr){
        console.log('ok');
        ChatModel.find({},function(err,data) {
            if (err) {
             console.error(err);
             return;
             } else {
                 for(var i = 0;i < data.length;i++){
                    _socket.emit('return_history', data[i].nickname, data[i].time, xssEscape(data[i].content), clr);
                }
             }
        });
    });

前端index.js 中 chat_Socket.chatHistoryEv()函数捕获return_history事件,调用chat_UI.addHistoryMessage添加到历史记录列表中:

chatHistoryEv:function(){
        socket.on('return_history',function(_nickname, _time, _content, clr) {
            console.log(_nickname, _time, _content, clr);
            chat_UI.addHistoryMessage(_time, _content, _nickname, clr);
        });
    },

整个显示历史记录的过程就结束了。

数据库的操作

前面我们已经设计好了数据库:

var mongoose = require('mongoose');

//连接数据库
var db = mongoose.createConnection('localhost','chatroom');
db.on('error',function(err) {
    console.error(err);
});
var Schema = mongoose.Schema;

//聊天记录表
var ChatSchema = new Schema({
    nickname: String,
    time: String,
    content: String
});
var ChatModel = db.model('chats',ChatSchema);

数据库的设计包括Schema 模式(数据记录的格式)、Model 编译模型、Documents 文档实例化。上面的代码中我们连接了chatroom数据库,设计了ChatSchema模式,编译了ChatModel 模型,编译好模型后我们创建一条新的记录只需要new一下就行。

保存新数据

var chatinfo = new ChatModel();
        chatinfo.nickname = _socket.nickname;
        chatinfo.time = _time;
        chatinfo.content = _content;
        chatinfo.save(function(err) {
            if (err) throw err;
        });

查询数据

ChatModel.find({nickname: _socket.nickname},function(err,data) {
            if (err) {
             console.log('存储失败' + err);
             return;
             } else {
             console.log('存储成功:' + data);
             }
        });

除了.find() 查找所有符合的数据,还有.findOne() 查找一条数据,第二个参数中的data就是返回的数据。

至此基本的逻辑和操作我们都了解了,接下来就是Codeing的时间了!

部署到vps上

将项目搬到/var/www/chatroom/下,这里我是用git传到github上然后git clone过去的。

安装mongodb服务

由于我的vps是32位的CentOS,一直很头痛这个32位还有CentOS,但是里面又配置了一些梯子,不想折腾就没换,连Docker也用不了。。。那怎么安装mongodb呢?

参考:CentOS 6.5系统中使用yum安装MongoDB 2.6 教程

创建mongodb.repo文件

在/etc/yum.repos.d/目录下创建文件mongodb.repo,它包含MongoDB仓库的配置信息,内容如下:

[mongodb]
name=MongoDB Repository
baseurl=http://downloads-distro.mongodb.org/repo/redhat/os/i686/
gpgcheck=0
enabled=1

执行安装命令

sudo yum install mongodb-org

启动MongoDB

sudo service mongod start

运行app.js

切换到项目目录,为了能一直自行程序,我们用forever模块:

sudo npm -g install forever  //安装

forever start app.js          //开启进程

forever list                  //查看所有进程

forever stopall                 //关闭所有进程

现在在你服务器的3000端口我们的聊天室已经完美运行了!

Code 429: Too many requests. [429 GET https://avoscloud.com/1.1/classes/Comment]
Powered By Valine
v1.4.14