WSGI: интерфейс сервер-приложение для Python
Опубликовано: 2022-03-11В 1993 году сеть все еще находилась в зачаточном состоянии, насчитывала около 14 миллионов пользователей и 100 веб-сайтов. Страницы были статичными, но уже существовала потребность в динамическом контенте, таком как свежие новости и данные. В ответ на это Роб МакКул и другие участники реализовали интерфейс Common Gateway Interface (CGI) на веб-сервере HTTPd Национального центра суперкомпьютерных приложений (NCSA) (предшественнике Apache). Это был первый веб-сервер, который мог обслуживать контент, созданный отдельным приложением.
С тех пор количество пользователей в Интернете резко возросло, а динамические веб-сайты стали повсеместными. При первом изучении нового языка или даже при первом изучении кода разработчики достаточно скоро захотят узнать, как подключить свой код к сети.
Python в Интернете и подъем WSGI
С момента создания CGI многое изменилось. Подход CGI стал непрактичным, поскольку требовал создания нового процесса при каждом запросе, что приводило к трате памяти и ЦП. Появились некоторые другие низкоуровневые подходы, такие как FastCGI](http://www.fastcgi.com/) (1996) и mod_python (2000), обеспечивающие различные интерфейсы между веб-фреймворками Python и веб-сервером. По мере распространения различных подходов выбор разработчиком фреймворка ограничивал выбор веб-серверов и наоборот.
Чтобы решить эту проблему, в 2003 году Филлип Дж. Эби предложил PEP-0333, интерфейс шлюза веб-сервера Python (WSGI). Идея заключалась в том, чтобы предоставить высокоуровневый универсальный интерфейс между приложениями Python и веб-серверами.
В 2003 году PEP-3333 обновил интерфейс WSGI, добавив поддержку Python 3. В настоящее время почти все фреймворки Python используют WSGI как средство, если не единственное, для связи со своими веб-серверами. Так это делают Django, Flask и многие другие популярные фреймворки.
Цель этой статьи — дать читателю представление о том, как работает WSGI, и позволить читателю создать простое приложение или сервер WSGI. Однако она не претендует на исчерпывающую полноту, и разработчикам, намеревающимся реализовать готовые к работе серверы или приложения, следует более внимательно изучить спецификацию WSGI.
Интерфейс Python WSGI
WSGI определяет простые правила, которым должны соответствовать сервер и приложение. Давайте начнем с рассмотрения этого общего шаблона.
Интерфейс приложения
В Python 3.5 интерфейсы приложений выглядят следующим образом:
def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]
В Python 2.7 этот интерфейс мало чем отличался; единственное изменение будет заключаться в том, что тело будет представлено объектом str
, а не bytes
.
Хотя в данном случае мы использовали функцию, подойдет любой вызываемый объект. Правила для объекта приложения здесь следующие:
- Должен быть вызываемым с параметрами
environ
иstart_response
. - Перед отправкой тела необходимо вызвать обратный вызов
start_response
. - Должен возвращать итерируемый объект с частями тела документа.
Другой пример объекта, удовлетворяющего этим правилам и производящего тот же эффект:
class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body
Интерфейс сервера
Сервер WSGI может взаимодействовать с этим приложением следующим образом:
def write(chunk): '''Write data back to client''' ... def send_status(status): '''Send HTTP status code''' ... def send_headers(headers): '''Send HTTP headers''' ... def start_response(status, headers): '''WSGI start_response callable''' send_status(status) send_headers(headers) return write # Make request to application response = application(environ, start_response) try: for chunk in response: write(chunk) finally: if hasattr(response, 'close'): response.close()
Как вы могли заметить, вызываемый объект start_response
возвращал вызываемый объект write
, который приложение может использовать для отправки данных обратно клиенту, но который не использовался в нашем примере кода приложения. Этот интерфейс write
устарел, и пока мы можем его игнорировать. Он будет кратко рассмотрен далее в статье.
Еще одной особенностью обязанностей сервера является вызов необязательного метода close
для итератора ответа, если он существует. Как указано в статье Грэма Дамплтона здесь, это часто упускаемая из виду функция WSGI. Вызов этого метода, если он существует , позволяет приложению освободить все ресурсы, которые оно еще может удерживать.
Аргумент среды Application environ
Параметр environ
должен быть объектом словаря. Он используется для передачи запроса и информации о сервере приложению, почти так же, как это делает CGI. Фактически все переменные среды CGI допустимы в WSGI, и сервер должен передавать все, что относится к приложению.
Хотя существует множество необязательных ключей, которые можно передать, некоторые из них являются обязательными. Возьмем в качестве примера следующий GET
-запрос:
$ curl 'http://localhost:8000/auth?user=obiwan&token=123'
Это ключи, которые должен предоставить сервер, и значения, которые они будут принимать:
Ключ | Стоимость | Комментарии |
---|---|---|
REQUEST_METHOD | "GET" | |
SCRIPT_NAME | "" | зависит от настройки сервера |
PATH_INFO | "/auth" | |
QUERY_STRING | "token=123" | |
CONTENT_TYPE | "" | |
CONTENT_LENGTH | "" | |
SERVER_NAME | "127.0.0.1" | зависит от настройки сервера |
SERVER_PORT | "8000" | |
SERVER_PROTOCOL | "HTTP/1.1" | |
HTTP_(...) | Заголовки HTTP, предоставленные клиентом | |
wsgi.version | (1, 0) | кортеж с версией WSGI |
wsgi.url_scheme | "http" | |
wsgi.input | Файлоподобный объект | |
wsgi.errors | Файлоподобный объект | |
wsgi.multithread | False | True , если сервер многопоточный |
wsgi.multiprocess | False | True , если сервер запускает несколько процессов |
wsgi.run_once | False | True , если сервер ожидает, что этот сценарий запустится только один раз (например, в среде CGI). |
Исключением из этого правила является то, что если один из этих ключей должен быть пустым (например, CONTENT_TYPE
в приведенной выше таблице), то они могут быть исключены из словаря, и будет предполагаться, что они соответствуют пустой строке.
wsgi.input
и wsgi.errors
Большинство ключей wsgi.input
environ
который должен содержать поток с телом запроса от клиента, и wsgi.errors
, где приложение сообщает обо всех обнаруженных ошибках. Ошибки, отправленные из приложения в wsgi.errors
, обычно отправляются в журнал ошибок сервера.
Эти два ключа должны содержать файловые объекты; то есть объекты, которые предоставляют интерфейсы для чтения или записи в виде потоков, точно так же, как объект, который мы получаем, когда открываем файл или сокет в Python. Поначалу это может показаться сложным, но, к счастью, Python предоставляет нам хорошие инструменты для решения этой задачи.
Во-первых, о каких потоках идет речь? Согласно определению WSGI, wsgi.input
и wsgi.errors
должны обрабатывать объекты bytes
в Python 3 и объекты str
в Python 2. В любом случае, если мы хотим использовать буфер в памяти для передачи или получения данных через WSGI интерфейс, мы можем использовать класс io.BytesIO
.

Например, если мы пишем сервер WSGI, мы можем предоставить тело запроса приложению следующим образом:
- Для Питона 2.7
import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
- Для Питона 3.5
import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)
На стороне приложения, если бы мы хотели преобразовать полученный поток ввода в строку, мы бы написали что-то вроде этого:
- Для Питона 2.7
readstr = environ['wsgi.input'].read() # returns str object
- Для Питона 3.5
readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object
Поток wsgi.errors
следует использовать для сообщения об ошибках приложения на сервер, а строки должны заканчиваться символом \n
. Веб-сервер должен позаботиться о преобразовании строки в другое окончание в зависимости от системы.
Аргумент start_response
вызываемого приложения
Аргумент start_response
должен быть вызываемым с двумя обязательными аргументами, а именно status
и headers
, и одним необязательным аргументом exc_info
. Оно должно быть вызвано приложением до того, как какая-либо часть тела будет отправлена обратно на веб-сервер.
В первом примере приложения в начале этой статьи мы вернули тело ответа в виде списка, и поэтому мы не можем контролировать, когда список будет перебираться. Из-за этого нам пришлось вызвать start_response
перед возвратом списка.
Во втором случае мы вызвали start_response
непосредственно перед получением первой (и в данном случае единственной) части тела ответа. Любой из способов допустим в рамках спецификации WSGI.
Со стороны веб-сервера вызов start_response
должен фактически не отправлять заголовки клиенту, а задерживать его до тех пор, пока в теле ответа не будет хотя бы одна непустая строка байтов для отправки обратно клиенту. Эта архитектура позволяет правильно сообщать об ошибках до самого последнего возможного момента выполнения приложения.
status
Аргумент start_response
Аргумент status
, передаваемый обратному вызову start_response
, должен быть строкой, состоящей из кода состояния HTTP и описания, разделенных одним пробелом. Допустимые примеры: '200 OK'
или '404 Not Found'
.
headers
Аргумент start_response
Аргумент headers
, передаваемый обратному вызову start_response
, должен быть list
tuple
Python, где каждый кортеж состоит из (header_name, header_value)
. И имя, и значение каждого заголовка должны быть строками (независимо от версии Python). Это редкий пример, когда тип имеет значение, так как это действительно требуется спецификацией WSGI.
Вот правильный пример того, как может выглядеть аргумент header
:
response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]
Заголовки HTTP нечувствительны к регистру, и если мы пишем веб-сервер, совместимый с WSGI, это следует учитывать при проверке этих заголовков. Кроме того, список заголовков, предоставляемых приложением, не должен быть исчерпывающим. Сервер несет ответственность за обеспечение существования всех необходимых заголовков HTTP перед отправкой ответа обратно клиенту, заполняя любые заголовки, не предоставленные приложением.
Аргумент exc_info
для start_response
Обратный вызов start_response
должен поддерживать третий аргумент exc_info
, используемый для обработки ошибок. Правильное использование и реализация этого аргумента имеет первостепенное значение для производственных веб-серверов и приложений, но выходит за рамки этой статьи.
Дополнительную информацию об этом можно получить в спецификации WSGI здесь.
Возвращаемое значение start_response
— обратный вызов write
В целях обратной совместимости веб-серверы, реализующие WSGI, должны возвращать возможность write
. Этот обратный вызов должен позволить приложению записывать данные ответа тела непосредственно обратно клиенту, а не передавать их серверу через итератор.
Несмотря на его присутствие, это устаревший интерфейс, и новые приложения должны воздерживаться от его использования.
Создание тела ответа
Приложения, реализующие WSGI, должны генерировать тело ответа, возвращая итерируемый объект. Для большинства приложений тело ответа не очень большое и легко помещается в памяти сервера. В этом случае наиболее эффективный способ отправки — все сразу, с итерацией, состоящей из одного элемента. В особых случаях, когда загрузка всего тела в память невозможна, приложение может возвращать его по частям через этот итерируемый интерфейс.
Здесь есть только небольшая разница между WSGI Python 2 и Python 3: в Python 3 тело ответа представлено bytes
объектами; в Python 2 правильный тип для этого — str
.
Преобразование строк UTF-8 в bytes
или str
— простая задача:
- Питон 3.5:
body = 'unicode stuff'.encode('utf-8')
- Питон 2.7:
body = u'unicode stuff'.encode('utf-8')
Если вы хотите узнать больше об обработке юникода и байтовых строк в Python 2, на YouTube есть хороший учебник.
Веб-серверы, реализующие WSGI, также должны поддерживать обратный вызов write
для обратной совместимости, как описано выше.
Тестирование вашего приложения без веб-сервера
Понимая этот простой интерфейс, мы можем легко создавать сценарии для тестирования наших приложений без необходимости запуска сервера.
Возьмем, к примеру, этот небольшой скрипт:
from io import BytesIO def get(app, path = '/', query = ''): response_status = [] response_headers = [] def start_response(status, headers): status = status.split(' ', 1) response_status.append((int(status[0]), status[1])) response_headers.append(dict(headers)) environ = { 'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), } response_body = app(environ, start_response) merged_body = ''.join((x.decode('utf-8') for x in response_body)) if hasattr(response_body, 'close'): response_body.close() return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body}
Таким образом, мы могли бы, например, инициализировать некоторые тестовые данные и имитировать модули в нашем приложении и выполнять вызовы GET
, чтобы проверить, отвечает ли оно соответствующим образом. Мы видим, что это не настоящий веб-сервер, но он взаимодействует с нашим приложением аналогичным образом, предоставляя приложению обратный вызов start_response
и словарь с нашими переменными среды. В конце запроса он использует итератор тела ответа и возвращает строку со всем ее содержимым. Подобные методы (или общий) могут быть созданы для разных типов HTTP-запросов.
Заворачивать
В этой статье мы не касались того, как WSGI обрабатывает загрузку файлов, так как это можно было бы считать более «продвинутой» функцией, не подходящей для вводной статьи. Если вы хотите узнать об этом больше, посмотрите раздел PEP-3333, посвященный обработке файлов.
Я надеюсь, что эта статья поможет лучше понять, как Python взаимодействует с веб-серверами, и позволит разработчикам использовать этот интерфейс интересными и творческими способами.
Благодарности
Я хотел бы поблагодарить моего редактора Ника МакКри за помощь в написании этой статьи. Благодаря его работе исходный текст стал намного четче, а некоторые ошибки не остались неисправленными.