Впервые было опубликовано в журнале "Системный администратор" #12 за 2008 год
Если вы разрабатываете Web-приложение, каркас для разработки Web-приложений, или даже Web-сервер на Python вам просто необходимо знание основ протокола WSGI — стандартного способа связи Web-сервера и Web-приложения.
Долгое время пользователи многих Web-приложений написанных на Python были ограничены в выборе Web-серверов которые они могли использовать совместно с приложениями. Разработчики приложений обычно ограничивались поддержкой одного (изредка - нескольких) способа подключения к Web-серверу. Одни приложения могли использовать CGI, или FastCGI, другие могли быть привязаны к модулю Apache mod_python. Некоторые из приложений могли поддерживать только API специфичное для одного, единственного, сервера. Такая ситуация затрудняла распространение Web-приложений написанных на Python и в конце 2003-го года впервые был предложен протокол WSGI.
Описание протокола
WSGI (расшифровывается как Web Server Gateway Interface — интерфейс шлюза Web-сервера) — это простой и универсальный интерфейс взаимодействия между Web-сервером и Web-приложением, впервые описанный в PEP-333 (http://www.python.org/dev/peps/pep-0333/). Под простотой в данном случае подразумевается лишь простота подключения приложения, но не простота реализации Web-приложений для авторов. Надо заметить, что основной целью разработки WSGI была разработка простого протокола, который бы мог разделить выбор каркасов для разработки Web-приложений от выбора Web-серверов. Это, в частности, позволяет разработчикам приложений (каркасов) и серверов концентрироваться на своей области специализации и отличает WSGI от более общих протоколов связи приложений с Web-серверами, таких как CGI, или FastCGI. С точки зрения WSGI цельное Web-приложение делится на две части: сервер (или шлюз) и непосредственно приложение (или каркас для построения приложений). Для обращения к приложению серверная часть использует вызываемый объект (это может быть функция, метод, класс, или экземпляр класса с методом __call__). WSGI также позволяет создавать приложения-посредники которые являются приложением для Web-сервера и сервером для Web-приложения. Такие посредники могут использоваться для предварительной обработки запросов к приложению, или последующей обработки его ответов. Ниже мы рассмотрим несколько примеров использования 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 — это всего лишь точка входа через которую сервер получает доступ к Web-приложению, или каркасу для построения Web-приложений.
Сторона сервера
Сервер (или шлюз) будет вызывать приложение для каждого HTTP запроса который ему предназначен. Для примера ниже представлен упрощенный шлюз CGI — WSGI. Пример использует упрощенную обработку ошибок т.к. по умолчанию ошибки будут выдаваться на sys.stderr и затем записываться в лог Web-сервера. Вызываемый объект приложения в данном случае передается как параметр функции:
import os import 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;
- Возможность запуска нескольких Web-приложений, или каркасов в одном процессе;
- Распределение нагрузки по нескольким сетевым приложениям;
- Обработка ответов приложения, например с помощью XSL;
Присутствие посредника в большинстве случае прозрачно и для сервера и для приложения, более того, посредники можно располагать один за другим, составляя таким образом «стек посредников».
Детали протокола
Как мы уже видели приложение должно принимать два аргумента, которые были названы environ и start_response, но могут иметь любые другие имена.
Первый параметр (environ) должен быть объектом словаря (dict) Python и содержит переменные среды похожие на переменные CGI. Этот объект также должен содержать обязательные для WSGI параметры, которые мы подробнее рассмотрим ниже, и может содержать переменные специфичные для конкретного Web-сервера.
Второй параметр (start_response) — это вызываемый объект которым приложение предваряет возвращение тела ответа и принимающий два обязательных параметра и один необязательный. Первый параметр (status) — статус ответа в виде строки, например "200 Ok". Второй параметр (response_headers в примере выше) — список кортежей (tuples) вида (имя_заголовка, значение_заголовка). Третий, необязательный параметр (exc_info) должен использоваться только при обработке ошибок и должен быть кортежем который возвращает функция sys.exc_info(). Надо заметить, что start_response не посылает заголовки сразу, а откладывает их до получения первой части тела ответа, что бы в случае ошибки их можно было заменить на заголовки сопутствующие ошибке. При этом start_response можно вызывать несколько раз только если передается третий параметр (exc_info).
После вызова сервером вызываемый объект приложения должен вернуть итерируемый объект возвращающий ноль, или несколько строк. Как мы видели в примерах выше этого можно достичь несколькими способами, например вернув список строк, или объект-итератор. При получении очередной части ответа сервер посылает ее клиенту без буферизации, но приложения могут осуществлять буферизацию ответа собственными силами.
В случае если итерируемый объект имеет метод close() он будет вызван по окончании обработки ответа сервером, даже в случае ошибки. Таким образом метод close() может использоваться для закрытия всех ресурсов приложения которые могли быть задействованы при создании ответа.
Переменные словаря environ
Словарь environ может содержать следующие CGI переменные: (Переменные которые может содержать словарь 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 переменные: (Обязательные для WSGI переменные в словаре environ)
Имя | Описание |
---|---|
wsgi.version | Кортеж (1, 0) представляющий версию WSGI — 1.0 |
wsgi.url_scheme | Строка представляющая схему из URL, обычно "http", или "https" |
wsgi.input | Объект похожий на файл из которого может быть прочитано тело запроса |
wsgi.errors | Объект похожий на файл в который приложение может выводить сообщения об ошибках |
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 выкинет переданное исключение. Это исключение не должно обрабатываться обработчиком, а будет обработано сервером и записано в журнал ошибок.
Пакет wsgiref
В стандартной библиотеке Python 2.5 появился новый пакет wsgiref, предоставляющий различные утилиты для упрощения работы с WSGI. Рассмотрим кратко его содержимое:
- wsgiref.util — содержит функции для работы со словарем environ. Например функцию для сборки полного URL из переменных environ;
- wsgiref.headers — содержит класс для упрощения работы с заголовками ответа в виде объекта похожего на словарь;
- wsgiref.simple_server — содержит функции и классы для создания простого WSGI сервера и демонстрационного приложения;
- wsgiref.validate — содержит функцию-обертку проверяющую WSGI приложение и сервер на соответствие WSGI спецификации;
- wsgiref.handlers — содержит базовые классы для создания WSGI серверов;
Заключение
На данный момент подавляющее большинство серверов и каркасов для создания Web-приложений поддерживает WSGI. В качестве примера серверов можно привести большое количество серверов написанных на Python, в том числе Web сервер из пакета Twisted (http://twistedmatrix.com/trac/) и также адаптеры для CGI и FastCGI, модули для Apache и Nginx. В качестве примера каркасов могут быть наиболее известные - Django, TurboGears и Pylons. Более полный список и последнюю информацию по WSGI можно получить на сайте http://www.wsgi.org. Описание работы WSGI изложенное в этой статье должно помочь в лучшем понимании готовых Web-приложений написанных с использованием этого протокола и также в написании собственных Web-приложений и серверов.