Search code examples
pythonwebsockettornado

where do i add code in tornado websocket server?


I just jump into websocket programing with basic knowledge of "Asynchronous" and "Threads", i have something like this

import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web
import socket
import uuid
import json
import datetime

class WSHandler(tornado.websocket.WebSocketHandler):
    clients = []

    def open(self):
        self.id = str(uuid.uuid4())
        self.user_info = self.request.remote_ip +' - '+ self.id
        print (f'[{self.user_info}] Conectado')

        client = {"sess": self, "id" : self.id}
        self.clients.append(client.copy())
      
    def on_message(self, message):
        print (f'[{self.user_info}] Mensaje Recivido: {message}')
        print (f'[{self.user_info}] Respuesta al Cliente: {message[::-1]}')
        self.write_message(message[::-1])
        self.comm(message)
 
    def on_close(self):
        print (f'[{self.user_info}] Desconectado')
        for x in self.clients:
            if x["id"] == self.id :
                self.clients.remove(x)

    def check_origin(self, origin):
        return True

application = tornado.web.Application([
    (r'/', WSHandler),
])
 
 
if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(80)
    myIP = socket.gethostbyname(socket.gethostname())
    print ('*** Websocket Server Started at %s***' % myIP)
    tornado.ioloop.IOLoop.instance().start()

my question is where do I add code ?, should I add everything inside the WShandler class, or outside, or in another file ? and when to use @classmethod?. for now there is no problem with the code when i add code inside the handler but i have just few test clients.


Solution

  • maybe not the full solution but just a few thoughts..

    You can maybe look at the tornado websocket chat example, here.

    First good change is, that their clients (waiters) is a set() which makes sure that every client is only contained once by default. And it is defined and accessed as a class variable. So you don't use self.waiters but cls.waiters or ClassName.waiters (in this case ChatSocketHandler.waiters) to access it.

    class ChatSocketHandler(tornado.websocket.WebSocketHandler):
        waiters = set()
    

    Second change is that they update every client (you could choose here to send the update not to all but only some) as a @classmethod, since they dont want to receive the instance (self) but the class (cls) and refer to the class variables (in their case waiters, cache and cach_size)

    We can forget about the cache and cache size here.

    So like this:

    @classmethod
        def send_updates(cls, chat):
            logging.info("sending message to %d waiters", len(cls.waiters))
            for waiter in cls.waiters:
                try:
                    waiter.write_message(chat)
                except:
                    logging.error("Error sending message", exc_info=True)
    

    On every API call a new instance of your handler will be created, refered to as self. And every parameter in self is really unique to the instance and related to the actual client, calling your methods. This is good to identify a client on each call. So a instance based client list like (self.clients) would always be empty on each call. And adding a client would only add it to this instance's view of the world.

    But sometimes you want to have some variables like the list of clients the same for all instances created from your class. This is where class variables (the ones you define directly under the class definition) and the @classmethod decorator come into play.

    @classmethod makes the method call independant from the a instance. This means that you can only access class variables in those methods. But in the case of a message broker this is pretty much what we want:

    • add clients to the class variable which is the same for all instances of your handler. And since it is defined as a set, each client is unique.

    • when receiving messages, send them out to all (or a subset of clients)

    • so on_message is a "normal" instance method but it calls something like: send_updates() which is a @classmethod in the end.

    • send_updates() iterates over all (or a subset) of clients (waiters) and uses this to send the actual updates in the end.

    From the example:

    @classmethod
        def send_updates(cls, chat):
            logging.info("sending message to %d waiters", len(cls.waiters))
            for waiter in cls.waiters:
                try:
                    waiter.write_message(chat)
                except:
                    logging.error("Error sending message", exc_info=True)
    

    Remember that you added waiters with waiters.append(self) so every waiter is really an instance and you are "simply" calling the instances (the instance is representing a caller) write_message() method. So this is not broadcasted but send to every caller one by one. This would be the place where you can separate by some criteria like topics or groups ...

    So in short: use @classmethod for methods that are independant from a specific instance (like caller or client in your case) and you want to make actions for "all" or a subset of "all" of your clients. But you can only access class variables in those methods. Which should be fine since it's their purpose ;)