Домой Edit me on GitHub

2020-12-05

Каналы передачи данных | Сетевое программирование | Базы данных | Основы Веб-программирования

WSGI (pep-333)

../_images/wsgi.svg

WSGI — стандарт взаимодействия между Python-программой, выполняющейся на стороне сервера, и самим веб-сервером, например Apache.

Идея:

В Python существует большое количество различного рода веб-фреймворков, тулкитов и библиотек. У каждого из них собственный метод установки и настройки, они не умеют взаимодействовать между собой. Это может стать затруднением для тех, кто только начинает изучать Python, так как, например, выбор определённого фреймворка может ограничить выбор веб-сервера и наоборот.

WSGI предоставляет простой и универсальный интерфейс между большинством веб-серверов и веб-приложениями или фреймворками.

../_images/wsgi_ianb.png

Пример работы WSGI (автор Ян Бикинг)

Application

По стандарту, WSGI-приложение должно удовлетворять следующим требованиям:

  • должно быть вызываемым (callable) объектом (обычно это функция или метод)
  • принимать два параметра:
    • словарь переменных окружения (environ)
    • обработчик запроса (start_response)
  • вызывать обработчик запроса с кодом HTTP-ответа и HTTP-заголовками
  • возвращать итерируемый объект с телом ответа

Простейшим примером WSGI-приложения может служить такая функция-генератор:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def simple_app(environ, start_response):
    """
    (dict, callable( status: str,
                     headers: list[(header_name: str, header_value: str)]))
                  -> body: iterable of strings
    """
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return 'Hello world!\n'
../_images/wsgi-app.png

WSGI-приложение

или то же самое в виде класса:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class AppClass(object):
    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"

Server/Gateway

Чтобы запустить наше WSGI приложение, нужен WSGI сервер. Он запускает WSGI приложение один раз при каждом HTTP запросе от клиента.

Задачи WSGI сервера:

  • Сформировать переменные окружения (environment)
  • Описать функцию обработчик запроса (start_response)
  • Передать их в WSGI приложение
  • Результат WSGI сервер отправляет по HTTP клиенту
  • а WSGI шлюз приводит к формату клиент-серверного протокола (CGI, FastCGI, SCGI, uWSGI, …) и передает их на Веб-сервер (например выводит в stdout, stderr).
../_images/server-app.png

WSGI-сервер

Пример WSGI-шлюза к CGI-серверу.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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:
            # Before the first output, send the stored headers
            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:
                    # Re-raise original exception if headers sent
                    raise Exception(exc_info[0], exc_info[1], exc_info[2])
            finally:
                exc_info = None     # avoid dangling circular ref
        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:    # don't send headers until body appears
                write(data)
        if not headers_sent:
            write('')   # send headers now if body was empty
    finally:
        if hasattr(result, 'close'):
            result.close()

Environment

Всегда словарь

  • environ это обычно копия переменных окружения ОС os.environ + стандартные CGI переменные.

    SCRIPT_NAME - содержит имя вызванного скрипта. Например: myapp.py

    PATH_INFO - путь к файлу /cgi-bin/myapp.py

  • также включает в себя дополнительные WSGI-специфичные переменные, наиболее важные из них:

    wsgi.input - представляет тело (body) HTTP запроса.

    wsgi.errors - указывает поток куда нужно выводить ошибки.

    wsgi.url_scheme - это просто «http» или «https».

start_response

Функция start_response принимает два обязательных аргумента:

  • status - строка содержащая статус HTTP ответа, например 200 OK.
  • response_headers - список кортежей, которые содержат заголовки ответа, например [('Content-Type', 'text/html'), ('Content-Length', '15').
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def simple_app(environ, start_response):
    """
    (dict, callable( status: str,
                     headers: list[(header_name: str, header_value: str)]))
                  -> body: iterable of strings
    """
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return 'Hello world!\n'

start_response возвращает вызываемый объект, обычно «write». write выводит тело ответа в поток вывода, используется при необычных обстоятельствах.

Предупреждение

Обратный вызов write плохо поддерживается серверами и веб-фреймворками, поэтому рекомендуется проектировать свои приложения без его вызова.

Обычно данные возвращаются таким образом:

def application(environ, start_response):
    start_response(status, headers)
    return ['content block 1',
            'content block 2',
            'content block 3']

Но можно делать и так:

def application(environ, start_response):
    write = start_response(status, headers)
    write('content block 1')
    return ['content block 2',
            'content block 3']

Запуск нашего приложения через WSGI-шлюз к CGI

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.

"""
Example from pep-333
"""
from cgi_gateway import run_with_cgi


def simple_app(environ, start_response):
    """
    (dict, callable( status: str,
                     headers: list[(header_name: str, header_value: str)]))
                  -> body: iterable of strings
    """
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return 'Hello world!\n'


class AppClass(object):
    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"


if __name__ == '__main__':
    run_with_cgi(AppClass)

Результат выполнения:

$ python 1.cgi.app.py
Status: 200 OK
Content-type: text/plain

Hello world!

Примечание

В исходных кодах к лекциям cgiserver.py делает этот пример доступным по адресу http://localhost:8000/wsgi/1.cgi.app.py

Middleware

../_images/server-middleware-app.png

WSGI-middleware

Помимо приложений и серверов, стандарт дает определение middleware-компонентов, предоставляющих интерфейсы как приложению, так и серверу. То есть для сервера middleware является приложением, а для приложения сервером. Это позволяет составлять «цепочки» WSGI-совместимых middleware.

Middleware могут брать на себя следующие функции (но не ограничиваются этим):

  • обработка сессий
  • аутентификация/авторизация
  • управление URL (маршрутизация запросов)
  • балансировка нагрузки
  • пост-обработка выходных данных (например, проверка на валидность)

Мы рассмотрим пример приложения, которое считает количество обращений и использует следующие middleware:

  • Обработчик исключений
  • Сессии
  • Сжатие Gzip
  • Пони
../_images/wsgi_as_onion.svg

Приложение

../_images/wsgi_as_onion_app.png
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def app(environ, start_response):
    # Except error
    if 'error' in environ['PATH_INFO'].lower():
        raise Exception('Detect "error" in URL path')

    # Session
    session = environ.get('paste.session.factory', lambda: {})()
    if 'count' in session:
        count = session['count']
    else:
        count = 1
    session['count'] = count + 1

    # Generate response
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [b'You have been here %d times!\n' % count, ]

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

../_images/wsgi_example.png

Т.к. протокол HTTP не сохраняет предыдущего состояния, то при обновлении страницы число не увеличится. Чтобы это произошло, нужно реализовать механизм сессий.

Обработчик исключений

../_images/wsgi_as_onion_evalexception.png
1
2
from paste.evalexception.middleware import EvalException
app = EvalException(app)

EvalException позволяет нам отлавливать ошибки и выводить их в браузере. Если мы перейдем по адресу http://localhost:8000/Errors_500, наше приложение найдет слово error в пути и искусственно вызовет исключение.

../_images/wsgi_example_error.png

Сессии

../_images/wsgi_as_onion_session.png
1
2
from paste.session import SessionMiddleware
app = SessionMiddleware(app)

SessionMiddleware добавляет cookie клиенту с ключом _SID_ и номером сессии.

Например _SID_=20150313142600-d18ec118fff970ad4fb3628fbf530bc4

Для каждой сессии на сервере в директории /tmp/ (по умолчанию) создается файл с таким же именем.

$ tree /tmp/
/tmp/
|-- 20150313094744-5d2e448000e6312d7c0b8a02ed954d22
`-- 20150313142600-d18ec118fff970ad4fb3628fbf530bc4

1 directory, 2 files

В этот файл записывается значение count для нашей сессии. При каждом обращении клиента SessionMiddleware находит файл с таким же именем как у cookie _SID_ десереализует объекты в нем и присваивает переменной окружения paste.session.factory. Таким образом мы можем хранить состояние сессии и при каждом обновлении будет отдаваться значение, увеличенное на 1.

../_images/wsgi_example_count.png

Сжатие Gzip

../_images/wsgi_as_onion_gzip.png
1
2
from paste.gzipper import middleware as GzipMiddleware
app = GzipMiddleware(app)

GzipMiddleware сжимает ответ методом gzip

../_images/wsgi_example_gzip.png

Pony

../_images/wsgi_as_onion_pony.png
1
2
from paste.pony import PonyMiddleware
app = PonyMiddleware(app)

Это самое важное расширение в WSGI. Доступно по адресу http://localhost:8000/pony.

../_images/wsgi_example_pony.png

Полный пример

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#! /usr/bin/env nix-shell
#! nix-shell -i python3 -p python3 python3Packages.paste
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2015 uralbash <root@uralbash.ru>
#
# Distributed under terms of the MIT license.

"""
3.http.middleware.py
"""

from paste.evalexception.middleware import EvalException
from paste.gzipper import middleware as GzipMiddleware
from paste.pony import PonyMiddleware
from paste.session import SessionMiddleware


def app(environ, start_response):
    # Except error
    if 'error' in environ['PATH_INFO'].lower():
        raise Exception('Detect "error" in URL path')

    # Session
    session = environ.get('paste.session.factory', lambda: {})()
    if 'count' in session:
        count = session['count']
    else:
        count = 1
    session['count'] = count + 1

    # Generate response
    start_response('200 OK', [('Content-Type', 'text/plain')])
    return [b'You have been here %d times!\n' % count, ]


app = EvalException(app)   # go to http://localhost:8000/Errors
app = SessionMiddleware(app)
app = GzipMiddleware(app)
app = PonyMiddleware(app)  # go to http://localhost:8000/pony

if __name__ == '__main__':
    from paste import reloader
    from paste.httpserver import serve

    reloader.install()
    serve(app, host='0.0.0.0', port=8000)

Свой middleware

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class GoogleRefMiddleware(object):
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        environ['google'] = False
        if 'HTTP_REFERER' in environ:
            if environ['HTTP_REFERER'].startswith('http://google.com'):
                environ['google'] = True
        return self.app(environ, start_response)

app = GoogleRefMiddleware(app)

GoogleRefMiddleware добавляет переменную окружения google, и если бы мы перешли на наш сайт из поиска google.com, тo это значение было бы True.

Кто использует WSGI?

  • BlueBream
  • bobo
  • Bottle
  • CherryPy
  • Django
  • Eventlet
  • Flask
  • Google App Engine’s webapp2
  • Gunicorn
  • prestans
  • mod_wsgi для Apache
  • MoinMoin
  • netius
  • Plone
  • Pylons
  • Pyramid
  • repoze
  • restlite
  • Tornado
  • Trac
  • TurboGears
  • Uliweb
  • webpy
  • Falcon
  • web2py
  • weblayer
  • Werkzeug
  • Zope
  • и многие другие

Аналоги

  • Rack – Ruby web server interface
  • PSGI – Perl Web Server Gateway Interface
  • JSGI – JavaScript web server gateway interface
  • WAI - Web Application Interface (Haskell)
  • Ring - Clojure
Previous: Встроенный сервер Next: Веб-программирование