Сообщить об ошибке.

Создание сетевых соединений из низкоуровнего кода asyncio в Python

В разделе рассмотрены низкоуровневые методы цикла событий модуля asyncio, при помощи которых можно создать и открыть сетевые потоковые соединения следующих видов TCP, UDP и Unix.

Прежде чем что-то делать с циклом событий, его необходимо создать или получить функциями, описанными в разделе "Создание, запуск и получение цикла событий".

Содержание:


loop.create_connection(protocol_factory, host=None, port=None, *, ssl=None, family=0, proto=0, flags=0, sock=None, local_addr=None, server_hostname=None, ssl_handshake_timeout=None, ssl_shutdown_timeout=None, happy_eyeballs_delay=None, interleave=None):

Метод loop.create_connection() открывает TCP соединение с заданным адресом, указанным host и port. Представляет собой сопрограмму.

Семейство сокетов может быть AF_INET или AF_INET6 в зависимости от хоста или аргумента family, если он предоставлен.

Тип сокета будет SOCK_STREAM.

Аргумент protocol_factory должен быть вызываемым объектом, возвращающим реализацию протокола модуля asyncio.

Метод loop.create_connection() попытается установить соединение в фоновом режиме. В случае успеха - возвращает пару (transport, protocol).

Хронологический синопсис основной операции выглядит следующим образом:

  1. Соединение установлено и для него создан транспорт.
  2. Объект аргумента protocol_factory вызывается без аргументов и ожидается, что он вернет экземпляр протокола.
  3. Экземпляр протокола связывается с транспортом, вызывая его метод Protocol.connection_made().
  4. В случае успеха возвращается кортеж (transport, protocol).

Созданный транспорт представляет собой зависящий от реализации двунаправленный поток.

Другие аргументы:

  • ssl:

    • если задано и не равно False, то создается транспорт SSL/TLS (по умолчанию создается простой транспорт TCP),
    • если ssl является объектом ssl.SSLContext, то этот контекст используется для создания транспорта,
    • если ssl имеет значение True, то используется контекст по умолчанию, возвращаемый из ssl.create_default_context().
  • server_hostname устанавливает или переопределяет имя хоста, с которым будет сопоставляться сертификат целевого сервера. Следует устанавливать, только если ssl не равен None. По умолчанию используется значение аргумента хоста. Если host пуст (у него нет значения по умолчанию), то необходимо передать значение server_hostname. Если server_hostname является пустой строкой, то сопоставление имен хостов отключено. Это представляет собой серьезную угрозу безопасности, допускающую потенциальные атаки типа злоумышленник посередине.

  • family, proto, flags - это необязательное семейство адресов, протокол и флаги, передаваемые в loop.getaddrinfo() для разрешения хоста. Все они должны быть целыми числами из соответствующих констант модуля socket.

  • happy_eyeballs_delay, если задан, то для этого соединения включается алгоритм Happy Eyeballs. Это должно быть число float, представляющее количество времени в секундах для ожидания завершения попытки подключения, прежде чем параллельно запускать следующую попытку. Как определено в RFC 8305 - это задержка попытки подключения, . Разумное значение по умолчанию, рекомендованное RFC, составляет 0,25 (250 миллисекунд).

    Алгоритм Happy Eyeballs: это требования к алгоритмам, которые уменьшают видимую для пользователя задержку с хостами с двойным стеком. Другими словами, когда на сервере настроены два протокола IPv4/IPv6, а IPv6 не работает, то клиентское приложение с двойным стеком испытывает значительную задержку соединения по сравнению с клиентом, работающим только с IPv4. Это нежелательно, потому что из-за этого клиент с двойным стеком ухудшает работу пользователя.

  • Interleave управляет переупорядочиванием адресов, когда имя хоста разрешается в несколько IP-адресов. Если 0 или не указано, то переупорядочивание не производится, и адреса проверяются в порядке, возвращаемом loop.getaddrinfo(). Если задано положительное целое число, то адреса чередуются по семействам адресов, и данное целое число интерпретируется как счетчик первого семейства адресов, как определено в RFC 8305. Если аргумент happy_eyeballs_delay не указан то устанавливается значение 0 (по умолчанию) или 1, если указан.

  • sock, если он задан, то должен быть существующим, уже подключенным объектом socket.socket(), который будет использоваться транспортом. Если аргумент sock указан , то следующие аргументы указывать не надо: host, port, family, proto, flags, happy_eyeballs_delay, interleave и local_addr.

  • local_addr, если задан, то должен является кортежем (local_host, local_port), используемым для локальной привязки сокета. Значения local_host и local_port ищутся с помощью loop.getaddrinfo(), аналогично host и port.

  • ssl_handshake_timeout - это (для TLS-соединения) время в секундах, в течение которого нужно дождаться завершения рукопожатия TLS, прежде чем разорвать соединение. По умолчанию - 60,0 секунд.

  • ssl_shutdown_timeout - это время в секундах, необходимое для ожидания завершения отключения SSL перед разрывом соединения. 30,0 секунд, если нет (по умолчанию).

Изменено в Python 3.6: Параметр сокета TCP_NODELAY устанавливается по умолчанию для всех соединений TCP.

Новое в Python 3.7: Добавлен параметр ssl_handshake_timeout.

Новое в Python 3.8: Добавлены параметры happy_eyeballs_delay и interleave.

Изменено в версии 3.11: Добавлен параметр ssl_shutdown_timeout.

Смотрите также функцию asyncio.open_connection() - это высокоуровневый альтернативный API. Он возвращает пару (StreamReader, StreamWriter), которые могут быть использованы непосредственно в коде async/await.

loop.create_datagram_endpoint(protocol_factory, local_addr=None, remote_addr=None, *, family=0, proto=0, flags=0, reuse_address=None, reuse_port=None, allow_broadcast=None, sock=None):

Метод loop.create_datagram_endpoint() создает UID соединение (соединение дейтаграммы). Представляет собой сопрограмму.

Семейство сокетов может быть AF_INET, AF_INET6 или AF_UNIX, в зависимости от хоста или аргумента family, если он предоставлен.

Тип сокета будет SOCK_DGRAM.

Аргумент protocol_factory должен быть вызываемым объектом, возвращающим реализацию протокола модуля asyncio.

В случае успеха - возвращает пару (transport, protocol)

Примечание. Аргумент reuse_address не поддерживается с версии Python 3.8.1 и полностью удален в Python 3.11, поскольку использование SO_REUSEADDR представляет серьезную проблему безопасности для UDP соединений. Явная передача reuse_address=True вызовет исключение.

Когда несколько процессов с разными UID назначают сокеты на одинаковый адрес сокета UDP с помощью SO_REUSEADDR, входящие пакеты могут случайным образом распределяться между сокетами.

Для поддерживаемых платформ, аргумент reuse_port может использоваться как замена аналогичной функциональности. В случае reuse_port=True, вместо него будет использоваться SO_REUSEPORT, что специально предотвращает присвоение сокетам одного и того же адреса сокета процессам с разными UID.

Другие аргументы:

  • local_addr, если задан, то представляет собой кортеж (local_host, local_port), используемый для локальной привязки сокета. Значения local_host и local_port ищутся с помощью loop.getaddrinfo().

  • remote_addr, если задан, является кортежем (remote_host, remote_port), используемым для подключения сокета к удаленному адресу. Значения remote_host и remote_port ищутся с помощью loop.getaddrinfo().

  • family, proto, flags - это необязательное семейство адресов, протокол и флаги, передаваемые в loop.getaddrinfo() для разрешения хоста. Все они должны быть целыми числами из соответствующих констант модуля socket.

  • reuse_port указывает ядру разрешить привязку этой конечной точки к тому же порту, к которому привязаны другие существующие конечные точки, если все они устанавливают этот флаг при создании. Этот параметр не поддерживается в Windows и некоторых системах Unix. Если в системе константа SO_REUSEPORT не определена, то эта возможность не поддерживается.

  • allow_broadcast сообщает ядру разрешить этой конечной точке отправлять сообщения на широковещательный адрес.

  • sock - это уже подключенный объект socket.socket(), который будет использоваться транспортом. Если указаны, local_addr и remote_addr, то следует их опустить, т.е. должны быть None.

Смотрите примеры протокола эхо-клиента UDP и примеры протокола эхо-сервера UDP.

Изменено в Python 3.8.1: аргумент reuse_address больше не поддерживается из соображений безопасности.

Изменено в Python 3.8: Добавлена ​​поддержка Windows.

Изменено в Python 3.11: аргумент reuse_address, отключенный в Python 3.9.0, 3.8.1, 3.7.6 и 3.6.10, полностью удален.

loop.create_unix_connection(protocol_factory, path=None, *, ssl=None, sock=None, server_hostname=None, ssl_handshake_timeout=None, ssl_shutdown_timeout=None):

Метод loop.create_unix_connection() создает соединение Unix. Представляет собой сопрограмму.

Семейство сокетов будет AF_UNIX. Тип сокета будет SOCK_STREAM.

В случае успеха возвращается кортеж (transport, protocol).

Аргумент path - это имя сокета домена Unix и является обязательным, если не указан аргумент sock. Поддерживаются абстрактные сокеты Unix, str, байты и пути Path.

Смотрите документацию по методу loop.create_connection() для получения информации об остальных аргументах этого метода.

Доступность: Unix.

Новое в Python 3.7: добавлен аргумент ssl_handshake_timeout.

Изменено в Python 3.7: теперь аргумент path может быть объектом path.

Изменено в Python 3.11: добавлен аргумент ssl_shutdown_timeout.

Пример UDP сервера и клиента.

UDP сервер.

UDP эхо-сервер, используя метод loop.create_datagram_endpoint(), отправляет обратно клиенту полученные данные:

import asyncio

class EchoServerProtocol:
    def connection_made(self, transport):
        self.transport = transport

    def datagram_received(self, data, addr):
        message = data.decode()
        print('Received %r from %s' % (message, addr))
        print('Send %r to %s' % (message, addr))
        self.transport.sendto(data, addr)

async def main():
    print("Starting UDP server")

    # Get a reference to the event loop as we plan to use
    # low-level APIs.
    loop = asyncio.get_running_loop()

    # One protocol instance will be created to serve all
    # client requests.
    transport, protocol = await loop.create_datagram_endpoint(
        lambda: EchoServerProtocol(),
        local_addr=('127.0.0.1', 9999))

    try:
        await asyncio.sleep(3600)  # Serve for 1 hour.
    finally:
        transport.close()

asyncio.run(main())

UDP клиент.

UDP эхо-клиент, используя метод loop.create_datagram_endpoint(), отправляет данные и закрывает транспорт, когда получает ответ:

import asyncio

class EchoClientProtocol:
    def __init__(self, message, on_con_lost):
        self.message = message
        self.on_con_lost = on_con_lost
        self.transport = None

    def connection_made(self, transport):
        self.transport = transport
        print('Send:', self.message)
        self.transport.sendto(self.message.encode())

    def datagram_received(self, data, addr):
        print("Received:", data.decode())

        print("Close the socket")
        self.transport.close()

    def error_received(self, exc):
        print('Error received:', exc)

    def connection_lost(self, exc):
        print("Connection closed")
        self.on_con_lost.set_result(True)

async def main():
    # Get a reference to the event loop as we plan to use
    # low-level APIs.
    loop = asyncio.get_running_loop()

    on_con_lost = loop.create_future()
    message = "Hello World!"

    transport, protocol = await loop.create_datagram_endpoint(
        lambda: EchoClientProtocol(message, on_con_lost),
        remote_addr=('127.0.0.1', 9999))

    try:
        await on_con_lost
    finally:
        transport.close()

asyncio.run(main())