Обработка закрытых вкладок браузера python aiohttp websockets

Я пытаюсь создать простой счетчик активных пользователей, используя aiohttp WebSockets и aioredis для хранения. Когда я добавляю новую вкладку в Google Chrome, мой счетчик идеально увеличивается на всех уже открытых вкладках. Однако, когда я закрываю вкладку, в других вкладках ничего не меняется.

Я думаю, что должен что-то упустить во всем механизме async/await, но не могу найти, что может быть не так.

Вот мое приложение

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 не обнаруживает отключение от Wi-Fi, но делает это на локальном хосте

Во-первых, все мои подключения проходят через локальный хост.

Во-вторых, код после цикла 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 не появилось никаких сообщений. Здесь определенно что-то идет не так...


person Alexandr Tatarinov    schedule 13.05.2018    source источник


Ответы (1)


Кому интересна эта проблема - решение.

В этом коде есть три проблемы). Два из них на самом деле не связаны с asyncio.

Во-первых, app['websockets'] это list, а remove(ws) по какой-то причине не может найти правильный экземпляр WebSocketResponse и удаляет другой WebSocketResponse из списка. Решение состоит в том, чтобы использовать set() вместо list для хранения активных веб-сокетов. Это потому, что 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 Отмена веб-обработчика.

В нем указано, что при каждом вызове await обработчик может быть завершен, если клиент прервал соединение. Это именно тот случай - в первые await после сброса соединения мой обработчик умер. Решения также представлены в документации, я решил использовать asyncio.shield .

person Alexandr Tatarinov    schedule 14.05.2018