Socket.IO
Socket.IOはnode.jsでwebsocketを使用するときのデファクトといっていいライブラリです。
他のSocket.IOが人気になったのは、以下のような利点のためです。
- websocketをサポートしていないブラウザでは、自動的にxhrなどのポーリング使い通信できる
- 接続に失敗しても再接続などを自動的に行う
ここではSocket.IOを使用する前提で、ELBを経由して複数のnodeサーバーをホストする場合についてまとめてみました。
インストールに関しては以下に書いてありますので割愛します。
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'}); | |
}); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js"></script> | |
<script type="text/javascript"> | |
var hostname = location.hostname; | |
$.ajaxSetup({cache: true}); | |
$(function(){ | |
function load(){ | |
$.getScript("/assets/js/client.js"); | |
} | |
$.getScript("http://" + hostname + ":3000/socket.io/socket.io.js", function(){ | |
load(); | |
}); | |
}); | |
</script> | |
<title>nodetest</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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$(function(){ | |
var socket = io.connect('http://'+location.hostname+':3000/'); | |
socket.on('connect', function(){ | |
$("#log").html($("#log").html() + "<br />" + (new Date()).toLocaleString()+ 'connected'); | |
}); | |
socket.on('disconnect', function(){ | |
$("#log").html($("#log").html() + "<br />" + (new Date()).toLocaleString()+'disconnected'); | |
}); | |
socket.on('info', function (data) { | |
$("#log").html($("#log").html() + "<br />" + (new Date()).toLocaleString()+ data.msg); | |
}); | |
socket.on('msg', function(data){ | |
$("#log").html($("#log").html() + "<br />" + (new Date()).toLocaleString()+ "<b>" + data.msg + "</b>"); | |
}); | |
$("#send").click(function(){ | |
var msg = $("#msg").val(); | |
if(!msg){ | |
alert("input your message"); | |
return; | |
} | |
socket.emit('msg', msg); | |
}); | |
}); |
ポートは3000番を利用します。
ここで、httpdを起動しておきます。
また、
node server.js
などで、nodeを起動しておきます。
AWS
単体構成
まず最初の例として、AWSは例として以下のような構成だとします。
nodeサーバーのEC2インスタンスはhtmlのホストとwebsocketの両方を行うので、80と3000のポートをセキュリティグループで開放しておきます。
nodeサーバーのEC2インスタンスはhtmlのホストとwebsocketの両方を行うので、80と3000のポートをセキュリティグループで開放しておきます。
画面を開いてみます。
普通に成功します。
通信をみてみると、websocketで通信されていることがわかります。
ELB
次に、nodeサーバーのインスタンスをもう一台追加し、新規作成したELB配下に2つのインスタンスをおきます。
ELBのリスナーを以下のように80番と3000番を設定します。
そしてELBのエンドポイントのURLをブラウザで開きます。
すると、xhr-pollingになり、接続と切断が繰り返されます。
また、画面を2つ開いてメッセージを送信しても相手に届かない場合があります。
注意点1:ELBはhttpではなくtcpでポートを設定
しかし、ELBはhttpリスナーの場合Upgradeヘッダを削ってしまうようです。
RFC6455 — The WebSocket Protocol 日本語訳
ELBでHTTPリスナーだとWebSocketは使えない
そのため、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が選べます
Configuring Socket.IO
これを使います。
redisサーバーを追加し、redisを起動しておきます。
インスタンスにはセキュリティグループなどでredisで使用するポートを開放しておきます。
そして、以下のようにserver.jsを変更します。
server.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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サーバーでセッション共有する必要があります。
以上です。