Websockets with Tornado: Get access from the “outs

2019-01-22 04:45发布

问题:

I'm starting to get into WebSockets as way to push data from a server to connected clients. Since I use python to program any kind of logic, I looked at Tornado so far. The snippet below shows the most basic example one can find everywhere on the Web:

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

class WSHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        print 'new connection'
        self.write_message("Hello World")

    def on_message(self, message):
        print 'message received %s' % message
        self.write_message('ECHO: ' + message)

    def on_close(self):
    print 'connection closed'


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


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

As it is, this works as intended. However, I can't get my head around how can get this "integrated" into the rest of my application. In the example above, the WebSocket only sends something to the clients as a reply to a client's message. How can I access the WebSocket from the "outside"? For example, to notify all currently connected clients that some kind event has occured -- and this event is NOT any kind of message from a client. Ideally, I would like to write somewhere in my code something like:

websocket_server.send_to_all_clients("Good news everyone...")

How can I do this? Or do I have a complete misundersanding on how WebSockets (or Tornado) are supposed to work. Thanks!

回答1:

This is building on Hans Then's example. Hopefully it helps you understand how you can have your server initiate communication with your clients without the clients triggering the interaction.

Here's the server:

#!/usr/bin/python

import datetime
import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web



class WSHandler(tornado.websocket.WebSocketHandler):
    clients = []
    def open(self):
        print 'new connection'
        self.write_message("Hello World")
        WSHandler.clients.append(self)

    def on_message(self, message):
        print 'message received %s' % message
        self.write_message('ECHO: ' + message)

    def on_close(self):
        print 'connection closed'
        WSHandler.clients.remove(self)

    @classmethod
    def write_to_clients(cls):
        print "Writing to clients"
        for client in cls.clients:
            client.write_message("Hi there!")


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


if __name__ == "__main__":
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(8888)
    tornado.ioloop.IOLoop.instance().add_timeout(datetime.timedelta(seconds=15), WSHandler.write_to_clients)
    tornado.ioloop.IOLoop.instance().start()

I made the client list a class variable, rather than global. I actually wouldn't mind using a global variable for this, but since you were concerned about it, here's an alternative approach.

And here's a sample client:

#!/usr/bin/python

import tornado.websocket
from tornado import gen 

@gen.coroutine
def test_ws():
    client = yield tornado.websocket.websocket_connect("ws://localhost:8888/ws")
    client.write_message("Testing from client")
    msg = yield client.read_message()
    print("msg is %s" % msg)
    msg = yield client.read_message()
    print("msg is %s" % msg)
    msg = yield client.read_message()
    print("msg is %s" % msg)
    client.close()

if __name__ == "__main__":
    tornado.ioloop.IOLoop.instance().run_sync(test_ws)

You can then run the server, and have two instances of the test client connect. When you do, the server prints this:

bennu@daveadmin:~$ ./torn.py 
new connection
message received Testing from client
new connection
message received Testing from client
<15 second delay>
Writing to clients
connection closed
connection closed

The first client prints this:

bennu@daveadmin:~$ ./web_client.py 
msg is Hello World
msg is ECHO: Testing from client
< 15 second delay>
msg is Hi there! 0

And the second prints this:

bennu@daveadmin:~$ ./web_client.py 
msg is Hello World
msg is ECHO: Testing from client
< 15 second delay>
msg is Hi there! 1

For the purposes of the example, I just had the server send the message to the clients on a 15 second delay, but it could be triggered by whatever you want.



回答2:

You need to keep track of all the clients that connect. So:

clients = []

def send_to_all_clients(message):
    for client in clients:
        client.write_message(message)

class WSHandler(tornado.websocket.WebSocketHandler):
    def open(self):
        send_to_all_clients("new client")
        clients.append(self)

    def on_close(self):
        clients.remove(self)
        send_to_all_clients("removing client")

    def on_message(self, message):
        for client in clients:
            if client != self:
                client.write_message('ECHO: ' + message)


回答3:

my solution for this: first add "if __name__ == '__main__':" - to the main.py. then import main.py into the websocket module. e.g. (import main as MainApp) . it is now possible to call a function in 'main.py' from within the ws.py/WebSocketHandler-function. - inside the Handler pass the message like so: MainApp.function(message)

i dunno if this is the opposite of elegant but it works for me.

..plus create and import a custom 'config.py' (thats looks like: someVar = int(0) ) into the 'mainApp.py' .. like so: import config as cfg --> now you can alter variables with cfg.someVar = newValue from inside the function in 'main.py' that once was called by the Handler from 'ws.py'.