0

I have a server running a loop that reads data from a device and I want to send them to all clients who connect on a websocket on tornado. I tried putting the loop inside the open function but then it can't handle on_close function or new connections.

What is best practice to do that?

#!/usr/bin/env python

import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web
import socket

class MyWebSocketServer(tornado.websocket.WebSocketHandler):
    def open(self):
        print('new connection'+self.request.remote_ip)
        try:
            while True:
                '''
                read and send data
                '''
        except Exception,error:
            print "Error on Main: "+str(error)

    def on_close(self):
        print('connection closed'+self.request.remote_ip)

application=tornado.web.Application([(r'/ws',MyWebSocketServer),])

if __name__=="__main__":
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(8000)
    print('start')
    tornado.ioloop.IOLoop.instance().start()

Thanks

Miky
  • 181
  • 1
  • 5
  • 15
  • 1
    You do not need to `while read`, but add another function `on_message`. Check the official doc: http://www.tornadoweb.org/en/stable/websocket.html#tornado.websocket.WebSocketHandler.on_message – Sraw May 11 '18 at 02:34
  • I know the on_message function, but in the while loop I read data from a device and I need to send them via websocket. – Miky May 11 '18 at 06:16
  • How are you reading data in the `while` loop? Seems like that code is blocking the server. – xyres May 11 '18 at 09:09
  • In the while loop there's code that continuously read data from a device connected via USB to serial IC. I need the while loop because of I need to get this data at a rate of about 50Hz – Miky May 11 '18 at 09:20
  • 1
    @Miky Yeah, the code inside the `while` loop is blocking the server, *i.e.* when it starts running, nothing else can run. Try running your `while` loop in a separate thread using `ThreadPoolExecutor`. – xyres May 11 '18 at 12:31
  • @xyres can you give me an example? and what about [**Process**](https://stackoverflow.com/questions/3474382/how-do-i-run-two-python-loops-concurrently)? what are the differences? – Miky May 11 '18 at 13:35
  • @Miky `multiprocessing.Process` will create a separate Python **process** to run your code in. Too much overhead if you ask me. Running your code in a separate **thread** will do the job. Also, the preferred way to spawn threads or processes when using Tornado (or asyncio) is by using [`ThreadPoolExecutor`](https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor) or [`ProcessPoolExecutor`](https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor). These functions make things easier than using `multiprocessing.Process` or `threading.Thread` directly. – xyres May 11 '18 at 21:51

1 Answers1

0

Here's a full example about running your blocking code in a separate thread and broadcasting messages to all connected clients.

...

from concurrent.futures import ThreadPoolExecutor

executor = ThreadPoolExecutor(max_workers=1) # spawn only 1 thread


class MyWebSocketServer(tornado.websocket.WebSocketHandler):
    connections = set() # create a set to hold connections

    def open(self):
        # put the new connection in connections set
        self.connections.add(self)

    def on_close(self):
        print('connection closed'+self.request.remote_ip)
        print('new connection'+self.request.remote_ip)
        # remove client from connections
        self.connections.remove(self)

    @classmethod
    def send_message(cls, msg):
        for client in cls.connections:
            client.write_message(msg)


def read_from_serial(loop, msg_callback):
    """This function will read from serial 
    and will run in aseparate thread

    `loop` is the IOLoop instance
    `msg_allback` is the function that will be 
    called when new data is available from usb
    """
    while True:
        # your code ...
        # ...
        # when you get new data
        # tell the IOLoop to schedule `msg_callback`
        # to send the data to all clients

        data = "new data"
        loop.add_callback(msg_callback, data)

...

if __name__ == '__main__':
    loop = tornado.ioloop.IOLoop.current()

    msg_callback = MyWebSocketServer.send_message

    # run `read_from_serial` in another thread
    executor.submit(read_from_serial, loop, msg_callback)

    ...

    loop.start()
xyres
  • 20,487
  • 3
  • 56
  • 85
  • a couple of questions: **1 )** why do you loop `connections` instead of using `self.write_message(msg)`? **2 )** how can I keyboardinterrupt the thread? – Miky May 11 '18 at 23:59
  • @Miky **1)** If you just do `self.write_message`, it will only write the message to the current client. In your question you said you wanted to send the message to all the connected clients, so that's why I looped through `connections`. **2)** If you KeyboardInterrupt, the whole process will stop - Tornado server as well as the thread `executor`. If you just want to stop the thread `executor`, you can catch the `KeyboardInterrupt` exception and use `executor.shutdown()` and it will stop the the thread. – xyres May 12 '18 at 08:27
  • and if I want to add a thread, e.g. to monitor cpu temperature periodically? – Miky May 15 '18 at 00:29
  • @Miky So add a thread. I don't understand what's confusing about it. – xyres May 15 '18 at 09:20
  • So I just have to set `max_workers=2` and add `executor.submit(monitor_cpu_temperature, loop, msg_callback)` to run the function and to be able to send data via ws right? – Miky May 15 '18 at 09:27
  • @Miky Oh, you were asking about `max_workers`. Yes, what you said is correct. – xyres May 15 '18 at 09:34