【python】web应用——Tornado

文章目录

1 Intro

1.1 一些概念

1.1.1 基本特性/优势

web框架和异步网络库,由于使用了异步网络I/O,Tornado可以维持数以万计的开放连接,这适合长轮循,WebSocket和其他需要对客户保持长期活跃连接的应用。

1.1.2 Tornado三个组件

• web 框架(RequestHandler)
• HTTP客户端和服务端的实现(HTTPServer和AsyncHTTPClient)
• 异步网络库,包括IOLoop和IOStream,可以作为实现HTTP组件的构建模块,也可以用于实现其他协议。

1.1.3 Tornado和WSGI

WSGI是一个同步接口,而Tornado是一个基于单线程的异步执行的并发模型,许多Tornado突出的特性在WSGI模型中不可用(长轮循和websockets)。不过,HTTP Server和web 框架一起实现了一种全栈的WSGI,可以使用Tornado的作为容器(WSGIContainer)的HTTP Server来搭配其他WSGI框架。Tornado提供WSGIContainer在单线程中支持WSGI应用和原生的Tornado RequestHandlers。WSGI应用最好搭配专门的WSGI服务器(gunicorn,uwsgi)使用。

1.2 安装

• tornado为类Unix系统设计,最好在类Unix环境中执行。Windows环境不支持Tornado部分特性

pip install tornado

1.3 hello world

# handler/hello.py
import tornado

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


# routing.py
from handler.hello import MainHandler

routing = [
    (r"/", MainHandler),
]


# web.py
import asyncio
import tornado

from routing import routing

def make_app():
    return tornado.web.Application(
        routing
    )

async def main():
    app = make_app()
    app.listen(8888)
    await asyncio.Event().wait()

if __name__ == "__main__":
    asyncio.run(main())

1.4 补充

  • 实时web特性要求对每个用户维持一个几乎空闲的长连接,在传统的同步web server中,为实现这个特性将为每个用户单独开一个线程,开销很大。Tornado使用单线程事件循环来最小化并发连接的开销。
  • 函数阻塞的原因:网络I/O,硬盘I/O,互斥量。
  • 异步的接口:回调参数,返回占位符(Future,Promise,Deferred),投递到队列,回调注册。Tornado中的异步操作主要使用两种接口:一种是占位符(Futures),一种是IOLoop使用的回调。

2 web 框架

2.1 主协程

主协程main由asyncio.run()启动,且主协程mian运行结束则整个web应用退出执行。注意主协程中用asyncio.Event()创建了一个事件shutdown_event,并在在shutdown_event上一直阻塞,直到事件循环中某个协程主动调用asyncio.Event.set(shutdown_event),那么主协程main退出,整个事件循环终止。

import asyncio
import tornado

from routing import routing

def make_app():
    return tornado.web.Application(
        routing
    )

async def fun(shutdown_event):
    await asyncio.sleep(3)
    asyncio.Event.set(shutdown_event)

async def main():
    app = make_app()
    app.listen(8888)
    shutdown_event = asyncio.Event()
    asyncio.create_task(fun(shutdown_event))
    await shutdown_event.wait()

if __name__ == "__main__":
    asyncio.run(main())

2.2 Application类

tornado.web.Application对象负责全局配置,如路由表(映射请求和handler)。路由表是一个列表,列表每一项包含URLSpec对象和一个RequestHandler对象。
初始化URLSpec对象,第一个参数是路径正则表达式(正则表达式中的捕获的分组将作为路径参数传递给RequestHadnler中定义的HTTP方法),第二个参数是RequestHandler,第三个参数是用于给RequestHandler对象初始化的字典,第四个参数是名称。

from tornado.web import url

app = Application([
    url(r"/", MainHandler),
    url(r"/story/([0-9]+)", StoryHandler, dict(db=db), name="story")
    ])

2.3 RequestHandler类

RequestHandler类的使用方式是继承它并重写以HTTP方法命名的成员函数,如get(),post()。在handler中,调用RequestHandler.render或者 RequestHandler.write来产生响应,注意在请求处理的全流程中,write方法可以多次调用来产生响应内容,并非是调用一次就终止全部处理流程。render()通过名称加载一个模板并且用给定参数渲染它。write()用于非模板输出(字符串,字节,JSON格式的字典)。
比较通用的方法是继承RequestHandler定义一个BaseHandler来重写诸如write_error,get_current_user等共用方法,然后再继承你自己的BaseHandler来构建应用。

2.3.1 请求处理的调用过程

initialize() -> prepare() -> HTTP method -> on_finish,在prepare中调用finish或者redirect将终止进一步处理。

2.3.2 一些成员(待补充)

RequestHandler.request来表示当前请求,RequestHandler.current_user表示当前用户。

2.3.3 一些方法(待补充)

  • write_error:为错误页面产出HTML
  • on_connection_close:当客户端失去连接时调用,应用可能探测连接情况并在失去连接后终止进一步处理,同时一些释放资源的操作也可以放在这里来做。不保证客户端一失去连接就检测出来,然后调用之。
  • get_current_user:查看用户权限
  • get_user_locale:返回当前用户的Locale对象(用于为用户管理当前语言和地区设置)
  • set_default_headers:可用于设置额外的响应头

2.3.4 例子:重写prepare

tornado本身不解析JSON请求体,如果Content-Type为application/json,则可以在self.prepare方法中预先使用json内置库处理self.request.body

def prepare(self):
    if self.request.headers.get("Content-Type", "").startswith("application/json"):
        self.json_args = json.loads(self.request.body)
    else:
        self.json_args = None

2.4 获取请求参数

2.4.1 参数

http://localhost:8080/info/:uid?subject=Math&examType=其中
uid是路径参数(path argument),http://localhost:8080/info/:uid 定位了资源位置。整个url中,? 后面的部分是查询参数(query argument),查询参数subject的值为"Math",查询参数examType的值为"期中",查询参数之间用&分隔。

2.4.2 获取参数列表

使用get_query_arguments(name)获取查询参数列表,使用get_body_arguments()获取放在请求体中的参数列表。比如,http://localhost:8080/info/:uid?subject=Math&subject=English,调用get_query_arguments(“subject”),返回[‘Math’, ‘English’]。

2.4.3 获取某个参数

使用self.get_query_argument(name)获取名为name的查询参数,使用self.get_body_argument(name)获取名为name的请求体参数。下面的handler例子中,浏览器访问localhost:8888/my/form,get()方法先被调用,将html写入响应。响应页面中呈现一个简单的输入框和submit按钮,在输入框输入文本后点击submit按钮,则浏览器发起一个post请求,然后post()方法被调用。通过self.get_body_argument(“message”)来获取post请求的参数。"message"是form标签中的属性名。

routing = [
    (r"/", MainHandler),
    (r"/my/form", MyFormHandler),
]

class MyFormHandler(tornado.web.RequestHandler):
    def get(self):
        self.write('<html><body><form action="/my/form" method="POST">'
                   '<input type="text" name="message">'
                   '<input type="submit" value="Submit">'
                   '</form></body></html>')

    def post(self):
        print('xxx')
        self.set_header("Content-Type", "text/plain")
        self.write("You wrote " + self.get_body_argument("message"))

2.5 文件上传

2.5.1 方式一:通过表单参数上传文件

self.request.files为字典,键为请求参数名,值为文件列表,文件列表的每项是一个字典,结构为{“filename”:…, “content_type”:…, “body”:…}。如上图,self.request.files={‘file’: [{‘filename’: ‘file0’, ‘content-type’: ‘xxx’, ‘body’: ‘xxx’}, {‘filename’: ‘file1’, ‘content-type’: ‘xxx’, ‘body’: ‘xxx’}]}。注意,只有请求头的Content-Type设置为multipart/form时,才可用。

2.5.2 方式二:通过请求体上传文件

如果文件通过self.request.body来保存上载的数据,则上载的文件将默认全部缓存在内存中。如果需要处理大文件,可以考虑使用stream_request_body类装饰器。

import asyncio
import logging
from urllib.parse import unquote

import tornado
from tornado import options

@tornado.web.stream_request_body
class PUTHandler(tornado.web.RequestHandler):
    def initialize(self):
        self.bytes_read = 0

    def data_received(self, chunk):
        self.bytes_read += len(chunk)

    def put(self, filename):
        filename = unquote(filename)
        mtype = self.request.headers.get("Content-Type")
        logging.info('PUT "%s" "%s" %d bytes', filename, mtype, self.bytes_read)
        self.write("OK")

2.6 错误处理

2.6.1 方式一:直接raise tornado.web.HTTPError

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        raise tornado.web.HTTPError(404, "Not Found!")

2.6.2 方式二:重写write_error来自定义error page

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        raise tornado.web.HTTPError(404, "Not Found!")

    def write_error(self, status_code, **kwargs):
        if status_code == 404:
            self.write("<h1>Oops! Page not found.</h1>")
        elif status_code == 500:
            self.write("<h1>Internal Server Error. Please try again later.</h1>")
        else:
            self.write("<h1>An error occurred.</h1>")         

2.6.3 方式三:设置status,写并返回响应

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        # Simulate an error condition
        self.set_status(404)  # Set HTTP status to 404 (Not Found)
        self.write("<h1>Oops! Page not found.</h1>") # Custom error message

2.6.4 tornado.web.Finish()

通常使用在请求生命周期中尽早返回响应的场景,比如鉴权,重定向等。

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        # Check for some condition
        user_authenticated = False
        
        if not user_authenticated:
            # If the user is not authenticated, raise Finish to stop further processing
            self.set_status(403)  # Forbidden
            self.write("You are not authorized to access this page.")
            raise tornado.web.Finish()  # Stop further processing and send the response immediately

        # Normal processing (this part won't be reached if Finish is raised)
        self.write("Welcome to the main page!")

class AnotherHandler(tornado.web.RequestHandler):
    def get(self):
        # Simulate an internal server error
        self.set_status(500)  # Set HTTP status to 500 (Internal Server Error)
        self.write("<h1>Internal Server Error. Please try again later.</h1>")  # Custom error message

2.6.5 default_handler_class

专门用于处理404的的handler,这样的handler只需重写prepare,产生错误页面响应即可。当访问路由表以外的路径时,将调用default_handler_class来处理。

class DefaultHandler(tornado.web.RequestHandler):
    def prepare(self):
        # Set the status to 404 (Not Found)
        self.set_status(404)
        # Write a custom message for the 404 error
        self.write("<h1>404 - Page Not Found</h1>")
        self.write("<p>The page you are looking for does not exist.</p>")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
        (r"/another", AnotherHandler),
        # The default handler catches all unmatched routes
    ], default_handler_class=DefaultHandler)  # Set the default handler to catch 404 errors

2.7 重定向

2.7.1 方法一:使用RequestHandler的redirect方法

self.redirect方法有一个可选参数permanent,默认为False,表示非永久重定向,将返回"302 Found",适合于在post请求后重定向到其他位置;permanent=True,表示永久重定向,返回"301 Move Permanently",适合于以搜索引擎优化方式来重定向到标准URL上(比如网址发生变更,要将访问从旧网址引到新的网址)。

import tornado.ioloop
import tornado.web
import asyncio

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("<h1>Welcome to the main page!</h1>")
        self.write('<p><a href="/redirect">Go to the redirected page</a></p>')

class RedirectHandler(tornado.web.RequestHandler):
    def get(self):
        # Redirect to a different URL (this can be absolute or relative)
        self.redirect("/another_page")  # Redirect to another route within the same app

class AnotherPageHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("<h1>You have been redirected!</h1>")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
        (r"/redirect", RedirectHandler),
        (r"/another_page", AnotherPageHandler),
    ])

async def main():
    app = make_app()
    app.listen(8888)
    await asyncio.Event().wait()

if __name__ == "__main__":
    asyncio.run(main())

2.7.2 方法二:使用RedirectHandler类

默认为永久重定向(permanent=True),如果需要设置为非永久重定向,则要给RedirectHandler对象的initialize()传入permanent=True的参数,即dict(url=“xxx”, permanent=False)。

app = tornado.web.Application([
    url(r"/photos/(.*)", MyPhotoHandler),
    url(r"/pictures/(.*)", tornado.web.RedirectHandler,
        dict(url=r"/photos/{0}")),
    ])

2.8 异步handler

使用tornado.locks.Condition():ConditionHandler处理get请求时,会阻塞在await self.condition.wait()上,直到TriggerHandler处理get请求时执行了self.condition.notify()。self.condition.notify()只允许一个其他wait在self.condition上的协程执行,self.condition.notify_all()将允许所有在self.condition上等待的协程执行。

import tornado.web
import tornado.locks
import asyncio

class ConditionHandler(tornado.web.RequestHandler):
    def initialize(self, condition):
        self.condition = condition

    async def get(self):
        # The first coroutine will wait for the condition to be notified
        await self.condition.wait()
        self.write("Condition met! Proceeding...")

class TriggerHandler(tornado.web.RequestHandler):
    def initialize(self, condition):
        self.condition = condition

    async def get(self):
        # Trigger the condition to notify waiting coroutines
        self.condition.notify()
        self.write("Notified waiting coroutines.")

def make_app():
    condition = tornado.locks.Condition()

    return tornado.web.Application([
        (r"/wait", ConditionHandler, dict(condition=condition)),
        (r"/notify", TriggerHandler, dict(condition=condition)),
    ])

3 Tornado 的模板系统

3.1 一些概念

  • 什么是模板:嵌入一些占位符的HTML文件,这些占位符将在模板文件被渲染时替换为运行时的数据。
  • 编译模板:将模板转换为Python函数或者可调用对象来生成最终的HTML文件,调用这个函数或者可调用对象时,嵌入模板的占位符(Python代码)将被执行。将模板编译好,web框架能更快地渲染模板,仅仅只需要调用这些已经经过编译生成的可调用对象即可。

3.2 使用模板

3.2.1 基本使用方式

导入模板文件得到模板对象,调用generate方法来生成。

# 方式一
t = template.Template("<html>{{ myvalue }}</html>")
print(t.generate(myvalue="XXX"))

# 方式二:
loader = template.Loader("/home/btaylor")
t = loader.load("test.html")
print(t.generate(myvalue="XXX"))

3.2.2 在web应用中使用

方式一:直接指定模板文件路径,导入之
def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ], template_path="templates")  # Set template directory here
方式二:继承tornado.template.BaseLoader,重写返回模板内容的load方法
class MemoryTemplateLoader(tornado.template.BaseLoader):
    def __init__(self, templates):
        self.templates = templates

    def load(self, name):
        # Return the template content from the dictionary
        if name in self.templates:
            return self.templates[name]
        raise tornado.web.HTTPError(404, f"Template '{name}' not found")

def make_app():
    # Define templates in memory (in a dictionary)
    templates = {
        "index": "<html><body><h1>Hello, {{ name }}!</h1></body></html>"
    }

    # Create an instance of our custom loader
    loader = MemoryTemplateLoader(templates)

    # Pass the custom loader to the application as a setting
    return tornado.web.Application([
        (r"/", MainHandler),
    ], template_loader=loader)

3.3 模板格式

3.3.1 基本格式

嵌入控制语句:使用 {% 和 %} 包裹,{% if len(items) > 2 %},这个控制块结束时要追加 {% end %}
嵌入表达式:使用 {{ 和 }} 包裹,{{ items[0] }}

3.3.2 一些模板语法

apply

用于执行代码里定义的函数。注意apply的函数名(greeting_func)是面向html的,在Python代码中实现的函数(greeting)需要映射到apply的函数名(类似实参传给形参)。

<!-- case1: single-parameter -->
{% apply linkify %}{{name}} said: {{message}}{% end %}

<!-- case2: multi-parameter -->

<!-- Call the greeting function using apply with two arguments -->
<h1>{{ apply(greeting_func, "Tornado User", "Seattle") }}</h1>

<!--
def greeting(name, city):
    return f"Hello, {name} from {city}!"

class MainHandler(RequestHandler):
    def get(self):
        # Pass the greeting function to the template context
        self.render("index.html", greeting_func=greeting)
-->
extend & block

extend用于引入父模板,以便于在子模板中重写父模板中定义的block。block用于定义一段HTML,子模板中定义的block将会覆盖父模板中定义的同名block。子模板不包含block标签的话,将被忽视。

<!-- base.html -->
<title>{% block title %}Default title{% end %}</title>

<!-- mypage.html -->
{% extends "base.html" %}
{% block title %}My page title{% end %}
include

作用类似于C/C++中的include,它将一个html的内容完全拷贝到使用include指定之处。

<!-- header.html -->
<header>
    <h1>Welcome to My Website</h1>
    <nav>
        <a href="/">Home</a>
        <a href="/about">About</a>
        <a href="/contact">Contact</a>
    </nav>
</header>


<!-- base.html -->
<!DOCTYPE html>
<html>
<head>
    <title>{% block title %}My Website{% endblock %}</title>
</head>
<body>
    {% include "header.html" %}

    <div id="content">
        {% block content %}
        <!-- Content from child templates will go here -->
        {% endblock %}
    </div>

    <footer>
        <p>&copy; 2025 My Website</p>
    </footer>
</body>
</html>


<!-- home.html -->
{% extends "base.html" %}

{% block title %}Home - My Website{% endblock %}

{% block content %}
    <h2>Welcome to the Home Page!</h2>
    <p>This is the homepage of my website. Feel free to explore.</p>
{% endblock %}


<!-- result -->
<!DOCTYPE html>
<html>
<head>
    <title>Home - My Website</title>
</head>
<body>
    <header>
        <h1>Welcome to My Website</h1>
        <nav>
            <a href="/">Home</a>
            <a href="/about">About</a>
            <a href="/contact">Contact</a>
        </nav>
    </header>

    <div id="content">
        <h2>Welcome to the Home Page!</h2>
        <p>This is the homepage of my website. Feel free to explore.</p>
    </div>

    <footer>
        <p>&copy; 2025 My Website</p>
    </footer>
</body>
</html>
autoescape

在当前html文件内默认使用tornado.escape.xhtml_escape进行转义(不被模板引擎执行,而当仅做字符串显式),当前html被include出去,则autoescape指令生效,autoescape作用域仅限于当前html文件。如果需要全局禁用autoescape,则可以传递autoescape=None参数给Application或者tornado.template.Loader。

<!-- use xhtml_escape to escape -->
{% autoescape xhtml_escape %} 

<!-- turn off escape --> 
{% autoescape None %}

<!-- case1: if user provides user_comment with "<script>alert('XSS');</script>", then template engine will render it when autoescape is turn off. Instead, the value of user_comment will be viewed as plain text when autoescape turning on. -->
<!DOCTYPE html>
<html>
<head>
    <title>User Comment</title>
</head>
<body>
    <h1>User Comment:</h1>

    <div>
        {% autoescape true %}
        <p>{{ user_comment }}</p>
        {% endautoescape %}
    </div>
</body>
</html>
module

一种和include相似的引入其他模板文件的指令,不过module指令可以传递参数。

<!-- main.html -->
<!DOCTYPE html>
<html>
<head>
    <title>Module Example</title>
</head>
<body>
    <h1>Welcome to the Module Example</h1>

    {% module Template("foo.html", arg=42) %}
</body>
</html>


<!-- foo.html -->
<p>The value of arg is: {{ arg }}</p>


<!-- result -->
<!DOCTYPE html>
<html>
<head>
    <title>Module Example</title>
</head>
<body>
    <h1>Welcome to the Module Example</h1>

    <p>The value of arg is: 42</p>
</body>
</html>

3.4 UI

继承tornado.web.UIModule,重写render方法,然后在html中通过module直接引用实现的UIModule子类。可以在UIModule子类中包含一些CSS,JavaScript函数。

3.4.1 例子

uimodules.py

UIModule子类,在uimodules.py中实现。重写方法embedded_css将和直接在HTML文件中添加{{ set_resources(embedded_css=".entry { margin-bottom: 1em; }") }}语句产生一样的效果,注意set_resources函数只有在{% module Template(...) %}被执行时才生效。

class UserProfileModule(tornado.web.UIModule):
    def embedded_css(self):
        return ".entry { margin-bottom: 1em; }"

    def render(self, user_name, user_bio):
        # Return the HTML template and pass user data to it
        return self.render_string("user_profile.html", user_name=user_name, user_bio=user_bio)
user_profile.html

UI Module对应的html文件。

<div class="user-profile">
    <h3>{{ user_name }}</h3>
    <p>{{ user_bio }}</p>
</div>
index.html

引用UI Module,即UserProfileModule,传入的两个命名参数将被传入重写的render方法中。

<!DOCTYPE html>
<html>
<head>
    <title>My Website</title>
</head>
<body>
    <h1>Welcome to My Website</h1>

    {% module UserProfileModule(user_name="John Doe", user_bio="A software developer based in New York.") %}

</body>
</html>
server.py
import tornado.ioloop
import tornado.web

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.render("index.html")

def make_app():
    return tornado.web.Application([
        (r"/", MainHandler),
    ], template_path="templates", ui_modules={"UserProfileModule": UserProfileModule})

async def main():
    app = make_app()
    app.listen(8888)
    await asyncio.Event().wait()

if __name__ == "__main__":
    asyncio.run(main())

4 其他话题

4.1 异步的理解

4.1.2 同步示例代码

from tornado.httpclient import HTTPClient

def synchronous_fetch(url):
    http_client = HTTPClient()
    response = http_client.fetch(url)
    return response.body

4.1.3 异步示例代码

from tornado.httpclient import AsyncHTTPClient

async def asynchronous_fetch(url):
    http_client = AsyncHTTPClient()
    response = await http_client.fetch(url)
    return response.body

4.1.4 异步代码执行的实质

耗时操作http_client.fetch(url)返回fetch_future(是一个Future对象),要返回的占位符是my_future,给fetch_future添加回调该函数on_fetch,该回调函数把fetch_future的结果传递给占位符my_future。

from tornado.concurrent import Future

def async_fetch_manual(url):
    http_client = AsyncHTTPClient()
    my_future = Future()
    fetch_future = http_client.fetch(url)
    def on_fetch(f):
        my_future.set_result(f.result().body)
    fetch_future.add_done_callback(on_fetch)
    return my_future

4.2 Tornado中的协程的组织

协程要放在事件循环中执行,在Tornado中,这个事件循环是tornado.ioloop.IOLoop,它是对asnycio的eventloop的封装。

async def divide(x, y):
    return x / y

if __name__ == '__main__':
    divide(1, 0)  # 直接调用协程将返回一个协程对象,并不实际执行协程中的任务。
    IOLoop.current().spawn_callback(divide, 1, 0)  # 给IOLoop添加协程,并没有启动IOLoop
    IOLoop.current().run_sync(lambda: divide(1, 0))  # 启动IOLoop,调度执行协程,直到所有协程执行完毕

4.2.1 协程执行的模式

在协程中调用阻塞操作

IOLoop的默认执行器是concurrent.futures.ThreadPoolExecutor。如果是CPU密集型的任务,可以使用。

concurrent.futures.ProcessPoolExecutor。
async def call_blocking():
    await IOLoop.current().run_in_executor(None, blocking_func, args)
并行执行协程
from tornado.gen import multi

async def parallel_fetch(url1, url2):
    resp1, resp2 = await multi([http_client.fetch(url1),
                                http_client.fetch(url2)])
周期性调用协程
async def minute_loop():
    while True:
        await do_something()
        await gen.sleep(60)

4.2.2 Tornado中协程间通信

Tornado’s tornado.queues实现了异步生产者/消费者模式的协程,类似于给线程库提供的用Python标准库的queue模块实现的模式。注意q.join(timeout)将会阻塞,直到队列为空或者时间到。

#!/usr/bin/env python3

import asyncio
import time
from datetime import timedelta

from html.parser import HTMLParser
from urllib.parse import urljoin, urldefrag

from tornado import gen, httpclient, queues

base_url = "http://www.tornadoweb.org/en/stable/"
concurrency = 10

async def get_links_from_url(url):
    pass

def remove_fragment(url):
    pass

def get_links(html):
    pass

async def main():
    q = queues.Queue()
    start = time.time()
    fetching, fetched, dead = set(), set(), set()

    async def fetch_url(current_url):
        if current_url in fetching:
            return

        print("fetching %s" % current_url)
        fetching.add(current_url)
        urls = await get_links_from_url(current_url)
        fetched.add(current_url)

        for new_url in urls:
            # Only follow links beneath the base URL
            if new_url.startswith(base_url):
                await q.put(new_url)

    async def worker():
        async for url in q:
            if url is None:
                return
            try:
                await fetch_url(url)
            except Exception as e:
                print("Exception: %s %s" % (e, url))
                dead.add(url)
            finally:
                q.task_done()

    await q.put(base_url)

    # Start workers, then wait for the work queue to be empty.
    workers = gen.multi([worker() for _ in range(concurrency)])
    await q.join(timeout=timedelta(seconds=300))
    assert fetching == (fetched | dead)
    print("Done in %d seconds, fetched %s URLs." % (time.time() - start, len(fetched)))
    print("Unable to fetch %s URLs." % len(dead))

    # Signal all the workers to exit.
    for _ in range(concurrency):
        await q.put(None)
    await workers

if __name__ == "__main__":
    asyncio.run(main())

4.3 安全

4.3.1 签名cookie

  • 未签名cookie的设置和读取:RequestHandler.get_cookie(“mycookie”),RequestHandler.set_cookie(“mycookie”, “myvalue”)。
  • 签名的cookie设置和读取:RequestHandler.get_signed_cookie(“mycookie”),RequestHandler.set_signed_cookie(“mycookie”, “myvalue”)。使用签名cookie需要设置cookie_secret。
    签名的cookie由cookie编码值,时间戳和HMAC签名组成。如果cookie过时或者签名不匹配,则get_signed_cookie返回None
    签名cookie默认时限为30日,可以通过给set_signed_cookie传递expires_days参数来设置,可以通过给get_signed_cookie传递max_age_days来筛选(如果cookie没有expire但是超过max_age_days,则get_signed_cookie将返回None)。

4.3.2 密钥

  • 签名cookie只能保证cookie不被篡改但是不能保证cookie的私密性。因为cookie_secret是对称密钥。
  • 密钥本身是str或者bytes类型的,如果只需要一个密钥则Application settings中的cookie_secret为str类型或者bytes类型。如果要设置多个secret,则cookie_secret为dict类型,其中键为key_version(整数类型) ,值为str或者bytes类型。
  • 如果设置多个secret,则同时还要给Application settings添加key_version,以指明应用当前使用哪个。如果需要更换secret,仅需变更key_version参数即可。RequestHandler.get_signed_cookie_key_version(cookie_name)可以在运行时查看应用当前正在使用的key_version。
# 单个secret的cookie_secret
application = tornado.web.Application([
    (r"/", MainHandler),
], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")

# 多个secret的cookie_secret
secrets = {
    1: '111',
    2: '222'
}
application = tornado.web.Application([
    (r"/", MainHandler),
], cookie_secret=secrets, key_version=1)

4.4 鉴权

4.4.1 get_current_user()

从RequestHandler.get_current_user()中获取存储在cookie中的user值。

import tornado
import asyncio

class BaseHandler(tornado.web.RequestHandler):
    def get_current_user(self):
        return self.get_signed_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_signed_cookie("user", self.get_argument("name"))
        self.redirect("/")

async def main():
    application = tornado.web.Application([
        (r"/", MainHandler),
        (r"/login", LoginHandler),
    ], cookie_secret="__TODO:_GENERATE_YOUR_OWN_RANDOM_VALUE_HERE__")
    application.listen(8888)
    await asyncio.Event().wait()

if __name__ == '__main__':
    asyncio.run(main())

4.4.2 装饰器tornado.web.authenticated

用于重定向到登录页面。当get方法被tornado.web.authenticated装饰,则向MainHandler发起get请求时,如用户未登录(self.current_user为None),则重定向到login_url指定的地址。如果用tornado.web.authenticated装饰post方法,则当用户没有登陆则返回403。

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

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

4.4.3 支持第三方鉴权:Google鉴权

class GoogleOAuth2LoginHandler(tornado.web.RequestHandler,
                               tornado.auth.GoogleOAuth2Mixin):
    async def get(self):
        if self.get_argument('code', False):
            user = await self.get_authenticated_user(
                redirect_uri='http://your.site.com/auth/google',
                code=self.get_argument('code'))
            # Save the user with e.g. set_signed_cookie
        else:
            await self.authorize_redirect(
                redirect_uri='http://your.site.com/auth/google',
                client_id=self.settings['google_oauth']['key'],
                scope=['profile', 'email'],
                response_type='code',
                extra_params={'approval_prompt': 'auto'})

5 补充

  • introspection:查看运行时对象的基本信息,如查看对象的内存地址,类型,有哪些属性和方法,是否为callable对象等。
  • 使用async/await的函数称为native coroutines,使用tornado.gen.coroutine decorator的函数被称为decorated coroutines。推荐使用native coroutines。
  • 为何引入async:for 希望__next__ 抛出StopIteration异常来终止迭代,如果__next__ 是协程,则直到await后,StopIteration异常才可见,因此使用普通for,来调用协程形式的__next__ ,则循环不会退出。索引async for被引入。
  • tornado.options:用于设置一些系统级别参数,这些参数并非静态配置死的,而是允许用户输入来覆盖之。
from tornado.options import define, options, parse_command_line

define("port", default=8888, help="run on the given port", type=int)
define("debug", default=True, help="run in debug mode")

async def main():
    parse_command_line()
    app = tornado.web.Application(
        ... ,
        debug=options.debug,
    )
    app.listen(options.port)
    await asyncio.Event().wait()

6 参考

https://www.tornadoweb.org/en/stable/guide.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值