tornado client on_message_callback is not respondi

2019-03-03 12:39发布

问题:

I am building a simple application for both a server and a client to communicate data in advance that I build more complex application. Purpose of this problem is very simple. In here, the client creates data over each seconds to the server and server also send back some data if the data received from client is text message like "send". I almost to create this similar application. But It didn't work so that I spent almost a week for this wasting my weekend.

The problem is that after the server received the message which means request for message to client, the server seemed to have sent data as per log but the client has no response. As my understanding, I think the callback function called cb_receive() should have responsed for this.

I created this problem with simple application below. Please let me know if you are good at asyncio and tornado libraries. Thanks again!

Server side

import tornado.ioloop
import tornado.web
import tornado.websocket
import os
from tornado import gen

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

    @gen.coroutine
    def on_message(self, message):
        print(message)
        yield self.write_message('notification : ', message)

    def on_close(self):
        print("A client disconnected!!")

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

Client side

from tornado.ioloop import IOLoop, PeriodicCallback
from tornado import gen
from tornado.websocket import websocket_connect



@gen.coroutine
def cb_receive(msg):
    print('msg----------> {}'.format(msg))

class Client(object):
    def __init__(self, url, timeout):
        self.url = url
        self.timeout = timeout
        self.ioloop = IOLoop.instance()
        self.ws = None
        self.connect()
        self.ioloop.start()

    @gen.coroutine
    def connect(self):
        print("trying to connect")
        try:
            self.ws = yield websocket_connect(self.url,on_message_callback=cb_receive)
        except Exception as e:
            print("connection error")
        else:
            print("connected")
            self.run()

    @gen.coroutine
    def run(self):
        while True:
            print('please input')
            msg = input()
            yield self.ws.write_message(msg)
            print('trying to send msg {}'.format(msg))

if __name__ == "__main__":
    client = Client("ws://localhost:8344", 5)

Please help me! I tried not only this tornado library above but also websockets and others. But it didn't work.

回答1:

Why is this happening?

It's happening because the while loop in the run method is iterating faster than Tornado could call the cb_receive.

A dirty hack to get around this is to sleep for a small time at the end of the loop. This way, IOLoop becomes free and can run other coroutines and callbacks.

Example:

while True:
    # other code ...
    yield gen.sleep(0.01)

If you run your client, you'll see the cb_receive callback being called whenever the server sends a message.

But this is a very bad solution. I just mentioned it so the actual problem can be apparent. And now, I think you know the reason why the cb_recieve callback wasn't being called.


What's the solution to this?

The real reason why this is problem is happening is because while loop is too fast. A dirty solution to that is to put the loop to sleep for a little time.

But that's a very inefficient solution. Because input() function is blocking in nature. So, when the while loop reaches at msg = input() line, the whole IOLoop just hangs there. This means Tornado cannot run anything else until you input a message. If the server sends more messages during that time, Tornado won't be able to run the callback.

Normally, a non-blocking application should be able to do other things while it is waiting for something or some event. For example, if Tornado is waiting for your input, it should be able to run other things while you haven't provided it any input. But that's not the case because input() function is blocking.

A better solution would be to take user input in a non-blocking manner. You can use sys.stdin for this task.

Example (modified code from this answer):

import sys

class Client:
    ...
    self.ioloop.add_handler(sys.stdin, self.handle_input, IOLoop.READ)


    @gen.coroutine
    def handle_input(self, fd, events):
        msg = fd.readline()
        yield self.ws.write_message(msg)

    @gen.coroutine
    def run(self):
        # the run method would have nothing
        # but you can put a print statement
        # here, remove everything else

        print("please input")



# a small optional modification to the cb_recieve function
@gen.coroutine
def cb_receive(msg):
    print('msg----------> {}'.format(msg))

    # the following print statement is just there
    # to mimic the while loop behaviour
    # to print a "please input" message 
    # to ask user for input because 
    # sys.stdin in always listening for input

    print("please input")