867 lines
35 KiB
Plaintext
867 lines
35 KiB
Plaintext
Overview
|
|
--------
|
|
[FriendFeed](http://friendfeed.com/)'s web server is a relatively simple,
|
|
non-blocking web server written in Python. The FriendFeed application is
|
|
written using a web framework that looks a bit like
|
|
[web.py](http://webpy.org/) or Google's
|
|
[webapp](http://code.google.com/appengine/docs/python/tools/webapp/),
|
|
but with additional tools and optimizations to take advantage of the
|
|
non-blocking web server and tools.
|
|
|
|
[Tornado](http://github.com/facebook/tornado) is an open source
|
|
version of this web server and some of the tools we use most often at
|
|
FriendFeed. The framework is distinct from most mainstream web server
|
|
frameworks (and certainly most Python frameworks) because it is
|
|
non-blocking and reasonably fast. Because it is non-blocking
|
|
and uses [epoll](http://www.kernel.org/doc/man-pages/online/pages/man4/epoll.4.html), it can handle 1000s of simultaneous standing connections,
|
|
which means the framework is ideal for real-time web services. We built the
|
|
web server specifically to handle FriendFeed's real-time features —
|
|
every active user of FriendFeed maintains an open connection to the
|
|
FriendFeed servers. (For more information on scaling servers to support
|
|
thousands of clients, see
|
|
[The C10K problem](http://www.kegel.com/c10k.html).)
|
|
|
|
Here is the canonical "Hello, world" example app:
|
|
|
|
import tornado.httpserver
|
|
import tornado.ioloop
|
|
import tornado.web
|
|
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
self.write("Hello, world")
|
|
|
|
application = tornado.web.Application([
|
|
(r"/", MainHandler),
|
|
])
|
|
|
|
if __name__ == "__main__":
|
|
http_server = tornado.httpserver.HTTPServer(application)
|
|
http_server.listen(8888)
|
|
tornado.ioloop.IOLoop.instance().start()
|
|
|
|
See [Tornado walkthrough](#tornado-walkthrough) below for a detailed
|
|
walkthrough of the `tornado.web` package.
|
|
|
|
We attempted to clean up the code base to reduce interdependencies between
|
|
modules, so you should (theoretically) be able to use any of the modules
|
|
independently in your project without using the whole package.
|
|
|
|
|
|
Download
|
|
--------
|
|
Download the most recent version of Tornado from GitHub:
|
|
|
|
> [tornado-0.2.tar.gz](/static/tornado-0.2.tar.gz)
|
|
|
|
You can also [browse the source](http://github.com/facebook/tornado) on GitHub. To install Tornado:
|
|
|
|
tar xvzf tornado-0.2.tar.gz
|
|
cd tornado-0.2
|
|
python setup.py build
|
|
sudo python setup.py install
|
|
|
|
After installation, you should be able to run any of the demos in the `demos`
|
|
directory included with the Tornado package.
|
|
|
|
./demos/helloworld/helloworld.py
|
|
|
|
### Prerequisites
|
|
|
|
Tornado has been tested on Python 2.5 and 2.6. To use all of the features of Tornado, you need to have [PycURL](http://pycurl.sourceforge.net/) and a JSON library like [simplejson](http://pypi.python.org/pypi/simplejson/) installed. Complete installation instructions for Mac OS X and Ubuntu are included below for convenience.
|
|
|
|
**Mac OS X 10.5/10.6**
|
|
|
|
sudo easy_install setuptools pycurl==7.16.2.1 simplejson
|
|
|
|
**Ubuntu Linux**
|
|
|
|
sudo apt-get install python-dev python-pycurl python-simplejson
|
|
|
|
|
|
Module index
|
|
------------
|
|
The most important module is [`web`](http://github.com/facebook/tornado/blob/master/tornado/web.py), which is the web framework
|
|
that includes most of the meat of the Tornado package. The other modules
|
|
are tools that make `web` more useful. See
|
|
[Tornado walkthrough](#tornado-walkthrough) below for a detailed
|
|
walkthrough of the `web` package.
|
|
|
|
### Main modules
|
|
* [`web`](http://github.com/facebook/tornado/blob/master/tornado/web.py) - The web framework on which FriendFeed is built. `web` incorporates most of the important features of Tornado
|
|
* [`escape`](http://github.com/facebook/tornado/blob/master/tornado/escape.py) - XHTML, JSON, and URL encoding/decoding methods
|
|
* [`database`](http://github.com/facebook/tornado/blob/master/tornado/database.py) - A simple wrapper around `MySQLdb` to make MySQL easier to use
|
|
* [`template`](http://github.com/facebook/tornado/blob/master/tornado/template.py) - A Python-based web templating language
|
|
* [`httpclient`](http://github.com/facebook/tornado/blob/master/tornado/httpclient.py) - A non-blocking HTTP client designed to work with `web` and `httpserver`
|
|
* [`auth`](http://github.com/facebook/tornado/blob/master/tornado/auth.py) - Implementation of third party authentication and authorization schemes (Google OpenID/OAuth, Facebook Platform, Yahoo BBAuth, FriendFeed OpenID/OAuth, Twitter OAuth)
|
|
* [`locale`](http://github.com/facebook/tornado/blob/master/tornado/locale.py) - Localization/translation support
|
|
* [`options`](http://github.com/facebook/tornado/blob/master/tornado/options.py) - Command line and config file parsing, optimized for server environments
|
|
|
|
### Low-level modules
|
|
* [`httpserver`](http://github.com/facebook/tornado/blob/master/tornado/httpserver.py) - A very simple HTTP server built on which `web` is built
|
|
* [`iostream`](http://github.com/facebook/tornado/blob/master/tornado/iostream.py) - A simple wrapper around non-blocking sockets to aide common reading and writing patterns
|
|
* [`ioloop`](http://github.com/facebook/tornado/blob/master/tornado/ioloop.py) - Core I/O loop
|
|
|
|
### Random modules
|
|
* [`s3server`](http://github.com/facebook/tornado/blob/master/tornado/s3server.py) - A web server that implements most of the [Amazon S3](http://aws.amazon.com/s3/) interface, backed by local file storage
|
|
|
|
|
|
Tornado walkthrough
|
|
-------------------
|
|
|
|
### Request handlers and request arguments
|
|
|
|
A Tornado web application maps URLs or URL patterns to subclasses of
|
|
`tornado.web.RequestHandler`. Those classes define `get()` or `post()`
|
|
methods to handle HTTP `GET` or `POST` requests to that URL.
|
|
|
|
This code maps the root URL `/` to `MainHandler` and the URL pattern
|
|
`/story/([0-9]+)` to `StoryHandler`. Regular expression groups are passed
|
|
as arguments to the `RequestHandler` methods:
|
|
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
self.write("You requested the main page")
|
|
|
|
class StoryHandler(tornado.web.RequestHandler):
|
|
def get(self, story_id):
|
|
self.write("You requested the story " + story_id)
|
|
|
|
application = tornado.web.Application([
|
|
(r"/", MainHandler),
|
|
(r"/story/([0-9]+)", StoryHandler),
|
|
])
|
|
|
|
You can get query string arguments and parse `POST` bodies with the
|
|
`get_argument()` method:
|
|
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
self.write('<html><body><form action="/" method="post">'
|
|
'<input type="text" name="message">'
|
|
'<input type="submit" value="Submit">'
|
|
'</form></body></html>')
|
|
|
|
def post(self):
|
|
self.set_header("Content-Type", "text/plain")
|
|
self.write("You wrote " + self.get_argument("message"))
|
|
|
|
If you want to send an error response to the client, e.g., 403 Unauthorized,
|
|
you can just raise a `tornado.web.HTTPError` exception:
|
|
|
|
if not self.user_is_logged_in():
|
|
raise tornado.web.HTTPError(403)
|
|
|
|
The request handler can access the object representing the current request
|
|
with `self.request`. The `HTTPRequest` object includes a number of useful
|
|
attribute, including:
|
|
|
|
* `arguments` - all of the `GET` and `POST` arguments
|
|
* `files` - all of the uploaded files (via `multipart/form-data` POST requests)
|
|
* `path` - the request path (everything before the `?`)
|
|
* `headers` - the request headers
|
|
|
|
See the class definition for `HTTPRequest` in `httpserver` for a complete list
|
|
of attributes.
|
|
|
|
|
|
### Templates
|
|
|
|
You can use any template language supported by Python, but Tornado ships
|
|
with its own templating language that is a lot faster and more flexible
|
|
than many of the most popular templating systems out there. See the
|
|
[`template`](http://github.com/facebook/tornado/blob/master/tornado/template.py) module documentation for complete documentation.
|
|
|
|
A Tornado template is just HTML (or any other text-based format) with
|
|
Python control sequences and expressions embedded within the markup:
|
|
|
|
<html>
|
|
<head>
|
|
<title>{{ title }}</title>
|
|
</head>
|
|
<body>
|
|
<ul>
|
|
{% for item in items %}
|
|
<li>{{ escape(item) }}</li>
|
|
{% end %}
|
|
</ul>
|
|
</body>
|
|
</html>
|
|
|
|
If you saved this template as "template.html" and put it in the same
|
|
directory as your Python file, you could render this template with:
|
|
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
items = ["Item 1", "Item 2", "Item 3"]
|
|
self.render("template.html", title="My title", items=items)
|
|
|
|
Tornado templates support *control statements* and *expressions*. Control
|
|
statements are surronded by `{%` and `%}`, e.g., `{% if len(items) > 2 %}`.
|
|
Expressions are surrounded by `{{` and `}}`, e.g., `{{ items[0] }}`.
|
|
|
|
Control statements more or less map exactly to Python statements. We support
|
|
`if`, `for`, `while`, and `try`, all of which are terminated with `{% end %}`.
|
|
We also support *template inheritance* using the `extends` and `block`
|
|
statements, which are described in detail in the documentation for the
|
|
[`template` module](http://github.com/facebook/tornado/blob/master/tornado/template.py).
|
|
|
|
Expressions can be any Python expression, including function calls. We
|
|
support the functions `escape`, `url_escape`, and `json_encode` by default,
|
|
and you can pass other functions into the template simply by passing them
|
|
as keyword arguments to the template render function:
|
|
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
self.render("template.html", add=self.add)
|
|
|
|
def add(self, x, y):
|
|
return x + y
|
|
|
|
When you are building a real application, you are going to want to use
|
|
all of the features of Tornado templates, especially template inheritance.
|
|
Read all about those features in the [`template` module](http://github.com/facebook/tornado/blob/master/tornado/template.py)
|
|
section.
|
|
|
|
Under the hood, Tornado templates are translated directly to Python.
|
|
The expressions you include in your template are copied verbatim into
|
|
a Python function representing your template. We don't try to prevent
|
|
anything in the template language; we created it explicitly to provide
|
|
the flexibility that other, stricter templating systems prevent.
|
|
Consequently, if you write random stuff inside of your template expressions,
|
|
you will get random Python errors when you execute the template.
|
|
|
|
|
|
### Cookies and secure cookies
|
|
|
|
You can set cookies in the user's browser with the `set_cookie` method:
|
|
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
if not self.get_cookie("mycookie"):
|
|
self.set_cookie("mycookie", "myvalue")
|
|
self.write("Your cookie was not set yet!")
|
|
else:
|
|
self.write("Your cookie was set!")
|
|
|
|
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:
|
|
|
|
application = tornado.web.Application([
|
|
(r"/", MainHandler),
|
|
], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")
|
|
|
|
Signed cookies contain the encoded value of the cookie in addition to a
|
|
timestamp and an [HMAC](http://en.wikipedia.org/wiki/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:
|
|
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
if not self.get_secure_cookie("mycookie"):
|
|
self.set_secure_cookie("mycookie", "myvalue")
|
|
self.write("Your cookie was not set yet!")
|
|
else:
|
|
self.write("Your cookie was set!")
|
|
|
|
|
|
### User authentication
|
|
|
|
The currently authenticated user is available in every request handler
|
|
as `self.current_user`, and in every template as `current_user`. By
|
|
default, `current_user` is `None`.
|
|
|
|
To implement user authentication in your application, you need to
|
|
override the `get_current_user()` method in your request handlers to
|
|
determine the current user based on, e.g., the value of a cookie.
|
|
Here is an example that lets users log into the application simply
|
|
by specifying a nickname, which is then saved in a cookie:
|
|
|
|
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=")
|
|
|
|
You can require that the user be logged in using the
|
|
[Python decorator](http://www.python.org/dev/peps/pep-0318/)
|
|
`tornado.web.authenticated`. If a request goes to a method with this
|
|
decorator, and the user is not logged in, they will be redirected to
|
|
`login_url` (another application setting). The example above could
|
|
be rewritten:
|
|
|
|
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)
|
|
|
|
If you decorate `post()` methods with the `authenticated` decorator, and
|
|
the user is not logged in, the server will send a `403` response.
|
|
|
|
Tornado comes with built-in support for third-party authentication schemes
|
|
like Google OAuth. See the [`auth` module](http://github.com/facebook/tornado/blob/master/tornado/auth.py) for more details. Check
|
|
out the Tornado Blog example application for a complete example that
|
|
uses authentication (and stores user data in a MySQL database).
|
|
|
|
|
|
### Cross-site request forgery protection
|
|
|
|
[Cross-site request forgery](http://en.wikipedia.org/wiki/Cross-site_request_forgery), or XSRF, is a common problem for personalized web applications. See the
|
|
[Wikipedia article](http://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
|
for more information on how XSRF works.
|
|
|
|
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`:
|
|
|
|
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:
|
|
|
|
<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](http://jquery.com/) function we use at FriendFeed for AJAX `POST`
|
|
requests that automatically adds the `_xsrf` value to all requests:
|
|
|
|
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 + ")"));
|
|
}});
|
|
};
|
|
|
|
|
|
### Static files and aggressive file caching
|
|
|
|
You can serve static files from Tornado by specifying the `static_path`
|
|
setting in your application:
|
|
|
|
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)
|
|
|
|
This setting will automatically make all requests that start with `/static/`
|
|
serve from that static directory, e.g., [http://localhost:8888/static/foo.png](http://localhost:8888/static/foo.png)
|
|
will serve the file `foo.png` from the specified static directory. We
|
|
also automatically serve `/robots.txt` and `/favicon.ico` from the static
|
|
directory (even though they don't start with the `/static/` prefix).
|
|
|
|
To improve performance, it is generally a good idea for browsers to
|
|
cache static resources aggressively so browsers won't send unnecessary
|
|
`If-Modified-Since` or `Etag` requests that might block the rendering of
|
|
the page. Tornado supports this out of the box with *static content
|
|
versioning*.
|
|
|
|
To use this feature, use the `static_url()` method in your templates rather
|
|
than typing the URL of the static file directly in your HTML:
|
|
|
|
<html>
|
|
<head>
|
|
<title>FriendFeed - {{ _("Home") }}</title>
|
|
</head>
|
|
<body>
|
|
<div><img src="{{ static_url("images/logo.png") }}"/></div>
|
|
</body>
|
|
</html>
|
|
|
|
The `static_url()` function will translate that relative path to a URI
|
|
that looks like `/static/images/logo.png?v=aae54`. The `v` argument is
|
|
a hash of the content in `logo.png`, and its presence makes the Tornado
|
|
server send cache headers to the user's browser that will make the browser
|
|
cache the content indefinitely.
|
|
|
|
Since the `v` argument is based on the content of the file, if you update
|
|
a file and restart your server, it will start sending a new `v` value,
|
|
so the user's browser will automatically fetch the new file. If the file's
|
|
contents don't change, the browser will continue to use a locally cached
|
|
copy without ever checking for updates on the server, significantly
|
|
improving rendering performance.
|
|
|
|
In production, you probably want to serve static files from a more
|
|
optimized static file server like [nginx](http://nginx.net/). You can
|
|
configure most any web server to support these caching semantics. Here
|
|
is the nginx configuration we use at FriendFeed:
|
|
|
|
location /static/ {
|
|
root /var/friendfeed/static;
|
|
if ($query_string) {
|
|
expires max;
|
|
}
|
|
}
|
|
|
|
|
|
### Localization
|
|
|
|
The locale of the current user (whether they are logged in or not) is
|
|
always available as `self.locale` in the request handler and as `locale`
|
|
in templates. The name of the locale (e.g., `en_US`) is available as
|
|
`locale.name`, and you can translate strings with the `locale.translate`
|
|
method. Templates also have the global function call `_()` available
|
|
for string translation. The translate function has two forms:
|
|
|
|
_("Translate this string")
|
|
|
|
which translates the string directly based on the current locale, and
|
|
|
|
_("A person liked this", "%(num)d people liked this", len(people)) % {"num": len(people)}
|
|
|
|
which translates a string that can be singular or plural based on the value
|
|
of the third argument. In the example above, a translation of the first
|
|
string will be returned if `len(people)` is `1`, or a translation of the
|
|
second string will be returned otherwise.
|
|
|
|
The most common pattern for translations is to use Python named placeholders
|
|
for variables (the `%(num)d` in the example above) since placeholders can
|
|
move around on translation.
|
|
|
|
Here is a properly localized template:
|
|
|
|
<html>
|
|
<head>
|
|
<title>FriendFeed - {{ _("Sign in") }}</title>
|
|
</head>
|
|
<body>
|
|
<form action="{{ request.path }}" method="post">
|
|
<div>{{ _("Username") }} <input type="text" name="username"/></div>
|
|
<div>{{ _("Password") }} <input type="password" name="password"/></div>
|
|
<div><input type="submit" value="{{ _("Sign in") }}"/></div>
|
|
{{ xsrf_form_html() }}
|
|
</form>
|
|
</body>
|
|
</html>
|
|
|
|
By default, we detect the user's locale using the `Accept-Language` header
|
|
sent by the user's browser. We choose `en_US` if we can't find an appropriate
|
|
`Accept-Language` value. If you let user's set their locale as a preference,
|
|
you can override this default locale selection by overriding `get_user_locale`
|
|
in your request handler:
|
|
|
|
class BaseHandler(tornado.web.RequestHandler):
|
|
def get_current_user(self):
|
|
user_id = self.get_secure_cookie("user")
|
|
if not user_id: return None
|
|
return self.backend.get_user_by_id(user_id)
|
|
|
|
def get_user_locale(self):
|
|
if "locale" not in self.current_user.prefs:
|
|
# Use the Accept-Language header
|
|
return None
|
|
return self.current_user.prefs["locale"]
|
|
|
|
If `get_user_locale` returns `None`, we fall back on the `Accept-Language`
|
|
header.
|
|
|
|
You can load all the translations for your application using the
|
|
`tornado.locale.load_translations` method. It takes in the name of the
|
|
directory which should contain CSV files named after the locales whose
|
|
translations they contain, e.g., `es_GT.csv` or `fr_CA.csv`. The method
|
|
loads all the translations from those CSV files and infers the list of
|
|
supported locales based on the presence of each CSV file. You typically
|
|
call this method once in the `main()` method of your server:
|
|
|
|
def main():
|
|
tornado.locale.load_translations(
|
|
os.path.join(os.path.dirname(__file__), "translations"))
|
|
start_server()
|
|
|
|
You can get the list of supported locales in your application with
|
|
`tornado.locale.get_supported_locales()`. The user's locale is chosen to
|
|
be the closest match based on the supported locales. For example, if the
|
|
user's locale is `es_GT`, and the `es` locale is supported, `self.locale`
|
|
will be `es` for that request. We fall back on `en_US` if no close match
|
|
can be found.
|
|
|
|
See the [`locale` module](http://github.com/facebook/tornado/blob/master/tornado/locale.py) documentation for detailed information
|
|
on the CSV format and other localization methods.
|
|
|
|
|
|
### UI modules
|
|
|
|
Tornado supports *UI modules* to make it easy to support standard, reusable
|
|
UI widgets across your application. UI modules are like special functional
|
|
calls to render components of your page, and they can come packaged with
|
|
their own CSS and JavaScript.
|
|
|
|
For example, if you are implementing a blog, and you want to have
|
|
blog entries appear on both the blog home page and on each blog entry page,
|
|
you can make an `Entry` module to render them on both pages. First, create
|
|
a Python module for your UI modules, e.g., `uimodules.py`:
|
|
|
|
class Entry(tornado.web.UIModule):
|
|
def render(self, entry, show_comments=False):
|
|
return self.render_string(
|
|
"module-entry.html", show_comments=show_comments)
|
|
|
|
Tell Tornado to use `uimodules.py` using the `ui_modules` setting in your
|
|
application:
|
|
|
|
class HomeHandler(tornado.web.RequestHandler):
|
|
def get(self):
|
|
entries = self.db.query("SELECT * FROM entries ORDER BY date DESC")
|
|
self.render("home.html", entries=entries)
|
|
|
|
class EntryHandler(tornado.web.RequestHandler):
|
|
def get(self, entry_id):
|
|
entry = self.db.get("SELECT * FROM entries WHERE id = %s", entry_id)
|
|
if not entry: raise tornado.web.HTTPError(404)
|
|
self.render("entry.html", entry=entry)
|
|
|
|
settings = {
|
|
"ui_modules": uimodules,
|
|
}
|
|
application = tornado.web.Application([
|
|
(r"/", HomeHandler),
|
|
(r"/entry/([0-9]+)", EntryHandler),
|
|
], **settings)
|
|
|
|
Within `home.html`, you reference the `Entry` module rather than printing
|
|
the HTML directly:
|
|
|
|
{% for entry in entries %}
|
|
{{ modules.Entry(entry) }}
|
|
{% end %}
|
|
|
|
Within `entry.html`, you reference the `Entry` module with the
|
|
`show_comments` argument to show the expanded form of the entry:
|
|
|
|
{{ modules.Entry(entry, show_comments=True) }}
|
|
|
|
Modules can include custom CSS and JavaScript functions by overriding
|
|
the `embedded_css`, `embedded_javascript`, `javascript_files`, or
|
|
`css_files` methods:
|
|
|
|
class Entry(tornado.web.UIModule):
|
|
def embedded_css(self):
|
|
return ".entry { margin-bottom: 1em; }"
|
|
|
|
def render(self, entry, show_comments=False):
|
|
return self.render_string(
|
|
"module-entry.html", show_comments=show_comments)
|
|
|
|
Module CSS and JavaScript will be included once no matter how many times
|
|
a module is used on a page. CSS is always included in the `<head>` of the
|
|
page, and JavaScript is always included just before the `</body>` tag
|
|
at the end of the page.
|
|
|
|
|
|
### Non-blocking, asynchronous requests
|
|
|
|
When a request handler is executed, the request is automatically finished.
|
|
Since Tornado uses a non-blocking I/O style, you can override this default
|
|
behavior if you want a request to remain open after the main request handler
|
|
method returns using the `tornado.web.asynchronous` decorator.
|
|
|
|
When you use this decorator, it is your responsibility to call
|
|
`self.finish()` to finish the HTTP request, or the user's browser
|
|
will simply hang:
|
|
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
@tornado.web.asynchronous
|
|
def get(self):
|
|
self.write("Hello, world")
|
|
self.finish()
|
|
|
|
Here is a real example that makes a call to the FriendFeed API using
|
|
Tornado's built-in asynchronous HTTP client:
|
|
|
|
class MainHandler(tornado.web.RequestHandler):
|
|
@tornado.web.asynchronous
|
|
def get(self):
|
|
http = tornado.httpclient.AsyncHTTPClient()
|
|
http.fetch("http://friendfeed-api.com/v2/feed/bret",
|
|
callback=self.async_callback(self.on_response))
|
|
|
|
def on_response(self, response):
|
|
if response.error: raise tornado.web.HTTPError(500)
|
|
json = tornado.escape.json_decode(response.body)
|
|
self.write("Fetched " + str(len(json["entries"])) + " entries "
|
|
"from the FriendFeed API")
|
|
self.finish()
|
|
|
|
When `get()` returns, the request has not finished. When the HTTP client
|
|
eventually calls `on_response()`, the request is still open, and the response
|
|
is finally flushed to the client with the call to `self.finish()`.
|
|
|
|
If you make calls to asynchronous library functions that require a callback
|
|
(like the HTTP `fetch` function above), you should always wrap your
|
|
callbacks with `self.async_callback`. This simple wrapper ensures that if
|
|
your callback function raises an exception or has a programming error,
|
|
a proper HTTP error response will be sent to the browser, and the connection
|
|
will be properly closed.
|
|
|
|
For a more advanced asynchronous example, take a look at the `chat` example
|
|
application, which implements an AJAX chat room using
|
|
[long polling](http://en.wikipedia.org/wiki/Push_technology#Long_polling).
|
|
|
|
|
|
### Third party authentication
|
|
|
|
Tornado's `auth` module implements the authentication and authorization
|
|
protocols for a number of the most popular sites on the web, including
|
|
Google/Gmail, Facebook, Twitter, Yahoo, and FriendFeed. The module includes
|
|
methods to log users in via these sites and, where applicable, methods to
|
|
authorize access to the service so you can, e.g., download a user's address
|
|
book or publish a Twitter message on their behalf.
|
|
|
|
Here is an example handler that uses Google for authentication, saving
|
|
the Google credentials in a cookie for later access:
|
|
|
|
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()
|
|
|
|
See the `auth` module documentation for more details.
|
|
|
|
|
|
Performance
|
|
-----------
|
|
Web application performance is generally bound by architecture, not frontend
|
|
performance. That said, Tornado is pretty fast relative to most popular
|
|
Python web frameworks.
|
|
|
|
We ran a few remedial load tests on a simple "Hello, world" application
|
|
in each of the most popular Python web frameworks
|
|
([Django](http://www.djangoproject.com/), [web.py](http://webpy.org/), and
|
|
[CherryPy](http://www.cherrypy.org/)) to get the baseline performance of
|
|
each relative to Tornado. We used Apache/mod_wsgi for Django and web.py
|
|
and ran CherryPy as a standalone server, which was our impression of how
|
|
each framework is typically run in production environments. We ran 4
|
|
single-threaded Tornado frontends behind an [nginx](http://nginx.net/)
|
|
reverse proxy, which is how we recommend running Tornado in production
|
|
(our load test machine had four cores, and we recommend 1 frontend per
|
|
core).
|
|
|
|
We load tested each with Apache Benchmark (`ab`) on the a separate machine
|
|
with the command
|
|
|
|
ab -n 100000 -c 25 http://10.0.1.x/
|
|
|
|
The results (requests per second) on a 2.4GHz AMD Opteron processor with
|
|
4 cores:
|
|
|
|
<div style="text-align:center;margin-top:2em;margin-bottom:2em"><img src="http://chart.apis.google.com/chart?chxt=y&chd=t%3A100%2C40%2C27%2C25%2C9&chco=609bcc&chm=t+8213%2C000000%2C0%2C0%2C11%7Ct+3353%2C000000%2C0%2C1%2C11%7Ct+2223%2C000000%2C0%2C2%2C11%7Ct+2066%2C000000%2C0%2C3%2C11%7Ct+785%2C000000%2C0%2C4%2C11&chs=600x175&cht=bhs&chtt=Web+server+requests%2Fsec+%28AMD+Opteron%2C+2.4GHz%2C+4+cores%29&chxl=0%3A%7CCherryPy+%28standalone%29%7Cweb.py+%28Apache%2Fmod_wsgi%29%7CDjango+%28Apache%2Fmod_wsgi%29%7CTornado+%281+single-threaded+frontend%29%7CTornado+%28nginx%3B+4+frontends%29%7C"/></div>
|
|
|
|
In our tests, Tornado consistently had 4X the throughput of the next fastest
|
|
framework, and even a single standalone Tornado frontend got 33% more
|
|
throughput even though it only used one of the four cores.
|
|
|
|
Not very scientific, but at a high level, it should give you a sense that we
|
|
have cared about performance as we built Tornado, and it shouldn't add too
|
|
much latency to your apps relative to most Python web development frameworks.
|
|
|
|
|
|
Running Tornado in production
|
|
-----------------------------
|
|
At FriendFeed, we use [nginx](http://nginx.net/) as a load balancer
|
|
and static file server. We run multiple instances of the Tornado web
|
|
server on multiple frontend machines. We typically run one Tornado frontend
|
|
per core on the machine (sometimes more depending on utilization).
|
|
|
|
This is a barebones nginx config file that is structurally similar to the
|
|
one we use at FriendFeed. It assumes nginx and the Tornado servers
|
|
are running on the same machine, and the four Tornado servers
|
|
are running on ports 8000 - 8003:
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
WSGI and Google AppEngine
|
|
-------------------------
|
|
Tornado comes with limited support for [WSGI](http://wsgi.org/). However,
|
|
since WSGI does not support non-blocking requests, you cannot use any
|
|
of the asynchronous/non-blocking features of Tornado in your application
|
|
if you choose to use WSGI instead of Tornado's HTTP server. Some of the
|
|
features that are not available in WSGI applications:
|
|
`@tornado.web.asynchronous`, the `httpclient` module, and the `auth` module.
|
|
|
|
You can create a valid WSGI application from your Tornado request handlers
|
|
by using `WSGIApplication` in the `wsgi` module instead of using
|
|
`tornado.web.Application`. Here is an example that uses the built-in WSGI
|
|
`CGIHandler` to make a valid
|
|
[Google AppEngine](http://code.google.com/appengine/) application:
|
|
|
|
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)
|
|
|
|
See the `appengine` example application for a full-featured AppEngine
|
|
app built on Tornado.
|
|
|
|
|
|
Caveats and support
|
|
-------------------
|
|
Tornado was refactored from the [FriendFeed](http://friendfeed.com/)
|
|
code base to reduce dependencies. This refactoring may have introduced
|
|
bugs. Likewise, because the FriendFeed servers have always run
|
|
[behind nginx](#running-tornado-in-production), Tornado has not been
|
|
extensively tested with HTTP/1.1 clients beyond Firefox. Tornado
|
|
currently does not attempt to handle multi-line headers and some types
|
|
of malformed input.
|
|
|
|
You can discuss Tornado and report bugs on [the Tornado developer mailing list](http://groups.google.com/group/python-tornado).
|