WebsocketとRedis Pub/Sub


Redisの[Pub/Sub機能](https://redis.io/topics/pubsub)を使うとWebsocketの通信情報を複数のサーバー間で同期させることができます。
スケールのためにWebsocketのサーバーを複数立てた場合、サーバー間で通信情報を同期する仕組みがないと、例えばチャットアプリケーションの場合、サーバーAに接続されたクライアントはサーバーBに接続されたクライアントとは正常にメッセージのやりとりができないことになってしまいます。
RedisのPub/Sub機能はそれを解消することができます。

![スクリーンショット 2020-12-28 20.44.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260824/6ae93db6-c8db-40b4-2373-e1682083728b.png)

## Redis Pub/Subとは

Pus/Subにでてくる主な概念です。

| 用語 | 説明 | 
| --- | ---- |
| Publish | 発行すること | 
| Subscribe | 購読すること | 
| Channel | 購読するチャンネル | 

ラジオで言えば
番組を配信することがPublish、
番組を聞くことがSubscribe、
番組そのものがChannelです。
ラジオの音は、番組を聞いている人にだけ配信されます。

Redis-cliを使ってpubsubを試してみます。

```
redis-server 　//Redisを起動
redis-cli   　　//CLIを起動
```

ターミナルのタブを二つ開き、一方のターミナルで

```
SUBSCRIBE MorningShow
```
もう一方のターミナルで

```
PUBLISH MorningShow "Good Morning!"
```
を実行すると、Subscribeしている側のタブに"Good Morning!"のメッセージが届きます。
![スクリーンショット_2020-12-27_17_42_18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260824/43c5882c-546d-0640-f3dd-f54a6b77583b.png)


## Node.jsでサンプルを動かす
Node.jsでwebsocket + Redis PubSubを使った簡単なサンプルです。ソースコードは[こちら](https://github.com/rihofujino/pubsub-demo)。

タブを複数開き、一方でメッセージを送ると両方で更新されます。※この時点ではまだpubsubは関係ありません
![ezgif.com-crop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260824/ed9d986f-440b-f91f-9618-0838795af6ff.gif)


まずはクライアント側です。接続ボタンが押されたらサーバーにアクセスします。サーバーからメッセージを受け取ったら画面に表示します。

```public/client.js
wsButton.onclick = function () {
    wsSendButton.disabled = false;

    ws = new WebSocket(`ws://${location.host}`);
    ws.onopen = function () {
        showMessage('WebSocket connection established');
    };
    ws.onmessage = function (message) {
        let data = JSON.parse(message.data)
        showMessage(JSON.stringify(data.message));
    };
     -- 省略 --
};
```

次にサーバー側です。
websocketのリスナーを作成と、Redisへの接続を行います。今回はdocker-composeで立てたRedisコンテナに接続するため、Redisホストをコンテナ名の'redis'としています（[参照](https://github.com/luin/ioredis/issues/763#issuecomment-446674624)）。publishとsubscribe用にRedisへの接続は2つ作っています。


```server.js
const express = require('express');
const http = require('http');
const WebSocket = require('ws');
const Redis = require('ioredis');
const app = express();

app.use(express.static('public'));
const server = http.createServer(app);
const wss = new WebSocket.Server({ noServer: true });

const redis = new Redis('redis'); //redis container
const client = new Redis('redis');
```

次がpubsubの処理です。
あらかじめ一方のRedisへの接続で'newMessage'というチャンネルをsubscribeしておきます。
クライアントからメッセージを受け取ったら、もう一方の接続で'newMessage'のチャンネルに対して受け取ったメッセージをpublishします。
'newMessage'チャンネルに新規メッセージが来たら、全てのクライアントにメッセージを配信します。

```server.js
function subscribeMessage(channel) {
  client.subscribe(channel);
  client.on('message', function(channel, message) {
    broadcast(JSON.parse(message))
  });
}
subscribeMessage('newMessage')

// broadcast message to all clients
function broadcast(message){
  wss.clients.forEach(function(client){
    client.send(JSON.stringify({
        message: message
    }))
  })
}

wss.on('connection', function (ws, request) {
  ws.on('message', function (message) {
    redis.publish('newMessage', JSON.stringify(message))
  });

  -- 省略 --
});
```

ここまでは一台のサーバーで動かしてもpubsubを使う意味がありません。
なので、次にNode.jsのdockerコンテナを2つ立て、一方のコンテナにメッセージが来たらもう一方のコンテナにもpubsubを通して同期されることを試します。

## Node.jsのコンテナ間で通信を同期させる
Node.jsのコンテナを立てるDockerfileです。[公式](https://nodejs.org/ja/docs/guides/nodejs-docker-webapp/)にあるものをそのまま利用してます。

```dockerfile

FROM node:12

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

CMD [ "node", "server.js" ]
```

Dockerfileを元にNode.jsのコンテナを2つ、またRedisコンテナをdocker-composeで作成します。
Nodeのコンテナは一つをlocalhost:3000、もう一つをlocalhost:3030でアクセスできるようにしています。

```docker-compose.yml
version: '3'
services:
  server1:
    build: .
    ports:
      - 3000:3000
  server2:
    build: .
    ports:
      - 3030:3000
  redis:
    image: redis:6
    ports:
      - 6379:6379
```

最後にコンテナを起動します。

```
docker-compose up --build
```

起動したら、ブラウザのタブを二つ開き、一方を[localhost:3000](http://localhost:3000/)、もう一方を[localhost:3030](http://localhost:3030/)にアクセスします。
一方のタブで送信したメッセージが、もう一方にも表示されれば成功です。
![ezgif.com-crop.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/260824/5c43b9d6-c67c-2972-2199-6a54737ed995.gif)

実際のアプリケーションはより複雑にはなりますが、概要を書きました。
より実践的なアプリケーション構成や実装方法は、参考にあるAWSのブログが分かりやすいと思います。

## 参考
* [Amazon ElastiCache for Redis を使ったChatアプリの開発](https://aws.amazon.com/jp/blogs/news/how-to-build-a-chat-application-with-amazon-elasticache-for-redis/)