Socket.io(v1.2)のスケールアウト。Redisを使って複数プロセスでのセッション共有してみる。


[Node.js Advent Calender](http://qiita.com/advent-calendar/2014/nodejs) の6日目です。順調に続いてますね。
この間の[東京Node学園祭2014](http://yosuke-furukawa.hatenablog.com/entry/2014/11/26/113915)で[@upgrade_ayp](https://twitter.com/upgrade_ayp)さんセッションで[@mizchi](https://twitter.com/mizchi)さんと隣の席でしたがここでもmizchiさんの隣(次の日)みたいです笑

##はじめに
今回のネタはNode.js/Socket.ioをスケールアウトするときに初心者向け記事です。
(実際にAWS上で複数サーバーを立ててNode.jsプロセスのロードバランシングをしたときにつまづいた経験から、おさらいを兼ねてやってみます。)

![](http://i.gyazo.com/297913aca4065e43695c4b661407d031.png)

こちらの記事([Node.jsってなんじゃ？（Socket.IOとELBのまとめ）](http://memocra.blogspot.jp/2013/02/nodejssocketioelb.html))にあるように複数プロセスでSocket.ioを共有する場合はRedisを使います。

Socket.ioの0.9系以前と1系以降とではけっこう中身が変わっている印象ですが、 **書籍や国内技術ブログなどは未だに0.9系以前で記述されたものが多い** です。
(↑でリンクを貼った記事もやはりSocket.ioが1系になる前の記事です)

今では1系が主流なので1系の書き方で書いてみます。

ちなみに1系では`socket.io-redis`を使う必要があります。 0.9系まではsocket.ioだけで出来たみたいですけど、1系では外部ライブラリ化したみたいですね。

この辺りは@[nulltask](https://twitter.com/nulltask)さんの[Express / Socket.IO をスケールアウトしてみよう](https://gist.github.com/nulltask/89e6f36e194c951697a0)を読むと勉強になります。

> ちなみに、socket.ioを利用していない部分からsocket.ioへ接続させるための`socket.io-emitter`も公開されています。
> [socket.io-emitter](https://github.com/Automattic/socket.io-emitter)

##環境
* Mac OS X 10.9
* Node.js 0.10.26
* [socket.io](http://socket.io/) 1.2.1
* [socket.io-redis](https://github.com/Automattic/socket.io-redis) 0.1.4
* redis 2.8.8

##モジュール準備

```
$ mkdir socketApp
$ cd socketApp
$ npm i socket.io
$ npm i socket.io-redis
```

###テストするプロセス数分の用意
今回はA,Bを用意

ソースコードはこちらの記事をもとにしています。
-> [Node.js + Socket.IO + jQuery で最小構成チャット](http://qiita.com/naga3/items/bdf6176537a5ac77a9b5)

####プロセスA ポート3001

```process_a.js
var fs = require('fs');
var app = require('http').createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.write("<h1>プロセスA</h1>");
  res.end(fs.readFileSync('index.html'));
}).listen(3001);

var io = require('socket.io').listen(app);
io.sockets.on('connection', function(socket) {
  socket.on('msg', function(data) {
    io.sockets.emit('msg', data);
    console.log('receive:', data);
  });
});
```

####プロセスB ポート3002

`process_a.js`と違うのはポート番号(3002)とres.write()の表示内容だけです。

```process_b.js
var fs = require('fs');
var app = require('http').createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.write("<h1>プロセスB</h1>");
  res.end(fs.readFileSync('index.html'));
}).listen(3002);

var io = require('socket.io').listen(app);
io.sockets.on('connection', function(socket) {
  socket.on('msg', function(data) {
    io.sockets.emit('msg', data);
　　 console.log('receive:', data);
  });
});
```

###フロント側

```
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>chat</title>
</head>
<body>
<script src="/socket.io/socket.io.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
$(function() {
  var socket = io.connect();
  $('form').submit(function() {
    socket.emit('msg', $('input').val());
    $('input').val('');
    return false;
  });
  
  socket.on('msg', function(data) {
  	    console.log(data);
    $('div').prepend(data + '<br>');

  });
});
</script>
<form><input></form><div></div>
</body>
</html>
```

###ディレクトリ構成

```
socketApp/
|
|---node_modules/
|      |---socket.io/
|      |___socket.io-redis/
|
|---index.html
|---process_a.js (ポート3001)
|___process_b.js (ポート3002)
```

###一度起動してみる

ターミナルでnodeプロセスAを起動します。

```
$ node process_a.js
```

ブラウザから`http://localhost:3001`にアクセスします。

別タブか別ウィンドウのターミナルでプロセスBを起動します。

```
$ node process_b.js
```

![](http://i.gyazo.com/33604812ecdba12d36da2641adb9045c.gif)

Socket.ioがプロセス毎で動いているのが分かると思います。

これをAとBのプロセスで共有するためにRedisを使います。

##redisの準備 (Mac OS XでRedisインストール)

Redisインストール

```
$ brew install redis
==> Downloading https://downloads.sf.net/project/machomebrew/Bottles/redis-2.8.8.mavericks.bottle.tar.gz
######################################################################## 100.0%
==> Pouring redis-2.8.8.mavericks.bottle.tar.gz
==> Caveats
To have launchd start redis at login:
    ln -sfv /usr/local/opt/redis/*.plist ~/Library/LaunchAgents
Then to load redis now:
    launchctl load ~/Library/LaunchAgents/homebrew.mxcl.redis.plist
Or, if you don't want/need launchctl, you can just run:
    redis-server /usr/local/etc/redis.conf
==> Summary
?  /usr/local/Cellar/redis/2.8.8: 10 files, 1.3M
```

Redis起動。デフォルトで`6379`ポートで起動します。

```
$ redis-server
[41618] 26 Nov 10:20:26.224 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
[41618] 26 Nov 10:20:26.226 * Increased maximum number of open files to 10032 (it was originally set to 256).
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 2.8.8 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in stand alone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 41618
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

[41618] 26 Nov 10:20:26.227 # Server started, Redis version 2.8.8
[41618] 26 Nov 10:20:26.227 * The server is now ready to accept connections on port 6379

```

##socket.io-redisを利用
ソースコードを書き換えます。先ほどのコードに2行追記してください。 

```process_a.js
var fs = require('fs');
var app = require('http').createServer(function(req, res) {
  res.writeHead(200, {'Content-Type': 'text/html'});
  res.write("<h1>プロセスA</h1>");
  res.end(fs.readFileSync('index.html'));
}).listen(3001);

var io = require('socket.io').listen(app);
var redis = require('socket.io-redis'); //++追記
io.adapter(redis({ host: 'localhost', port: 6379 })); //++追記

io.sockets.on('connection', function(socket) {
  socket.on('msg', function(data) {
    io.sockets.emit('msg', data);
    console.log('receive:', data);
  });
});
```

```process_b.js
(省略 process_a.jsと同様に2行追記)
```

Node.jsを再起動して先ほどと同様にアクセスしてみます。

![](http://i.gyazo.com/83c0be188490c9ea72b57610ba1afc01.gif)


これで複数プロセス間でセッションが共有できたことが分かると思います。

##まとめ
Node.js/Socket.ioをスケールアウトさせようとするとこんな感じの道を通ることになると思います。AWSでやる場合はELBの都合上もう少し設定をごにょごにょする必要ありですけど、基本的な考え方はこんな感じです。

最近は[Milkcocoa](https://mlkcca.com/)ばかり使っていたので久しぶりにSocke.ioを触りましたがやっぱり自分でサーバー側作るのは楽しいですね。

Automattic社とGuillermo氏さまさまです。

次は@t_wadaさんです。
>なんかすごい人たちに囲まれてたんですな汗