Node + Redis でチャットアプリを作る(完) - Socket.ioを使おうとして挫折した話
開発の軌跡
NodeでRedisでExpressでSessionをはってやるまで - AtAsAtAmAtArA
Node+Redsi+Expressでとても実装に不安な会員登録&ログイン機能を実装した - AtAsAtAmAtArA
Node + Redisでチャット的ななにかをつくる - チャットロビーとチャットルームの作成 - AtAsAtAmAtArA
開発成果物リポジトリは github に。
結論
チャットだけならSocket.ioなしでできた。Socket.ioでRedis通してチャットしようとして破綻した。難しい。よくわからない。
app.js
今までの実装は過去ログの通り。
Socket.ioのExpressとのセッション同期あたりとかもろもろ基本パクりコードです。
//socket.io var app = module.exports = express.createServer(), io = require('socket.io').listen(app); var parseCookie = require('connect').utils.parseCookie; var RedisStore = require('connect-redis')(express); var sessionStore = new RedisStore(); io.configure(function() { io.set('authorization', function(data, callback) { if (data.headers.cookie) { var cookie = parseCookie(data.headers.cookie); sessionStore.get(cookie['connect.sid'], function(err, session) { if (err || !session) { callback('Error', false); } else { data.session = session; callback(null, true); } }); } else { callback('No cookie', false); } }); }); / * ... * / / Routes / / * ... * / app.get('/room/:id?', function(req, res){ io.sockets.on('connection', function (socket){ console.log("Got connected to server!"); socket.on("to_server", function(data){ redis.rpop("room:" + req.session.room, function(err, latest_chat){ if (latest_chat === null) return false; redis.rpush("room:" + req.session.room, latest_chat); socket.emit('to_client', latest_chat); socket.broadcast.emit('to_client', latest_chat); }); }); socket.on('disconnect', function() { console.log('disconnected'); }); }); redis.lrange("room:" + req.params.id, 0, -1,function(err, chat){ res.render('room', { title: "chat_room", chat: chat }); }); });
connect-redisつかってるけどそれとは別にconnectというモジュールが必要だそうで、いれた。なんだか腑に落ちない感じがするけど仕方がないし、よくわからないのだからそうするしかない。
今まで通りRoutesに渡して処理しようとしたけれど、そうするとio.socket.onのconnectionがちゃんと貼れないっぽい。なので仕方ないからapp.jsの方で書いた。
room.jade
クライアントサイド
script(src = '/socket.io/socket.io.js') script //-var socket = io.connect('http://192.168.56.101'); var socket = io.connect('http://localhost'); socket.on('connect', function(data){ socket.emit('to_server', 'data'); socket.on('to_client', function(data){ p = document.createElement('p'); p.textContent = data; document.getElementById('chat').appendChild(p); }); }) h1= title p Welcome to #{title} for obj in chat p #{obj} #chat p form(action = "/room", method = "POST") label 発言する: input(type = "text", name = "chat") input(type = "submit", name = "発言する")
socket.io.jsを読み込んでやるのは素直に。to_server のくだりは正直必要ない気がしたんだけど、ないと動かなかったっぽいからいれた。ほんとうによくわかっていない。難しい。
あとは単純に Socket.io でemit されたものを #chat に appendChild しているだけです。
Socket.ioの流れとか
Socket.ioの流れは軽くつかんだ。
- クライアントでsocket.io.jsを読み込む
- クライアントでio.connectを読み込む
- クライアントでconnectイベント発生
- サーバーでconnectionイベントを受け取る
- クライアントからmessageイベントをemitする
- サーバーがmessageイベントをonする
- サーバーがmessageイベントをemitする
- クライアントがmessageイベントをonする
基本的にはこれだけで、「イベント駆動がガリガリ」って感じである。オプションにはいろいろあってbroadcastするとかjsonでわたすとか摘発性で渡すとかある。概念がわかったのはいいがいろいろハマるのがキツいけど。
問題点
どうもconnectionsが複数呼び出されてしまうらしく null がはいったり時系列がぐっちゃぐっちゃになる。nullに関してはたぶん rpop したときの値が null だから
if (latest_chat === null) return false;
こんな感じで無理やり処理してやったからいいけれど、それとは別にコネクションが複数張られてしまうみたい。回避策がわからない。なので
あ き ら め た
まあ
Node + Redisでチャットをつくってみるってだけだから Socket.io は余談だったんだけど、それにしてもできないのは難しい……くやしい……でも感じちゃう……///。本来ならチャット部分を先にSocket.ioで実装して、後からなんかクロールするみたいにログをRedisに取る、っていうのがいいアプローチなんだろうか?わからん。
次の話
同じようなチャットシステムをNode + Mongoでつくるよ。ざっと調べた限りNode, Mongodb, mongoose, connect-mongoあたりがキーワードかな?Socket.ioに関してはなんかもう無理にやんなくても別の機会でいいやって気分ではある*1。
参考
Socket.IO API 解説 - Block Rockin’ Codes
Socket.IO と Express でセッションの共有 - Block Rockin’ Codes
Node.js and Socket.io: Authentication All The Way Down I Am The Rockstar
JsonData • Writing Node apps and bleeding everywhere
マジでパクりコードでさーせんだし、パクったのにちゃんとできてないっていうすいませんっていうね……!
*1:そこまでしてSocket.ioを使いたいわけじゃないし、もうちょっと成熟したらきっともっとラクなライブラリつくってくれる人が出てくるだろうし、そもそもNodeが1.0でもないものを仕様とか細かいところでハマって遊ぶのはいまは勘弁かな、という感じ。もうちょっと枯れたシステムなら自分が悪いって思えるけど、Nodeまわりはまだいろいろと実験段階かな……という印象