Search code examples
python-3.xwebsocketserverclienttornado

Tornado websocket server and websocket client concurrently in one loop with AsyncIO


I want to run a tornado websocket server together with a separate websocket client concurrently in the same loop so that I can receive data from that single websocket client and send it from the tornado server out to all connected clients.

I can run the tornado server and I can run the websocket client, but the tornado server is not responding to a client request. I get something like "waiting for response from 127.0.0.1:8000".

I guess I'm having trouble with the asynchrony. I assume that my websocket client is blocking the whole process...

Does anyone have an idea?

Thanks!

Tornado Server:

import os.path
import tornado.web
import tornado.websocket
import tornado.httpserver
import asyncio
from ws_client import WebsocketClient


URL = "ws://echo.websocket.org"
tornado_connections = set()
ws_echo = None

class Application(tornado.web.Application): 
   def __init__(self):
       handlers = [
           (r"/", IndexHandler),
           (r"/ws", WsHandler)
           ]
       settings = dict(
           template_path=os.path.join(os.path.dirname(__file__), "template"), 
           static_path=os.path.join(os.path.dirname(__file__), "static"), 
       )
       tornado.web.Application.__init__(self, handlers, **settings)

class IndexHandler(tornado.web.RequestHandler): 
   def get(self):
       self.render("index_test.html")

class WsHandler(tornado.websocket.WebSocketHandler): 
   async def open(self):
       if self not in tornado_connections:
           await tornado_connections.add(self)
           await ws_echo.update_connections(connections=tornado_connections)
           print('TORNADO: client connected.')

   def on_message(self, message): 
       print(message)

   def on_close(self):
       if self in tornado_connections:
           tornado_connections.remove(self)
           print('TORNADO: client disconnected.')


async def start_tornado_server():
   app = Application()
   server = tornado.httpserver.HTTPServer(app) 
   server.listen(8000)

async def start_ws_client():
   ws_echo = WebsocketClient(url=URL, connections=tornado_connections)
   await ws_echo.connect()

async def main():
   await start_tornado_server()
   asyncio.create_task(start_ws_client())

asyncio.run(main())

Websocket Client:

import websocket
import asyncio


class WebsocketClient:
    def __init__(self, url, connections):
        self.url = url
        self.connections = connections

    def __on_open(self):
        print('Echo client connected')
        self.ws.send("Websocket rocks!")

    def __on_message(self, msg):
        print("on_messaeg: ", msg)

    def __on_close(self):
        print("Websocket closed")

    async def connect(self):
        self.ws = websocket.WebSocketApp(
            self.url,
            on_open=self.__on_open,
            on_message=self.__on_message,
            on_close=self.__on_close,
        )
        await self.ws.run_forever()

    async def disconnect(self):
        await self.ws.close()

    async def update_connections(self, connections):
        self.connections = connections
        await print("connections: ", len(self.connections))

JavaScript Websocket Client:

var ws = new WebSocket("ws://127.0.0.1:8000/ws");

ws.onopen = function () {
    ws.send("Client CONNECTED");
};

ws.onmessage = function (evt) {
    document.getElementById("p1").innerHTML = evt.data;
};

ws.onclose = function () {
    console.log("Client DISCONNECTED");
};

Solution

  • The websocket-client library (or at least the run_forever method you're using) is synchronous and cannot be combined with asyncio except by running it in its own thread.

    Instead, you need an asynchronous websocket client implementation, such as Tornado's websocket_connect (or aiohttp's ws_connect)