[Node.js] 基于Socket.IO 的私聊
原文地址: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/
最新文章
- MySQL、MongoDB、Redis数据库Docker镜像制作
- AngularJS in Action读书笔记5(实战篇)——在directive中引入D3饼状图显示
- POJ 2142 The Balance【扩展欧几里德】
- python库requests登录zhihu
- Java发送邮件,所遇到的常见需求
- Android 设置控件可见与不可见
- JBoss 系列六十九:CDI 基本概念
- 篇一:eclipse创建maven工程
- NPOI 图片在单元格等比缩放且居中显示
- ansible的logging模块用来写日志
- Kafka入门 --安装和简单实用
- MySQL 主从同步遇到的问题及解决方案
- [C++]Linux之多进程运行代码框架
- 用scrapy框架爬取映客直播用户头像
- Linux学习笔记(第七章)
- Web前端学习笔记之前端跨域知识总结
- 非常粗糙的react网页ppt
- 在Zookeeper中,znode是一个跟Unix文件系统路径相似的节点,可以往这个节点存储或获取数据
- CocoaPods介绍与使用(转)
- 各种排序算法C++
热门文章
- easyUI 的tree 修改节点,sql递归查询
- ng2-timesheet, 一个timesheet.js的angular2复制版
- 【腾讯Bugly干货分享】React移动web极致优化
- Viewbox在UWP开发中的应用
- 设计模式之美:Behavioral Patterns(行为型模式)
- sublime 2/3 for mac link to command
- [.net 面向对象编程基础] (3) 基础中的基础——数据类型
- SQL Server 性能优化之——T-SQL TVF和标量函数
- iis日志查看
- 基于Vue封装分页组件