前回のSocket.ioサーバをスケールアウトするでは、sticky-sessionモジュールを使用してSocket.ioサーバを複数プロセスで運用する方法について書いた。
sticky-sessionを使用することによってクライアントがサーバとコネクションを張れない問題は解決できたが、まだ課題は残っている。
それは、2つのクライアントが別々のSocket.ioサーバプロセスに接続した場合に、この2つのクライアント間でメッセージを送受信することができないという問題。
例えば、クライアントAがサーバXに接続し、クライアントBがサーバYに接続したとすると、現段階ではサーバXがクライアントAから受け取ったメッセージを、サーバYに知らせるすべがない。
この問題を解決するために、Socket.ioではsocket.io-redisという拡張Adapterを用意している。メッセージのルーティングを担うモジュールのことをSocket.ioではAdapterと呼んでいて、socket.io-redisはデフォルトのAdapterであるsocket.io-adapterを置き換える。
socket.io-redisを使用したサーバの実装は以下の通り。
socket.io-redisをインポートして、Adapterとしてセットするだけ。
const cluster = require('cluster');
const io = require('socket.io')();
const sticky = require('sticky-session');
const http = require('http');
const redis = require('socket.io-redis'); // 追加
const server = http.createServer((req, res) => {
res.end('worker: ' + cluster.worker.id);
});
io.adapter(redis({host: '127.0.0.1', port: 6379})); // 追加
io.attach(server);
isWorker = sticky.listen(server, 3000);
if (isWorker) {
io.on('connection', (socket) => {
console.log(`worker: ${cluster.worker.id}, connected, id: ${socket.id}`);
socket.on('chat message', (user, message) => {
data = `${message} from ${user}`;
console.log(data);
socket.broadcast.emit('chat message', data);
});
socket.on('disconnect', () => {
console.log(`disconnected, id: ${socket.id}`);
});
});
}
2つの別々のホストからこのサーバに接続する。
※クライアントのプログラムはこちら
# クライアントA
$ node client.js A
connected, id: S1H5pJcm8Kun5lOoAAAA
# クライアントB
$ node client.js B
connected, id: 7HLiEmHioqbLuINtAAAA
# サーバ
$ node index.js
worker: 2, connected, id: S1H5pJcm8Kun5lOoAAAA
worker: 4, connected, id: 7HLiEmHioqbLuINtAAAA
サーバのログから、クライアントAとBは別々のSocket.ioサーバプロセスに接続されたことがわかる。
前回までの実装では、クライアントAが送信したメッセージをサーバブロードキャストしても、クライアントBがメッセージを受取ることはできなかったが、Redisを介した実装ではクライアントBがメッセージを受取ることができる。
# クライアントA
$ node client.js A
connected, id: S1H5pJcm8Kun5lOoAAAA
Hello # クライアントAが送信したメッセージ
Hi from B # クライアントBが送信したメッセージ
# クライアントB
$ node client.js B
connected, id: 7HLiEmHioqbLuINtAAAA
Hello from A # クライアントAが送信したメッセージ
Hi # クライアントBが送信したメッセージ
裏側で何が起きているか
Redisをモニターしていると、裏側でどんなやり取りがあるのかがわかる。
socket.io-redisは、RedisのPUB/SUBを利用している。
まず、socket.io-redisを実装したサーバを起動すると、サーバはsocket.io#/#
というチャンネルをSUBSCRIBEする(使用するネームスペースによって変わる)。
4行出力されているのは、Socket.ioサーバワーカーが4つ起動しているから。
$ redis-cli monitor
OK
"subscribe" "socket.io#/#" "socket.io-request#/#" "socket.io-response#/#"
"subscribe" "socket.io#/#" "socket.io-request#/#" "socket.io-response#/#"
"subscribe" "socket.io#/#" "socket.io-request#/#" "socket.io-response#/#"
"subscribe" "socket.io#/#" "socket.io-request#/#" "socket.io-response#/#"
次に、クライアントAがサーバXに接続してメッセージを送信すると、サーバはメッセージをRedisに対してPUBLISHする。
"publish" "socket.io#/#" "\x93\xa66n0BWC\x83\xa4type\x02\xa4data\x92\xacchat message\xacHello from A\xa3nsp\xa1/\x83\xa6except\x91\xb4S1H5pJcm8Kun5lOoAAAA\xa5rooms\xc4\xa5flags\x81\xa9broadcast\xc3"
socket.io#/#
チャンネルに対してPUBLISHしているので、このチャンネルをSUBSCRIBEしているサーバYがメッセージの通知を受取り、それをクライアントBに送信することができるという仕組み。