С аббревиатурой WSGI я столкнулся, когда возникла задача развертывания Django-приложения, а mod_python у меня что-то не захотел работать. И в то время для меня WSGI было неким buzz-word, туманным и далеким. Так или иначе, Django я "завел" при помощи flup и lighttpd, но "виски" засел у меня занозой в мозгу.
Вспомнилась эта заноза не так давно, когда я стал читать блог Бена Бангерта Groovie. Бен апологет WSGI и создатель веб-фреймворка Pylons (надеюсь, у меня будет время рассказать о нем). Отправной точкой в моем "погружении" стали статьи на XML.com. А мотивом к написанию этой статьи стал тот факт, что информации по WSGI на русском просто нет. Что ж, постараюсь восполнить этот пробел.
Теория
WSGI - стандарт обмена данными между веб-сервером (backend) и веб-приложением (frontend). Под это определение попадают многие вещи, тот же самый CGI. Так что поясню.
Во-первых, WSGI - Python-специфичный стандарт, его описывает PEP 333. Во-вторых, он еще не принят (статус Draft, черновик). Эти оговорки для того, чтобы не испытывать лишних иллюзий. Между тем, стандарт нужный и уже используемый. Для меня WSGI - это в первую очередь возможность комбинировать различные back- и frontend’ы.
Теперь, что касается самого стандарта. Он описывает интерфейсы веб-приложения и веб-сервера.
Приложение - принимает в качестве параметров переменные окружения (в виде словаря) и исполняемый объект выполнения запроса. Возвращает итератор.
Сервер - тут чуть сложнее. В переменных окружения, к стандартным переменным веб-сервера, добавляются WSGI-специфичные. Особо останавливаться я на этом не буду, сошлюсь лишь на все тот же PEP 333, где приведен пример. Просто-напросто реализация на стороне сервера меня интересует постольку-поскольку, поэтому на ней не задерживаюсь.
Прослойка, middleware - самое интересное. Middleware "работает" в обе стороны. Т.е. у нее входной и выходной интерфейс идентичны. Я бы провел аналогию с декоратором. Middleware добавляет некую функциональность в исходное веб-приложение, например live debug, или http auth. Причем, можно выстраивать цепочки middleware.
Теперь попробуем все это на практике…
Практика
Итак, погружаемся в WSGI. Пишем простенькое WSGI-приложение:
def app(environ, start_response):
start_response('200 OK', [('Content-type', 'text/plain')])
return ['Hello here']
Все достаточно просто - как и говорилось выше, приложение принимает в качестве аргументов словарь переменных окружения (environ
) и исполняемый объект выполнения запроса (start_response
). Далее, посылаем начало ответа серверу и возвращаем сам ответ в виде итератора (в данном случае - в виде обычного списка).
Теперь встает вопрос о запуске нашего приложения. Для этого воспользуемся библиотекой wsgiref. Счастливчики с Python 2.5 в этом месте широко улыбаются, потому что у них wsgiref
уже есть. Запускаем так:
from wsgiref import simple_server
server = simple_server.WSGIServer(
('', 8080),
simple_server.WSGIRequestHandler,
)
server.set_app(app)
server.serve_forever()
Тоже все достаточно просто - создаем объект сервера со стандартным обработчиком, задаем ему порт 8080 для ожидания соединений, указываем какое WSGI-приложение выполнять и запускаем сервер.
Пока что преимущества WSGI не ощущаются.
Теперь усложним задачу. Попробуем написать такой сервер, который бы работал с произвольным WSGI-приложением, и приложение, которое бы работало с произвольным WSGI-сервером. Что ж, приступим.
В начале определю, что значит "произвольный": скрипту, который реализует тот или иной компонент, передается в качестве аргумента "путь" к другому, парному, компоненту. И пусть они взаимодействуют. Чтобы не усложнять код, я написал маленький модуль, helper
, который и делает всю "машинерию" по преобразованию полного имени компонента (пакет.модуль.объект
) в компонент-объект. Итак, наше "тривиальное приложение" стало выглядеть так:
def app(environ, start_response):
start_response('200 OK', [('Content-type', 'text/html')])
sorted_keys = environ.keys()
sorted_keys.sort()
result = ['<html><body><h1>TrivialWSGIApp in action</h1>'] +
['<p>Sample WSGI application. Just show your environment.</p><p><ul>'] +
['<li> %s => %s</li>' % (str(k), str(environ[k])) for k in sorted_keys] +
['</ul></p></body></html>']
return result
if __name__ == '__main__':
import sys
import helper
server = helper.get_arg(sys.argv, "Usage: trivial_wsgi_app.py package.wsgi.server_callable")
server(app)
Немного "усложнили" приложение - теперь оно показывает доступные переменные окружения, ну и плюс код, запускающий парный компонент - WSGI-сервер, переданный как параметр.
А "тривиальный сервер" стал выглядеть так:
from wsgiref import simple_server, validate
class TrivialWSGIServer(object):
def __init__(self, app):
self.app = app
self.server = simple_server.WSGIServer(
('', 8080),
simple_server.WSGIRequestHandler,
)
self.server.set_app(validate.validator(self.app))
def serve(self):
self.server.serve_forever()
def runner(app):
TrivialWSGIServer(app).serve()
if __name__ == '__main__':
import sys
import helper
app = helper.get_arg(sys.argv, "Usage: trivial_wsgi_server.py package.wsgi.app")
runner(app)
У него добавились: "исполнитель" runner
, чтобы в один шаг запускать приложение на запуск и код для запуска парного компонента - WSGI-приложения. Отмечу одну из "прослоек" (middleware), которая здесь используется - validator
- проверяет, что "диалог" между сервером и приложением идет в рамках стандарта.
Запуск осуществляется следующим образом:
trivial_wsgi_app.py trivial_wsgi_server.runner
или так:
trivial_wsgi_server.py trivial_wsgi_app.app
Усложняем задачу. Теперь напишем WSGI-сервер средствами Twisted, но с таким же "интерфейсом запуска"
from twisted.internet import reactor
from twisted.web2 import wsgi, channel, server
class TwistedWSGIServer(object):
def __init__(self, app):
self.app = app
self.wsgi_res = wsgi.WSGIResource(app)
self.site = server.Site(self.wsgi_res)
self.factory = channel.HTTPFactory(self.site)
def serve(self):
reactor.listenTCP(8080, self.factory)
reactor.run()
def runner(app):
TwistedWSGIServer(app).serve()
if __name__ == '__main__':
import sys
import helper
app = helper.get_arg(sys.argv, "Usage: twisted_wsgi_server.py package.wsgi.app")
runner(app)
Здесь мы воспользовались WSGI-сервером, встроенным в Twisted Web2, ну а процедура старта Twisted-приложения описана здесь.
Теперь пробуем запустить с нашим приложением:
twisted_wsgi_server.py trivial_wsgi_app.app
Работает. Еще больше усложним задачу и напишем Nevow-приложение с WSGI-интерфейсом (правда, с некоторыми оговорками):
from nevow import rend, loaders, wsgi, tags, inevow
class NevowPage(rend.Page):
addSlash = True
docFactory = loaders.stan(
tags.html[
tags.head[tags.title['Nevow WSGI hello app']],
tags.body[
tags.h1(id='title')['Nevow WSGI hello app'],
tags.p(id='welcome')['Welcome to the Nevow (WSGI powered). Just show your environment.'],
tags.p(id='environment')[tags.invisible(render=tags.directive('environ'))]
]
]
)
def render_environ(self, context, data):
environ = inevow.IRequest(context).environ
sorted_keys = environ.keys()
sorted_keys.sort()
inner = [tags.li[k, " => ", str(environ[k])] for k in sorted_keys]
return tags.ul[inner]
app = wsgi.createWSGIApplication(NevowPage())
if __name__ == '__main__':
import sys
import helper
server = helper.get_arg(sys.argv, "Usage: nevow_wsgi_app.py package.wsgi.server_callable")
server(get_wsgi_app())
Особо углубляться в код не буду, тем более, что есть желание сделать Nevow одной из тем разговора.
Теперь можно комбинировать WSGI-сервера и WSGI-приложения в любых сочетаниях - результат будем идентичным. Естественно, что часть возможностей, которые не укладываются в WSGI, будут недоступны. Напримерб в Twisted Web2, WSGI-приложение выполняется в отдельном потоке, так что воспользоваться асинхронными "фишками" Twisted не получится. Поэтому использовать Nevow с Twisted через WSGI - нонсенс. Об использовании Twisted в веб-приложениях, я думаю, расскажу в ближайшее время. А приведенный код можно получить с code.google.com.
Заключение
WSGI достаточно простое и эффективное решение проблемы взаимодействия веб-сервера и веб-приложения. Как любой компромисс, он не идеален "везде и всюду", однако для большинства случаев - это разумный выбор. Некоторые используют WSGI не только как стандарт взаимодействия веб-сервера и веб-приложения, но и обособленных библиотек между собой. Возможно, в чем то этот подход оправдан.