如何在python-trio中的KeyboardInterrupt之后清理连接

时间:2018-02-14 23:56:09

标签: python python-3.x python-3.5 python-3.6 python-trio

我的班级何时连接到服务器应立即发送登录字符串,之后当会话结束时,它应发出注销字符串并清理插座。以下是我的代码。

import trio

class test:

    _buffer = 8192
    _max_retry = 4

    def __init__(self, host='127.0.0.1', port=12345, usr='user', pwd='secret'):
        self.host = str(host)
        self.port = int(port)
        self.usr = str(usr)
        self.pwd = str(pwd)
        self._nl = b'\r\n'
        self._attempt = 0
        self._queue = trio.Queue(30)
        self._connected = trio.Event()
        self._end_session = trio.Event()

    @property
    def connected(self):
        return self._connected.is_set()

    async def _sender(self, client_stream, nursery):
        print('## sender: started!')
        q = self._queue
        while True:
            cmd = await q.get()
            print('## sending to the server:\n{!r}\n'.format(cmd))
            if self._end_session.is_set():
                nursery.cancel_scope.shield = True
                with trio.move_on_after(1):
                    await client_stream.send_all(cmd)
                nursery.cancel_scope.shield = False
            await client_stream.send_all(cmd)

    async def _receiver(self, client_stream, nursery):
        print('## receiver: started!')
        buff = self._buffer
        while True:
            data = await client_stream.receive_some(buff)
            if not data:
                print('## receiver: connection closed')
                self._end_session.set()
                break
            print('## got data from the server:\n{!r}'.format(data))

    async def _watchdog(self, nursery):
        await self._end_session.wait()
        await self._queue.put(self._logoff)
        self._connected.clear()
        nursery.cancel_scope.cancel()

    @property
    def _login(self, *a, **kw):
        nl = self._nl
        usr, pwd = self.usr, self.pwd
        return nl.join(x.encode() for x in ['Login', usr,pwd]) + 2*nl

    @property
    def _logoff(self, *a, **kw):
        nl = self._nl
        return nl.join(x.encode() for x in ['Logoff']) + 2*nl

    async def _connect(self):
        host, port = self.host, self.port
        print('## connecting to {}:{}'.format(host, port))
        try:
            client_stream = await trio.open_tcp_stream(host, port)
        except OSError as err:
            print('##', err)
        else:
            async with client_stream:
                self._end_session.clear()
                self._connected.set()
                self._attempt = 0
                # Sign in as soon as connected
                await self._queue.put(self._login)
                async with trio.open_nursery() as nursery:
                    print("## spawning watchdog...")
                    nursery.start_soon(self._watchdog, nursery)
                    print("## spawning sender...")
                    nursery.start_soon(self._sender, client_stream, nursery)
                    print("## spawning receiver...")
                    nursery.start_soon(self._receiver, client_stream, nursery)

    def connect(self):
        while self._attempt <= self._max_retry:
            try:
                trio.run(self._connect)
                trio.run(trio.sleep, 1)
                self._attempt += 1
            except KeyboardInterrupt:
                self._end_session.set()
                print('Bye bye...')
                break

tst = test()
tst.connect()

我的逻辑不起作用。好吧,如果我杀了netcat监听器,它就有效,所以我的会话看起来如下:

## connecting to 127.0.0.1:12345
## spawning watchdog...
## spawning sender...
## spawning receiver...
## receiver: started!
## sender: started!
## sending to the server:
b'Login\r\nuser\r\nsecret\r\n\r\n'

## receiver: connection closed
## sending to the server:
b'Logoff\r\n\r\n'

请注意Logoff字符串已经发出,虽然它在这里没有意义,因为到那时连接已经被打破了。

但是,当用户Logoff时,我的目标是KeyboardInterrupt。在这种情况下,我的会话看起来类似于:

## connecting to 127.0.0.1:12345
## spawning watchdog...
## spawning sender...
## spawning receiver...
## receiver: started!
## sender: started!
## sending to the server:
b'Login\r\nuser\r\nsecret\r\n\r\n'

Bye bye...

请注意,Logoff尚未被发送。

有什么想法吗?

1 个答案:

答案 0 :(得分:4)

这里你的调用树看起来像:

connect
|
+- _connect*
   |
   +- _watchdog*
   |
   +- _sender*
   |
   +- _receiver*

*表示4个三重奏任务。 _connect任务位于托儿所区块的末尾,等待子任务完成。 _watchdogawait self._end_session.wait()任务被阻止,_sender中的await q.get()任务被阻止,_receiver中的await client_stream.receive_some(...)任务被阻止。< / p>

当你点击control-C时,那么标准的Python语义就是突然出现的任何Python代码都会引发KeyboardInterrupt。在这种情况下,您运行了4个不同的任务,因此随机选择其中一个被阻止的操作[1],并引发KeyboardInterrupt。这意味着可能会发生一些不同的事情:

  • 如果_watchdog wait来电提升KeyboardInterrupt,则_watchdog方法会立即退出,因此它甚至都不会尝试发送logout 1}}。然后作为展开堆栈的一部分,三人取消所有其他任务,一旦他们退出,KeyboardInterrupt一直向前传播,直到它到达finally中的connect块。此时,您尝试使用self._end_session.set()通知监视程序任务,但它已不再运行,因此它没有注意到。

  • 如果_sender q.get()来电提升KeyboardInterrupt,则_sender方法会立即退出,即使_watchdog也是如此确实要求它发送注销消息,它不会在那里注意到。无论如何,三人然后继续取消看门狗和接收器任务,事情如上所述。

  • 如果_receiver&#39; receive_all来电加注KeyboardInterrupt ......同样的事情就会发生。

  • 轻微的细微之处:_connect也可以收到KeyboardInterrupt,它会做同样的事情:取消所有孩子,然后在允许KeyboardInterrupt之前等待他们停止继续传播。

如果你想要可靠地捕获control-C然后用它做一些事情,那么它在某个随机点被提出的这项业务是非常麻烦的。最简单的方法是使用Trio's support for catching signals来捕获signal.SIGINT信号,这是Python通常转换为KeyboardInterrupt的信号。 (&#34; INT&#34;代表&#34;中断&#34;。)类似于:

async def _control_c_watcher(self):
    # This API is currently a little cumbersome, sorry, see
    # https://github.com/python-trio/trio/issues/354
    with trio.catch_signals({signal.SIGINT}) as batched_signal_aiter:
        async for _ in batched_signal_aiter:
            self._end_session.set()
            # We exit the loop, restoring the normal behavior of
            # control-C. This way hitting control-C once will try to
            # do a polite shutdown, but if that gets stuck the user
            # can hit control-C again to raise KeyboardInterrupt and
            # force things to exit.
            break

然后开始与其他任务一起运行。

您还遇到的问题是,在_watchdog方法中,它会将logoff请求放入队列中 - 从而安排稍后由_sender任务发送的消息 - 以及然后立即取消所有任务,以便_sender任务可能没有机会看到消息并对其作出反应!一般来说,当我仅在必要时使用任务时,我发现我的代码工作得更好。当您想要发送消息时,为什么不让代码直接发送消息stream.send_all,而不是拥有发送者任务然后将消息放入队列中?您需要注意的一件事是,如果您有多个可能同时发送内容的任务,您可能希望使用trio.Lock()来确保他们不会通过调用{{1}来相互碰撞在同一时间:

send_all

如果你这样做,你或许可以完全摆脱看门狗任务和async def send_all(self, data): async with self.send_lock: await self.send_stream.send_all(data) async def do_logoff(self): # First send the message await self.send_all(b"Logoff\r\n\r\n") # And then, *after* the message has been sent, cancel the tasks self.nursery.cancel() 事件。

关于您的代码的其他一些注意事项,我在这里:

  • 这样多次调用_end_session是不寻常的。通常的风格是在程序的顶部调用一次,并将所有真实代码放入其中。退出trio.run后,所有三人组合状态都将丢失,您绝对不会执行任何并发任务(因此,可能 正在倾听并注意到您对trio.run的来电!)。一般来说,几乎所有Trio函数都假设您已经在_end_session.set()的调用中。事实证明,现在你可以在开始三重奏之前调用trio.run而不会出现异常,但这基本上只是巧合。

  • trio.Queue()内使用屏蔽对我来说很奇怪。屏蔽通常是您几乎从不想要使用的高级功能,我不认为这是一个例外。

希望有所帮助!如果你想更多地谈论这样的样式/设计问题,但是担心它们可能对于堆栈溢出来说太模糊了(&#34;这个程序设计得好吗?&#34;),那么随意放下{ {3}}

[1]嗯,实际上三人可能出于各种原因挑选主要任务,但这并不能保证,无论如何它在这里没有什么区别。