pythonを使ってsocket.io-redisへイベントをemitする


## はじめに

AJAX+ポーリングで実装していたチャットサーバなどを、後付けでwebsocketを使ってリアルタイム通信にしようとすると、図1のような構成になることもあると思います。

![python-websocket-1](https://engineer.blog.lancers.jp/wp-content/uploads/2017/12/python-websocket-1.png)

まず、端末Aと端末Bがそれぞれ[socket.io](https://socket.io/)ライブラリを使ってwebsocketコネクションを確立
1. AJAXでメッセージをPOST
2. メッセージの内容をDBに保存
3. 端末Bに新しいメッセージが作成されたことを伝えるために[socket.io-php-emitter](https://github.com/rase-/socket.io-php-emitter)ライブラリを使ってイベントをemit(パブリッシュ)する
4. node.jsが3のイベントを[socket.io-redis
](https://github.com/socketio/socket.io-redis)ライブラリを使ってサブスクライブする
5. node.jsは、端末B, C, D...に対して新しいメッセージの内容とともに画面を更新するためのイベントをemitする

図1の構成のシステムがすでにある状態で、websocketで繋がっている端末A, B, C...にPythonでイベントをemitしたい場合の方法を紹介します。基本的には図1のステップ3で行なっていることと同じことをPythonで実行するだけです。構成としては図2のようになります。

![python-websocket-2](https://engineer.blog.lancers.jp/wp-content/uploads/2017/12/python-websocket-2.png)

1. [socket.io-redis
](https://github.com/socketio/socket.io-redis)形式に則ったイベントをemit(publish)
2. 1のイベントをサブスクライブ
3. 1のイベントに応じた処理を行うためのイベントをクライアントに対してemit
  クライアントはそのイベントを受け取り、処理を行う


PythonにもPHPのように[socket.io-php-emitter](https://github.com/rase-/socket.io-php-emitter)のようなライブラリがあれば、わざわざブログで紹介するほどのことでもないのですが、Pythonにはsocket.io-emitterを実現するライブラリがありません。
[socket.io-redisのProtocol](https://github.com/socketio/socket.io-redis#protocol)について言及している項目に[Python用のsocket.io-redis](https://github.com/GameXG/socket.io-python-emitter)が紹介されているのですが、名前空間を指定する`Of`とルーム名を指定する`In`が機能していないので使えません。
socket.io-redisは[RedisのPublishコマンド](https://redis.io/commands/publish)を発行しているに過ぎないので、自分でpublishコマンドを構築します。



## チャネル名
チャネル名は[socket.io-redis](https://github.com/socketio/socket.io-redis#protocol)によると次の通りに指定します。

特定のnamespace全体にイベントをemitしたい場合のチャネル名は以下の通り
```
prefix + '#' + namespace + '#'
```

特定のnamespaceのroomにイベントをemitしたい場合のチャネル名は以下の通り
```
prefix + '#' + namespace + '#' + room + '#'
```

socket.ioの場合はprefixのデフォルトは`socket.io`になります。
namespaceを`/`とし、room名を`room_777`とする場合は、例えば以下のようにchannel_nameを準備します。

```
prefix = 'socket.io'
namespace_name = '/'
room_name = 'room_777'

channel_name = '{0}#{1}#{2}#'.format(prefix, namespace_name, room_name)
```


## emitするデータの形式
socket.ioのemitで送信されるデータは[MessagePack](https://msgpack.org/ja)と呼ばれる形式でシリアライズされています。
socket.ioで送信する際のデータの形式は以下の通りです。

```
data = [
    'emitter',
    {
        # socket.ioはv1からバイナリ形式のデータ(画像・動画)も送信できるようになりました。
        # 通常のテキストデータの送信と区別するためにtypeキーが用意されています。
        # テキストデータの場合は2をセットし、バイナリデータの場合はは5をセットします。
        'type': 2,
        'data': [
            # emitしたいイベント名をセットします
            'my_event_name',
            # 送信したいデータを辞書形式でセットします
            {
                'id': 'message_id',
                'created_at': '2017-12-19 14:00:00',
                'body': 'message_body'
            }
        ],
        # namespaceをセットします
        'nsp': namespace_name
    },
    {
        # room_nameをセットします
        'rooms': [room_name],
        'flags': []
    }
]
```

上記データをmsgpackを使ってシリアライズします。

```
import msgpack

data_packed = msgpack.packb(data)

print(data_packed)
b'\x93\xa7emitter\x83\xa4type\x02\xa4data\x92\xadmy_event_name\x83\xa2id\xaamessage_id\xaacreated_at\xb32017-12-19 14:00:00\xa4body\xacmessage_body\xa3nsp\xa1/\x82\xa5rooms\x91\xa8room_777\xa5flags\x90'
```

## Publish

node.js側に事前にセットしておいたイベント名でpublishすればsocket.ioのイベントがemitされます。

pythonにおけるRedisクライアントは[こちら
](https://redis-py.readthedocs.io/en/latest/)がおすすめです。


```
import redis

r = redis.StrictRedis()
r.publish(channel_name, data_packed)
```

redisで次のようにコマンドが発行されていれば正常に動作しています。

```
redis-cli monitor | grep PUBLISH

1513649216.0000000 [0 127.0.0.1:50283] "PUBLISH" "socket.io#/#room_777#" "\x93\xa7emitter\x83\xa4type\x02\xa4data\x92\xadmy_event_name\x83\xa2id\xaamessage_id\xaacreated_at\xb32017-12-19 14:00:00\xa4body\xacmessage_body\xa3nsp\xa1/\x82\xa5rooms\x91\xa8room_777\xa5flags\x90"
```