Различия между версиями 33 и 48 (по 15 версиям)
Версия 33 от 2010-06-17 20:25:30
Размер: 25404
Редактор: Ilya Kutukov
Комментарий:
Версия 48 от 2010-06-18 06:57:25
Размер: 48079
Редактор: RostislavDzinko
Комментарий:
Удаления помечены так. Добавления помечены так.
Строка 182: Строка 182:
You can set cookies in the user's browser with the set_cookie method: cookies можно установить в обозревателе пользователя с помощью метода '''set_cookie''':
Строка 194: Строка 194:
Cookies are easily forged by malicious clients. If you need to set cookies to, e.g., save the user ID of the currently logged in user, you need to sign your cookies to prevent forgery. Tornado supports this out of the box with the set_secure_cookie and get_secure_cookie methods. To use these methods, you need to specify a secret key named cookie_secret when you create your application. You can pass in application settings as keyword arguments to your application: Часто бывает, что cookies очень просто подделываются зловредными клиентами. Если вам нужно установить cookies чтобы, например, сохранить идентификатор авторизованного пользователя, вам следует их подписать для предотвращения подделки. Tornado предоставляет такую возможность из коробки через использование методов '''set_secure_cookie''' и '''get_secure_cookie'''. Чтобы использовать эти методы, вы должны указать секретный ключ, который называется '''cookie_secret''', когда создаете приложение. Для реализации этого вам нужно передать его в настройках приложения как ключевой аргумент:
Строка 202: Строка 202:
Signed cookies contain the encoded value of the cookie in addition to a timestamp and an HMAC signature. If the cookie is old or if the signature doesn't match, get_secure_cookie will return None just as if the cookie isn't set. The secure version of the example above: Подписанные cookies содержат закодированное значение cookie плюс timestamp и подпись (сигнатуру) HMAC. Если cookie устарела, или подпись не совпадает, метод '''get_secure_cookie''' вернет '''None''', как будто cookie не была установлена. Вот защищенная версия примера, приводимого выше:
Строка 216: Строка 216:
Проверка подлинности пользователя доступна в каждом обработчике запроса как '''self.current_user''', и в каждом шаблоне как '''current_user'''. По умолчанию '''current_user''' имеет значение None.

Для осуществления аутентификации в вашем приложении вам нужно переопределить метод '''get_current_user()''' в вашем обработчике запроса, чтобы определить текущего пользователя, например, значение куки. Ниже приведен пример, который позволяет пользователям войти в приложение просто указав ник, который потом сохраняется в куке:

{{{#!highlight python
class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_secure_cookie("user")

class MainHandler(BaseHandler):
    def get(self):
        if not self.current_user:
            self.redirect("/login")
            return
        name = tornado.escape.xhtml_escape(self.current_user)
        self.write("Hello, " + name)

class LoginHandler(BaseHandler):
    def get(self):
        self.write('<html><body><form action="/login" method="post">'
                   'Name: <input type="text" name="name">'
                   '<input type="submit" value="Sign in">'
                   '</form></body></html>')

    def post(self):
        self.set_secure_cookie("user", self.get_argument("name"))
        self.redirect("/")

application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")
}}}

Вы можете потребовать, чтобы пользователь аутентифицировался в приложении используя декоратор '''tornado.web.authenticated'''. Если запрос передается методу с этим декоратором и пользователь не залогинен он будет перенаправлен на '''login_url''' (который вы указываете в настройках приложения). Перепишем пример выше с использованием этого декоратора:

{{{#!highlight python
class MainHandler(BaseHandler):
    @tornado.web.authenticated
    def get(self):
        name = tornado.escape.xhtml_escape(self.current_user)
        self.write("Hello, " + name)

settings = {
    "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
    "login_url": "/login",
}
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)
}}}

Если вы используете этот декоратор аутентификации для метода '''post()''' и пользователь не залогинен, то сервер пошлет в ответ ошибку 403.

Tornado поставляется со встроенной поддержкой схем аутентификации сторонних производителей, например [[http://code.google.com/intl/ru-RU/apis/accounts/docs/OAuth.html|Google OAuth]]. Посмотрите документуацию по модулю [[http://github.com/facebook/tornado/blob/master/tornado/auth.py|auth]] для получения подробной информации, либо раздел документации посвященный аутентификации с помощью сторонних разработчиков.

Также рекомендуем ознакомиться с демонстрационным приложением '''Blog''', идущим в комплекте с Tornado, чтобы получить представление об устройстве приложения, использующего аутентификацию и сохраняющего пользовательские данные в БД [[http://www.mysql.com/|MySQL]]
Строка 218: Строка 277:
[[http://en.wikipedia.org/wiki/Cross-site_request_forgery|Cross-site request forgery]], или XSRF (CSRF) - обычная проблема персонализированных веб приложений. Смотрите [[http://en.wikipedia.org/wiki/Cross-site_request_forgery|статью в википедии]] для получения более полной информации о, как работает XSRF.

The generally accepted solution to prevent XSRF is to cookie every user with an unpredictable value and include that value as an additional argument with every form submission on your site. If the cookie and the value in the form submission do not match, then the request is likely forged.

Tornado comes with built-in XSRF protection. To include it in your site, include the application setting xsrf_cookies:

{{{#!highlight python
settings = {
    "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
    "login_url": "/login",
    "xsrf_cookies": True,
}
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)
}}}

If xsrf_cookies is set, the Tornado web application will set the _xsrf cookie for all users and reject all POST requests hat do not contain a correct _xsrf value. If you turn this setting on, you need to instrument all forms that submit via POST to contain this field. You can do this with the special function xsrf_form_html(), available in all templates:

{{{#!highlight html
<form action="/login" method="post">
  {{ xsrf_form_html() }}
  <div>Username: <input type="text" name="username"/></div>
  <div>Password: <input type="password" name="password"/></div>
  <div><input type="submit" value="Sign in"/></div>
</form>
}}}

If you submit AJAX POST requests, you will also need to instrument your JavaScript to include the _xsrf value with each request. This is the jQuery function we use at FriendFeed for AJAX POST requests that automatically adds the _xsrf value to all requests:

{{{#!highlight javascript
function getCookie(name) {
    var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
    return r ? r[1] : undefined;
}

jQuery.postJSON = function(url, args, callback) {
    args._xsrf = getCookie("_xsrf");
    $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
        success: function(response) {
        callback(eval("(" + response + ")"));
    }});
};
}}}
Строка 219: Строка 324:

Вы можете обслуживать статические файлы с помощью Tornado, через указание настройки '''static_path''' в вашем приложении.

{{{#!highlight python
settings = {
    "static_path": os.path.join(os.path.dirname(__file__), "static"),
    "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
    "login_url": "/login",
    "xsrf_cookies": True,
}
application = tornado.web.Application([
    (r"/", MainHandler),
    (r"/login", LoginHandler),
], **settings)
}}}

Эта настройка автомтически сделает все запросы, которые начинаются со '''/static/''' обслуживаемыми из директории для статичных файлов, например [[http://localhost:8888/static/foo.png|http://localhost:8888/static/foo.png]] использует '''foo.png''' из директории '''static_path'''. Также Tornado автоматически обслужвает файлы '''/robots.txt''' и '''/favicon.ico''' из директории статики (даже если перфикс '''/static/''' не указан).

В случае с браузерами, то неплохой идеей для увеличения производительности является агрессивное кэширование ресурсов, так чтобы не посылать ненужные запросы '''If-Modified-Since''' или '''Etag''', ожидание обработки которых блокирует отображение страницы. Торнадо, будучи развернутым, сразу предоставляет такую возможность с помощью контроля версий статического контента.

Чтобы использовать эту особенность нужно вызвать метод '''static_url()''' из вашего шаблона вместо того, чтобы вводить напрямую путь к статичному файлу в HTML:

{{{#!highlight html
<html>
   <head>
      <title>FriendFeed - {{ _("Home") }}</title>
   </head>
   <body>
     <div><img src="{{ static_url("images/logo.png") }}"/></div>
   </body>
 </html>
 }}}
 
Функция '''static_url()''' преобразует относительный путь в URI вроде '''/static/images/logo.png?v=aae54'''. Аргумент '''v''' - это хэш содержимого '''logo.png''', и его наличие говорит Tornado отослать заголовки браузеру пользователя, что, в свою очередь, заставляет браузер кэшировать статичный файл на неопределенно долгове время.
 
Так как аргумент '''v''' основан на содержимом файла, то если вы изменяете файл и перезапускаете сервер, начинает передаваться новое значение '''v''' и браузер пользователя загрузит новую версию файла. Если содержимое файла не изменилось, браузер продолжит использовать локальный кэш даже не проверяя изменения на сервере, что существенно увеличивает скорость отображения страницы.
 
В производстве вы возможно захотите обслуживать статику более оптимизированным для этой цели сервером вроде [[http://nginx.net/|ngnix]]. Вы можете сконфигурировать почти любой web-сервер так, чтобы он поддерживал подобную семантику кэширования. Вот пример конфигурации ngnix, который мы изпользуем в проекте [[http://friendfeed.com|FriendFeed]]
 
{{{#!highlight bash
location /static/ {
   root /var/friendfeed/static;
   if ($query_string) {
       expires max;
   }
}
}}}
Строка 324: Строка 476:
В качестве более развернутого примера вы можете посмотреть приложение '''chat''', идущее в качестве примера с Tornado, которое является чатом на основе AJAX и использует т.н. <a href="http://en.wikipedia.org/wiki/Push_technology#Long_polling">длинные запросы</a> (long polling)

== Аутентификация от третих разработчиков ==
В качестве более развернутого примера вы можете посмотреть приложение '''chat''', идущее в качестве примера с Tornado, которое является чатом на основе AJAX и использует т.н. [[http://en.wikipedia.org/wiki/Push_technology#Long_polling|длинные запросы]] (long polling)

== Аутентификация с помощью сторонних разработчиков ==

Модуль '''auth''' Tornado предоставляет протоколы авторизации и аутентификации для ряда наиболее популярных веб-сайтов, включая [[http://google.com|Google/Gmail]], [[http://facebook.com|Facebook]], [[http://twitter.com|Twitter]], [[http://yahoo.com|Yahoo]] и [[http://friendfeed.com|FriendFeed]]. Модуль включает методы авторизации пользователей через эти сайты и, где это возможно, методы доступа к сервисам, так что вы можете, например, загрузить пользовательские контакты или опубликовать сообщение в Твиттере, используя учетную запись пользователя.

Вот пример обработчика, котрый использует Google для аутентификации, сохраняя полномочия в cookie для дальнейшего доступа:

{{{#!highlight python
class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
    @tornado.web.asynchronous
    def get(self):
        if self.get_argument("openid.mode", None):
            self.get_authenticated_user(self.async_callback(self._on_auth))
            return
        self.authenticate_redirect()

    def _on_auth(self, user):
        if not user:
            self.authenticate_redirect()
            return
        # Save the user with, e.g., set_secure_cookie()
}}}

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

Более подробно вы можете узнать про это из документации к модулю '''auth'''.
Строка 330: Строка 506:
Производительность web-приложений в первую очередь ограничена архитектурой, а не производительностью frontend-а. И тем не менее Tornado весьма быстр по отношению к большинству популярных web-фреймворков на Python.

Мы запустили несколько тестов на нагрузку, используя простое приложение типа "Hello, world!" на каждом из наиболее популярных Python web-фреймворков ([[http://www.djangoproject.com/|Django]], [[http://webpy.org/|web.py]] и [[http://www.cherrypy.org/|CherryPy]]) чтобы получить общее представление о производительности каждого из них по отношению к Tornado. Мы использовали Apache/mod_wsgi для Django и web.py, CherryPy был запущен на отдельном сервере, что соответствовало нашим представлениям о типичном окружении производственных решений каждого из фреймворков. Мы запустили 4 однопоточных Tornado фронтэнда за обратным прокси [[http://nginx.net/|ngnix]], что соответствует нашим рекомендациям по запуску Tornado в производство. (наш тестовый сервер был четырехядерным и мы рекомендуем по одному фронтэнду на ядро).

Тест каждого решения на нагрузку осуществлялся с помощью Apache Benchmark ([[http://httpd.apache.org/docs/2.0/programs/ab.html|ab]]) на отдельной машине с помощью команды

{{{#!highlight bash
ab -n 100000 -c 25 http://10.0.1.x/
}}}

Результаты (обработанных запросов в секунду) на четырехядерном процессоре 2.4GHz AMD Opteron:
||Tornado (ngnix; 4 frontends)||8213||
||Tornado (1 single-threaded frontend)||3353||
||Django (Apache/mod_wsgi)||2223||
||web.py (Apache/mod_wsgi)||2066||
||CherryPy (standalone)||785||

По результатам наших тестов Tornado четырехкратно превосходил следующий по производительности фреймворк, и даже в однопоточном варианте (с использованием только одного ядра из четырех) оказался на 33% быстрее его.

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

Строка 332: Строка 530:
В проекте FriendFeed мы используем [[http://nginx.net/|ngnix]] в качестве балансировщика нагрузки и сервера статики. У нас запущено множество экземпляров web-сервера Tornado на множестве серверов, обслуживающих фронтэнд. Обычно мы запускаем один фронтэнд Tornado на аппаратное ядро (иногда больше, в зависимости от ситуации).

Это готовый конфигурационный файл ngnix, который структурно схож с тем, что мы используем в проекте FriendFeed. В данном случае предполагается, что ngnix и Tornado запущены на одной машине и четыре сервера Tornado запущены на портах с 8000 по 8003:

{{{#!highlight bash
user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log;
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    use epoll;
}

http {
    # Enumerate all the Tornado servers here
    upstream frontends {
        server 127.0.0.1:8000;
        server 127.0.0.1:8001;
        server 127.0.0.1:8002;
        server 127.0.0.1:8003;
    }

    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    access_log /var/log/nginx/access.log;

    keepalive_timeout 65;
    proxy_read_timeout 200;
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    gzip on;
    gzip_min_length 1000;
    gzip_proxied any;
    gzip_types text/plain text/html text/css text/xml
               application/x-javascript application/xml
               application/atom+xml text/javascript;

    # Only retry if there was a communication error, not a timeout
    # on the Tornado server (to avoid propagating "queries of death"
    # to all frontends)
    proxy_next_upstream error;

    server {
        listen 80;

        # Allow file uploads
        client_max_body_size 50M;

        location ^~ /static/ {
            root /var/www;
            if ($query_string) {
                expires max;
            }
        }
        location = /favicon.ico {
            rewrite (.*) /static/favicon.ico;
        }
        location = /robots.txt {
            rewrite (.*) /static/robots.txt;
        }

        location / {
            proxy_pass_header Server;
            proxy_set_header Host $http_host;
            proxy_redirect false;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Scheme $scheme;
            proxy_pass http://frontends;
        }
    }
}
}}}
Строка 334: Строка 609:
Торнадо поставляется с ограниченной поддержкой [[http://wsgi.org/|WSGI]]. Тем не менее, так как WSGI не поддерживает неблокирующие запросы вы не сможете использовать какие либо асинхронные/неблокирующие функции Tornado в вашем приложении, если вы предпочтете использовать WSGI вместо HTTP сервера Tornado. Некоторые возможности, недоступные для использования в WSGI приложениях: '''@tornado.web.asynchronous''', the '''httpclient module''', and the '''auth''' module.

Вы можете создать рабочее WSGI приложение с обработчиками запросов Tornado, используя '''WSGIApplication''' в модуле '''wsgi''' вместо '''tornado.web.Application'''. Далее приведен пример который использует встроенный WSGI '''CGIHandler''' для создания рабчего приложения [[http://code.google.com/appengine/|Google AppEngine]]:

{{{#!highlight python
import tornado.web
import tornado.wsgi
import wsgiref.handlers

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

if __name__ == "__main__":
    application = tornado.wsgi.WSGIApplication([
        (r"/", MainHandler),
    ])
    wsgiref.handlers.CGIHandler().run(application)
}}}

Посмотрите пример '''appengine''' для того, чтобы ознакомиться с полноценным приложением AppEngine, построенным на Tornado.
Строка 336: Строка 633:
'''Перевод: Ростислав Дзинько''' Tornado является переработанным движком FriendFeed у которого были убранны специфичные для проекта зависимости. Рефакторинг мог породить ошибки. Так как серверы FriendFeed всегда работали за ngnix, Tornado не был тщательно протестирован с клиентами HTTP/1.1 кроме Firefox. На данный момент Tornado не обрабатывает многострочные заголовки и некоторые варианты неправильно сформированных запросов.

Вы можете обсудить Tornado и сообщить об ошибках в [[http://groups.google.com/group/python-tornado|рассылке для разработчиков Tornado]]

'''Перевод: Ростислав Дзинько, Илья Кутуков'''

Документация к веб-серверу Tornado

http://www.tornadoweb.org/static/tornado.png

Tornado - открытая версия масштабируемого, неблокирующего сервера и инструментов, которые используются сервисом FriendFeed. Приложение FriendFeed написано с использованием фреймворка, который выглядит как web.py или веб приложение Google, но имеет ряд дополнительных инструментов и оптимизаций с целью получения всех выгод от низлежащей неблокирующей инфраструктуры.

Обзор

Веб-сервер, который обслуживает сервис FriendFeed относительно простой, неблокирующий веб сервер, написанный на языке Python. Приложение FriendFeed написано с использованием веб фреймворка written using a web framework that looks a bit like web.py or Google's webapp, but with additional tools and optimizations to take advantage of the non-blocking web server and tools.

Tornado - открытая версия масштабируемого, неблокирующего сервера и инструментов, которые используются сервисом FriendFeed. Данный фреймворк отличается от большинства мейнстримовских (и, определенно, большинства Python фреймворков), потому что использование неблокирующего принципа дает достаточную быстроту. Благодаря этому, а также благодаря использованию epoll, он может обрабатывать тысячи одновременных соединений, что значит, этот фреймворк идеален для создания веб сервисов реального времени. Мы построили веб сервер специфиWe built the web server конкретно для того, чтобы обрабатывать функции сервиса FriendFeed в реальном времени — каждый активный пользователь сервися поддерживает открытое соединение с серверами. (Для получения дополнительной информации о масштабировании серверов для поддержки тисяч клиентов, прочитайте о проблеме C10K.)

Загрузка

Загрузите последнюю версию Tornado с GitHub:

На GitHub Вы также можете просмореть исходники. Чтобы установить Tornado:

   1 tar xvzf tornado-0.2.tar.gz
   2 cd tornado-0.2
   3 python setup.py build
   4 sudo python setup.py install

После установки, вы получите возможность запустить любое из демонстрационных приложений из папки demos, которые включены в пакет Tornado.

   1 ./demos/helloworld/helloworld.py

Подготовка

Tornado тестировался на Python 2.5 и 2.6. Для того, чтобы использовать все функции Tornado, вам нужны библиотеки !PycURL !JSON, например simplejson. Полные инструкции по установке на Mac OS X и Ubuntu показаны ниже.

Mac OS X 10.5/10.6

   1 sudo easy_install setuptools pycurl==7.16.2.1 simplejson

Ubuntu Linux

   1 sudo apt-get install python-dev python-pycurl python-simplejson

Список модулей

Наиболее важный модуль - web, который является фреймворком, включающим в себя большую часть функционала пакета Tornado. Другие модули - это инструменты, которые делают модуль web богаче. Смотрите Руководство по Tornado ниже для более полной информации о пакете web.

Основные модули

  • web - веб фреймворк, на котором построен FriendFeed. web содержит большинство важных функций Tornado

  • escape - методы кодирование/декодирование XHTML, JSON и URL

  • database - Простая обертка вокруг !MySQLdb для упрощения спользования СУБД MySQL

  • template - язык шаблонов, в основе которого находится язык Python

  • httpclient - неблокирующий HTTP клиент созданный для работы с модулями web и httpserver

  • auth - реализация схем аутентификации и авторизации от третих разработчиков (Google OpenID/OAuth, Facebook Platform, Yahoo BBAuth, FriendFeed OpenID/OAuth, Twitter OAuth)

  • locale - поддержка локализации/интернационализации

  • options - синтаксический анализатор файлов настроек и аргументов коммандной строки, оптимизированный для использования в среде сервера

Низкоуровневые модули

  • httpserver - очень простой HTTP сервер, на основе которого построен модуль web

  • iostream - простая обертка вокруг неблокирующих сокетов для обеспечения общих шаблонов считывания и записи

  • ioloop - основная петля ввода/вывода

Другие модули

  • s3server - веб сервер, который реализует большую часть интерфейса Amazon S3, с локальным файловым хранилищем данных

Руководство

Обработчики и параметры запросов

Веб Приложение Tornado отображает (maps) URL или шаблоны URL в подклассы tornado.web.RequestHandler. Эти классы определяют get() и post() методы для обработки запросов HTTP GET и POST по этому URL.

Этот код отображает корневой URL / в MainHandler, а шаблон URL /story/([0-9]+) в StoryHandler. Регулярные выражения передаются в качестве аргументов методам класс RequestHandler:

   1 class MainHandler(tornado.web.RequestHandler):
   2     def get(self):
   3         self.write("You requested the main page")
   4 
   5 class StoryHandler(tornado.web.RequestHandler):
   6     def get(self, story_id):
   7         self.write("You requested the story " + story_id)
   8 
   9 application = tornado.web.Application([
  10     (r"/", MainHandler),
  11     (r"/story/([0-9]+)", StoryHandler),
  12 ])

С помощью метода get_argument() вы можете получить аргументы строки запроса и распарсить тело POST запроса:

   1 class MainHandler(tornado.web.RequestHandler):
   2     def get(self):
   3         self.write('<html><body><form action="/" method="post">'
   4                    '<input type="text" name="message">'
   5                    '<input type="submit" value="Submit">'
   6                    '</form></body></html>')
   7 
   8     def post(self):
   9         self.set_header("Content-Type", "text/plain")
  10         self.write("You wrote " + self.get_argument("message"))

Если вы хотите отправить сообщение об ошибке клиенту, например, 403 Unauthorized, вам достаточно вызвать исключение tornado.web.HTTPError:

   1 if not self.user_is_logged_in():
   2     raise tornado.web.HTTPError(403)

Обработчик запросов имеет доступ к объекту, который отражает состояние текущего запроса через атрибут self.request. Объект HTTPRequest object содержит ряд полезных атрибутов, среди них:

  • arguments - все аргументы GET и POST запроса
  • files - все загруженные файлы (через запросы multipart/form-data POST)

  • path - путь запроса (все, что находится в строке запроса перед ?)

  • headers - заголовки запроса

Смотрите определение класса HTTPRequest в модуле httpserver для получения полного списка атрибутов.

Шаблоны

Вы можете использовать любой язык шаблонов, который поддерживает язык Python, но Tornado идет вместе со своим собственным, который намного быстрее и гибче чем множество популярных систем шаблонов. Для полной информации обратитесь к документации к модулю template.

Шаблон Tornado - простой (или другой текстовый формат) с вложенными управляющими последовательностями языка Python, которые являются выражениями, заключенными в разметку:

   1 <html>
   2    <head>
   3       <title>{{ title }}</title>
   4    </head>
   5    <body>
   6      <ul>
   7        {% for item in items %}
   8          <li>{{ escape(item) }}</li>
   9        {% end %}
  10      </ul>
  11    </body>
  12  </html>

Если вы сохранили этот шаблон как "template.html", и положили в ту же папку, что и файл Python, отобразите этот шаблон следующим образом:

   1 class MainHandler(tornado.web.RequestHandler):
   2     def get(self):
   3         items = ["Item 1", "Item 2", "Item 3"]
   4         self.render("template.html", title="My title", items=items)

Шаблоны Tornado поддерживают упаравляющие последовательности и выражения. Управляющие последовательности заключены в символы {% и %}, например, {% if len(items) > 2 %}. Выражения заключены в символы { { и } }, например, { { items[0] } }.

Управляющие последовательности более менее точно соответствують предложениям языка Python. Мы поддерживаем if, for, while, и try, каждое из которых заканчивается последовательностью символов {% end %}. Мы также поддерживаем наследование шаблонов через предложения extends и block, которые более детально описаны в документации к модулю template.

Выражения могут быть любыми, соответствующими языку Python, включая вызовы функций. Мы поддерживаем функции escape, url_escape, и json_encode по молчанию, но вы можете передать любые другие функции в шаблон через ключевые аргументы функции render модуля template:

   1 class MainHandler(tornado.web.RequestHandler):
   2     def get(self):
   3         self.render("template.html", add=self.add)
   4 
   5     def add(self, x, y):
   6         return x + y

Когда вы разрабатываете реальное приложение, вам не нужны все функции шаблонов Tornado, особенно наследование. Почитайте о всех функциях шаблонов в документации к модулю template.

Под капотом, шаблоны Tornado транслируются прямо в Python. Выражения из шаблонов копируются без изменений в Python функцию, которая представляет шаблон. Мы не пытаемся предостеречь вас от каких либо действий в языке шаблонов; мы создали его для того, чтобы обеспечивать гибкость, которая не свойственна другим, более строгим языкам шаблонов. Следовательно, если вы напишите внутри выражений какую нибудь несуразицу, то получите ошибки Python при выполнении шаблона.

Cookies и защищенные cookies

cookies можно установить в обозревателе пользователя с помощью метода set_cookie:

   1 class MainHandler(tornado.web.RequestHandler):
   2     def get(self):
   3         if not self.get_cookie("mycookie"):
   4             self.set_cookie("mycookie", "myvalue")
   5             self.write("Your cookie was not set yet!")
   6         else:
   7             self.write("Your cookie was set!")

Часто бывает, что cookies очень просто подделываются зловредными клиентами. Если вам нужно установить cookies чтобы, например, сохранить идентификатор авторизованного пользователя, вам следует их подписать для предотвращения подделки. Tornado предоставляет такую возможность из коробки через использование методов set_secure_cookie и get_secure_cookie. Чтобы использовать эти методы, вы должны указать секретный ключ, который называется cookie_secret, когда создаете приложение. Для реализации этого вам нужно передать его в настройках приложения как ключевой аргумент:

   1 application = tornado.web.Application([
   2     (r"/", MainHandler),
   3 ], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")

Подписанные cookies содержат закодированное значение cookie плюс timestamp и подпись (сигнатуру) HMAC. Если cookie устарела, или подпись не совпадает, метод get_secure_cookie вернет None, как будто cookie не была установлена. Вот защищенная версия примера, приводимого выше:

   1 class MainHandler(tornado.web.RequestHandler):
   2     def get(self):
   3         if not self.get_secure_cookie("mycookie"):
   4             self.set_secure_cookie("mycookie", "myvalue")
   5             self.write("Your cookie was not set yet!")
   6         else:
   7             self.write("Your cookie was set!")

Аутентификация пользователей

Проверка подлинности пользователя доступна в каждом обработчике запроса как self.current_user, и в каждом шаблоне как current_user. По умолчанию current_user имеет значение None.

Для осуществления аутентификации в вашем приложении вам нужно переопределить метод get_current_user() в вашем обработчике запроса, чтобы определить текущего пользователя, например, значение куки. Ниже приведен пример, который позволяет пользователям войти в приложение просто указав ник, который потом сохраняется в куке:

   1 class BaseHandler(tornado.web.RequestHandler):
   2     def get_current_user(self):
   3         return self.get_secure_cookie("user")
   4 
   5 class MainHandler(BaseHandler):
   6     def get(self):
   7         if not self.current_user:
   8             self.redirect("/login")
   9             return
  10         name = tornado.escape.xhtml_escape(self.current_user)
  11         self.write("Hello, " + name)
  12 
  13 class LoginHandler(BaseHandler):
  14     def get(self):
  15         self.write('<html><body><form action="/login" method="post">'
  16                    'Name: <input type="text" name="name">'
  17                    '<input type="submit" value="Sign in">'
  18                    '</form></body></html>')
  19 
  20     def post(self):
  21         self.set_secure_cookie("user", self.get_argument("name"))
  22         self.redirect("/")
  23 
  24 application = tornado.web.Application([
  25     (r"/", MainHandler),
  26     (r"/login", LoginHandler),
  27 ], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")

Вы можете потребовать, чтобы пользователь аутентифицировался в приложении используя декоратор tornado.web.authenticated. Если запрос передается методу с этим декоратором и пользователь не залогинен он будет перенаправлен на login_url (который вы указываете в настройках приложения). Перепишем пример выше с использованием этого декоратора:

   1 class MainHandler(BaseHandler):
   2     @tornado.web.authenticated
   3     def get(self):
   4         name = tornado.escape.xhtml_escape(self.current_user)
   5         self.write("Hello, " + name)
   6 
   7 settings = {
   8     "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
   9     "login_url": "/login",
  10 }
  11 application = tornado.web.Application([
  12     (r"/", MainHandler),
  13     (r"/login", LoginHandler),
  14 ], **settings)

Если вы используете этот декоратор аутентификации для метода post() и пользователь не залогинен, то сервер пошлет в ответ ошибку 403.

Tornado поставляется со встроенной поддержкой схем аутентификации сторонних производителей, например Google OAuth. Посмотрите документуацию по модулю auth для получения подробной информации, либо раздел документации посвященный аутентификации с помощью сторонних разработчиков.

Также рекомендуем ознакомиться с демонстрационным приложением Blog, идущим в комплекте с Tornado, чтобы получить представление об устройстве приложения, использующего аутентификацию и сохраняющего пользовательские данные в БД MySQL

Защита от CSRF

Cross-site request forgery, или XSRF (CSRF) - обычная проблема персонализированных веб приложений. Смотрите статью в википедии для получения более полной информации о, как работает XSRF.

The generally accepted solution to prevent XSRF is to cookie every user with an unpredictable value and include that value as an additional argument with every form submission on your site. If the cookie and the value in the form submission do not match, then the request is likely forged.

Tornado comes with built-in XSRF protection. To include it in your site, include the application setting xsrf_cookies:

   1 settings = {
   2     "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
   3     "login_url": "/login",
   4     "xsrf_cookies": True,
   5 }
   6 application = tornado.web.Application([
   7     (r"/", MainHandler),
   8     (r"/login", LoginHandler),
   9 ], **settings)

If xsrf_cookies is set, the Tornado web application will set the _xsrf cookie for all users and reject all POST requests hat do not contain a correct _xsrf value. If you turn this setting on, you need to instrument all forms that submit via POST to contain this field. You can do this with the special function xsrf_form_html(), available in all templates:

   1 <form action="/login" method="post">
   2   {{ xsrf_form_html() }}
   3   <div>Username: <input type="text" name="username"/></div>
   4   <div>Password: <input type="password" name="password"/></div>
   5   <div><input type="submit" value="Sign in"/></div>
   6 </form>

If you submit AJAX POST requests, you will also need to instrument your JavaScript to include the _xsrf value with each request. This is the jQuery function we use at FriendFeed for AJAX POST requests that automatically adds the _xsrf value to all requests:

   1 function getCookie(name) {
   2     var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
   3     return r ? r[1] : undefined;
   4 }
   5 
   6 jQuery.postJSON = function(url, args, callback) {
   7     args._xsrf = getCookie("_xsrf");
   8     $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST",
   9         success: function(response) {
  10         callback(eval("(" + response + ")"));
  11     }});
  12 };

Статические файлы и агрессивное кеширование файлов

Вы можете обслуживать статические файлы с помощью Tornado, через указание настройки static_path в вашем приложении.

   1 settings = {
   2     "static_path": os.path.join(os.path.dirname(__file__), "static"),
   3     "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=",
   4     "login_url": "/login",
   5     "xsrf_cookies": True,
   6 }
   7 application = tornado.web.Application([
   8     (r"/", MainHandler),
   9     (r"/login", LoginHandler),
  10 ], **settings)

Эта настройка автомтически сделает все запросы, которые начинаются со /static/ обслуживаемыми из директории для статичных файлов, например http://localhost:8888/static/foo.png использует foo.png из директории static_path. Также Tornado автоматически обслужвает файлы /robots.txt и /favicon.ico из директории статики (даже если перфикс /static/ не указан).

В случае с браузерами, то неплохой идеей для увеличения производительности является агрессивное кэширование ресурсов, так чтобы не посылать ненужные запросы If-Modified-Since или Etag, ожидание обработки которых блокирует отображение страницы. Торнадо, будучи развернутым, сразу предоставляет такую возможность с помощью контроля версий статического контента.

Чтобы использовать эту особенность нужно вызвать метод static_url() из вашего шаблона вместо того, чтобы вводить напрямую путь к статичному файлу в HTML:

   1 <html>
   2    <head>
   3       <title>FriendFeed - {{ _("Home") }}</title>
   4    </head>
   5    <body>
   6      <div><img src="{{ static_url("images/logo.png") }}"/></div>
   7    </body>
   8  </html>

Функция static_url() преобразует относительный путь в URI вроде /static/images/logo.png?v=aae54. Аргумент v - это хэш содержимого logo.png, и его наличие говорит Tornado отослать заголовки браузеру пользователя, что, в свою очередь, заставляет браузер кэшировать статичный файл на неопределенно долгове время.

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

В производстве вы возможно захотите обслуживать статику более оптимизированным для этой цели сервером вроде ngnix. Вы можете сконфигурировать почти любой web-сервер так, чтобы он поддерживал подобную семантику кэширования. Вот пример конфигурации ngnix, который мы изпользуем в проекте FriendFeed

   1 location /static/ {
   2    root /var/friendfeed/static;
   3    if ($query_string) {
   4        expires max;
   5    }
   6 }

Локализация

Модули пользовательского интерфейса

Для облегчения поддержки стандартных и многократно используемых виджетов вашими приложениями, в торнадо включена поддержка модулей пользовательского интерфейса (UI Modules). По сути, они являются особыми функциональными запросами на отображение компонентов вашей страницы и они идут в комплекте с собственными таблицами CSS и функциями JavaScript.

Допустим, что у нас есть блог и мы хотим, чтобы записи этого блога присутствовали как на главной странице, так и на отдельных страницах записей. Для этого вы можете сделать класс Entry, унаследовав его от tornado.web.UIModule, вызывая который вы сможете отображать модуль записи на любой типе страницы. Для начала, создайте отдельный файл python в котором мы будем хранить модули интерфейса: uimodules.py и разместите в него следующий код:

   1 class Entry(tornado.web.UIModule):
   2     def render(self, entry, show_comments=False):
   3         return self.render_string(
   4             "module-entry.html", show_comments=show_comments)

В настройках приложения укажите uimodules.py в качестве файла с модулями интерфейса, используя опцию ui_modules.

   1 class HomeHandler(tornado.web.RequestHandler):
   2     def get(self):
   3         entries = self.db.query("SELECT * FROM entries ORDER BY date DESC")
   4         self.render("home.html", entries=entries)
   5 
   6 class EntryHandler(tornado.web.RequestHandler):
   7     def get(self, entry_id):
   8         entry = self.db.get("SELECT * FROM entries WHERE id = %s", entry_id)
   9         if not entry: raise tornado.web.HTTPError(404)
  10         self.render("entry.html", entry=entry)
  11 
  12 settings = {
  13     "ui_modules": uimodules,
  14 }
  15 application = tornado.web.Application([
  16     (r"/", HomeHandler),
  17     (r"/entry/([0-9]+)", EntryHandler),
  18 ], **settings)

В основном шаблоне home.html вы не включаете html-код напрямую, а ссылаетесь на модуль Entry

   1 {% for entry in entries %}
   2   {{ modules.Entry(entry) }}
   3 {% end %}

В шаблоне отдельной записи entry.html вы также ссылаетесь на модуль Entry, но уже с аргументом show_comments , чтобы показать расширенный вариант модуля записи:

   1 {{ modules.Entry(entry, show_comments=True) }}

В модули могут включаться произвольные CSS таблицы стилей и функции JavaScript посредством переопределеня методов embedded_css, embedded_javascript, javascript_files или css_files.

   1 class Entry(tornado.web.UIModule):
   2     def embedded_css(self):
   3         return ".entry { margin-bottom: 1em; }"
   4 
   5     def render(self, entry, show_comments=False):
   6         return self.render_string(
   7             "module-entry.html", show_comments=show_comments)

Модули CSS и JavaScript будут подключены к итоговой странице единократно, вне зависимости от количества вызовов модуля пользовательского интерфейса в пределах нее. CSS будет включена в элемент <head> страницы, а JavaScript будет размещен прямо перед тегом </body> в конце страницы.

Неблокирующие, асинхронные запросы

Обычно, когда заканчивается выполнение обработчика HTTP запроса, запрос автоматически завершается. Так как Tornado использует неблокирующий стиль ввода-вывода, но вы можете переназначить стандартное поведение, если хотите чтобы запрос оставался открытым после возврата из основного метода обработчика, используя декоратор класса-обработчика tornado.web.asynchronous.

При его использовании вы должны сами вызвать self.finish(), чтобы завершить HTTP запрос, или вы создадите лишнюю нагрузку на браузер пользователя или "повесите" его.

   1 class MainHandler(tornado.web.RequestHandler):
   2     @tornado.web.asynchronous
   3     def get(self):
   4         self.write("Hello, world")
   5         self.finish()

Ниже приведен рабочий пример, который делает обращение к FriendFeed API, используя встроенный асинхронный HTTP клиент Торнадо:

   1 class MainHandler(tornado.web.RequestHandler):
   2     @tornado.web.asynchronous
   3     def get(self):
   4         http = tornado.httpclient.AsyncHTTPClient()
   5         http.fetch("http://friendfeed-api.com/v2/feed/bret",
   6                    callback=self.async_callback(self.on_response))
   7 
   8     def on_response(self, response):
   9         if response.error: raise tornado.web.HTTPError(500)
  10         json = tornado.escape.json_decode(response.body)
  11         self.write("Fetched " + str(len(json["entries"])) + " entries "
  12                    "from the FriendFeed API")
  13         self.finish()

Когда метод get() возвращает управление, HTTP запрос остается незавершенным. Когда, наконец, вызывается метод on_responce(), запрос все еще открыт. В конце метода ответа мы должны отдать браузеру результат выполнения обработчика и закрыть соединение посредством вызова self.finish().

Если вы вызываете асинхронные библиотечные функции, которые требуют ответа (например функция HTTP fetch в примере выше), вы должны обязательно обернуть их self.async_callback. Эта простая обертка гарантирует, что если ваша функция создаст исключение или программную ошибку, то браузеру будет послан соответствующий HTTP-ответ с кодом ошибки и соединение будет корректно завершено.

В качестве более развернутого примера вы можете посмотреть приложение chat, идущее в качестве примера с Tornado, которое является чатом на основе AJAX и использует т.н. длинные запросы (long polling)

Аутентификация с помощью сторонних разработчиков

Модуль auth Tornado предоставляет протоколы авторизации и аутентификации для ряда наиболее популярных веб-сайтов, включая Google/Gmail, Facebook, Twitter, Yahoo и FriendFeed. Модуль включает методы авторизации пользователей через эти сайты и, где это возможно, методы доступа к сервисам, так что вы можете, например, загрузить пользовательские контакты или опубликовать сообщение в Твиттере, используя учетную запись пользователя.

Вот пример обработчика, котрый использует Google для аутентификации, сохраняя полномочия в cookie для дальнейшего доступа:

   1 class GoogleHandler(tornado.web.RequestHandler, tornado.auth.GoogleMixin):
   2     @tornado.web.asynchronous
   3     def get(self):
   4         if self.get_argument("openid.mode", None):
   5             self.get_authenticated_user(self.async_callback(self._on_auth))
   6             return
   7         self.authenticate_redirect()
   8 
   9     def _on_auth(self, user):
  10         if not user:
  11             self.authenticate_redirect()
  12             return
  13         # Save the user with, e.g., set_secure_cookie()

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

Более подробно вы можете узнать про это из документации к модулю auth.

Производительность

Производительность web-приложений в первую очередь ограничена архитектурой, а не производительностью frontend-а. И тем не менее Tornado весьма быстр по отношению к большинству популярных web-фреймворков на Python.

Мы запустили несколько тестов на нагрузку, используя простое приложение типа "Hello, world!" на каждом из наиболее популярных Python web-фреймворков (Django, web.py и CherryPy) чтобы получить общее представление о производительности каждого из них по отношению к Tornado. Мы использовали Apache/mod_wsgi для Django и web.py, CherryPy был запущен на отдельном сервере, что соответствовало нашим представлениям о типичном окружении производственных решений каждого из фреймворков. Мы запустили 4 однопоточных Tornado фронтэнда за обратным прокси ngnix, что соответствует нашим рекомендациям по запуску Tornado в производство. (наш тестовый сервер был четырехядерным и мы рекомендуем по одному фронтэнду на ядро).

Тест каждого решения на нагрузку осуществлялся с помощью Apache Benchmark (ab) на отдельной машине с помощью команды

   1 ab -n 100000 -c 25 http://10.0.1.x/

Результаты (обработанных запросов в секунду) на четырехядерном процессоре 2.4GHz AMD Opteron:

Tornado (ngnix; 4 frontends)

8213

Tornado (1 single-threaded frontend)

3353

Django (Apache/mod_wsgi)

2223

web.py (Apache/mod_wsgi)

2066

CherryPy (standalone)

785

По результатам наших тестов Tornado четырехкратно превосходил следующий по производительности фреймворк, и даже в однопоточном варианте (с использованием только одного ядра из четырех) оказался на 33% быстрее его.

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

Запуск Tornado на производстве

В проекте FriendFeed мы используем ngnix в качестве балансировщика нагрузки и сервера статики. У нас запущено множество экземпляров web-сервера Tornado на множестве серверов, обслуживающих фронтэнд. Обычно мы запускаем один фронтэнд Tornado на аппаратное ядро (иногда больше, в зависимости от ситуации).

Это готовый конфигурационный файл ngnix, который структурно схож с тем, что мы используем в проекте FriendFeed. В данном случае предполагается, что ngnix и Tornado запущены на одной машине и четыре сервера Tornado запущены на портах с 8000 по 8003:

   1 user nginx;
   2 worker_processes 1;
   3 
   4 error_log /var/log/nginx/error.log;
   5 pid /var/run/nginx.pid;
   6 
   7 events {
   8     worker_connections 1024;
   9     use epoll;
  10 }
  11 
  12 http {
  13     # Enumerate all the Tornado servers here
  14     upstream frontends {
  15         server 127.0.0.1:8000;
  16         server 127.0.0.1:8001;
  17         server 127.0.0.1:8002;
  18         server 127.0.0.1:8003;
  19     }
  20 
  21     include /etc/nginx/mime.types;
  22     default_type application/octet-stream;
  23 
  24     access_log /var/log/nginx/access.log;
  25 
  26     keepalive_timeout 65;
  27     proxy_read_timeout 200;
  28     sendfile on;
  29     tcp_nopush on;
  30     tcp_nodelay on;
  31     gzip on;
  32     gzip_min_length 1000;
  33     gzip_proxied any;              
  34     gzip_types text/plain text/html text/css text/xml
  35                application/x-javascript application/xml
  36                application/atom+xml text/javascript;
  37 
  38     # Only retry if there was a communication error, not a timeout
  39     # on the Tornado server (to avoid propagating "queries of death"
  40     # to all frontends)
  41     proxy_next_upstream error;
  42 
  43     server {
  44         listen 80;
  45 
  46         # Allow file uploads
  47         client_max_body_size 50M;
  48 
  49         location ^~ /static/ {
  50             root /var/www;
  51             if ($query_string) {
  52                 expires max;
  53             }
  54         }
  55         location = /favicon.ico {
  56             rewrite (.*) /static/favicon.ico;
  57         }
  58         location = /robots.txt {
  59             rewrite (.*) /static/robots.txt;
  60         }
  61 
  62         location / {
  63             proxy_pass_header Server;
  64             proxy_set_header Host $http_host;
  65             proxy_redirect false;
  66             proxy_set_header X-Real-IP $remote_addr;
  67             proxy_set_header X-Scheme $scheme;
  68             proxy_pass http://frontends;
  69         }
  70     }
  71 }

WSGI и Google AppEngine

Торнадо поставляется с ограниченной поддержкой WSGI. Тем не менее, так как WSGI не поддерживает неблокирующие запросы вы не сможете использовать какие либо асинхронные/неблокирующие функции Tornado в вашем приложении, если вы предпочтете использовать WSGI вместо HTTP сервера Tornado. Некоторые возможности, недоступные для использования в WSGI приложениях: @tornado.web.asynchronous, the httpclient module, and the auth module.

Вы можете создать рабочее WSGI приложение с обработчиками запросов Tornado, используя WSGIApplication в модуле wsgi вместо tornado.web.Application. Далее приведен пример который использует встроенный WSGI CGIHandler для создания рабчего приложения Google AppEngine:

   1 import tornado.web
   2 import tornado.wsgi
   3 import wsgiref.handlers
   4 
   5 class MainHandler(tornado.web.RequestHandler):
   6     def get(self):
   7         self.write("Hello, world")
   8 
   9 if __name__ == "__main__":
  10     application = tornado.wsgi.WSGIApplication([
  11         (r"/", MainHandler),
  12     ])
  13     wsgiref.handlers.CGIHandler().run(application)

Посмотрите пример appengine для того, чтобы ознакомиться с полноценным приложением AppEngine, построенным на Tornado.

Предостережения и поддержка

Tornado является переработанным движком FriendFeed у которого были убранны специфичные для проекта зависимости. Рефакторинг мог породить ошибки. Так как серверы FriendFeed всегда работали за ngnix, Tornado не был тщательно протестирован с клиентами HTTP/1.1 кроме Firefox. На данный момент Tornado не обрабатывает многострочные заголовки и некоторые варианты неправильно сформированных запросов.

Вы можете обсудить Tornado и сообщить об ошибках в рассылке для разработчиков Tornado

Перевод: Ростислав Дзинько, Илья Кутуков

Документации/Tornado-web (последним исправлял пользователь RostislavDzinko 2010-07-26 17:37:55)