我正在尝试使用 aiohttp
WebSockets和 aioredis
创建简单的活动用户计数器进行存储 . 当我在 Google Chrome
中添加新选项卡时,我的计数器在所有已打开的选项卡中完美递增 . 但是,当我关闭选项卡时,其他选项卡中没有任何更改 .
我想我应该在整个异步/等待机器中遗漏一些东西,但找不到可能出错的东西 .
这是我的应用程序
import asyncio
import aiohttp
from aiohttp import web
import aioredis
class CounterView(web.View):
async def get(self):
request = self.request
app = request.app
ws = web.WebSocketResponse()
app['websockets'].append(ws)
await ws.prepare(request)
count = int(await app['db'].incr('counter'))
for ws in app['websockets']:
await ws.send_json({'msg': {'count': count}})
async for msg in ws:
if msg.type == aiohttp.WSMsgType.TEXT:
await ws.send_str(msg.data)
elif msg.type == aiohttp.WSMsgType.ERROR:
print('ws connection closed with exception %s' %
ws.exception())
app['websockets'].remove(ws)
# Execution stops here (on await app['db'] ...) and never returns
count = int(await app['db'].decr('counter'))
for ws in app['websockets']:
await ws.send_json({'msg': {'count': count}})
return ws
async def init_app(loop):
app = web.Application(loop=loop)
db = await aioredis.create_redis('redis://localhost', loop=loop)
app['db'] = db
app['websockets'] = []
app.add_routes([
web.get('', CounterView),
])
return app
if __name__ == '__main__':
loop = asyncio.get_event_loop()
web.run_app(init_app(loop))
和index.html模板
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
How many people seeing this page now: <span id="counter"></span>
</body>
<script>
window.onload = function () {
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = function (event) {
const data = JSON.parse(event.data);
let span = document.getElementById('counter');
console.log(data.msg.count);
span.innerHTML = data.msg.count;
}
};
</script>
</html>
我也试过 Firefox
,有些奇怪的事情发生在那里 .
打开两个标签,两者都得到 counter = 2
. 然后重新加载 - 在其中获得 1
,在第二个中仍然是 2
. 再次重新加载第一个标签 - 得到 2
. 在此之后,每次重新加载都会给出 2
.
直到我重新加载第二个选项卡 - 相同的进程(重新加载 - 1
- 重新加载 - 2
在那里发生并在第一个选项卡中重复)
此外,我试图应用https://stackoverflow.com/a/48695448/6627564这个答案,但没有任何改变 .
调试显示代码最多执行 count = int(await app['db'].decr('counter'))
然后跳转到某处永不返回 .
任何帮助是极大的赞赏 . 据我所知,事件循环应该在此行之后返回执行 . 也许协程以某种方式被破坏,但我没有在库中找到任何代码 .
我的问题与Python Asyncio Websocket not detecting a disconnect on wifi but does on localhost中描述的不同
首先,我的连接遍布localhost .
其次, async for msg in ws
循环之后的代码实际上开始执行,并且调试显示实际调用了 ws.close()
方法 . 但是下一个 await
上面有一个上下文切换,执行不再继续 .
我也尝试使用 ws = web.WebSocketResponse(heartbeat=1.0)
激活乒乓,但我在Dev Tools中看不到任何消息 . 我在 await ws.prepare(request)
之后添加了单个 await ws.ping()
,遗憾的是,Dev Tools中没有出现任何消息 . 这里肯定有问题......
1 回答
对于任何对此问题感兴趣的人 - 解决方案 .
这段代码有三个问题) . 其中两个实际上与asyncio无关 .
首先,
app['websockets']
是list
,由于某种原因,remove(ws)
无法找到正确的WebSocketResponse
实例,并从列表中删除了另一个WebSocketResponse
. 解决方案是使用set()
而不是list
来存储活动的websockets . 这是因为set.discard()
使用__hash__
魔术方法,list.remove()
使用__eq__
方法 . 不幸的是,我找不到WebSocketResponse
中__eq__
的实现细节,但__hash__
正在使用内置id
函数来保证正确的工作 .其次,看看这一行
局部变量
ws
在for
循环中被覆盖 . 解决方案是只使用其他变量名进行迭代,如other_ws
第三个描述在
aiohttp
的文档Web Handler Cancellation中 .它声明在每个
await
调用时,如果客户端已断开连接,则可以终止处理程序 . 情况确实如此 - 在连接断开后的第一个await
上,我的处理程序死了 . 解决方案也在文档中提供,我决定使用asyncio.shield
.