ランサーズ等のサービスを開発・運用する中で得た知識やノウハウを紹介しています。

thumbnail

Labels:  DevOps 投稿者:s.t

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

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

はじめに

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

python-websocket-1

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

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

python-websocket-2

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

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

チャネル名

チャネル名はsocket.io-redisによると次の通りに指定します。

特定の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と呼ばれる形式でシリアライズされています。
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クライアントはこちら
がおすすめです。

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"

Posted by 高田茂臣(takada.shigeomi@lancers.co.jp)

ランサーズではサービスを成長させてくれるエンジニア、デザイナーを募集しています!
ご興味がある方は、以下URLよりご応募ください。

PHPエンジニア
Rubyエンジニア
サーバーサイドエンジニア
SREエンジニア
フロントエンドエンジニア
その他採用情報

関連記事

thumbnail
Storing 80 million records in Redis on cheap

Hi, I am Martin and this article is about my journey optimizing Redis as key-value cache for millions of records. We have been usi …

開発環境のDocker化 その後

インフラエンジニアの金澤です。 開発環境をDocker化してから1年経ちましたので、その後のアップデートについて書きたいと思います。 前提 ランサーズでは、Dockerを開発を以下の目的で導入しました PCリソース(HDD、メモリ)を削減 コンテナ単位でサーバ …

AWSのスポットインスタンス運用

はじめまして。ランサーズインフラエンジニアの金澤です。 AWSのスポットインスタンスを利用したことはありますか? スポットインスタンスとは、AWSの余剰インスタンスを入札制で提供する仕組みです。 各インスタンスの1時間あたりの料金比較しても、スポットインスタン …