原文地址:http://www.moye.me/2015/01/02/node_socket-io/
引子
最近聽到這么一個問題:Socket.IO 怎么實現私聊?換個提法:怎么定位到人(端),或者說怎么標識到連接,而不是依賴每個連接的socket.id。好問題。
在 Socket.IO Real-Time Web Application Development 的指引下,形成了如下思路:
- 服務端在每個用戶初次進入系統時,產生session_id
- 服務端強制用戶輸入昵稱,與session_id對應
- 服務端的Socket.IO在連接時,可以拿到
socket.request.headers.cookie
,從這個cookie中解析出session_id,將socket 連接與 Web框架的context中的session_id 對應上 - 在服務端使用一個數組來保存如上三者產生的對應關系:
[{name, session_id, socket} , ...]
- 有了對應關系的數組,就能定位到人並分清 [我] 和 [其他人],也便能夠利用保存的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年代火得不行的聊天室 :)
源碼
完整的源碼放在我的Github上:https://github.com/rockdragon/socketchat,想讓它跑起來,你需要把 Node 升到 0.11.14(因為用到了 Co V4 ),當然,README.MD里有詳細的設置說明。
更多文章請移步我的blog新地址: http://www.moye.me/