Появилась задача написать веб интерфейс управления устройством. Управлять устройством будет Raspberry Pi. Логика управления — python, соответственно и интерфейс хотелось бы python. Хочу поделится своим опытом.

  • 1. lighttpd mod_cgi и простой скрипт
  • 2. web.py на порту 8080
  • 3. WCGI интерфейс
  • 4. Простой сервер WSGI
  • 5. WSGI с использованием wsgiref
  • 6. WSGI c помощью flup
  • 7. web.py приложение с использованием flup
  • 8. Немного особенностей


1. Для решения задачи «в лоб» был поднят lighttpd c mod_cgi:

sudo apt-get install lighttpd
sudo nano /etc/lighttpd/lighttpd.conf

Отрывок lighttpd.conf:

#mod_cgi shoud be on
server.modules = (
    "mod_access",
    "mod_alias",
    "mod_compress",
    "mod_redirect",
    "mod_cgi",
    "mod_rewrite",
)
#rule enables cgi script
cgi.assign = (".py" => "/usr/bin/python")

/var/www/index.py:

print "Content-Type: text/html\n\n"
print "Hello World!"

теперь localhost/index.py отвечал бодрым «Hello World!»

Когда lighttpd встречает файл с расширением .py передает его на выполнение python-у и его результатом отвечает на запрос. Грубо говоря перенаправляет stdout.
После некоторых попыток написания интерфейса «с нуля», был рожден HtmlGenerator, который позволил не перегружать код html-тегами, весьма упростил, но все таки не решил проблемы в комплексе.

2. Решено было поэкспериментировать с веб фреймворками.
Под руку попался wep.py, простенький и маловесный.
code.py:

#! /usr/bin/python
#
import web
urls = ( '/', 'index',)

class index:
    def GET(self):
        return "Hello, world!"

if __name__ == "__main__":
    web.application(urls, globals()).run()

Минимальный код и на порту 8080 висит наше веб приложение
Казалось бы пробросить алиас на порт 8080, организовать авто запуск скрипта и все готово.
Да но нет, эксперименты на слабеньком компьютере показали что присутствие нашего скрипта заставляет машинку изрядно «дуться». Кроме того есть lighttpd с mod_cgi.

Как же связать простой скрипт и веб приложение.

3. Согласно описанию WSGI, для его реализации необходим интерфейс такого вида
#! /usr/bin/python
#
def myapp(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type','text/plain')]
    start_response(status, response_headers)
    return ['Hello World!\n']

Ничего военного, но и что-то не работает, чего-то не хватает.
Момент который был неясным после прочтения вики и других статей, что же все таки запустит наш интерфейс.

4. Для запуска WSGI приложения нужен сервер. Пример скрипта который может выступать в роли простого сервера WSGI:
wsgi.py:
#! /usr/bin/python
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()

Теперь добавив к нашему интерфейсу его запуск получим скрипт который ответит уже на нашем lighttpd или apache, по адресу localhost/app.py
/var/www/app.py:
#! /usr/bin/python
include wsgi

def myapp(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type','text/plain')]
    start_response(status, response_headers)
    return ['Hello World!\n']

if __name__ == '__main__':
    wsgi.run_with_cgi(myapp)


5. Для python 2.7 доступен модуль wsgiref который может реализовать WSGI сервер
#! /usr/bin/python
import wsgiref.handlers

def myapp(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type','text/plain')]
    start_response(status, response_headers)
    return ['Hello World!\n']

if __name__ == '__main__':
    wsgiref.handlers.CGIHandler().run(myapp)


6. Реализация WSGI c помощью flup:
установим flup
sudo apt-get install python-flup

#! /usr/bin/python
import flup.server.fcgi

def myapp(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type','text/plain')]
    start_response(status, response_headers)
    return ['Hello World!\n']

if __name__ == '__main__':
    flup.server.fcgi.WSGIServer(myapp).run()


7. Простое web.py приложение с использованием flup:
/var/www/app.py:
#! /usr/bin/python
import web
urls = (  '/', 'index', )

class index:
    def GET(self):
        return "Hello World!"

if __name__ == '__main__':
    web.application(urls, globals()).run()

приложение станет доступным по адресу localhost/app.py

8. По умолчанию web.py использует flup, но можно обойтись и без него.
Для запуска web.py на wsgiref необходимо:
web.application(urls, globals()).cgirun()

B ссылках на скрипты web.py в конце не забывать ставить '/' (app.py/), иначе ответом будет «not found». По-хорошему необходимо создать rewrite правило:
# mod_rewrite configuration.
url.rewrite-once = (
    "^/favicon.ico$" => "/favicon.ico", 
    "^/(.*)$" => "app.py/$1" ,)

Для отладки в скриптов полезно добавить:
import cgitb
cgitb.enable()

тогда будут видны ошибки.

Остается опробовать:
modwsgi
paste
pylons

Полезные ссылки:
WSGI wiki
wep.py
WSGI — протокол связи Web-сервера с Python приложением
WSGI, введение
How to serve a WSGI application via CGI
WSGI.org
Сравнение эффективности способов запуска веб-приложений на языке Python

Комментарии (10)


  1. Pavel_Osipov
    23.04.2015 20:52
    +2

    А из этого ничего не пробовали?
    Twisted
    Tornado Web Server
    cyclone.io
    Весьма удобные штуки


    1. un_def
      23.04.2015 22:29
      +3

      CherryPy
      Flask

      ______ (впишите свой фреймворк/веб-сервер)


  1. hardex
    23.04.2015 22:29
    +5

    Я вас очень прошу, закопайте уже lighttpd обратно.


    1. dimitrimus
      23.04.2015 23:33
      +2

      а что с ним не так?


  1. kAIST
    24.04.2015 01:06
    +1

    После некоторых попыток написания интерфейса «с нуля», был рожден HtmlGenerator, который позволил не перегружать код html-тегами, весьма упростил, но все таки не решил проблемы в комплексе.

    А чем родной шаблонизатор из того же web.py не угодил?
    Я на той же малинке запускаю тот же web.py без стороннего сервера (через cherrypy насколько я знаю работает), просто:
    sudo python code.py 80
    Для текущих задач вполне хватает.


    1. tarasii Автор
      24.04.2015 14:27

      Я упоминал что просто запустив скрипт c приложением web.py запускается и сервер на порту по умолчанию 8080.
      При таком запуске web.py не находит должного окружения и запускает «свой» WSGI сервер позаимствованный у cherrypy фреймворка. Поднять http сервер на питоне — не очень сложная задача. Фреймворки у себя под юбкой прячут уйму интересного. HtmlGenerator написан для моего удобства, на фреймворк он явно не тянет. Там нет ни рендеренга шаблонов, ни сессий ничего просто «помагатор», которого вполне хватает чтоб выдать данные на просмотр:

      print(mh.Html("MyHtml", mh.JavaScript("console.log('start');"),mh.Header("Main:"),"")
      

      Никаких фреймворков все бысто и просто.


  1. EvgeniyKirov
    24.04.2015 01:30

    Давненько я не слышал про flup…


  1. Jekel
    24.04.2015 13:08

    А я то думал тут будет что-то про Raspberry Pi кроме упоминания в первой строке.
    Про то как писать и запускать wsgi скрипты написано миллион и одна статья, зачем еще одна?


    1. vit1251
      26.04.2015 16:10

      Я думаю, что не хватает оглавления (индекса), но это просто не полное.
      Так что не вините автора он хорошее дело делает.


  1. bromzh
    25.04.2015 03:21

    Зачем использовать CGI, если у питона есть свой протокол (WSGI) и довольно быстрые серверы (UWSGI/gevent/etc, например, вот их сравнение). В рамках такого проекта можно не ставить nginx/apache/lighttpd, а использовать питоновский сервер напрямую.
    Ну и советую рассмотреть фреймворки, типа flask или bottle. Последний удобен тем, что занимает 1 файл, его легко подключить в небольшой проект, без установки зависимостей. Всего 2 файла (плюс немного шаблонов), интерпретатор, и простенький веб-интерфейс готов.