• [Node.js] 基于Socket.IO 的私聊


    原文地址:http://www.moye.me/2015/01/02/node_socket-io/

    引子

    Socket.IO Real-Time Web Application Development最近听到这么一个问题:Socket.IO 怎么实现私聊?换个提法:怎么定位到人(端),或者说怎么标识到连接,而不是依赖每个连接的socket.id。好问题。

    在 Socket.IO Real-Time Web Application Development 的指引下,形成了如下思路:

    1. 服务端在每个用户初次进入系统时,产生session_id
    2. 服务端强制用户输入昵称,与session_id对应
    3. 服务端的Socket.IO在连接时,可以拿到socket.request.headers.cookie,从这个cookie中解析出session_id,将socket 连接与 Web框架的context中的session_id 对应上
    4. 在服务端使用一个数组来保存如上三者产生的对应关系:[{name, session_id, socket} , ...]
    5. 有了对应关系的数组,就能定位到人并分清 [我] 和 [其他人],也便能够利用保存的socket 进行私聊

    有了思路,就可以动手实践了:

    Server端

    ES6 的生成器太好用,做 Node Web 就从 koa 开始吧。那么,我的 package.json 看起来就会有这些依赖的库:

    "co": "^4.0",        
    "koa": "^0.14.0", 
    "koa-mount": "*",  
    "koa-ejs": "*",
    "koa-static": "*",
    "koa-router": "*",
    "koa-session": "*",
    "co-body": "*"

    用户列表

    思路中提到的用户列表,就是一个简单的数组:[{name, session_id, socket} , ...],围绕它的操作也特别简单(socketHandler:

    //暴露给Web的接口
    module.exports.addUser = addUser; 
    module.exports.otherUsers = otherUsers;
    
    var users = [];
    
    function findInUsers(session_id) {//通过session_id查找
        var index = -1;
        for (var j = 0, len = users.length; j < len; j++) {
            if (users[j].session_id === session_id)
                index = j;
        }
        return index;
    }
    function addUser(name, session_id) {
        var index = findInUsers(session_id);
        if (index === -1) //不存在则重新添加
            users.push({name: name, session_id: session_id, 
                socket: null});
        else { //只更新昵称
            if (users[index].name !== name)
                users[index].name = name;
        }
    }
    function setUserSocket(session_id, socket){//更新用户socket
        var index = findInUsers(session_id);
        if (index !== -1){
            users[index].socket = socket;
        }
    }
    function findUser(session_id) {
        var index = findInUsers(session_id);
        return index > -1 ? users[index] : null;
    }
    function otherUsers(session_id){//其他人
        var results = [];
        for (var j = 0, len = users.length; j < len; j++) {
            if (users[j].session_id !== session_id)
                results.push({session_id: users[j].session_id, 
                    name: users[j].name});
        }
        return results;
    }

    Session存储

    koa-session 这个库提供了 session 存储功能,它的使用非常简单:

    var koa = require('koa');
    var session = require('koa-session');
    
    var app = koa();
    app.keys = [config.SECRET];
    app.use(session(app));

    此外,koa-session会在Web request的cookie中会附上一个 koa:sess的session_id 标识串,那么,在 socket.io 的事件侦听中,我们可以这么用它:

    io.on('connection', function (socket) {
        var sessionId = getSessionId(socket.request.headers.cookie, 
            'koa:sess');
        if(sessionId){
             setUserSocket(sessionId, socket);
         }
     });
    
    function getSessionId(cookieString, cookieName) {
        var matches = new RegExp(cookieName + 
             '=([^;]+);', 'gmi').exec(cookieString);
        return matches[1] ? matches[1] : null;
    }

    用户登录

    所谓的登录,就是让用户输入一个昵称,将它与session_id对应上,并存储到前述用户数组中。假设我们的路由路径为 /chat,登录action路径为/chat/login,那么这个路由看起来是这样:

    var Router = require('koa-router'),
        router = new Router();
    var parse = require('co-body');
    var socketHandler = require('../../middlewares/socketHandler');
    
    // GET /chat
    router.get('/', function *() {
        var session_id = this.cookies.get('koa:sess');
        var name = this.session.name;
        if(session_id && name) {//添加到用户列表
            socketHandler.addUser(name, session_id);
            yield this.render('../www/views/chat'); //使用ejs
        } else {
            this.redirect('/chat/login');
        }
    });
    
    // GET /chat/login 使用ejs模板
    router.get('/login', function*(){
        yield this.render('../www/views/login')
    });
    // POST /chat/login 接收form提交: <input name='name'>
    router.post('/login', function*(){
        var body = yield parse(this);
        this.session.name = body.name || 'guest';
        this.redirect('/chat')
    });
    
    module.exports = router;

    广播和私聊消息处理

    io.on('connection', function (socket) {
        socket.on('broadcast', function (data) {
            //广播
            var fromUser = findUser(sessionId);
            if(fromUser) {
                socket.broadcast.emit('broadcast', {
                    name: fromUser.name,
                    msg: data.msg
                });
            }
        });
        
        socket.on('private', function (data) {
            //私聊 {to_session_id, msg}
            var fromUser = findUser(sessionId);
            if(fromUser) {
                var toUser = findUser(data.to_session_id);
                if (toUser)
                    toUser.socket.emit('private', {
                        name: fromUser.name,
                        msg: data.msg
                    });
            }
        });
    });

    客户端

    在连接到服务端后,客户端会定时拉取其他人的列表:

    //定时获取其他人列表
    function updateOthers() {
        $.post('/chat/others', function (others) {
             //...若干丑陋的UI DOM操作代码
             setTimeout(updateOthers, 1000);
        });
    }
    setTimeout(updateOthers, 1000);

    对应的,服务端会有一个这样的接口:

    // POST /chat/others 其他人列表
    router.post('/others', function*(){
        var session_id = this.cookies.get('koa:sess');
        var name = this.session.name;
        if(session_id && name) {
            this.type = 'application/json';
            this.body = socketHandler.otherUsers(session_id);
        } else {
            this.status = 404;
        }
    });

    运行效果

    在三个不同的浏览器中跑起来,宛如上世纪90年代火得不行的聊天室 :)

    Chat Room Demo

    源码

    完整的源码放在我的Github上:https://github.com/rockdragon/socketchat,想让它跑起来,你需要把 Node 升到 0.11.14(因为用到了 Co V4 ),当然,README.MD里有详细的设置说明。

    更多文章请移步我的blog新地址: http://www.moye.me/  

  • 相关阅读:
    Google analytics平均页面停留时间为何是0
    用易语言拦截窗口消息(带例程)
    OS + Linux RHEL / RedHat Enterprise 5 / 6 / 6.3
    Oracle学习教程:动态SQL与游标详解
    图解Oracle数据库(二)
    Oracle数据库中listener.ora sqlnet.ora tnsnames.ora的区别
    正确理解javascript的this关键字
    深入理解Javascript之this关键字
    Oracle中动态SQL详解
    oracle维护常用SQL语句(查看系统表和视图)
  • 原文地址:https://www.cnblogs.com/moye/p/node_socket-io.html
Copyright © 2020-2023  润新知