[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/  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM