昨日の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/ にアクセスすると無事に動作しました。