如何检测asyncio中的写入失败?

时间:2015-08-24 05:51:49

标签: python python-asyncio

作为一个简单的例子,考虑下面的/ dev / zero的网络等价物。 (或者更现实地说,只是发送大文件的Web服务器。)

如果客户端提前断开连接,则会收到大量日志消息:

WARNING:asyncio:socket.send() raised exception.

但我没有找到任何方法来捕捉所述异常。假设的服务器继续从磁盘读取千兆字节并将它们发送到一个无效的套接字,而不需要客户端部分,并且您已经获得了DoS攻击。

我从文档中发现的唯一一件事是从一个读取中产生,一个空字符串表示关闭。但这并不好,因为普通客户端不会发送任何内容,阻止写入循环。

使用流API或其他方式检测失败写入或被通知TCP连接已关闭的正确方法是什么?

代码:

from asyncio import *
import logging

@coroutine
def client_handler(reader, writer):
    while True:
        writer.write(bytes(1))
        yield from writer.drain()

logging.basicConfig(level=logging.INFO)
loop = get_event_loop()
coro = start_server(client_handler, '', 12345)
server = loop.run_until_complete(coro)
loop.run_forever()

2 个答案:

答案 0 :(得分:5)

我做了一些挖掘asyncio源代码,以扩展dano's回答为什么在没有明确地将控制权传递给事件循环的情况下不会引发异常的原因。这是我发现的。

调用yield from wirter.drain()可以控制StreamWriter.drain协程。此协程checks用于提出StreamReaderProtocolStreamReader上设置的任何例外情况。但是由于我们将控制权交给了drain,协议还没有机会设置异常。 drain然后控制FlowControlMixin._drain_helper协程。这个协程立即返回,因为还没有设置更多的标志,并且控件最终返回调用yield from wirter.drain()的协程。

所以我们已经完全循环而没有控制事件循环以允许它处理其他协同程序并将异常冒泡到writer.drain()

yield之前

drain()为传输/协议提供了设置适当标志和异常的机会。

这是一个模拟正在发生的事情,所有嵌套调用都崩溃了:

import asyncio as aio

def set_exception(ctx, exc):
  ctx["exc"] = exc

@aio.coroutine
def drain(ctx):
  if ctx["exc"] is not None:
    raise ctx["exc"]

  return

@aio.coroutine
def client_handler(ctx):
  i = 0
  while True:
    i += 1
    print("write", i)
    # yield # Uncommenting this allows the loop.call_later call to be scheduled.
    yield from drain(ctx)

CTX = {"exc": None}

loop = aio.get_event_loop()
# Set the exception in 5 seconds
loop.call_later(5, set_exception, CTX, Exception("connection lost"))
loop.run_until_complete(client_handler(CTX))
loop.close()

这应该由asyncio开发人员在Streams API的上游修复。

答案 1 :(得分:2)

这有点奇怪,但实际上允许异常通过强制它在一次迭代中控制事件循环来到达import asyncio import logging @asyncio.coroutine def client_handler(reader, writer): while True: writer.write(bytes(1)) yield # Yield to the event loop yield from writer.drain() logging.basicConfig(level=logging.INFO) loop = asyncio.get_event_loop() coro = asyncio.start_server(client_handler, '', 12345) server = loop.run_until_complete(coro) loop.run_forever() 协程:

ERROR:asyncio:Task exception was never retrieved
future: <Task finished coro=<client_handler() done, defined at aio.py:4> exception=ConnectionResetError(104, 'Connection reset by peer')>
Traceback (most recent call last):
  File "/usr/lib/python3.4/asyncio/tasks.py", line 238, in _step
    result = next(coro)
  File "aio.py", line 9, in client_handler
    yield from writer.drain()
  File "/usr/lib/python3.4/asyncio/streams.py", line 301, in drain
    raise exc
  File "/usr/lib/python3.4/asyncio/selector_events.py", line 700, in write
    n = self._sock.send(data)
ConnectionResetError: [Errno 104] Connection reset by peer

如果我这样做,当我终止客户端连接时会得到此输出:

yield from writer.drain()

我真的不太清楚为什么你需要明确地让事件循环控制异常才能通过 - 现在没有时间去挖掘它。我假设有一些需要翻转以指示连接被丢弃,并且在循环中调用{{1}}(可以通过事件循环进行短路)可以防止这种情况发生,但我真的不是当然。如果我有机会进行调查,我会用该信息更新答案。