2012年11月10日土曜日

Node.jsってなんじゃ?(redisでSocket.IOをスケール)


前回は、redisをインストールして生でつかってみました。
今回はnode.jsでredisを利用してみたいと思います。

マルチユーザーのサーバーでのプッシュ配信はSocket.IOが定番ですが、
サーバーが増えた時にある問題が生じます。
例えばサーバーを2つに増やして、サーバーAでブロードキャストしても
サーバーBのクライアントでは受信できないのです。

以前の記事で作成したチャットプログラムを例にしてみます。

サーバー側のjs
$ cat /home/appadmin/chat/node/chat.js
var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

var io = require('socket.io').listen(server);
io.sockets.on('connection', function (socket) {
  socket.emit('info', { msg: 'welcome' });
  socket.on('msg', function (msg) {
    io.sockets.emit('msg', {msg: msg});
  });
  socket.on('disconnect', function(){
    socket.emit('info', {msg: 'bye'});
  });
});


クライアント側のjs

$ cat /home/appadmin/chat/public/assets/js/client.js
$(function(){
    var socket = io.connect('http://'+location.hostname+':3001/');
    socket.on('connect', function() {
      $("#log").html($("#log").html() + "<br />" + 'connected');
      socket.on('info', function (data) {
        $("#log").html($("#log").html() + "<br />" + data.msg);
      });
      socket.on('msg', function(data){
        $("#log").html($("#log").html() + "<br />" + "<b>" + data.msg + "</b>");
      });
      $("#send").click(function(){
        var msg = $("#msg").val();
        if(!msg){
          alert("input your message");
          return;
        }
        socket.emit('msg', msg);
      });
    });
  });


画面
$ cat /home/appadmin/chat/public/index.html
<!DOCTYPE html>
<html>
<head>
     <meta charset="UTF-8">
     <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" type="text/javascript" charset="utf-8"></script>
     <script type="text/javascript">
     $(function(){
         function load(){
             $.getScript("assets/js/client.js");
         }
         $.getScript("http://" + location.hostname + ":3001/socket.io/socket.io.js", function(){
             load();
         });
     });
     </script>
     <title>Node A</title>
</head>
<body>
     <input id="msg" type="text" style="width:400px;"></input>
     <input id="send" type="button" value="send" /></br >
     <div id="log" style="width:400px;height:400px;overflow:auto;border:1px solid #000000;"></div>
</html>


サーバー側のjsをforeverで起動します。
$ forever start chat.js

これをサーバーAとします。
同じ内容を別のサーバーBに配置し、同じようにnodeを起動します。

2つのサーバーにアクセスすると、以下のように同じ画面が表示されます。




サーバーAで「a」と入力します。
サーバー側では接続した全ユーザーに投稿内容をブロードキャストし、ユーザーの画面に「a」が表示されます。
しかし、サーバーBには何も表示されません。
おなじようにサーバーBで、「b」と投稿してもサーバーAに接続した画面にはなにも表示されません。
接続がサーバーAとサーバーBで共有されていないためです。

ここで登場するのがredisです。
Socket.IOではデフォルトで接続情報をローカルメモリに保存しています。
これをMemoryStoreと呼びますが、Socket.IOにはRedisStoreというredisに接続情報を保存するオプションも存在します。
このオプションを選択することで、分散されたnodeサーバーが同じredisサーバーを参照し
各nodeの接続情報を共有することができます。


それではサーバー側のjsを修正してRedisStoreを使ってみます。
接続するredisは前回設定したredisサーバーにします。

var server = require('http').createServer(function(req, res){
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.end('server connected');
});
server.listen(3001);

var io = require('socket.io').listen(server);

//RedisStoreを読み込みます
var RedisStore = require('socket.io/lib/stores/redis');
//redisサーバーの接続先情報を定義します
opts = {host:'10.0.0.200', port:6379};
//storeをRedisStoreにし、redisPub, redisSub, redisClientをredisサーバーに向けます
io.set('store', new RedisStore({redisPub:opts, redisSub:opts, redisClient:opts}));

io.sockets.on('connection', function (socket) {
  socket.emit('info', { msg: 'welcome' });
  socket.on('msg', function (msg) {
    io.sockets.emit('msg', {msg: msg});
  });

  socket.on('disconnect', function(){
    socket.emit('info', {msg: 'bye'});
  });
});


これで再起動します。
$ forever restart chat.js


再度2つの画面をリロードして、サーバーAの画面に「a」サーバーBの画面に「b」と入力してみます。




おお、両方の画面に「a」「b」が表示されました!
これで台数が増えてもすべてのユーザーが同じ空間でコミュニケーションすることができます。

以上です。