引子
最近听到这么一个问题: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 看起来就会有这些依赖的库:
1 2 3 4 5 6 7 8 |
"co": "^4.0", "koa": "^0.14.0", "koa-mount": "*", "koa-ejs": "*", "koa-static": "*", "koa-router": "*", "koa-session": "*", "co-body": "*" |
用户列表
思路中提到的用户列表,就是一个简单的数组:[{name, session_id, socket} , ...]
,围绕它的操作也特别简单(socketHandler:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
//暴露给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 存储功能,它的使用非常简单:
1 2 3 4 5 6 |
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 的事件侦听中,我们可以这么用它:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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,那么这个路由看起来是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
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; |
广播和私聊消息处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
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 }); } }); }); |
客户端
在连接到服务端后,客户端会定时拉取其他人的列表:
1 2 3 4 5 6 7 8 |
//定时获取其他人列表 function updateOthers() { $.post('/chat/others', function (others) { //...若干丑陋的UI DOM操作代码 setTimeout(updateOthers, 1000); }); } setTimeout(updateOthers, 1000); |
对应的,服务端会有一个这样的接口:
1 2 3 4 5 6 7 8 9 10 11 |
// 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里有详细的设置说明。
打赏作者
您的支持将激励我继续创作!
三哥好厉害,我好菜
请问应用要怎么启动?我用命令行node http://www.js会报错,两个依赖包也都安装上了。
具体报什么错