aiohttpを使ってWebsocket通信してみる

昨日のWebsocketのテストの続きです。こうなったら WebサーバもPythonで書ききってしまいたいものです。しかし非同期動作が必要です。で、asyncio とセットで調べていたら、

https://docs.aiohttp.org/en/stable/

が見つかったので、早速試してみました。CPythonでの拡張のようなので、

(wsocket) pi@rpi2:~/python3/wsocket $ sudo apt install python3-dev

としてから、

(wsocket) pi@rpi2:~/python3/wsocket $ pip install aiohttp

としてインストールします。
サンプルコード

from aiohttp import web

async def handle(request):
    name = request.match_info.get('name', "Anonymous")
    text = "Hello, " + name
    return web.Response(text=text)

app = web.Application()
app.add_routes([web.get('/', handle),
                web.get('/{name}', handle)])

if __name__ == '__main__':
    web.run_app(app)

をaioserver.pyという名前で保存して実行して問題ないことを確認します。これを改変して、HTMLのソースを返すのと、Websocketも実装します。Websocketの実装は、

https://github.com/ftobia/aiohttp-websockets-example/blob/master/server.py

を参考にして、昨日試した readthedocs.io の websockets のサーバ側を書き換えて組み込んでみました。HTMLのソースはもちろんそのクライアント側です。これでHTTPサーバ、Websocketサーバ、クライアントのHTMLが一つのファイルになることになります。

#!/usr/bin/env python

import os,asyncio,json,logging
import aiohttp.web

logging.basicConfig()

text = '''<!DOCTYPE html>
<html>
    <head>
        <title>WebSocket demo</title>
        <style type="text/css">
            body {
                font-family: "Courier New", sans-serif;
                text-align: center;
            }
            .buttons {
                font-size: 4em;
                display: flex;
                justify-content: center;
            }
            .button, .value {
                line-height: 1;
                padding: 2rem;
                margin: 2rem;
                border: medium solid;
                min-height: 1em;
                min-width: 1em;
            }
            .button {
                cursor: pointer;
                user-select: none;
            }
            .minus {
                color: red;
            }
            .plus {
                color: green;
            }
            .value {
                min-width: 2em;
            }
            .state {
                font-size: 2em;
            }
        </style>
    </head>
    <body>
        <div class="buttons">
            <div class="minus button">-</div>
            <div class="value">?</div>
            <div class="plus button">+</div>
        </div>
        <div class="state">
            <span class="users">?</span> online
        </div>
        <script>
            var minus = document.querySelector('.minus'),
                plus = document.querySelector('.plus'),
                value = document.querySelector('.value'),
                users = document.querySelector('.users'),
                websocket = new WebSocket("ws://rpi2.local:8080/ws");
            minus.onclick = function (event) {
                websocket.send(JSON.stringify({action: 'minus'}));
            }
            plus.onclick = function (event) {
                websocket.send(JSON.stringify({action: 'plus'}));
            }
            websocket.onmessage = function (event) {
                data = JSON.parse(event.data);
                switch (data.type) {
                    case 'state':
                        value.textContent = data.value;
                        break;
                    case 'users':
                        users.textContent = (
                            data.count.toString() + " user" +
                            (data.count == 1 ? "" : "s"));
                        break;
                    default:
                        console.error(
                            "unsupported event", data);
                }
            };
        </script>
    </body>
</html>
'''

async def handle(request):
    return aiohttp.web.Response(body=text,content_type='text/html')

STATE = {"value": 0}
USERS = set()

def state_event():
    return json.dumps({"type": "state", **STATE})

def users_event():
    return json.dumps({"type": "users", "count": len(USERS)})

async def notify_state():
    if USERS:  # asyncio.wait doesn't accept an empty list
        message = state_event()
        await asyncio.wait([user.send_str(message) for user in USERS])

async def notify_users():
    if USERS:  # asyncio.wait doesn't accept an empty list
        message = users_event()
        await asyncio.wait([user.send_str(message) for user in USERS])

async def register(websocket):
    USERS.add(websocket)
    await notify_users()

async def unregister(websocket):
    USERS.remove(websocket)
    await notify_users()

async def websocket_handler(request):
    print('Websocket connection starting')

    websocket = aiohttp.web.WebSocketResponse()
    await websocket.prepare(request)
    await register(websocket)
    try:
        print('Websocket connection ready')
        async for msg in websocket:
            print(msg)
            data = json.loads(msg.data)
            if data["action"] == "minus":
                STATE["value"] -= 1
                await notify_state()
            elif data["action"] == "plus":
                STATE["value"] += 1
                await notify_state()
            else:
                logging.error("unsupported event: {}", data)
    finally:
        await unregister(websocket)

    print('Websocket connection closed')
    return websocket

app = aiohttp.web.Application()
app.add_routes([aiohttp.web.get('/', handle),
                aiohttp.web.get('/ws', websocket_handler)])

HOST = os.getenv('HOST', '0.0.0.0')
PORT = int(os.getenv('PORT', 8080))

if __name__ == '__main__':
    aiohttp.web.run_app(app, host=HOST, port=PORT)

これで、 http://rpi2.local:8080/ にアクセスすると無事に動作しました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)