python статьи

WSGI - протокол связи Web-сервера с Python приложением

Dmitry Vasiliev 23:21, 2012 2 6 19:41, 2009 3 5

Впервые было опубликовано в журнале "Системный администратор" #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-приложений и серверов.