I'm trying to use Python's asyncio to run multiple servers together, passing data between them. For my specific case I need a web server with websockets, a UDP connection to an external device, as well as database and other interactions. I can find examples of pretty much any of these individually but I'm struggling to work out the correct way to have them run concurrently with data being pushed between them.
The closest I have found here is here: Communicate between asyncio protocol/servers (although I've been unable to make it run on Python 3.6)
For a more concrete example: How would I take the following aiohttp example code from https://github.com/aio-libs/aiohttp:
from aiohttp import web
async def handle(request):
name = request.match_info.get('name', "Anonymous")
text = "Hello, " + name
return web.Response(text=text)
async def wshandler(request):
ws = web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
if msg.type == web.MsgType.text:
await ws.send_str("Hello, {}".format(msg.data))
elif msg.type == web.MsgType.binary:
await ws.send_bytes(msg.data)
elif msg.type == web.MsgType.close:
break
return ws
app = web.Application()
app.router.add_get('/echo', wshandler)
app.router.add_get('/', handle)
app.router.add_get('/{name}', handle)
web.run_app(app)
and the following TCP echo server sample (http://asyncio.readthedocs.io/en/latest/tcp_echo.html):
import asyncio
async def handle_echo(reader, writer):
data = await reader.read(100)
message = data.decode()
addr = writer.get_extra_info('peername')
print("Received %r from %r" % (message, addr))
print("Send: %r" % message)
writer.write(data)
await writer.drain()
print("Close the client socket")
writer.close()
loop = asyncio.get_event_loop()
coro = asyncio.start_server(handle_echo, '127.0.0.1', 8888, loop=loop)
server = loop.run_until_complete(coro)
# Serve requests until Ctrl+C is pressed
print('Serving on {}'.format(server.sockets[0].getsockname()))
try:
loop.run_forever()
except KeyboardInterrupt:
pass
# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()
and combine them into a single script where any messages received via either websockets or the TCP echo server were sent out to all clients of either?
And how would I add a piece of code that (say) every second sent a message to all clients (for the sake of argument the current timestamp)?
First you need to get all of your coroutines into a single event loop. You can start by avoiding convenience APIs that start the event loop for you such as
run_app
. Instead ofweb.run_app(app)
, write something like:Then run the echo server setup, and both are ready to share the asyncio event loop. At the end of the script, start the event loop using
loop.run_forever()
(or in any other way that makes sense in your application).To broadcast information to clients, create a broadcast coroutine and add it to the event loop:
Finally, await the broadcast in each coroutine created for a client, such as
handle_echo
:It should be straightforward to modify the websockets handler coroutine to await and relay the broadcast data in the same manner.
Based on the advice of @user4815162342 this is my "working" code. I'm posting it as an answer because it is a complete working script which achieves all the requirements of my original question, but it isn't perfect as it doesn't currently exit cleanly.
When run, it will accept web connections on port 8080 and tcp (eg telnet) connections on 8081. Any messages received via its web form or telnet will be broadcast to all connections. Additionally, every 5s the time will be broadcast.
Advice on how to exit cleanly (ctrl+C with web connections established generates multiple "Task was destroyed but it is pending!" errors) would be appreciated so I can update this answer.
(The code is quite long as it contains embedded HTML and JS for the websockets component.)