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!
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()
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.
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")