I have the following Python websocket server. It gets some usage data from my server and then sends it through a websocket connection:
#!/usr/bin/env python3
import json
import asyncio
import websockets
import websockets.server
import data_parser as dp
async def handler(websocket: websockets.server.WebSocketServerProtocol):
"""
Handles data connection and dispatches necessary data
"""
# Receive and parse the amount of days wanted
message = await websocket.recv()
json_message = json.loads(message)
days = json_message["days"]
for i, data in enumerate(dp.get_upto_nth_usage_data(days)):
await websocket.send(json.dumps(data, separators=(",", ":")))
to_send = {"finished": 1}
await websocket.send(json.dumps(to_send, separators=(",", ":")))
async def main():
async with websockets.serve(handler, "", 8001):
await asyncio.Future() # run forever
if __name__ == "__main__":
asyncio.run(main())
The problem is that I have to send large chunks of data(inside the for loop) over each websocket connection and when a new user tries to make a connection, he has to wait for the last user to receive all his data to be able to begin receiving his own. Is there a way to open a new port to handle new websocket connections or should I use another kind of protocol to send data (and if so, which would be the best choice)?
Related
I am writing a script to receive fmp4 data from a WebSocket. After a few seconds, the connection seemingly drops and if I reconnect, there is a noticeable lag-spike in the video file.
here's my driver code:
import websockets
import asyncio
async def listen():
url = URL
while True:
async with websockets.connect(url) as ws:
try:
while True:
msg = await ws.recv()
print(msg)
except Exception:
print(f"Connection Dropped, reconnecting...")
await ws.close()
asyncio.run(listen())
I've tried switching up the ping interval, removing the keepalive ping, and reducing the read size, but all to no avail. How is this sort of problem mitigated?
I am running two loops asynchronously, and want both to have access to the same websocket connection. One function periodic_fetch() fetches some data periodically (every 60 seconds) and sends a message to the websocket if a condition is met. The other retrieve_websocket() receives messages from the websocket and perform some action if a condition is met. As of now, I connect to the websocket in both functions, but that means retrieve_websocket() will not receive the response to the websocket message sent by periodic_fetch(). How do I create one websocket connection and use the same one in both loops as they run asynchronously? My code:
# Imports
import asyncio
import websockets
from datetime import datetime
websocket_url = "wss://localhost:5000/"
# Simulate fetching some data
async def fetch_data():
print("Fetching started")
await asyncio.sleep(2)
return {"data": 2}
# Receive and analyze websocket data
async def retrieve_websocket():
async with websockets.connect(websocket_url) as ws:
while True:
msg = await ws.recv()
print(msg)
# Perform some task if condition is met
# Periodically fetch data and send messages to websocket
async def periodic_fetch():
async with websockets.connect(websocket_url) as ws:
while True:
print(datetime.now())
fetch_task = asyncio.create_task(fetch_data())
wait_task = asyncio.create_task(asyncio.sleep(60))
res = await fetch_task
# Send message to websocket
await ws.send("Websocket message")
# Wait the remaining wait duration
await wait_task
loop = asyncio.get_event_loop()
cors = asyncio.wait([periodic_fetch(), retrieve_websocket()])
loop.run_until_complete(cors)
The solution was to open the connection in a separate function and use asyncio.gather() passing in the two functions with the websocket as parameter.
async def run_script():
async with websockets.connect(websocket_url) as ws:
await asyncio.gather(periodic_fetch(ws), retrieve_websocket(ws))
asyncio.run(run_script())
In Python, I'm using "websockets" library for websocket client.
import asyncio
import websockets
async def init_sma_ws():
uri = "wss://echo.websocket.org/"
async with websockets.connect(uri) as websocket:
name = input("What's your name? ")
await websocket.send('name')
greeting = await websocket.recv()
The problem is the client websocket connection is disconnected once a response is received. I want the connection to remain open so that I can send and receive messages later.
What changes do I need to keep the websocket open and be able to send and receive messages later?
I think your websocket is disconnected due to exit from context manager after recv().
Such code works perfectly:
import asyncio
import websockets
async def init_sma_ws():
uri = "wss://echo.websocket.org/"
async with websockets.connect(uri) as websocket:
while True:
name = input("What's your name? ")
if name == 'exit':
break
await websocket.send(name)
print('Response:', await websocket.recv())
asyncio.run(init_sma_ws())
In your approach you used a asynchronous context manager which closes a connection when code in the block is executed. In the example below an infinite asynchronous iterator is used which keeps the connection open.
import asyncio
import websockets
async def main():
async for websocket in websockets.connect(...):
try:
...
except websockets.ConnectionClosed:
continue
asyncio.run(main())
More info in library's docs.
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
Recently I've gotten into the "crypto mania" and have started writing my own wrappers around the API's on some exchanges.
Binance in particular has an a streaming websocket endpoint.
where you can stream data but via a websocket endpoint.
I thought I'd try this out on my own using sanic.
here is my websocket route
#ws_routes.websocket("/hello")
async def hello(request, ws):
while True:
await ws.send("hello")
now I have 2 clients on 2 different machines connecting to it
async def main():
async with aiohttp.ClientSession() as session:
ws = await session.ws_connect("ws://192.168.86.31:8000/hello")
while True:
data = await ws.receive()
print(data)
however only one of the clients will be able to connect and receive the sent data from the server. I'm assuming that because of the while loop its blocking and preventing the other connection from connecting because it doesn't yield?
how do we make it stream to multiple clients without blocking the other connections?
I looked into adding more workers and it seems to do the trick but what I don't understand is thats not a very scalable solution. because each client would be its own worker and if you have thousands or even just 10 clients that would be 10 workers 1 per client.
so how does Binance do their websocket streaming? or hell how does the twitter stream endpoint work?
how is it able to serve an infinite stream to multiple concurrent clients?
because ultimately thats what I'm trying to do
The way to solve this would be something like this.
I am using the sanic framework
class Stream:
def __init__(self):
self._connected_clients = set()
async def __call__(self, *args, **kwargs):
await self.stream(*args, **kwargs)
async def stream(self, request, ws):
self._connected_clients.add(ws)
while True:
disconnected_clients = []
for client in self._connected_clients: # check for disconnected clients
if client.state == 3: # append to a list because error will be raised if removed from set while iterating over it
disconnected_clients.append(client)
for client in disconnected_clients: # remove disconnected clients
self._connected_clients.remove(client)
await asyncio.wait([client.send("Hello") for client in self._connected_clients]))
ws_routes.add_websocket_route(Stream(), "/stream")
keep track of each websocket session
append to a list or set
check for invalid websocket sessions and remove from your websocket sessions container
do an await asyncio.wait([ws_session.send() for ws_session [list of valid sessions]]) which is basically a broadcast.
5.profit!
this is basically the pubsub design pattern
Something like this maybe?
import aiohttp
import asyncio
loop = asyncio.get_event_loop()
async def main():
async with aiohttp.ClientSession() as session:
ws = await session.ws_connect("ws://192.168.86.31:8000/hello")
while True:
data = await ws.receive()
print(data)
multiple_coroutines = [main() for _ in range(10)]
loop.run_until_complete(asyncio.gather(*multiple_coroutines))