Redisを用いて複数サーバー構成でのリアルタイムチャットアプリを作ってみた


作ったもの

今回はRedisのお話です。
RedisのPubSubを用いることで、複数台サーバー構成のシステムでもリアルタイムチャットを実現可能にしました!


(左は3000番ポート、右は3001番ポートと別々のサーバへ接続しています)

背景

先日CyberAgentさん主催のヒダッカソンに参加してきました!
ハッカソンというよりはISUCONに近いイベントだったのですが、アプリケーション/データベース/インフラと様々な領域での知識と技術が求められ、自分自身の能力不足を痛感しました…
とはいえ凹んでいても周りのエンジニアとの差は深まる一方なので、悔しさをバネに最近はLinuxコマンドやDB周りの知識を増やすべく本やWebページを読みながら勉強しています!

Redisとは

RedisはREmote DIctionary Serviceの略で、Salvatore Sanfilippoさんによって作られたキーバリューストア。 
インメモリDBのひとつでメモリ上にデータを保存するため、CPUから直接アクセスすることができ、読み取り/書き込みが高速に行えます。秒間10万SET以上の操作を処理できるだとか…凄すぎ。

PubSubとは

PubSubとはPublish/Subscribeの略で、出版-購読型モデルを意味します。
PubSubにおいて必要になるアクションは2つです。

  1. Subscriber(購読者)が事前にどのチャネルをSubscribeしておくか設定する。
  2. Publisher(出版者)がチャネルにメッセージを投げる。

Publisherが使用するチャネル=Subscriberが購読しているチャネル、であればPublisherがメッセージを投げた際にSubscriberは即座にイベントを受け取ることが可能になります。

実装

server.js
const app = require('express')()
const server = require('http').createServer(app)
const io = require('socket.io')(server)
const redis = require('redis')
const sub = redis.createClient()
const pub = sub.duplicate()

app.get('/', (req, res) => {
  res.sendFile(`${__dirname}/index.html`)
})

io.on('connection', socket => {
  socket.on('chat', msg => {
    pub.publish('chat', msg)
  })
})

sub.on('message', (channel, message) => {
  io.emit('chat', message)
})

sub.subscribe('chat')

server.listen(3000, () => {
  console.log('listening on localhost:3000')
})
secondServer.js
const app = require('express')()
const server = require('http').createServer(app)
const io = require('socket.io')(server)
const redis = require('redis')
const sub = redis.createClient()
const pub = sub.duplicate()

app.get('/', (req, res) => {
  res.sendFile(`${__dirname}/index.html`)
})

io.on('connection', socket => {
  socket.on('chat', msg => {
    pub.publish('chat', msg)
  })
})

sub.on('message', (channel, message) => {
  io.emit('chat', message)
})

sub.subscribe('chat')

server.listen(3001, () => {
  console.log('listening on localhost:3001')
})
<!doctype html>
<html>
  <head>
    <title>Socket.IO chat</title>
    <style>
      * { margin: 0; padding: 0; box-sizing: border-box; }
      body { font: 13px Helvetica, Arial; }
      form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
      form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
      form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
      #messages { list-style-type: none; margin: 0; padding: 0; }
      #messages li { padding: 5px 10px; }
      #messages li:nth-child(odd) { background: #eee; }
      #messages { margin-bottom: 40px }
    </style>
  </head>
  <body>
    <ul id="messages"></ul>
    <form action="">
      <input id="m" autocomplete="off" /><button>Send</button>
    </form>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.1.1/socket.io.js"></script>
    <script src="https://code.jquery.com/jquery-1.11.1.js"></script>
    <script>
      $(function () {
        var socket = io();
        $('form').submit(function(){
          socket.emit('chat', $('#m').val());
          $('#m').val('');
          return false;
        });
        socket.on('chat', function(msg){
          $('#messages').append($('<li>').text(msg));
          window.scrollTo(0, document.body.scrollHeight);
        });
      });
    </script>
  </body>
</html>

$ node server.js
$ node secondServer.js

苦戦したところ

途中メッセージを投げてもサーバー側で一切イベントを受け取ってくれない、という壁にぶつかりました。
1時間ほど悩んでいたところで、サーバ側とフロント側で使用しているSocket.ioのバージョンが違うことに気づきました…失態!

関連記事

ヒダッカソンの舞台裏