web-dev-qa-db-ja.com

Asyncio + aiohttp-単一のハンドラーでPub / Subとwebsocketの読み取り/書き込みを再実行

私は現在 aiohttp で遊んでおり、WebSocket接続を備えたモバイルアプリのサーバーアプリケーションとしてどのように機能するかを確認しています。

これは単純な「Helloworld」の例です( ここでは要点として ):

import asyncio
import aiohttp
from aiohttp import web


class WebsocketEchoHandler:

    @asyncio.coroutine
    def __call__(self, request):
        ws = web.WebSocketResponse()
        ws.start(request)

        print('Connection opened')
        try:
            while True:
                msg = yield from ws.receive()
                ws.send_str(msg.data + '/answer')
        except:
            pass
        finally:
            print('Connection closed')
        return ws


if __name__ == "__main__":
    app = aiohttp.web.Application()
    app.router.add_route('GET', '/ws', WebsocketEchoHandler())

    loop = asyncio.get_event_loop()
    handler = app.make_handler()

    f = loop.create_server(
        handler,
        '127.0.0.1',
        8080,
    )

    srv = loop.run_until_complete(f)
    print("Server started at {sock[0]}:{sock[1]}".format(
        sock=srv.sockets[0].getsockname()
    ))
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass
    finally:
        loop.run_until_complete(handler.finish_connections(1.0))
        srv.close()
        loop.run_until_complete(srv.wait_closed())
        loop.run_until_complete(app.finish())
    loop.close()

問題

ここで、以下に説明する構造を使用したいと思います(node server = python aiohttp)。より具体的には、 Redis Pub/Sub メカニズムを asyncio -redisWebsocketEchoHandlerでWebSocket接続とRedisの両方の読み取りと書き込みを行います。

WebsocketEchoHandlerは完全に単純なループであるため、これをどのように行うべきかわかりません。 Tornadobrükva を使用するコールバックを使用します。

http://goldfirestudios.com/blog/136/Horizontally-Scaling-Node.js-and-WebSockets-with-Redis

追加の(おそらくオフトピックの)質問

Redis をすでに使用しているので、次の2つのアプローチのどちらを使用する必要がありますか。

  1. 「クラシック」ウェブアプリのように、すべてのコントローラー/ビューを用意し、メッセージングなどに Redis を使用します。
  2. Webアプリは、クライアントと Redis の間の単なるレイヤーであり、タスクキューとしても使用されます(最も単純な Python RQ )。すべての要求は労働者に委任する必要があります。

編集

http://goldfirestudios.com/blog/136/Horizo​​ntally-Scaling-Node.js-and-WebSockets-with-Redis からの画像

編集2

明確にする必要があるようです。

  • Websocketのみのハンドラーを上に示します
  • Redis Pub/Subハンドラーは次のようになります:

    class WebsocketEchoHandler:
    
        @asyncio.coroutine
        def __call__(self, request):
            ws = web.WebSocketResponse()
            ws.start(request)
    
            connection = yield from asyncio_redis.Connection.create(Host='127.0.0.1', port=6379)
            subscriber = yield from connection.start_subscribe()
            yield from subscriber.subscribe(['ch1', 'ch2'])
    
            print('Connection opened')
            try:
                while True:
                    msg = yield from subscriber.next_published()
                    ws.send_str(msg.value + '/answer')
            except:
                pass
            finally:
                print('Connection closed')
            return ws
    

    このハンドラーは、Redisチャネルch1およびch2にサブスクライブし、これらのチャネルから受信したすべてのメッセージをWebSocketに送信します。

  • このハンドラーが欲しい:

    class WebsocketEchoHandler:
    
        @asyncio.coroutine
        def __call__(self, request):
            ws = web.WebSocketResponse()
            ws.start(request)
    
            connection = yield from asyncio_redis.Connection.create(Host='127.0.0.1', port=6379)
            subscriber = yield from connection.start_subscribe()
            yield from subscriber.subscribe(['ch1', 'ch2'])
    
            print('Connection opened')
            try:
                while True:
                    # If message recived from redis OR from websocket
                    msg_ws = yield from ws.receive()
                    msg_redis = yield from subscriber.next_published()
                    if msg_ws:
                        # Push to redis / do something else
                        self.on_msg_from_ws(msg_ws)
                    if msg_redis:
                        self.on_msg_from_redis(msg_redis)
            except:
                pass
            finally:
                print('Connection closed')
            return ws
    

    ただし、次のコードは常に順番に呼び出されるため、websocketからの読み取りはRedisからの読み取りをブロックします。

    msg_ws = yield from ws.receive()
    msg_redis = yield from subscriber.next_published()
    

イベントで読み取りを実行したい。ここでイベントは2つのソースのいずれかから受信したメッセージです。

17
Michael

2つのwhileループを使用する必要があります。1つはWebSocketからのメッセージを処理し、もう1つはredisからのメッセージを処理します。メインハンドラーは、2つのコルーチンを開始します。1つは各ループを処理し、次にそれらの両方を待機します。

class WebsocketEchoHandler:
    @asyncio.coroutine
    def __call__(self, request):
        ws = web.WebSocketResponse()
        ws.start(request)

        connection = yield from asyncio_redis.Connection.create(Host='127.0.0.1', port=6379)
        subscriber = yield from connection.start_subscribe()
        yield from subscriber.subscribe(['ch1', 'ch2'])

        print('Connection opened')
        try:
            # Kick off both coroutines in parallel, and then block
            # until both are completed.
            yield from asyncio.gather(self.handle_ws(ws), self.handle_redis(subscriber))
        except Exception as e:  # Don't do except: pass
            import traceback
            traceback.print_exc()
        finally:
            print('Connection closed')
        return ws

    @asyncio.coroutine
    def handle_ws(self, ws):
        while True:
            msg_ws = yield from ws.receive()
            if msg_ws:
                self.on_msg_from_ws(msg_ws)

    @asyncio.coroutine
    def handle_redis(self, subscriber):
        while True:
            msg_redis = yield from subscriber.next_published()
            if msg_redis:
                self.on_msg_from_redis(msg_redis)

このようにして、他のソースを気にすることなく、2つの潜在的なソースのいずれかから読み取ることができます。

24
dano

最近、python 3.5以降でasyncawaitを使用できます。

async def task1(ws):
    async for msg in ws:
        if msg.type == WSMsgType.TEXT:
            data = msg.data
            print(data)
            if data:
                await ws.send_str('pong')
## ch is a redis channel
async def task2(ch):
    async for msg in ch1.iter(encoding="utf-8", decoder=json.loads):
        print("receving", msg)
        user_token = msg['token']
        if user_token in r_cons.keys():
            _ws = r_cons[user_token]
            await  _ws.send_json(msg)

coroutines = list()
coroutines.append(task1(ws))
coroutines.append(task2(ch1))

await asyncio.gather(*coroutines)

これは私がしていることです。WebSocketが複数のソースからのメッセージを待つ必要がある場合。

ここでの主なポイントは、asyncio.gatherを使用して、前述の@danoのように2つのコルーチンを一緒に実行することです。

1
tyan