Node + Redis でチャットアプリを作る(完) - Socket.ioを使おうとして挫折した話

結論

チャットだけなら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

*1:そこまでしてSocket.ioを使いたいわけじゃないし、もうちょっと成熟したらきっともっとラクなライブラリつくってくれる人が出てくるだろうし、そもそもNodeが1.0でもないものを仕様とか細かいところでハマって遊ぶのはいまは勘弁かな、という感じ。もうちょっと枯れたシステムなら自分が悪いって思えるけど、Nodeまわりはまだいろいろと実験段階かな……という印象