Python 3 websockets - 在关闭连接之前发送消息

时间:2017-01-27 10:42:57

标签: python python-3.x websocket python-asyncio

我是Stack Overflow的新手(虽然是一个长期的“追踪者”!)所以请温柔地对待我!

我正在尝试学习Python,尤其是使用websockets的Asyncio。

在网上搜索了示例/教程我已经将以下微小的聊天应用程序放在一起,并且可以在它变得越来越庞大(更多命令等)之前使用一些建议,并且变得难以重构。

我的主要问题是,为什么(在发送DISCONNECT命令时)是否需要asyncio.sleep(0)才能在关闭连接之前发送断开连接验证消息?

除此之外,我在这个结构的正确轨道上吗?

我觉得有太多异步/等待,但我无法完全理解为什么。

在几个小时内盯着教程和S / O帖子似乎没有帮助,所以我想我会直接得到一些专家建议!

我们开始,简单的WS服务器响应“nick”,“msg”,“test”& “断开”命令。不需要前缀,即“nick Rachel”。

import asyncio
import websockets
import sys

class ChatServer:

    def __init__(self):
        print("Chat Server Starting..")
        self.Clients = set()
        if sys.platform == 'win32':
            self.loop = asyncio.ProactorEventLoop()
            asyncio.set_event_loop(self.loop)
        else:
            self.loop = asyncio.get_event_loop()

    def run(self):
        start_server = websockets.serve(self.listen, '0.0.0.0', 8080)
        try:
            self.loop.run_until_complete(start_server)
            print("Chat Server Running!")
            self.loop.run_forever()
        except:
            print("Chat Server Error!")

    async def listen(self, websocket, path):

        client = Client(websocket=websocket)
        sender_task = asyncio.ensure_future(self.handle_outgoing_queue(client))

        self.Clients.add(client)
        print("+ connection: " + str(len(self.Clients)))

        while True:
            try:
                msg = await websocket.recv()
                if msg is None:
                    break

                await self.handle_message(client, msg)

            except websockets.exceptions.ConnectionClosed:
                break

        self.Clients.remove(client)
        print("- connection: " + str(len(self.Clients)))

    async def handle_outgoing_queue(self, client):
        while client.websocket.open:
            msg = await client.outbox.get()
            await client.websocket.send(msg)


    async def handle_message(self, client, data):

        strdata = data.split(" ")
        _cmd = strdata[0].lower()

        try:
            # Check to see if the command exists. Otherwise, AttributeError is thrown.
            func = getattr(self, "cmd_" + _cmd)

            try:
                await func(client, param, strdata)
            except IndexError:
                await client.send("Not enough parameters!")

        except AttributeError:
            await client.send("Command '%s' does not exist!" % (_cmd))

    # SERVER COMMANDS

    async def cmd_nick(self, client, param, strdata):
        # This command needs a parameter (with at least one character). If not supplied, IndexError is raised
        # Is there a cleaner way of doing this? Otherwise it'll need to reside within all functions that require a param
        test = param[1][0]


        # If we've reached this point there's definitely a parameter supplied
        client.Nick = param[1]
        await client.send("Your nickname is now %s" % (client.Nick))

    async def cmd_msg(self, client, param, strdata):
        # This command needs a parameter (with at least one character). If not supplied, IndexError is raised
        # Is there a cleaner way of doing this? Otherwise it'll need to reside within all functions that require a param
        test = param[1][0]

        # If we've reached this point there's definitely a parameter supplied
        message = strdata.split(" ",1)[1]

        # Before we proceed, do we have a nickname?
        if client.Nick == None:
            await client.send("You must choose a nickname before sending messages!")
            return

        for each in self.Clients:
            await each.send("%s says: %s" % (client.Nick, message))

    async def cmd_test(self, client, param, strdata):
        # This command doesn't need a parameter, so simply let the client know they issued this command successfully.
        await client.send("Test command reply!")

    async def cmd_disconnect(self, client, param, strdata):
        # This command doesn't need a parameter, so simply let the client know they issued this command successfully.
        await client.send("DISCONNECTING")
        await asyncio.sleep(0)      # If this isn't here we don't receive the "disconnecting" message - just an exception in "handle_outgoing_queue" ?
        await client.websocket.close()


class Client():
    def __init__(self, websocket=None):
        self.websocket = websocket
        self.IPAddress = websocket.remote_address[0]
        self.Port = websocket.remote_address[1]

        self.Nick = None
        self.outbox = asyncio.Queue()

    async def send(self, data):
        await self.outbox.put(data)

chat = ChatServer()
chat.run()

2 个答案:

答案 0 :(得分:1)

您的代码使用无限大小Queues,这意味着.put()调用.put_nowait()并立即返回。 (如果您确实希望在代码中保留这些队列,请考虑使用队列中的“无”作为关闭连接并将client.websocket.close()移至handle_outgoing_queue())的信号。

另一个问题:请考虑将for x in seq: await co(x)替换为await asyncio.wait([co(x) for x in seq])。尝试使用asyncio.sleep(1)来体验巨大的差异。

我认为更好的选择是放弃所有发件箱Queue,然后继续内置的asyncio队列和ensure_future。 websockets包已在其实现中包含队列。

答案 1 :(得分:1)

我想指出的是,websockets的作者在2017年7月17日的帖子中指出,当连接关闭但是在某些时候发生了变化时,websockets 习惯于返回None。相反,他建议您使用try并处理异常。 OP的代码显示了对None和try / except的检查。 None检查是不必要的冗长,显然甚至不准确,因为在当前版本中,websocket.recv()在客户端关闭时不返回任何内容。

解决“主要”问题,它看起来像种类的竞争条件。请记住,asyncio通过四处走动并触摸所有等待的元素来实现它的工作,以便轻轻推动它们。如果在清除队列之前的某个时刻处理了“close connection”命令,则客户端将永远不会获取队列中的最后一条消息。添加async.sleep会为循环添加额外的步骤,并可能使您的队列清空任务超前于“关闭连接”。

解决等待的数量,这就是你需要完成多少异步事件才能完成目标。如果你在任何时候阻止,你将停止你想要继续的所有其他任务。