先日参加したCyberAgentの短期インターンでredisのPub/Subについて学ぶ機会があったので実際に動かして遊んでみました。
redis
redisとは?
redisとはキャッシュやセッション管理でよく用いられるオンメモリ(=高速!)のNoSQLデータストアです。KVS(Key Value Store)と言い切っていいのかはちょっとよくわからないのでお茶を濁しておきますw
Pub/Sub
Pub/Subはredisの中でもマイナーな機能で知らない人も多いと思います。(実際に僕は今回初めて知りました)
簡単に説明すると、リアルタイムでデータの送受信を行うことができるデータストアです。Pub(=Publicsh)することで、Sub(=Subscribe)しているクライアントにデータを流すことができます。
Pub/SubとWebSocket
やっていることはWebSocketに近いです。違いとしては、WebSocket単体では同じサーバーに接続している人にしかデータを流すことができませんが、WebSocketとPub/Subを組み合わせることで、複数のサーバーにまたがってWebSocketのデータを共有することが可能になります。
WebSocketサーバーをWebSocketで接続しても同じことが実装できるでしょうが、これを専門にしているredisを使った方がパフォーマンスは上がると思います。また、コード的にもやっていることがシンプルになります。
なぜこのようなことを考えないといけないかというと、ロードバランシングしたときに誰がどこに接続するかを管理する必要がでてきたり、再接続の際に同じサーバーにいくように設計しないといけないからです。(個人開発だとそんなこと考えないですよね、、w)
WebSocketのnodeサーバーを実装する
redisを介したサーバーを作る前にまずはWebSocketを実装したnodeのサーバーを立ててみましょう。公式のサンプルを参考に、
var express = require('express')
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);
app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html');
});
io.on('connection', function (socket) {
socket.on('chat message', function (msg) {
io.emit('chat message', msg);
});
});
app.use("/static", express.static(__dirname + '/static'));
http.listen(3000, function () {
console.log('listening on *:' + 3000);
});
こんな感じで実装できます。一部静的ファイルをホスティングしたりしています。よくある構成かと。
フロントの方は、
import React from "react";
import ReactDOM from "react-dom";
import io from "socket.io-client"
class App extends React.Component {
constructor() {
super()
this.state = {
msg: [],
value: ""
}
this.socket = io()
}
componentDidMount() {
this.socket.on("chat message", msg => this.setState({msg: [...this.state.msg, msg]}))
}
handleChange(e) {
this.setState({value: e.target.value})
}
handleSubmit() {
this.socket.emit("chat message", this.state.value)
this.setState({value: ""})
}
msgList() {
return this.state.msg.map((m, i) => <li key={i}>{m}</li>)
}
render() {
return (
<div>
<input type="text" value={this.state.value} onChange={e => this.handleChange(e)} />
<button onClick={() => this.handleSubmit()}>送信</button>
<ul>{this.msgList()}</ul>
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
こんな感じでreactの実装にしてみました。wsの接続と表示だけなのでコンポーネント分割などはせずに、雑にcomponentDidMount
でwsのコネクションを張ってますw
いやぁ、socket.io
めっちゃ便利だなと。。
reactをよしなにbuildして、サーバーを起動するとwsで複数のタブ間で同期できている様子が確認できると思います。
複数のサーバー間でWebSocketを同期する
さて、本題のredisで複数サーバーを同期します。設計としては、docker-composeで2台のWebSocketサーバー(websocket1:3000, websocket2:3001)を立てます。さらに、redisも立ててそこに、http://redis:6379
でアクセスできるようにlinkを張ります。
で、このredisを介してデータの同期ができるようになります。
dockerコンテナ間の通信とかに関してはこちらを参照してください。
WebSocketのDockerfile
要件としては、nodeが動く・expressが動く・静的ファイル&サーバーAppが配置されている、の3つです。
from node
WORKDIR /var/www/app
RUN apt-get update
RUN npm install express socket.io socket.io-redis
COPY ./server .
CMD node server.js
また、server.jsを以下のように書き換えます。
var express = require('express')
var app = express();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var redis = require('socket.io-redis');
app.get('/', function (req, res) {
res.sendFile(__dirname + '/index.html');
});
io.adapter(redis({ host: 'redis', port: 6379 }));
io.on('connection', function (socket) {
socket.on('chat message', function (msg) {
io.emit('chat message', msg);
});
});
app.use("/static", express.static(__dirname + '/static'));
http.listen(3000, function () {
console.log('listening on *:' + 3000);
});
それ用のライブラリが充実しているので爆速で実装することができてしまいます。
あとは、起動設定をdocker-compose.ymlに記述するだけです。
version: '2'
services:
websocket1:
build: .
ports:
- "3000:3000"
links:
- "redis:redis"
websocket2:
build: .
ports:
- "3001:3000"
links:
- "redis:redis"
redis:
image: redis
こんな感じ。
これを、
docker-compose up -d
で起動すると、、、
こんな感じで異なるhost間でもwsを同期することができました。
反省点
- container内とホストOSでデータの同期をしていないから開発には向いてない(動かすことが目的やったし別にいいんだが)
- サーバー内のredisのホストがハードコーディング(これも動かすことが目的だったから&上が解決できたら別に問題ない)
- データの永続化(せっかくストレージ使ってたのにっていう)
- フロントのデータ管理
勉強がてら上記のリファクタリングもありかなって思ってる次第でございます。。
コードはこちら
redis-pubsub(github)