Дмитрий Васильев
WSGI – протокол связи веб-сервера
с Python-приложением
Если вы разрабатываете веб-приложение, каркас для разработки веб-приложений или даже веб-сервер на языке Python, вам необходимо знание основ протокола WSGI – стандартного способа связи веб-сервера и веб-приложения.
Долгое время пользователи многих веб-приложений, написанных на Python, были ограничены в выборе веб-серверов, которые они могли использовать совместно с приложениями. Разработчики приложений обычно ограничивались поддержкой одного (изредка – нескольких) способа подключения к веб-серверу. Одни приложения могли использовать CGI, или FastCGI, другие могли быть привязаны к модулю Apache mod_python. Некоторые из приложений могли поддерживать только API, специфичное для одного-единственного сервера. Такая ситуация затрудняла распространение веб-приложений, написанных на Python, и в конце 2003 года впервые был предложен протокол WSGI.
Описание протокола
WSGI (расшифровывается как Web Server Gateway Interface – интерфейс шлюза веб-сервера) – это простой и универсальный интерфейс взаимодействия между веб-сервером и веб-приложением, впервые описанный в PEP-333 (http://www.python.org/dev/peps/pep-0333). Под простотой в данном случае подразумевается лишь простота подключения приложения, но не простота реализации веб-приложений для авторов. Надо заметить, что основной целью разработки WSGI была разработка простого протокола, который мог бы разделить выбор каркасов для разработки веб-приложений от выбора веб-серверов. Это, в частности, позволяет разработчикам приложений (каркасов) и серверов концентрироваться на своей области специализации и отличает WSGI от более общих протоколов связи приложений с веб-серверами, таких как CGI, или FastCGI. С точки зрения WSGI цельное веб-приложение делится на две части: сервер (или шлюз) и непосредственно приложение (или каркас для построения приложений). Для обращения к приложению серверная часть использует вызываемый объект (это может быть функция, метод, класс или экземпляр класса с методом __call__). WSGI также позволяет создавать приложения-посредники, которые являются приложением для веб-сервера и сервером для веб-приложения. Такие посредники могут использоваться для предварительной обработки запросов к приложению или последующей обработки его ответов. Дальше мы рассмотрим несколько примеров использования WSGI и затем обратимся к деталям протокола.
Сторона приложения
Как уже говорилось, приложение – это вызываемый объект, который принимает два аргумента. Приложения должны допускать возможность многократных вызовов, что является обычной ситуацией практически для всех серверов (исключая вызовы с помощью CGI). Далее представлены два примера приложения. Первое приложение реализовано в виде функции:
def simple_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type','text/plain')]
start_response(status, response_headers)
return ['Hello world!\n']
Здесь функция использует второй аргумент для передачи статуса и заголовков ответа и затем возвращает тело ответа в виде списка строк. Второе приложение реализовано в виде класса:
class AppClass:
def __init__(self, environ, start_response):
self.environ = environ
self.start = start_response
def __iter__(self):
status = '200 OK'
response_headers = [('Content-type','text/plain')]
self.start(status, response_headers)
yield "Hello world!\n"
В данном случае объект класса будет представлять из себя итератор, который на первом шаге итерации использует второй аргумент для передачи статуса и заголовков ответа и затем вернет тело ответа.
Надо отметить, что приложение с точки зрения WSGI – это всего лишь точка входа, через которую сервер получает доступ к веб-приложению или каркасу для построения веб-приложений.
Сторона сервера
Сервер (или шлюз) будет вызывать приложение для каждого HTTP-запроса, который ему предназначен. Для примера представлен упрощенный шлюз CGI – WSGI. Пример использует упрощенную обработку ошибок, т.к. по умолчанию ошибки будут выдаваться на sys.stderr и затем записываться в лог веб-сервера. Вызываемый объект приложения в данном случае передается как параметр функции:
import osimport sys def run_with_cgi(application): environ = dict(os.environ.items()) environ['wsgi.input'] = sys.stdin environ['wsgi.errors'] = sys.stderr environ['wsgi.version'] = (1, 0) environ['wsgi.multithread'] = False environ['wsgi.multiprocess'] = True environ['wsgi.run_once'] = True if environ.get('HTTPS', 'off') in ('on', '1'): environ['wsgi.url_scheme'] = 'https' else: environ['wsgi.url_scheme'] = 'http' headers_set = [] headers_sent = [] def write(data): if not headers_set: raise AssertionError("write() before start_response()") elif not headers_sent: # Перед выводом первых данных вывести # сохраненные заголовки status, response_headers = headers_sent[:] = headers_set sys.stdout.write('Status: %s\r\n' % status) for header in response_headers: sys.stdout.write('%s: %s\r\n' % header) sys.stdout.write('\r\n') sys.stdout.write(data) sys.stdout.flush() def start_response(status, response_headers, exc_info=None): if exc_info: try: if headers_sent: # Если заголовки были отправлены, # выкинуть исключение raise exc_info[0], exc_info[1], exc_info[2] finally: exc_info = None elif headers_set: raise AssertionError("Headers already set!") headers_set[:] = [status, response_headers] return write result = application(environ, start_response) try: for data in result: # Не отправляем заголовки, пока не видно тела if data: write(data) if not headers_sent: # Отправляем заголовки, если тело было пустое write('') finally: if hasattr(result, 'close'): result.close()
Посредник: сервер и приложение в одном
Как уже было замечено, некоторые объекты могут играть сразу две роли – быть сервером для какого-либо приложения и приложением для сервера. Вот примеры ситуаций, для которых могут быть полезны WSGI-посредники:
- перенаправление запроса на различные приложения, в зависимости от URL после соответствующего изменения environ;
- возможность запуска нескольких веб-приложений, или каркасов в одном процессе;
- распределение нагрузки по нескольким сетевым приложениям;
- обработка ответов приложения, например, с помощью XSL.
Присутствие посредника в большинстве случаев прозрачно и для сервера, и для приложения, более того, посредников можно располагать одного за другим, составляя таким образом «стек посредников».
Детали протокола
Как мы уже видели, приложение должно принимать два аргумента, которые были названы environ и start_response, но могут иметь любые другие имена.
Первый параметр (environ) должен быть объектом словаря (dict) Python и содержит переменные среды, похожие на переменные CGI. Этот объект также должен содержать обязательные для WSGI параметры, которые мы подробнее рассмотрим позже, и может содержать переменные, специфичные для конкретного веб-сервера.
Второй параметр (start_response) – это вызываемый объект, которым приложение предваряет возвращение тела ответа, и принимающий два обязательных параметра и один необязательный. Первый параметр (status) – статус ответа в виде строки, например «200 Ok». Второй параметр (response_headers в примере выше) – список кортежей (tuples) вида (имя_заголовка, значение_заголовка). Третий, необязательный, параметр (exc_info) должен использоваться только при обработке ошибок и должен быть кортежем, который возвращает функция sys.exc_info(). Надо заметить, что start_response не посылает заголовки сразу, а откладывает их до получения первой части тела ответа, чтобы в случае ошибки их можно было заменить на заголовки, сопутствующие ошибке. При этом start_response можно вызывать несколько раз, только если передается третий параметр (exc_info).
После вызова сервером вызываемый объект приложения должен вернуть итерируемый объект, возвращающий ноль, или несколько строк. Как мы видели в предыдущих примерах выше, этого можно достичь несколькими способами, например, вернув список строк, или объект-итератор. При получении очередной части ответа сервер посылает ее клиенту без буферизации, но приложения могут осуществлять буферизацию ответа собственными силами.
В случае если итерируемый объект имеет метод close(), он будет вызван по окончании обработки ответа сервером, даже в случае ошибки. Таким образом метод close() может использоваться для закрытия всех ресурсов приложения, которые могли быть задействованы при создании ответа.
Переменные словаря environ
Словарь environ может содержать следующие CGI переменные (см. таблицу 1).
Таблица 1. Переменные, которые может содержать словарь environ
Имя
|
Наличие
|
Описание
|
REQUEST_METHOD
|
Обязательный
|
Метод запроса, например GET или POST
|
SCRIPT_NAME
|
Может быть пустым
|
Начальная порция пути в URL, соответствующая объекту приложения
|
PATH_INFO
|
Может быть пустым
|
Остаток пути в URL, соответствующий цели запроса внутри приложения
|
QUERY_STRING
|
Может быть пустым или отсутствовать
|
Часть URL, которая следует за «?»
|
CONTENT_TYPE
|
Может быть пустым или отсутствовать
|
Содержимое заголовка Content-Type в HTTP-запросе
|
CONTENT_LENGTH
|
Может быть пустым или отсутствовать
|
Содержимое заголовка Content‑Length в HTTP-запросе
|
SERVER_NAME
|
Обязательный
|
Имя сервера
|
SERVER_PORT
|
Обязательный
|
Порт сервера
|
SERVER_PROTOCOL
|
Обязательный
|
Версия протокола, который использует клиент для посылки запроса, например HTTP/1.0 или HTTP/1.1
|
HTTP_переменные
|
Необязательны
|
Переменные, соответствующие заголовкам запроса, переданным клиентом
|
Также словарь environ должен содержать следующие, обязательные для WSGI, переменные (см. таблицу 2).
Таблица 2. Обязательные для WSGI переменные в словаре environ
Имя
|
Описание
|
wsgi.version
|
Кортеж (1, 0), представляющий версию WSGI – 1.0
|
wsgi.url_scheme
|
Строка, представляющая схему из URL, обычно http или https
|
wsgi.input
|
Объект, похожий на файл, из которого может быть прочитано тело запроса
|
wsgi.output
|
Объект, похожий на файл, в который приложение может выводить сообщения об ошибках
|
wsgi.multithread
|
True, если объект приложения может быть одновременно вызван из нескольких потоков
|
wsgi.multiprocess
|
True, если соответствующие объекты приложения могут быть одновременно вызваны в нескольких процессах
|
wsgi.run_once
|
True, если сервер предполагает (но не гарантирует), что приложение будет вызвано только один раз во время жизни текущего процесса
|
Плюс environ может содержать специфичные для конкретного сервера переменные и переменные среды. Чтобы увидеть набор переменных, передаваемых приложению, можно использовать, например, следующее простое WSGI-приложение:
def application(environ, start_response):
lines = []
for key, value in environ.items():
lines.append("%s: %r" % (key, value))
start_response("200 OK", [("Content-Type", "text/plain")])
return ["\n".join(lines)]
Обработка ошибок
В общем случае приложение должно обрабатывать внутренние ошибки и выводить соответствующее сообщение клиенту. Конечно, прежде чем выводить сообщение об ошибке, приложение не должно начинать выводить нормальный ответ. Чтобы обойти эту ситуацию, используется третий параметр start_response – exc_info:
try:
# Код обычного приложения
status = "200 OK"
response_headers = [("content-type", "text/plain")]
start_response(status, response_headers)
return ["OK"]
except:
# В реальном коде различные ошибки должны обрабатываться
# различными обработчиками и не должен использоваться пустой except
status = "500 Error"
response_headers = [("content-type", "text/plain")]
start_response(status, response_headers, sys.exc_info())
return ["Error"]
Если приложение еще не начало вывод тела ответа, когда произошло исключение, то вызов start_response в обработчике будет нормальным и клиенту вернется ответ об ошибке. В случае если на момент ошибки уже был начат вывод ответа, start_response выкинет переданное исключение. Это исключение не должно обрабатываться обработчиком, а будет обработано сервером и записано в журнал ошибок.
Заключение
На данный момент подавляющее большинство серверов и каркасов для создания веб-приложений поддерживает WSGI. В качестве примера можно привести большое количество серверов, написанных на Python, в том числе веб-сервер из пакета Twisted (http://twistedmatrix.com/trac), а также адаптеры для CGI и FastCGI, модули для Apache и nginx. В качестве примера каркасов могут быть наиболее известные – Django, TurboGears и Pylons. Более полный список и последнюю информацию по WSGI можно получить на сайте http://www.wsgi.org.
Описание работы WSGI, изложенное в этой статье, должно помочь в лучшем понимании готовых веб-приложений, написанных с использованием этого протокола, а также в написании собственных веб-приложений и серверов.
Приложение
Пакет wsgiref
В стандартной библиотеке Python 2.5 появился новый пакет wsgiref, предоставляющий различные утилиты для упрощения работы с WSGI. Рассмотрим кратко его содержимое:
- wsgiref.util – содержит функции для работы со словарем environ. Например, функцию для сборки полного URL из переменных environ;
- wsgiref.headers – содержит класс для упрощения работы с заголовками ответа в виде объекта, похожего на словарь;
- wsgiref.simple_server – содержит функции и классы для создания простого WSGI-сервера и демонстрационного приложения;
- wsgiref.validate – содержит функцию-обертку, проверяющую WSGI-приложение и сервер на соответствие WSGI-спецификации;
- wsgiref.handlers – содержит базовые классы для создания WSGI-серверов.