Спецификация ASGI (интерфейс шлюза асинхронного сервера)

Версия: 3.0 (2019-03-20)

Аннотация

В этом документе предлагается стандартный интерфейс между серверами сетевых протоколов (в частности, веб-серверами) и приложениями Python, предназначенный для работы с несколькими распространенными стилями протоколов (включая HTTP, HTTP/2 и WebSocket).

Эта базовая спецификация предназначена для фиксации набора API, с помощью которых эти серверы взаимодействуют и запускают код приложений; каждый поддерживаемый протокол (например, HTTP) имеет подспецификацию, которая описывает, как кодировать и декодировать этот протокол в сообщениях.

Обоснование

Спецификация WSGI хорошо зарекомендовала себя с момента своего появления и обеспечила большую гибкость в выборе фреймворка Python и веб-сервера. Однако ее дизайн безвозвратно привязан к циклу запрос/ответ в стиле HTTP, и все больше протоколов, которые не следуют этому шаблону, становятся стандартной частью веб-программирования (в частности, WebSocket).

ASGI пытается сохранить простой интерфейс приложения, обеспечивая при этом абстракцию, которая позволяет отправлять и получать данные в любое время и из разных потоков или процессов приложения.

Он также берет принцип превращения протоколов в совместимые с Python, асинхронные наборы сообщений и обобщает его на две части: стандартизированный интерфейс для коммуникации, вокруг которого можно строить серверы (этот документ), и набор стандартных форматов сообщений для каждого протокола.

Его основная цель - обеспечить возможность написания кода HTTP/2 и WebSocket наряду с обычным кодом для работы с HTTP, однако, часть этого проекта подразумевает обеспечение легкого пути для использования существующих серверов и приложений WSGI, поскольку большая часть веб-использования Python полагается на WSGI, и обеспечение легкого пути вперед является критическим для принятия. Подробности этой совместимости описаны в спецификации ASGI-HTTP.

Обзор

ASGI состоит из двух различных компонентов:

  • Сервер протокола, который завершает сокеты и преобразует их в соединения и сообщения о событиях для каждого соединения.

  • Приложение приложение, которое живет внутри протокольного сервера, создается один раз для каждого соединения и обрабатывает сообщения о событиях по мере их поступления.

Как и WSGI, сервер размещает внутри себя приложение и отправляет ему входящие запросы в стандартизированном формате. Однако, в отличие от WSGI, приложения являются инстанцированными объектами, которым передаются события, а не простые вызываемые объекты, и должны выполняться как asyncio-совместимые корутины (на основном потоке; они могут свободно использовать потоки или другие процессы, если им нужен синхронный код).

В отличие от WSGI, соединение ASGI состоит из двух отдельных частей:

  • Область соединения, которая представляет собой протокольное соединение с пользователем и сохраняется до закрытия соединения.

  • События, которые отправляются в приложение по мере того, как что-то происходит на соединении.

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

Каждый экземпляр приложения связан с одним входящим «сокетом» или соединением, и ожидается, что он проработает в течение всего времени существования этого соединения плюс немного дольше, если необходимо произвести очистку. Некоторые протоколы могут не использовать традиционные сокеты; спецификации ASGI для этих протоколов должны определять время жизни области действия (экземпляра) и время его закрытия.

Детали спецификации

Область подключения

Каждое подключение пользователя к приложению ASGI приводит к созданию экземпляра этого приложения для данного подключения. Как долго он живет и какую информацию получает при создании, называется областью действия соединения.

Например, в HTTP область видимости соединения длится всего один запрос, но содержит большую часть данных запроса (кроме тела запроса HTTP, поскольку оно передается через события).

Однако в WebSocket область действия соединения длится до тех пор, пока сокет подключен. Область содержит такую информацию, как путь WebSocket, но такие детали, как входящие сообщения, передаются в виде событий.

Некоторые протоколы могут предоставлять вам область подключения с очень ограниченной информацией, потому что они инкапсулируют что-то вроде рукопожатия. Определение каждого протокола должно содержать информацию о том, как долго длится его область подключения, и какую информацию вы получите внутри нее.

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

События

ASGI декомпозирует протоколы на серию событий, на которые приложение должно реагировать. Для HTTP это просто два события по порядку - http.request и http.disconnect. Для чего-то вроде WebSocket это может быть больше похоже на websocket.connect, websocket.send, websocket.receive и, наконец, websocket.disconnect.

Каждое событие представляет собой dict с ключом верхнего уровня type, который содержит строку Unicode типа сообщения. Пользователи могут свободно придумывать свои собственные типы сообщений и отправлять их между экземплярами приложений для событий высокого уровня - например, приложение чата может отправлять сообщения чата с типом пользователя mychat.message. Предполагается, что приложения должны уметь обрабатывать смешанный набор событий, некоторые из которых исходят от входящего клиентского соединения, а некоторые - от других частей приложения.

Поскольку эти сообщения могут быть отправлены по сети, они должны быть сериализуемыми, и поэтому им разрешено содержать только следующие типы:

  • Строки байтов

  • Строки Юникода

  • Целые числа (в пределах знакового 64-битного диапазона)

  • Числа с плавающей точкой (в пределах диапазона двойной точности IEEE 754; без Nan или бесконечности)

  • Списки (кортежи должны быть закодированы как списки)

  • Dicts (ключи должны быть строками Unicode)

  • Булевы

  • None

Приложения

Примечание

В версии 3.0 формат приложений изменился и теперь используется один вызываемый объект, а не два вызываемых. Двухвызывной формат документирован ниже в разделе «Наследные приложения»; серверы могут легко реализовать его поддержку с помощью библиотеки asgiref.compatibility, и должны стараться поддерживать его.

Приложения ASGI должны представлять собой один асинхронный вызываемый модуль:

coroutine application(scope, receive, send)
  • scope: Область действия соединения, словарь, содержащий по крайней мере type ключ, определяющий входящий протокол.

  • receive, ожидаемый вызываемый элемент, который будет выдавать новый словарь событий, когда он будет доступен

  • send, ожидаемый вызываемый модуль, принимающий в качестве позиционного аргумента один словарь событий, который вернется, как только отправка будет завершена или соединение будет закрыто

Приложение вызывается один раз за «соединение». Определение соединения и его продолжительность диктуются спецификацией протокола. Например, для HTTP это один запрос, а для WebSocket - одно соединение WebSocket.

Как scope, так и формат сообщений, которые вы отправляете и получаете, определяются одним из прикладных протоколов. scope должен быть dict. Ключ scope["type"] всегда будет присутствовать и может быть использован для определения входящего протокола. Ключ scope["asgi"] также будет присутствовать как словарь, содержащий ключ scope["asgi"]["version"], который соответствует версии ASGI, которую реализует сервер. Если он отсутствует, версия должна быть по умолчанию "2.0".

Также может существовать версия, специфичная для конкретной спецификации, представленная как scope["asgi"]["spec_version"]. Это позволяет отдельным спецификациям протокола вносить улучшения без изменения общей версии ASGI.

Специфичные для протокола подспецификации охватывают эти области применения и форматы сообщений. Они эквивалентны спецификации ключей в дикте environ для WSGI.

Наследные приложения

Наследие (v2.0) ASGI-приложений определяется как вызываемое:

application(scope)

Который возвращает другой, ожидаемый callable:

coroutine application_instance(receive, send)

Значения scope, receive и send те же, что и в более новом одновызываемом приложении, но обратите внимание, что первый вызываемый элемент является синхронным.

Первый вызываемый элемент вызывается при запуске соединения, а второй вызываемый элемент вызывается сразу после этого.

Этот стиль был упразднен в версии 3.0, поскольку двухвызывная компоновка была признана ненужной. Сейчас он уже устарел, но существуют приложения, написанные в этом стиле, и поэтому важно поддерживать их.

В модуле asgiref.compatibility имеется набор совместимости, который позволяет обнаруживать устаревшие приложения и без проблем конвертировать их в новый однопротокольный стиль. Серверам рекомендуется поддерживать оба типа начиная с ASGI 3.0 и постепенно отказываться от поддержки по умолчанию с течением времени.

Спецификации протокола

Они описывают стандартизированную область применения и форматы сообщений для различных протоколов.

Одним общим ключом для всех диапазонов и сообщений является type - способ указать, какой тип диапазона или сообщения принимается.

В диапазонах ключ type должен быть строкой Unicode, например "http" или << 2 >>>, как определено в соответствующей спецификации протокола.

В сообщениях значение type должно быть обозначено как protocol.message_type, где protocol соответствует типу области видимости, а message_type определяется спецификацией протокола. Примеры значения сообщения type включают http.request и websocket.send.

Примечание

Приложения должны активно отклонять любой протокол, который они не понимают, с выдачей Exception (любого типа). Несоблюдение этого правила может привести к тому, что сервер будет думать, что вы поддерживаете протокол, который вы не поддерживаете, что может сбить с толку при использовании протокола Lifespan, так как сервер будет ждать начала работы, пока вы ему не скажете.

Текущие спецификации протокола:

Middleware

Можно иметь «промежуточное ПО» ASGI - код, который играет роль сервера и приложения, принимая область видимости и ожидаемые параметры отправки/получения, потенциально изменяя их, а затем вызывая внутреннее приложение.

Когда промежуточное ПО изменяет область видимости, оно должно сделать копию объекта области видимости, прежде чем изменить его и передать внутреннему приложению, так как в противном случае изменения могут просочиться наверх. В частности, вы не должны полагать, что копия scope, которую вы передаете приложению, является той, которую оно в итоге использует, так как на пути может стоять другое промежуточное ПО; поэтому не храните ссылку на него и не пытайтесь изменить его за пределами начального конструктора ASGI, который получает scope. Ваш единственный шанс добавить его - это сделать это до того, как вы передадите управление дочернему приложению.

Обработка ошибок

Если сервер получает некорректный словарь событий - например, с неизвестным type, отсутствующими ключами, которые должен иметь тип события, или с неправильными типами Python для объектов (например, строки Unicode для заголовков HTTP) - он должен поднять исключение из send, ожидаемое обратно в приложение.

Если приложение получает недопустимый словарь событий от receive, оно должно вызвать исключение.

В обоих случаях наличие дополнительных ключей в словаре событий не должно вызывать исключения. Это позволяет не нарушать спецификацию протокола с течением времени.

Серверы могут выводить ошибки, которые всплывают из запущенных экземпляров приложений, как им заблагорассудится - записывать в консоль, отправлять в syslog или использовать другие варианты - но они должны завершить экземпляр приложения и связанное с ним соединение, если это произойдет.

Обратите внимание, что сообщения, полученные сервером после закрытия соединения, не считаются ошибками. В этом случае вызываемая переменная send awaitable должна работать как no-op.

Дополнительные корутины

Фреймворки или приложения могут захотеть запустить дополнительные корутины в дополнение к корутине, запускаемой для каждого экземпляра приложения. Поскольку в Python 3.7 нет возможности сделать их родительскими по отношению к coroutine экземпляра, приложения должны убедиться, что все coroutine, запущенные в рамках выполнения экземпляра приложения, завершаются либо до, либо одновременно с coroutine экземпляра приложения.

Любые coroutines, которые продолжают выполняться вне этого окна, не имеют никаких гарантий относительно времени их жизни и могут быть завершены в любое время.

Расширения

Бывают случаи, когда серверы протоколов могут захотеть предоставить специфические для сервера расширения за пределами основной спецификации протокола ASGI, или когда изменения в спецификации тестируются перед внедрением.

Для этого случая использования мы определяем общий шаблон для extensions - именованных дополнений к спецификации протокола, которые являются необязательными, но которые, если они предоставлены сервером и понятны приложению, могут быть использованы для получения большей функциональности.

Это достигается через запись extensions в словаре scope, который сам является dict. Расширения имеют строковое имя Unicode, которое согласовывается между серверами и приложениями.

Если сервер поддерживает расширение, он должен поместить запись в словарь extensions под именем расширения, а значение этой записи должно быть dict. Серверы могут предоставить любую дополнительную информацию об области видимости, которая является частью расширения, внутри этого значения или, если расширение только указывает, что сервер принимает дополнительные события через вызываемый модуль send, оно может быть просто пустым dict.

В качестве примера представьте, что сервер протокола HTTP хочет предоставить расширение, позволяющее отправлять новое событие обратно на сервер, который пытается промыть буфер отправки по сети до уровня ОС. Он предоставляет пустую запись в словаре extensions, чтобы сигнализировать, что он может обрабатывать событие:

scope = {
    "type": "http",
    "method": "GET",
    ...
    "extensions": {
        "fullflush": {},
    },
}

Если приложение видит это, то оно знает, что может послать пользовательское событие (скажем, типа http.fullflush) через вызываемую переменную send.

Строки и Юникод

В этом документе и во всех подспецификациях байтовая строка относится к типу bytes в Python 3. Unicode string относится к типу str в Python 3.

В этом документе никогда не будет указано просто string - все строки являются одним из двух точных типов.

Все упомянутые ключи dict (включая ключи для scopes и events) являются строками Unicode.

История версий

  • 3.0 (2019-03-04): Изменен стиль приложения с одним вызовом

  • 2.0 (2017-11-28): Первоначальная спецификация ASGI на основе неканального уровня

Вернуться на верх