带标准输入的 Tornado 客户端

Tornado client with stdin

我正在尝试构建一个使用 Python 的多人游戏。我正在使用 Tornado 构建客户端和服务器。 理想情况下,我希望发生的事情如下:

(a) 客户端等待用户从命令行输入

(b) 当客户端获得用户输入时,将用户输入发送给服务器

(c) 服务器在其上模拟一些处理(这将是游戏引擎)并将响应发送回客户端。

服务器

"""
    Server module for game server
"""

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.websocket
import uuid
import json

class Controller(object):
    def __init__(self):
        self.players = ()
        self.connections = ()

    def check(self):
        return "Hi"

    def run_somthing(self, text):
        new_text = "Server: " + text
        return new_text

class InitMessageHandler(tornado.web.RequestHandler):
    def get(self):
        user_data = {}
        user_data['user_id'] = str(uuid.uuid4())
        self.write(json.dumps(user_data))

class GameHandler(tornado.websocket.WebSocketHandler):

    def open(self):
        # called anytime a new connection with this server is opened
        print("Client connected") 
        print("Client sent: ", self)
        if seif not in self.application.controller.connections:
            self.application.controller.connections.add(self)

    def on_message(self):
        # called anytime a new message is received
        pass

    def check_origin(self, origin):
        return True

    def on_close(self):
        # called a websocket connection is closed
        if self in self.application.controller.connections:
            self.application.controller.connections.remove(self)

class Server(tornado.web.Application):
    def __init__(self):
        self.controller = Controller()
        handlers = [
            (r"/join", InitMessageHandler),
            (r"/game", GameHandler)
        ]
        tornado.web.Application.__init__(self, handlers)

if __name__ == "__main__":
    tornado.options.parse_command_line()
    try:
        application = Server()
        server = tornado.httpserver.HTTPServer(application)
        server.listen(8888)
        tornado.ioloop.IOLoop.instance().start()
    except KeyboardInterrupt:
        tornado.ioloop.IOLoop.instance().stop()
        print("closed") 

客户

"""
    Client module for the game clients(Players)
"""

import tornado.ioloop
import tornado.websocket
import requests
import json
import sys
import tornado.gen

class Client(object):
    def __init__(self, join_url, play_url):
        self.wsconn = None
        self.join_url = join_url
        self.play_url = play_url
        #self.io_loop = tornado.ioloop.IOLoop.instance()
        #self.io_loop.add_handler(sys.stdin, self.handle_user_input, tornado.ioloop.IOLoop.READ)
        self.user_details = {}
        self.has_initialised = False
        #self.io_loop.start()
        self.main()

    def get_user_input(self, question=None):
        str_choice = input(question)
        while any((str_choice is None, not str_choice.strip())):
            print("You entered an empty answer")
            str_choice = input(question)
        return str_choice 

    def _merge_dicts(*dict_args):
        """
        Given any number of dicts, shallow copy and merge into a new dict,
        precedence goes to key value pairs in latter dicts.
        """
        result = {}
        for dictionary in dict_args:
            result.update(dictionary)
        return result

    def generate_wsmessage(self):
        msg_line = input("Enter message to send to server")
        while any((msg_line is None, not msg_line.strip())):
            print("You entered an empty answer")
            msg_line = input("Enter message to send to server")
        msg = {}
        msg['message'] = msg_line
        msg_to_send = self._merge_dicts(self.user_details, msg)
        return json.dumps(msg_to-send)

    def init(self):
        print("Heh")
        username = self.get_user_input("What is your username? ")
        print("Getting initial user details")
        req = requests.get(self.join_url)
        response = json.loads(req.text)
        print(response)
        self.user_details['name'] = username
        self.user_details['user_id'] = response['user_id']
        self.has_initialised = True

    def server_recv(self, msg):
        print("Server has connected on websocket socket with msg=", msg)

    @tornado.gen.coroutine
    def connect_on_websocket(self):
        try:
            self.wsconn = yield tornado.websocket.websocket_connect(self.play_url, on_message_callback=self.server_recv)
        except Exception as e:
            print("Connection error: {}".format(e))
        else:
            print("Connected")

    @tornado.gen.coroutine
    def send_wsmessage(self):
        msg = self.generate_wsmessage()
        yield self.wsconn.write_message(msg)

    @tornado.gen.coroutine
    def communicate_with_websocket(self):
        self.send_wsmessage()
        while True:
            recv_msg = yield self.wsconn.read_message()
            if recv_msg is None: 
                self.wsconn.close()
                break
            yield tornado.gen.sleep(0.1)
            self.send_wsmessage()
        print("IoLoop terminate")

    def main(self):
        choice = input("Do you want to continue(y/n)? ")
        if choice == "y" and self.has_initialised == False:
            print("Yup")
            self.init()
        if self.has_initialised == True:
            self.connect_on_websocket()
        self.communicate_with_websocket()

if __name__ == "__main__":
    try:
        client = Client("http://localhost:8888/join", "ws://localhost:8888/game")
        tornado.ioloop.IOLoop.instance().start()
    except (SystemExit, KeyboardInterrupt):
        print("Client closed")

通过阅读一些在线示例,我想出了上面的代码,但它不起作用。所以我的主要问题是

如何使 Tornado 协程与标准输入一起工作(命令行输入)

我的其他问题是:

(a) 我编写的代码是否适合使用 Tornado 协程?

(b) 如果不是,你能ELI5吗?我也很欣赏真正以有趣的方式(在任何中间级别)使用 Tornado 的代码示例,以便我可以从中学习。

(c) 在Python中有没有更直观的方法来做我想做的事?像 Flask+Gevents 或 Twisted 版本或可能更容易使用的纯套接字版本?

感谢您的帮助。

编辑:Flan 为我指出了一些错误,我修正了它,现在可以使用了。

正如我目前所见,问题不在于stdin 交互,而是您使用协程的错误方式。您的 connect_on_websocketcommunicate_with_websocket 函数是协程,但您将它们用作普通函数,它不会起作用。我提出这些更改。

  1. 制作 main() 协程(添加装饰器),不要调用它,从 Client.__init__().
  2. 中移除
  3. 在 name=main block schedule client.main() 调用 tornado.ioloop.IOLoop.instance().add_callback(client.main).
  4. mainall 中,您的代码更改协程函数(使用 @tornado.gen.coroutine)调用 yield,例如 yield self.connect_on_websocket()只有 self.connect_on_websocket()

这应该足够了,您可以继续进行开发。

编辑后的代码是

服务器:

"""
    Server module for game server
"""

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
import tornado.websocket
import uuid
import json

class Controller(object):
    def __init__(self):
        self.players = set()
        self.connections = set()

    def check(self):
        return "Hi"

    def run_somthing(self, text):
        new_text = "Server: " + text
        return new_text

class InitMessageHandler(tornado.web.RequestHandler):
    def get(self):
        print("Client has asked for initial details")   
        user_data = {}
        user_data['user_id'] = str(uuid.uuid4())
        self.write(json.dumps(user_data))

class GameHandler(tornado.websocket.WebSocketHandler):

    def open(self):
        # called anytime a new connection with this server is opened
        print("Client connected") 
        print("Client sent: ", self)
        if self not in self.application.controller.connections:
            self.application.controller.connections.add(self)

    def on_message(self, message):
        # called anytime a new message is received
        print("Received from client ,msg=", message)
        msg = "Server: " + message
        self.write_message(json.dumps(msg))

    def check_origin(self, origin):
        return True

    def on_close(self):
        # called a websocket connection is closed
        if self in self.application.controller.connections:
            self.application.controller.connections.remove(self)

class Server(tornado.web.Application):
    def __init__(self):
        self.controller = Controller()
        handlers = [
            (r"/join", InitMessageHandler),
            (r"/game", GameHandler)
        ]
        tornado.web.Application.__init__(self, handlers)

if __name__ == "__main__":
    tornado.options.parse_command_line()
    try:
        application = Server()
        server = tornado.httpserver.HTTPServer(application)
        server.listen(8888)
        tornado.ioloop.IOLoop.instance().start()
    except KeyboardInterrupt:
        tornado.ioloop.IOLoop.instance().stop()
        print("Server closed") 

客户

import tornado.ioloop
import tornado.websocket
import requests
import json
import sys
import tornado.gen

class Client(object):
    def __init__(self, join_url, play_url):
        self.wsconn = None
        self.join_url = join_url
        self.play_url = play_url
        self.user_details = {}

    def get_user_input(self, question=None):
        str_choice = input(question)
        while any((str_choice is None, not str_choice.strip())):
            print("You entered an empty answer")
            str_choice = input(question)
        return str_choice 

    def _merge_dicts(*dict_args):
        """
        Given any number of dicts, shallow copy and merge into a new dict,
        precedence goes to key value pairs in latter dicts.
        """
        result = {}
        for dictionary in dict_args:
            result.update(dictionary)
        return result

    def generate_wsmessage(self):
        msg_line = self.get_user_input("Enter message to send to server: ")
        msg = {}
        msg['message'] = msg_line
        msg['user_id'] = self.user_details['user_id']
        msg['user_name'] = self.user_details['user_name']
        print("Message to send: ", msg)
        return json.dumps(msg)

    def init(self):
        print("Heh")
        username = self.get_user_input("What is your username? ")
        print("Getting initial user details")
        req = requests.get(self.join_url)
        response = json.loads(req.text)
        print(response)
        self.user_details['user_name'] = username
        self.user_details['user_id'] = response['user_id']

    @tornado.gen.coroutine
    def connect_on_websocket(self):
        try:
            self.wsconn = yield tornado.websocket.websocket_connect(self.play_url)
        except Exception as e:
            print("Connection error: {}".format(e))
        else:
            print("Server has connected to ")
            yield self.send_wsmessage()

    @tornado.gen.coroutine
    def send_wsmessage(self):
        msg = self.generate_wsmessage()
        if not self.wsconn:
            raise RuntimeError('Web socket connection is closed.')
        yield self.wsconn.write_message(json.dumps(msg))
        yield self.communicate_with_websocket()

    @tornado.gen.coroutine
    def communicate_with_websocket(self):
        recv_msg = None
        while True:
            recv_msg = yield self.wsconn.read_message()
            if recv_msg is None: 
                self.wsconn.close()
                break
            print("Server has replied with message=", recv_msg)
            yield self.send_wsmessage()
        print("IoLoop terminate")

    @tornado.gen.coroutine
    def main(self):
        choice = input("Do you want to continue(y/n)? ")
        if choice == "y":
            print("Yup")
            self.init()
            yield self.connect_on_websocket()
        if choice == "n":
            sys.exit()

if __name__ == "__main__":
    try:
        client = Client("http://localhost:8888/join", "ws://localhost:8888/game")
        tornado.ioloop.IOLoop.instance().add_callback(client.main)
        tornado.ioloop.IOLoop.instance().start()
    except (SystemExit, KeyboardInterrupt):
        print("Client closed")

对于 (a)、(b),查看 here。对于 (c),另一次。