share facebook facebook twitter menu hatena pocket slack

2013.02.12 TUE

Node.jsってなんじゃ?(Socket.IOとELBのまとめ)

三浦 悟

WRITTEN BY 三浦 悟

過去の記事でnode.jsの話題をいくつか取り上げて来ましたが、node.jsではAmazonのELBと併用した際に問題があり、
その注意点をまとめてみました。

node.jsの参考記事一例 :
AWS SDK for Node.js ってなんじゃ?
Node.jsってなんじゃ?(knox:S3にアクセス)

○Socket.IO

Socket.IOはnode.jsでwebsocketを使用する時のデファクトと言えるライブラリです。
他のSocket.IOが人気になったのは、以下のような利点のためです。

  • websocketをサポートしていないブラウザでは、自動的にxhr等のポーリングを使い通信できる
  • 接続に失敗しても再接続等を自動的に行う

ここではSocket.IOを使用する前提で、ELBを経由して複数のnodeサーバをホストする場合についてまとめてみました。

Socket.IOのインストールに関しては、WebSocketってなんじゃ?(Node編2 Socket.IOでプッシュ通信)の記事に
記載してあります。

nodeサーバには以下のようにファイルを配置し、publicをhttpdのドキュメントルートにしておきます。
(上記の記事等で使用したチャットアプリになります)

app
├── node
│ └── server.js
└── public
├── assets
│ └── js
│ └── client.js
├── health.txt
└── index.html

public/health.txtはELB用のヘルスチェックファイルで、中身はありません。
その他、各ファイルの内容は以下の通りです。

・server.js

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

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'});
});
});

・index.html






nodetest






・client.js

$(function(){
var socket = io.connect('http://'+location.hostname+':3000/');

socket.on('connect', function(){
$("#log").html($("#log").html() + "
" + (new Date()).toLocaleString()+ 'connected');
});
socket.on('disconnect', function(){
$("#log").html($("#log").html() + "
" + (new Date()).toLocaleString()+'disconnected');
});
socket.on('info', function (data) {
$("#log").html($("#log").html() + "
" + (new Date()).toLocaleString()+ data.msg);
});
socket.on('msg', function(data){
$("#log").html($("#log").html() + "
" + (new Date()).toLocaleString()+ "" + data.msg + "");
});
$("#send").click(function(){
var msg = $("#msg").val();
if(!msg){
alert("input your message");
return;
}
socket.emit('msg', msg);
});
});

ポートは3000番を利用し、ここでhttpdを起動しておきます。
また、下記のようにnodeを起動しておきます。

node server.js

○AWS

・単体構成

まず最初の例として、AWSは以下のような構成だとします。
nodeサーバのEC2インスタンスはhtmlのホストとwebsocketの両方を担うため、80と3000のポートを
セキュリティグループで開放しておきます。

画面を開いてみます。

問題なく成功します。
通信を確認してみると、websocketで通信されていることがわかります。

・ELB

次に、nodeサーバのインスタンスをもう一台追加し、新規作成したELB配下に2つのインスタンスを配置します。

以下のように80番と3000番をELBのリスナーに設定します。

そして、ELBのエンドポイントのURLをブラウザで開きます。

そうすると、xhr-pollingになり、接続と切断が繰り返されます。
また、画面を2つ開いてメッセージを送信しても相手に届かない場合があります。

注意点1 : ELBはhttpではなくtcpでポートを設定

websocket通信ではクライアントとサーバの間のハンドシェイクにUpgradeヘッダを送信します。
しかし、ELBはhttpリスナーの場合Upgradeヘッダを削ってしまうようです。

そのため、Socket.IOはwebsocketが使えないと判断し、次善策の一つとして通常のhttp通信で
xhr-pollingで接続することになります。
これはajaxの通信と同じです。
そこで、ELBでは上記リンクの通り、websocket用のリスナーはhttpの3000番ではなくtcpの3000番を
設定する必要があります。

注意点2 : Redisでセッション共有を行う

接続や切断が繰り返されるのはxhrのポーリングの度にハンドシェイクが確立したサーバとは別のサーバへ
接続に行くからのようです。
これはELBのリスナー設定をtcpにした場合も同様で、一度websocket通信が確立したかのように見えても、
次に接続した時に別のサーバに繋がると、接続が切れたもしくはwebsocketに失敗したと判断し、
xhr-polling等他の方法で通信しようとするようです。

根本的な問題として、2つのnodeサーバの間で接続情報(セッション)が共有されないため、ELBを通して
node1とnode2にそれぞれ接続したクライアント間ではメッセージのやり取りができません。

そこで、nodeサーバのバックエンドとして、redisを利用してセッション共有を行う方法が有効です。
socket.ioはセッションを保持する方式としてローカルメモリを使用するMemoryStoreを使用しますが、
オプションにてRedisではRedisStoreを使用することができます。

LearnBoost / socket.io : Configuring Socket.IO

上記のモジュールを使用します。
redisサーバを追加し、redisを起動しておきます。
インスタンスにはセキュリティグループ等でredisで使用するポートを開放しておきます。

そして、以下のようにserver.jsを変更します。

・server.js

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

var RedisStore = require('socket.io/lib/stores/redis');
opts = {host:'xxx.xxx.xxx.xxx', port:6379};
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'});
});
});

以上で、redisサーバでnode1,node2のセッションを共有できます。

何度か試し、正しく通信できていることを確認しました。

○まとめ

注意点としては、ELBを使用した場合はtcpでリッスンすること。
それと、ELBに限らずnodeサーバをスケールする場合は、redisサーバでセッション情報を共有する必要があります。

こちらの記事はなかの人(memorycraft)監修のもと掲載しています。
元記事は、こちら

三浦 悟

三浦 悟

高円寺在住のなんじゃ系男子 またの名をmemorycraftといいます。 炭水化物大好き 日々の「なんじゃ?」を記事にしてます。