python aiohttp websockets关闭浏览器选项卡处理

时间:2018-05-13 17:40:04

标签: python websocket redis python-asyncio aiohttp

我正在尝试使用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

中描述的不同

首先,我的连接遍布本地主机。

其次,async for msg in ws循环之后的代码实际上开始执行,并且调试显示实际调用了ws.close()方法。但是下一个await有一个上下文切换,执行不再继续。

我也尝试使用ws = web.WebSocketResponse(heartbeat=1.0)激活乒乓,但我无法在Dev Tools中看到任何消息。我在await ws.ping()之后添加了单个await ws.prepare(request),很遗憾,Dev Tools中没有出现任何消​​息。这里肯定出了问题...

1 个答案:

答案 0 :(得分:3)

对于任何对此问题感兴趣的人 - 解决方案。

此代码中有三个问题)。其中两个实际上与asyncio无关。

首先,app['websockets']list,由于某种原因,remove(ws)无法找到正确的WebSocketResponse实例,并从列表中删除了另一个WebSocketResponse。 解决方案是使用set()而不是list来存储活动的websockets。这是因为set.discard()使用__hash__魔术方法,list.remove()使用__eq__方法。很遗憾,我无法在__eq__中找到WebSocketResponse的实施细节,但__hash__正在使用内置id函数来保证正确的工作。

其次,看看这一行

ws = web.WebSocketResponse()
....
......
for ws in app['websockets']:
   await ws.send_json({'msg': {'count': count}})

ws循环中覆盖了局部变量for。 解决方案是使用其他变量名进行迭代,例如other_ws

第三个问题在aiohttp的文档Web Handler Cancellation中有所描述。

它声明在每个await调用时,如果客户端已断开连接,则可以终止处理程序。情况确实如此 - 在连接断开后的第一个await我的处理程序死了。解决方案也在文档中提供,我决定使用asyncio.shield