`
freeway2000
  • 浏览: 25311 次
社区版块
存档分类
最新评论

Tornado文档翻译[转]

阅读更多

 

(对 http://www.tornadoweb.org/documentation 上的 Tornado documentation 的翻译。)

1   概述

FriendFeed 所使用的 Web 服务器,是一款使用 Python 编写的,相对简单的非阻塞式 Web 服务器。其应用程序所使用的 Web 框架,看起来有些像 web.py 或者是 Google 的 webapp ,但添加了一些有用的工具,并且针对非阻塞式的服务器环境作了特别优化。

Tornado 就是这个 Web 服务器,及其它在 FriendFeed 中常用工具的开源版本。 Tornado 这个框架相较于现在的主流 Web 服务器框架,包括大多数 Python 的框架,给人的感觉都很不一样,因为它的工作方式是非阻塞式的,而且它非常快。得利于其非阻塞的方式和对 epoll 的运用, Tornado 每秒可以处理 1000 个标准连接,在那些对即时性要求很高的 Web 服务应用场合来说, Tornado 无疑是一个很好的选择。我们使用它来处理 FriendFeed 中的那些即时性的功能需求——每一个活动的用户都会一直保持一个对 FriendFeed 服务器的连接。(关于如何处理成千上万的客户端的连接问题,参阅 The C10K problem

这是经典的 Hello, world 示例:

 

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__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()
 

 

 

查看后面的 Tornado攻略 以了解更多关于 tornado.web 包的细节。

我们努力尝试整理好代码,以最大程度地降低各模块之间的相关性。所以,一般情况下,你可以在项目中独立地使用需要的模块,而不必把整个包都弄进去。

2   下载和安装

自动安装
Tornado 在 PyPI 的列表当中,你可以使用 pip 或者 easy_install 来自动安装它,当然,别忘了之前先把 libcurl 安装上。查看下面的 安装依赖 一节。注意一点,使用 pipeasy_install 安装的 Tornado 并没有包含源代码中的那些 demo 应用。
手动安装

下载 tornado-1.2.1.tar.gz

 

 

tar xvzf tornado-1.2.1.tar.gz
cd tornado-1.2.1
python setup.py build
sudo python setup.py install
 

 

Tornado 的代码是托管在 GitHub 上。对于 Python 2.6 以上的版本,因为标准库中已经包括了对 epoll 的支持,所以你可以不用安装,而只是简单地将 tornado 的目录添加到 PYTHONPATH 就可以使用了。

2.1   安装依赖

Tornado 在 Pyhton 2.5, 2.6, 2.7 当中都经过了测试。要使用 Tornado 的所有功能,你需要安装 PycURL (7.18.2 或更高版本),及 simplejson (仅对 Python 2.5 需要, 2.6 开始,标准库当中已经包括了对 JSON 的支持)。 Mac OS X 及 Ubuntu 中的完整简便安装方法如下:

Mac OS X 10.6 (Python 2.6+)

 

sudo easy_install setuptools pycurl

 

Ubuntu Linux (Pyrhon 2.6+)

 

sudo apt-get install python-pycurl

 

Ubuntu Linux (Python 2.5)

 

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

 

3   模块索引

最重要的一个模块是 web 这个 Web 框架,它包括了 Tornado 的大部分主要功能。其它的模块都是工具性质的,以便让 web 模块更好使用。查看后面的 Tornado攻略 一节了解 web 模块的使用方法。

3.1   主要模块

  • web – FriendFeed 使用的 Web 框架,实现了 Tornado 的大多数重要的功能。
  • escape – 关于 XHTML, JSON, URL 编码解码的一些方法。
  • database – 对 MySQLdb 的一个简单封装。
  • template – 基于 Python 的 web 模板系统。
  • httpclient – 被设计用于同 webhttpserver 协同工作的非阻塞式 HTTP 客户端。
  • auth – 第三方认证的实现(包括有 Google OpenID/OAuth, Facebook Platform, Yahoo BBAuth, FriendFeed OpenID/OAuth, Twitter OAuth)。
  • locale – 一套本地化、翻译机制。
  • options – 针对服务器环境的命令行、配置文件解析工具。

3.2   底层模块

  • httpserver – 服务于 web 模块的一个非常简单的 HTTP 服务器的实现。
  • iostream – 对非阻塞式的常用 socket 读写的简单封装。
  • ioloop – 核心的 I/O 循环。

3.3   其它模块

  • s3server – 一个 Web 服务器,实现了 Amazon S3 的大部分接口,依靠本地文件存储实现。

4   Tornado攻略

4.1   请求处理和请求参数

一个 Tornado 的 Web 应用会将一组 URL 映射到 tornado.web.RequestHandler 的子类上去。在其子类中,定义了 get()post() 这些方法用以处理不同的 HTTP 请求。

下面的代码将根路径 / 映射到 MainHandler ,将一个 URL 模式 /story/([0-9]+) 映射到 StoryHandler 。正则表达式匹配的分组会作为参数引入 RequestHandler 中的相应方法中:

 

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),
])
  

你可以使用 get_argument() 方法获取到发送过来的参数:

 

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"))
 

 

 

上传的文件被放在 self.request.files 当中,通过 name 对应到一个文件列表,每一个文件的格式是 {"filename":..., "content_type":..., "body":...}

如果你想要返回一个错误信息给客户端,比如 403 unauthorized ,只需要抛出一个 tornado.web.HTTPError 异常:

 

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

 

请求的处理器对象可以访问到请求的原始对象,通过 self.request 。这是一个 HTTPRequest 对象,它包括的一些常用属性有:

  • argument – 所有的 GETPOST 的参数。
  • files – 通过 multipart/form-dataPOST 方式上传的所有文件。
  • path – 请求的路径( ? 前面的部分)
  • headers – 请求的头部信息。

完整的属性请查看源码 httpserverHTTPRequest 定义部分。

4.2   RequestHandler中的主要方法

除了 get()post() 以外,在 RequestHandler 中的其它方法被设计用于需要时在子类中重写。对于一个请求的处理,是按如下顺序进行的:

  1. 每一个请求会创建一个新的 RequestHandler 对象。
  2. initialize() 方法被调用,同时 Application 中的关键字参数会被传入。( initialize() 方法是 Tornado 1.1 中新添加的,老版本中用的是 __init__ )。 initialize 方法一般只是把传入的参数存到成员变量中,而不会产生一些输出或者调用像 send_error 之类的方法。
  3. prepare() 被调用。这个方法通常在一个基类中被定义,然后在子类中重用。不管 HTTP 访问使用的是哪种方法 GET 或者 POST ,这个方法都会被调用。 prepare 会产生输出信息,如果它调用了 finish 或者 send_error 这些方法,那么整个处理流程就此结束。
  4. 对于 HTTP 方法的相关方法被调用,像 get()post()put() 这些。如果 URL 的正则表达式模式中有分组匹配,那么相关匹配会作为参数传入方法。

这是一个示范 initialize() 方法的例子:

 

class ProfileHandler(RequestHandler):
    def initialize(self, database):
        self.database = database

    def get(self, username):
        ...

app = Application([
    (r'/user/(.*)', ProfileHandler, dict(database=database)),
    ])
 其它可用,并且经常复写的方法有:

 

  • get_error_html(self, status_code, exception-None, **kargs) – 返回 HTML 字符串作为错误页。
  • get_current_user(self) – 查看下面的 用户认证 一节。
  • get_user_locale(self) – 返回当前用户的 locale 对象。
  • get_login_url(self) – 返回一个登陆地址,在 @authenticated 装饰器中会用到(默置认是 Application 中的设)。
  • get_template_path(self) – 返回模板文件的路径(默认是 Application 中的设置)。

4.3   模板

你可以使用被 Python 支持的任何一种模板语言。但是, 相较于其它模板而言的话,Tornado 自己的模板系统,会更快,并且也更灵活。具体的可以查看 template 模块的源码。

一个 Tornado 模板,就是一个包含了 Python 控制结构和一些表达式的 HTML 文件(或者其它什么的文本文件),像这样:

 

<html>
   <head>
      <title>{{ title }}</title>
   </head>
   <body>
     <ul>
       {% for item in items %}
         <li>{{ escape(item) }}</li>
       {% end %}
     </ul>
   </body>
 </html>

 

 如果你把上面的代码命名为 template.html 存到当前目录的话,你可以使用下面的方法渲染它:

 

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 的模板支持控制结构和表达式。控制结构是使用 {% %} 包起来的,比如 {% if len(items) > 2 %} 。表达式是使用 {{ }} 包起来的,比如 {{ items[0] }}

控制结构很像 Python 自己的那一套,现在支持 if, for, while, try ,使用时都需要用 {% %} 包起来。同时,也支持使用 extends 实现模板的继承,还有 block 的块结构。更多细节请查看 template 模块的源码。

表达式可以是任意的 Python 表达式。包括函数调用。模板中的相关代码,会在一个单独的名字空间中被执行,这个名字空间包括了以下的一些对象和方法。(注意,下面列表中的对象或方法在使用 RequestHandler.render 或者 render_string 时才存在的,如果你在 RequestHandler 外面直接使用 template 模块,则它们中的大部分是没有的)。

  • escape : alias for tornado.escape.xhtml_escape
  • xhtml_escape : alias for tornado.escape.xhtml_escape
  • url_escape : alias for tornado.escape.url_escape
  • json_encode : alias for tornado.escape.json_encode
  • squeeze : alias for tornado.escape.squeeze
  • linkify : alias for tornado.escape.linkify
  • datetime : the Python datetime module
  • handler : the current RequestHandler object
  • request : alias for handler.request
  • current_user : alias for handler.current_user
  • locale : alias for handler.locale
  • _ : alias for handler.locale.translate
  • static_url : alias for handler.static_url
  • xsrf_form_html : alias for handler.xsrf_form_html
  • reverse_url : alias for Application.reverse_url
  • All entries from the ui_methods and ui_modules Application settings
  • Any keyword arguments passed to render or render_string

在实际项目中,通常你希望使用到 Tornado 模板的全部功能,特别是继承。具体的细节参阅 template 模块的源码。(一些功能,像 UIModules 是在 web 模块中实现的)。

在实现上, Tornado 的模板会直接转成 Python 代码。表达式会直接复制过去。 Tornado 没有对模板作任何的限制,没有什么沙盒等保护机制。所以,当你写在模板中的语句有什么错误时,你会直接得到这个错误。

4.4   Cookies和安全Cookies

你可以使用 set_cookie 方法在用户的浏览中设置 cookies :

 

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 本身很容易被恶意的客户端伪造。如果你想在 cookies 中保存当前登陆用户的 id 之类的信息,那么最好对 cookie 作签名,以防止伪造。为此, Tornado 单独提供了 set_secure_cookieget_secure_cookie 方法。要使用这些方法,你需要在创建应用时提供一个密钥,叫 cookie_secret 。你可以把它作为一个关键词参数传入应用的设置中:

 

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

 

cookies 的签名,包括有当前的 cookies 值,再另外加上时间戳和一个 HMAC 签名。如果此 cookie 已经过期,或者签名不对,那么 get_secure_cookies 会返回 None , cookie 不存在也是返回 None 。安全 cookie 的例子如下:

 

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!")

 

4.5   用户认证

当前已经认证的用户被保存在每一个请求处理器的 self.current_user 当中,同时在模板的 current_user 中也是。默认情况下, current_userNone

在你自己的应用实现用户认证的功能,需要复写请求处理中 get_current_user() 这个方法,在其中判定当前用户的状态,比如 cookie 。下面的例子让用户简单地使用一个 nickname 登陆应用,之后会把登陆信息保存到 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=")

 

对于那些必须要求用户登陆的操作,可以使用装饰器 tornado.web.authenticated 。如果一个方法套上了这个装饰器,但是当前用户并没有登陆的话,页面会被重定向到 login_url (应用配置中的一个选项),上面的例子可以被改写成:

 

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 内部集成了对第三方认证形式的支持,比如 Google 的 OAuth 。参阅 auth 模块以了解更多的细节。在 Tornado 的源码中,有一个 Blog 的例子,展示了一个完整的用户认证的应用(用户数据需要保存到 MySQL 数据库中)。

4.6   跨站伪造请求的防范

跨站伪造请求 或者说 XSRF ,对于个别站点来说,是一个普遍存在的安全问题。 Wikipedia article 上有关于 XSRF 是如何进行攻击的更多的信息。

当前,防范 XSRF 的一种有效方法,是对每一个用户都记录一个无法预先知道的 cookie 数据,然后要求所有提交上来的请求中都必须带有这个 cookie 数据。如果此数据不匹配,那么这个请求就可能是被伪造的。

Tornado 内建有 XSRF 的防范机制,要使用它,在应用配置中加上 xsrf_cookies

 

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

 

如果设置了 xsrf_cookies ,那么 Tornado 的 Web 应用将对所有用户设置一个 _xsrf 的 cookie 值,并且假如过来的 POST PUT DELET 请求中没有这个 cookie 值,那么请求会被直接拒绝。如果使用这个机制,那么在所有会被提交的表单中,你都需要加上一个域,以提供此值。有一个 xsrf_form_html() 可以帮你在模板中做到这点:

 

<form action="/new_message" method="post">
    {{ xsrf_form_html() }}
    <input type="text" name="message"/>
    <input type="submit" value="Post"/>
</form>
 

 

如果你是使用 AJAX 的方式,以 POST 方法提交数据的话,你仍然需要在每一个请求中通过脚本添加上 _xsrf 这个值。下面是在 FriendFeed 中的 AJAX 的 POST 请求, 使用了 jQuery 来自动添加上 _xsrf

 

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 + ")"));
    }});
};

 

对于 PUT DELETE 这些方法,(和 POST 一样不是在 URL 中以编码形式显式传递参数),你可能需要在 HTTP 头中以 X-XSRFToken 这个参数传递 XSRF token 。

4.7   静态文件和主动式文件缓存

你能通过在应用配置中指定 static_path 选项来提供静态文件服务:

 

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 ,会从指定的静态文件目录中访问到 foo.png 这个文件。同时, /robots.txt/favicon.ico 也是会自动作为静态文件处理(即使它们不是以 /static/ 开头)。

 

为了提高性能,在浏览器主动缓存静态文件是个不错的主意。这样,浏览器就不需要发送不必要的 If-Modified-SinceEtag 请求,从而影响页面的渲染速度。 Tornado 有一套单独的机制支持这种做法。

要使用这个功能,那么在模板中就不要直接输入静态文件的 URL 地址,而是使用 static_url() 这个方法来提供 URL 地址:

 

<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 值,这样,浏览器就会请求服务器以获取新的文件内容。如果文件的内容没有改变,浏览器就会一直使用本地缓存的文件,这样可以显著提高页面的渲染速度。

在生产环境下,你可能会使用更有利于静态文件伺服环境的服务器,比如 nginx 。大多数的 Web 服务器,都可以配置对静态文件缓存的支持,下面是 FriendFeed 使用的 nginx 的相关配置:

 

location /static/ {
    root /var/friendfeed/static;
    if ($query_string) {
        expires max;
    }
 }
4.8   本地化

 

不管是否登陆,当前用户的 locale 设置都被保存在请求处理器的 self.locale 对象中,同时模板中的 locale 也是。 locale 的名字(如 en_US )是 locale.name 这个变量,你可以使用 locale.translate 方法来进行本地化的翻译。在模板中,有一个全局的方法叫 _() ,它的作用就是进行本地化的翻译。这个翻译方法有两种使用形式:

 

_("Translate this string")

 

将字符串直接翻成本地语言,还有一种是:

 

_("A person liked this",
    "%(num)d people liked this", len(people)) % {"num": len(people)}

 

这种形式,会根据第三个参数,来决定是使用单数或是复数的翻译。上面的例子中,如果 len(people)1 的话,就使用第一种形式的翻译,否则,就使用第二种形式的翻译。

进行字符串翻译常用的一种形式,就是使用 Python 的 placeholders 语法,(上面的 %(num)d 就是一个 placeholders ),因为 placeholders 相较于普通的格式化字符串占位符,没有顺序的限制。

一个本地化翻译的模板例子:

 

<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>

 

默认情况下,我们通过 Accept-Language 这个头来判定用户的 locale ,如果没有,则取 en_US 这个值。如果希望用户手动设置一个 locale 偏好,可以在处理请求的类中复写 get_user_locale 方法:

 

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"]

 

如果 get_user_locale 返回 None ,那么就会再去取 Accept-Language 头的值。

你可以使用 tornado.locale.load_translations 方法获取应用中的所有已存在的翻译。它会找到包含有特定名字的 csv 文件的目录,像 es_GT.csv fr_CA.csv 这些 csv 文件。然后从这些 CSV 文件中读取出所有的与特定语言相关的翻译内容。一般,我们在 main() 方法中就调用一下这个方法:

 

def main():
    tornado.locale.load_translations(
        os.path.join(os.path.dirname(__file__), "translations"))
    start_server()

 

同时,你可以使用 tornado.locale.get_supported_locales() 方法得到被支持的 locale 列表。用户当前的 locale 设置,会使服务器选择一个最接近的,可被支持的 locale 作为翻译的目标语言。比如,用户的 locale 是 es_GT ,而只有 es 是可被支持的,那么 self.locale 的值就是 es 。如果连最接近的都没有找到的话,就会取 es_US

查看 locale 模块的源码,获取关于 CSV 文件具体的格式信息,及其它与本地化机制相关的内容。

4.9   UI模块

Tornado 支持的 UI 模块,可以使你创建标准的,在应用中可以被重用的 UI 组件。 UI 模块的作用,大概就是定义一些特殊的方法,这些方法可以在页面中渲染出具体的组件,这些组件可以有自己的 CSS 和 JavaScript 。

举一个例子,如果你正在写一个博客的应用,你希望在首页,和具体的博客内容页,都要显示文章的内容,那么你就可以创建一个 Entry 模型用以在两个页面渲染文章的内容。第一步,为你的 UI 模块创建一个 Python 文件,比如叫 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)

 

然后通过 ui_modules 配置项告诉 Tornado 在应用当中使用 uimodules.py

 

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) #**

 

home.html 当中,与其直接输入相关的 HTML 代码,引用 Entry 模型是更优雅的做法:

 

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

 

entry.html 中,同样也是引用 Entry 模型,只是把参数 show_comments 设置为 True

 

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

 

每一个 UI 模型,都可以配置自己的 CSS 和 JavaScript ,相关的方法是复写 embedded_css embedded_javascript javascipt_files css_files

 

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)

 

模型的 CSS 和 JavaScript 部分只会被渲染一次,无论一页中有多少个相同的 UI 组件。 CSS 是在页面的 <head> 部分,而 JavaScript 被渲染在 </body> 之前的位置。

4.10   非阻塞式的异步请求

当一个处理请求的行为被执行之后,这个请求会自动地结束。因为 Tornado 当中使用了一种非阻塞式的 I/O 模型,所以你可以改变这种默认的处理行为——让一个请求一直保持连接状态,而不是马上返回,直到一个主处理行为返回。要实现这种处理方式,只需要简单地使 用 tornado.web.asynchronous 装饰器就可以了。

使用了这个装饰器之后,只有当显式地调用 self.finish() ,请求才会被返回,在这期间,用户的浏览器会一直处于等待服务器响应的状态:

 

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

 

下面是一个使用 Tornado 内置的异步请求 HTTP 客户端去调用 FriendFeed 的 API 的例子:

 

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.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()

例子中,当 get() 方法返回时,请求处理还没有完成。在 HTTP 客户端执行它的回调函数 on_response() 时,从浏览器过来的请求仍然是存在的,只有在显式调用了 self.finish() 之后,才会把响应返回到浏览器。

关于更多异步请求的高级例子,可以参阅 demo 中的 chat 这个实现。它是一个使用了 long polling 方式的 AJAX 聊天室。一个长连接的用户,在客户端关闭连接时应该调用一下可能被复写了的 on_connection_close() 方法,以便做一些清理性的工作,更详细的内容,查查看源码中的该方法实现,以避免走入一些误区。

4.11   第三方认证

Tornado 的 auth 模块实现了现在很多流行站点的用户认证方式,包括 Google/Gmail , Facebook , Twitter , Yahoo 以及 FriendFeed 。这个模块可以让用户使用这些站点的账户来登陆你自己的应用,然后你就可以在授权的条件下访问原站点的一些服务,比如下载用户的地址薄,在 Twitter 上发一条推等。

这里有一个例子,使用了 Google 的账户认证,之后把 Google 授权了一个签名串保存到 cookie 当中,以便之后的访问之用:

 

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._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()

 

查看 auth 模块的源码以了解更多的细节。

 

 

5   关于性能

一个 Web 应用的性能表现,主要看它的整体架构,而不仅仅是前端的表现。Tornado 相较于其它的 Python Web 框架,它要快得多。

我们在一些流行的 Python Web 框架上——有 Djangoweb.pyCherryPy ,针对最简单的 Hello, world 例子作了一个测试。对于 Django 和 web.py ,我们使用 Apache/mod_wsgi 的方式来带, CherryPy 就让它自己裸跑。这种部署方案,也是在生产环境中各框架常用的。对于我们的 Tornado ,使用的部署方案为前端使用 nginx 做反向代理,带 4 个线程模式的 Tornado ,这种方案也是我们推荐的在生产环境下的 Tornado 部署方案(根据具体的硬件情况,我们推荐一个 CPU 核对应一个 Tornado 伺服实例,我们的 CPU 是四核的)。

测试的具体方法是,使用 Apache Benchmark ( ab ) 工具,进行并发访问测试:

 

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

 

在 AMD Opteron 2.4GHz 的四核机器上,结果如下图所示:

在我们的测试当中,相较于第二快的服务器, Tornado 在数据上的表现也是它的 4 倍之多。即使只用了一个 CPU 核的裸跑模式, Tornado 也有 33% 的优势。

当然,这仅仅是一个理想状态下的测试结果。但是也足以说明我们对于 Tornado 在性能上的态度。对你的应用而言, Tornado 相较于其它框架在性能上是有优势的。

 

 

 

6   生产环境下的部署

在 FriendFeed 中,我们使用 nginx 做负载均衡,同时也使用它作静态文件的伺服。在多台服务器上,同时部署多个 Tornado 实例。通常,对应 CPU 的内核数,一个核对应一个 Tornado 线程。

因为我们的 Web 服务器是跑在负载均衡服务器后面的,所以,需要把 xheaders=True 传到 HTTPServer 的构造器当中去。这是为了让 Tornado 使用像 X-Real-IP 的头部信息来获取用户的真实 IP 地址,而不是使用传统的方法,因为那样你只能得到这个负载均衡服务器的 IP 地址。

下面是 nginx 配置文件的一个示例,整体上与我们在 FriendFeed 中使用的差不多。它假设 nginx 和 Tornado 是跑在同一台机器上的,四个 Tornado 服务跑在 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;
        }
    }
}

 

7   WSGI 和 Google AppEngine

Tornado 只对 WSGI 提供了有限的支持,即使如此,因为 WSGI 并不支持非阻塞式的请求,所以如果你使用 WSGI 代替 Tornado 自己的 HTTP 服务的话,那么你将无法使用 Tornado 的异步非阻塞式的请求处理方式。比如 @tornado.web.asynchronoushttpclient 模块, auth 模块,这些都无法使用。

你可以使用 wsgi 模块中的 WSGIApplication 来切换到 WSGI ,而不用之前我们使用的 tornado.web.Applicaion 。下面的例子展示使用内置的 WSGI CGIHandler 来创建一个 Google AppEngine 应用:

 

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)

 

查看 demo 中的 appengine ,了解如何使用 Tornado 创建一个完整的 AppEngine 应用。

8   支持

因为 FriendFeed ,以及其它的一些基于 Tornado 的大型应用都是运行在 nginx 或者 Apache 代理之后 的,所以,现在 Tornado 的 HTTP 服务部分并不完整,它无法处理多行的头部信息,同时对于一些非标准的输入也无能为力。

你可以在 Tornado邮件列表 上讨论和提交 Bug 。

Tornado 是 Facebook’s open source technologies 中的一个,以 Apache Licence, Version 2.0 的方式进行授权。

原文档的许可形式为 Creative Commons 3.0

 

 

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics