Search code examples
pythonpython-3.xwebsockettornado

How to make a Tornado websocket client recieve server notifications?


I'm trying to make an application that normally sends a request to a server and receives a response. If it would have been only that, I'd go with HTTP and call it a deal. But some requests to the server make a change for other clients, so I want the server to send all the affected clients a message that they should update.
For that, I've chosen WebSockets protocol and the Tornado library to work with it using Python. The simple message exchange was pretty straightforward, despite the asynchrony. However, the WebSocket client is really not that configurable and I've been struggling to make a client listen for incoming notifications without this interrupting the main message exchange.

The server part is represented by the tornado.websocket.WebSocketHandler, which has an on_message method:

from tornado.websocket import WebSocketHandler

class MyHandler(WebSocketHandler):
    def on_message(self, message):
        print('message:', message)

And I'd like something like that in the client part, which is only represented by a function tornado.websocket.websocket_connect (source). This function initiates a tornado.websocket.WebSocketClientConnection (source) object, which has an on_message method, but due to the entangled asynchronous structure, I haven't been able to override it properly, without breaking the main message exchange.

Another way I tried to go was the on_message_callback. This sounded like something I could use, but I couldn't figure out how to use it with read_message. This was my best attempt:

import tornado.websocket
import tornado.ioloop

ioloop = tornado.ioloop.IOLoop.current()

def clbk(message):
    print('received', message)

async def main():
    url = 'server_url_here'
    conn = await tornado.websocket.websocket_connect(url, io_loop = ioloop, on_message_callback=clbk)
    while True:
        print(await conn.read_message())  # The execution hangs here
        st = input()
        conn.write_message(st)

ioloop.run_sync(main)

With this being the server code:

import tornado.ioloop
import tornado.web
import tornado.websocket
import os


class EchoWebSocket(tornado.websocket.WebSocketHandler):
    def open(self):
        self.write_message('hello')

    def on_message(self, message):
        self.write_message(message)
        self.write_message('notification')

if __name__ == "__main__":
    app = tornado.web.Application([(r"/", EchoWebSocket)])
    app.listen(os.getenv('PORT', 8080))
    tornado.ioloop.IOLoop.current().start()

I don't know what's going on here. Am I even going in the right direction with this?


Solution

  • There are two issues here:

    1. Use on_message_callback or loop on await read_message(), but not both. If you give a callback the messages will only be passed to that callback and not saved for use by read_message.
    2. input is blocking and doesn't play well with Tornado. It's fine in this little toy demo but if you want to do something like this in production you'll probably want to do something like wrap a PipeIOStream around sys.stdin and use stream.read_until('\n').