diff --git a/docs/faq.rst b/docs/faq.rst index 7bc110abee9..78fcc1365cf 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -129,3 +129,59 @@ peer:: ws.send_str(answer) finally: await redis.unsubscribe('channel:1') + + +.. _aiohttp_faq_terminating_websockets: + +How to programmatically close websocket server-side? +---------------------------------------------------- + + +For example we have an application with two endpoints: + + + 1. ``/echo`` a websocket echo server that authenticates the user somehow + 2. ``/logout_user`` that when invoked needs to close all open websockets for that user. + +Keep in mind that you can only ``.close()`` a websocket from inside the handler task, and since the handler task +is busy reading from the websocket, it can't react to other events. + +One simple solution is keeping a shared registry of websocket handler tasks for a user +in the :class:`aiohttp.web.Application` instance and ``cancel()`` them in ``/logout_user`` handler:: + + async def echo_handler(request): + + ws = web.WebSocketResponse() + user_id = authenticate_user(request) + await ws.prepare(request) + request.app['handlers'][user_id].add(asyncio.Task.current_task()) + + try: + async for msg in ws: + # handle incoming messages + ... + + except asyncio.CancelledError: + print('websocket cancelled') + finally: + request.app['handlers'][user_id].remove(asyncio.Task.current_task()) + await ws.close() + return ws + + async def logout_handler(request): + + user_id = authenticate_user(request) + + for task in request.app['handlers'][user_id]: + task.cancel() + + # return response + ... + + def main(): + loop = asyncio.get_event_loop() + app = aiohttp.web.Application(loop=loop) + app.router.add_route('GET', '/echo', echo_handler) + app.router.add_route('POST', '/logout', logout_handler) + app['websockets'] = defaultdict(set) + aiohttp.web.run_app(app, host='localhost', port=8080) diff --git a/docs/web.rst b/docs/web.rst index eaeb53b6991..fd10fcb335c 100644 --- a/docs/web.rst +++ b/docs/web.rst @@ -536,9 +536,13 @@ with the peer:: return ws -Reading from the *WebSocket* (``await ws.receive()``) **must only** be -done inside the request handler *task*; however, writing -(``ws.send_str(...)``) to the *WebSocket* may be delegated to other tasks. +.. _aiohttp-web-websocket-read-same-task: + +Reading from the *WebSocket* (``await ws.receive()``) and closing it (``await ws.close()``) +**must only** be done inside the request handler *task*; however, writing +(``ws.send_str(...)``) to the *WebSocket* and cancelling the handler task +may be delegated to other tasks. See also :ref:`FAQ section `. + *aiohttp.web* creates an implicit :class:`asyncio.Task` for handling every incoming request. @@ -556,6 +560,7 @@ incoming request. Parallel reads from websocket are forbidden, there is no possibility to call :meth:`aiohttp.web.WebSocketResponse.receive` + or :meth:`aiohttp.web.WebSocketResponse.close` from two tasks. See :ref:`FAQ section ` for