文章目录
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>© 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>© 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

880

被折叠的 条评论
为什么被折叠?



