Python3 and asyncio: how to implement websocket server as asyncio instance? - python

I have multiple servers, each server is instance returning by asyncio.start_server. I need my web_server to works with websockets, to have possibility getting data using my javascript client. As I can see, asyncio do not provide websockets, only tcp sockets. Maybe I missed something ? I want to implement websocket server that I can using in asyncio.gather like below:
loop = asyncio.get_event_loop()
login_server = LoginServer.create()
world_server = WorldServer.create()
web_server = WebServer.create()
loop.run_until_complete(
asyncio.gather(
login_server.get_instance(),
world_server.get_instance(),
web_server.get_instance()
)
)
try:
loop.run_forever()
except KeyboardInterrupt:
pass
loop.close()
I do not want to use aiohttp cause if using like in code above aiohttp just blocks another tasks. I need something that will be non-blocking and that will have access to data of another servers (login and world). Does it possible with asyncio ? Does asyncio provide something like websockets ? How to implement websocket server for using in asyncio.gather ?

Well, finally I've implemented WebServer for using in another thread with asyncio. The code (WebServer code):
from aiohttp import web
class WebServer(BaseServer):
def __init__(self, host, port):
super().__init__(host, port)
#staticmethod
async def handle_connection(self, request: web.web_request):
ws = web.WebSocketResponse()
await ws.prepare(request)
async for msg in ws:
Logger.debug('[Web Server]: {}'.format(msg))
return ws
#staticmethod
def run():
app = web.Application()
web.run_app(app, host=Connection.WEB_SERVER_HOST.value, port=Connection.WEB_SERVER_PORT.value)
And how to run:
executor = ProcessPoolExecutor()
loop.run_until_complete(
asyncio.gather(
login_server.get_instance(),
world_server.get_instance(),
loop.run_in_executor(executor, WebServer.run)
)
)

Related

asyncio loop blocking django http and websocket requests

i've been trying to build application where i need to listen to the postgres notify listen constantly and send those messages via websocket and also need the usual django apis, i'm able to run both separately, but as initiate the db listener it starts blocking http or websocket requests
connection = get_db_conn()
connection.set_isolation_level(psycopg2.extensions.ISOLATION_LEVEL_AUTOCOMMIT)
cur = connection.cursor()
cur.execute("LISTEN new_item_added;")
async def db_listen():
print('Started Listening to DB notify ...')
while True:
await asyncio.sleep(1)
data = await queue.get()
await NotificationConsumer.send_data(data.payload) # class method to send over websocket
print("message received: ", data.payload)
def listen_callback():
connection.poll()
queue.put_nowait(connection.notifies.pop(0))
# calling this function from django app's __init__.py file
def initiate_db_listener():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.add_reader(connection, listen_callback)
loop.run_until_complete(db_listen())
if __name__ == '__main__':
initiate_db_listener()
i tried many variations of it, all are blocking the main thread

How to iterate over a loop to make multiple client connections with socket server async

Here I have the async code in python-socketio from the docs https://python-socketio.readthedocs.io/
I'm able to establish the single connection between client and server and emit few messages. but i just want to create more number of clients connected to socket server and parallelly wants to observe the performance of the emit messages.
client.py
import random
import gevent
import time
import socketio
import asyncio
class ConnectionTest():
def __init__(self):
self.sio = socketio.AsyncClient(handle_sigint=True, logger=True, engineio_logger=True)
async def listen_to_redis(self):
await self.sio.connect('http://dfs.confec.co.in/?socket_qa2=True&session_id='+str(random.randint(1,100000)))
#self.sio.on('custom_event')
async def on_custom_event(data):
await self.sio.emit('message', '27 July 2015) was an Indian aerospace scientist')
#self.sio.on('client')
async def on_client(data):
pass
async def run(self):
await self.listen_to_redis()
async def main():
async def iterations(i):
print("connection :",i)
obj1 = ConnectionTest()
await obj1.run()
await obj1.sio.wait()
x = [iterations(i) for i in range(2000)]
await asyncio.gather(*x)
if __name__ == '__main__':
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(main())
Server.py
import uvicorn
import asyncio
import socketio
import aiohttp
url='redis://my_master:q2_gadse#10.1.7.4:6379/0'
channel='public_chat'
mgr = socketio.AsyncRedisManager(url=url, channel=channel, write_only=False)
sio = socketio.AsyncServer(async_mode='aiohttp',client_manager=mgr, async_handlers=True, logger=True, engineio_logger=True)
app = aiohttp.web.Application()
sio.attach(app)
#sio.on('connect')
async def test_connect(sid, environ):
print('connected', sid)
#sio.event
async def message(sid, message):
await sio.emit('client', message)
#sio.on('disconnect')
async def test_disconnect(sid):
print('Client disconnected')
if __name__=='__main__':
aiohttp.web.run_app(app, host='0.0.0.0', port=31451)
Update:
I'm able to get the performance for multiple asyncclient objects and i updated the code as well but I m not sure of why the cpu is 100% all the time with any of the deployment strategies. My target is to get atleast 5000 connections with a single processor and earlier my target was 10,000.
Hardware which I have is 4GB ram with 2 cores and 4 cores. I'm running client and server in different linux nodes. I see the client is quickly hitting the 100-200%cpu and server is somehow managing as I was using redis as a middleware to emit from outside script.
I have gone through the python-socketio document but I have not seen the section for performance tuning/Load testing/Stress like socket.io in nodejs. Here's the stats that I would like to show and how can i achieve the connections with atleast 5000 per process. Is there any code changes i could make for it. I need valuable suggestion as I was struck with it.
stress stats

Call async functions in blocking context in Tornado

I want to implement a service based on web sockets in the Tornado framework. When a user closes a web socket, I want to notify the other users about this. However, on_close is apparently a blocking function and my _broadcast(str) -> None function is async.
How can I call this function anyway?
from tornado import websocket
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class SocketHandler(websocket.WebSocketHandler):
async def open(self, *args, conns, **kwargs):
logger.info(f"Opened a new connection to client {id(self)}")
self._conns = conns
async def on_message(self, message):
logger.info(f"Client {id(self)} sent message: {message}")
await self._broadcast(message)
def on_close(self):
logger.info(f"Client {id(self)} has left the scene")
self._conns.remove(self)
self._broadcast("something") # TODO
async def _broadcast(self, msg):
for conn in self._conns:
try:
await conn.write_message(msg)
except websocket.WebSocketClosedError:
pass
app = web.Application([
(r'/ws', SocketHandler)
])
if __name__ == '__main__':
app.listen(9000)
ioloop.IOLoop.instance().start()
The simple solution you're looking for is to use asyncio.create_task when calling the coroutine:
def on_close(self):
logger.info(f"Client {id(self)} has left the scene")
self._conns.remove(self)
asyncio.create_task(self._broadcast("something"))
(the legacy Tornado version of this function is tornado.gen.convert_yielded, but now that Tornado and asyncio are integrated there's no reason not to use the asyncio version for native coroutines)
But for this particular problem, the use of await in your _broadcast function is not ideal. Awaiting a write_message is used to provide flow control, but create_task doesn't do anything useful with the backpressure provided by await. (write_message is fairly unusual in that it is fully supported to call it both with and without await). In fact, it applies backpressure to the wrong things - one slow connection will slow notifications to all the others that come after it.
So in this case I'd advise making _broadcast a regular synchronous function:
def _broadcast(self, msg):
for conn in self._conns:
try:
conn.write_message(msg)
except websocket.WebSocketClosedError:
pass
If you want to be better able to control memory usage (via the flow control provided by await write_message), you'll need a more complicated solution, probably involving a bounded queue for each connection (in on_close, use put_nowait to add the message to every connection's queue, then have a task that reads from the queue and writes the message with await write_message)
i think a solution that involves using an asyncio.Queue should work for you.
i made a small class as a mock-up to test this out:
import asyncio
import time
class Thing:
on_close_q = asyncio.Queue()
def __init__(self):
self.conns = range(3)
def on_close(self, id):
time.sleep(id)
print(f'closing {id}')
self.on_close_q.put_nowait((self, id))
async def notify(self, msg):
print('in notify')
for conn in range(3):
print(f'notifying {conn} {msg}')
async def monitor_on_close():
print('monitoring')
while True:
instance, id = await Thing.on_close_q.get()
await instance.notify(f'{id} is closed')
from there, you'll need to run monitor_on_close in the ioloop you get from tornado. i've never used tornado, but i think adding something like this to your __main__ block should work:
ioloop.IOLoop.current().add_callback(monitor_on_close)

Best way to share data with a producer coroutine loop from the python websockets package?

I want to set up a websocket server in python (using this websocket library) to send data that is generated through a "produce_msg" function. This function does some intense calculations, produces a message and should trigger the sending via websockets.
What is the best way to share the data with the producer coroutine loop and trigger the sending?
I think calling the "produce_msg" function inside the producer coroutine is possible. But can I also call a send function from inside the "producer_msg function? I want to use a websocket class to provide the send functionality to users of this class.
Browser-based example with producer coroutine loop from websocket library website:
import asyncio
import websockets
async def producer_handler(websocket, path):
while True:
message = await produce_msg()
await websocket.send(message)
start_server = websockets.serve(producer_handler, "127.0.0.1", 5678)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
Update
I managed to build a version that works using a callback function. In my example I use produce_msg() as callback function.
Till now I did not manage to build a version in which the Websocket class has a send_msg() function that can be called to send a message (instead of a callback function).
import asyncio
import websockets
import datetime
import random
import time
class Websocket:
def __init__(self, callback, ip="127.0.0.1", port=5678):
self.callback = callback
self.ip = ip
self.port = port
self.start()
def start(self):
start_server = websockets.serve(self.handler, self.ip, self.port)
asyncio.get_event_loop().run_until_complete(start_server)
asyncio.get_event_loop().run_forever()
async def handler(self, websocket, path):
while True:
msg = self.callback()
await websocket.send(msg)
if __name__ == "__main__":
def produce_msg():
time.sleep(2.4)
return datetime.datetime.utcnow().isoformat() + "Z"
ws = Websocket(produce_msg)

listen to multiple socket with websockets and asyncio

I am trying to create a script in python that listens to multiple sockets using websockets and asyncio, the problem is that no matter what I do it only listen to the first socket I call.
I think its the infinite loop, what are my option to solve this? using threads for each sockets?
async def start_socket(self, event):
payload = json.dumps(event)
loop = asyncio.get_event_loop()
self.tasks.append(loop.create_task(
self.subscribe(event)))
# this should not block the rest of the code
await asyncio.gather(*tasks)
def test(self):
# I want to be able to add corotines at a different time
self.start_socket(event1)
# some code
self.start_socket(event2)
this is what I did eventually, that way its not blocking the main thread and all subscriptions are working in parallel.
def subscribe(self, payload):
ws = websocket.WebSocket(sslopt={"cert_reqs": ssl.CERT_NONE})
ws.connect(url)
ws.send(payload)
while True:
result = ws.recv()
print("Received '%s'" % result)
def start_thread(self, loop):
asyncio.set_event_loop(loop)
loop.run_forever()
def start_socket(self, **kwargs):
worker_loop = asyncio.new_event_loop()
worker = Thread(target=self.start_thread, args=(worker_loop,))
worker.start()
worker_loop.call_soon_threadsafe(self.subscribe, payload)
def listen(self):
self.start_socket(payload1)
# code
self.start_socket(payload2)
# code
self.start_socket(payload3)
Your code appears incomplete, but what you've shown has two issues. One is that run_until_complete accepts a coroutine object (or other kind of future), not a coroutine function. So it should be:
# note parentheses after your_async_function()
asyncio.get_event_loop().run_until_complete(your_async_function())
the problem is that no matter what I do it only listen to the first socket I call. I think its the infinite loop, what are my option to solve this? using threads for each sockets?
The infinite loop is not the problem, asyncio is designed to support such "infinite loops". The problem is that you are trying to do everything in one coroutine, whereas you should be creating one coroutine per websocket. This is not a problem, as coroutines are very lightweight.
For example (untested):
async def subscribe_all(self, payload):
loop = asyncio.get_event_loop()
# create a task for each URL
for url in url_list:
tasks.append(loop.create_task(self.subscribe_one(url, payload)))
# run all tasks in parallel
await asyncio.gather(*tasks)
async def subsribe_one(self, url, payload):
async with websockets.connect(url) as websocket:
await websocket.send(payload)
while True:
msg = await websocket.recv()
print(msg)
One way to efficiently listen to multiple websocket connections from a websocket server is to keep a list of connected clients and essentially juggle multiple conversations in parallel.
E.g. A simple server that sends random # to each connected client every few secs:
import os
import asyncio
import websockets
import random
websocket_clients = set()
async def handle_socket_connection(websocket, path):
"""Handles the whole lifecycle of each client's websocket connection."""
websocket_clients.add(websocket)
print(f'New connection from: {websocket.remote_address} ({len(websocket_clients)} total)')
try:
# This loop will keep listening on the socket until its closed.
async for raw_message in websocket:
print(f'Got: [{raw_message}] from socket [{id(websocket)}]')
except websockets.exceptions.ConnectionClosedError as cce:
pass
finally:
print(f'Disconnected from socket [{id(websocket)}]...')
websocket_clients.remove(websocket)
async def broadcast_random_number(loop):
"""Keeps sending a random # to each connected websocket client"""
while True:
for c in websocket_clients:
num = str(random.randint(10, 99))
print(f'Sending [{num}] to socket [{id(c)}]')
await c.send(num)
await asyncio.sleep(2)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
try:
socket_server = websockets.serve(handle_socket_connection, 'localhost', 6789)
print(f'Started socket server: {socket_server} ...')
loop.run_until_complete(socket_server)
loop.run_until_complete(broadcast_random_number(loop))
loop.run_forever()
finally:
loop.close()
print(f"Successfully shutdown [{loop}].")
A simple client that connects to the server and listens for the numbers:
import asyncio
import random
import websockets
async def handle_message():
uri = "ws://localhost:6789"
async with websockets.connect(uri) as websocket:
msg = 'Please send me a number...'
print(f'Sending [{msg}] to [{websocket}]')
await websocket.send(msg)
while True:
got_back = await websocket.recv()
print(f"Got: {got_back}")
asyncio.get_event_loop().run_until_complete(handle_message())
Mixing up threads and asyncio is more trouble than its worth and you still have code that will block on the most wasteful steps like network IO (which is the essential benefit of using asyncio).
You need to run each coroutine asynchronously in an event loop, call any blocking calls with await and define each method that interacts with any awaitable interactions with an async
See a working e.g.: https://github.com/adnantium/websocket_client_server

Categories