使用python3.6和python3.7在asyncio中使用wait_for进行套接字泄漏

时间:2019-07-23 11:20:37

标签: python python-asyncio

在某些情况下,asyncio.wait_for可能会导致套接字泄漏。

简明示例:

async def _create_connection(timeout=60, ssl_obj):
    loop = asyncio.get_event_loop()
    connector = loop.create_connection(MyEchoClientProtocol, '127.0.0.1', 5000, ssl=ssl_obj)
    connector = asyncio.ensure_future(connector)
    tr, pr = await asyncio.wait_for(connector, timeout=timeout, loop=loop)
    return tr, pr

async def main():
    ...
    res = await asyncio.wait_for(_acquire_impl(), timeout=timeout, loop=loop)

如果我的理解正确,wait_for应该以两种方式起作用

  1. 内部任务已完成,外部任务将收到结果-在这种情况下为传输和协议
  2. 内部任务被取消并且没有建立连接

我提供了客户端和服务器的源代码,因此可以在您的系统上轻松重现该问题。

可以使用minica

轻松生成证书和密钥

我发现,如果我抓到CancelledError并将done_callback添加到内部任务中,就像这样:

    try:
        tr, pr = await asyncio.wait_for(connector, timeout=timeout, loop=loop)
        return tr, pr
    except asyncio.CancelledError as e:
        connector.add_done_callback(_done_callback)
        raise e

然后在_done_callback内部,我可以访问传输和协议对象并手动关闭传输以防止泄漏:

客户

import asyncio
import io
import struct
import functools
import ssl as ssl_module
import socket
import collections
import time
import traceback

class MyEchoClientProtocol(asyncio.Protocol):

    def connection_made(self, transport):
        print('connection_made', transport)
        query = 'hello world'
        transport.write(query.encode('latin-1'))

    def data_received(self, data):
        print('data_received', data)

    def connection_lost(self, exc):
        print('connection_lost', exc)


async def create_connection(ssl_obj, timeout=60):
    loop = asyncio.get_event_loop()
    connector = loop.create_connection(MyEchoClientProtocol, '127.0.0.1', 5000, ssl=ssl_obj)
    connector = asyncio.ensure_future(connector)
    tr, pr = await asyncio.wait_for(connector, timeout=timeout, loop=loop)
    return tr, pr

async def main(timeout, ssl_obj):
    async def _acquire_impl():
        try:
            proxy = await create_connection(ssl_obj) 
        except Exception:
            raise
        else:
            return proxy

    res = await asyncio.wait_for(_acquire_impl(), timeout=timeout, loop=loop)
    return res

async def test_cancel():
    sc = ssl_module.create_default_context(ssl_module.Purpose.SERVER_AUTH, cafile='localhostc.crt')
    sc.check_hostname = False
    sc.verify_mode = ssl_module.CERT_NONE
    for i in range(10): # try 50 times
        timeout = 0.003
        try:
            tr, pr = await main(
                timeout=timeout, ssl_obj=sc
            )
            tr.close()
        except asyncio.TimeoutError as e:
            print('timeouterror', repr(e))
    await asyncio.sleep(600)


import asyncio
loop = asyncio.get_event_loop()
loop.run_until_complete(test_cancel())
loop.run_forever()
loop.close()

服务器

import asyncio
import ssl

async def handle_echo(reader, writer):
    data = await reader.read(100)
    message = data.decode()
    addr = writer.get_extra_info('peername')
    print("Received %r from %r" % (message, addr))

    print("Send: %r" % message)
    writer.write(data)
    await writer.drain()


loop = asyncio.get_event_loop()

sc = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
sc.load_cert_chain('localhost.crt', 'localhost.key')

coro = asyncio.start_server(handle_echo, '127.0.0.1', 5000, loop=loop, ssl=sc)
server = loop.run_until_complete(coro)

try:
    loop.run_forever()
except KeyboardInterrupt:
    pass

# Close the server
server.close()
loop.run_until_complete(server.wait_closed())
loop.close()

脚本完成后,我运行netstat -a | grep 5000 | grep ESTAB | awk '{ print $5 }' | sort | uniq -c | grep 5000,并且有许多未关闭的连接。

输出取决于您的硬件,因此您可能需要调整超时参数

问题

  1. 为什么会这样?
  2. 这是预期的行为吗?也许我缺少有关asyncio和python的一些基本知识。
  3. 在使用超时时防止套接字泄漏的正确方法是什么?

环境 操作系统:Ubuntu 16.04 LTS python:Python 3.6.6或Python 3.7.3

0 个答案:

没有答案