原文地址: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/

