05、tornado的web基础
项目GIthub源码地址:[https://github.com/vannesspeng/TornadoForum]
一、tornado之helloworld
这里不再赘述了,直接上代码:
import time
from tornado import web
import tornado
web.URLSpec
class MainHandler(web.RequestHandler):
# 当客户端发起不同的http方法的时候, 只需要重载handler中的对应的方法即可
# 下面的get方法,响应http请求中的get请求,请求类型与对应的方法如下:
# "GET":get(), "HEAD":head(), "POST":post(), "DELETE":delete(), "PATCH":patch(), "PUT":put(),"OPTIONS":options()
async def get(self, *args, **kwargs):
time.sleep(5)
self.write("hello world")
# 二、程序运行入口
if __name__ == "__main__":
# 1、实例化,application对象
app = web.Application([
("/", MainHandler), # 配置路由 http://localhost:8888/这个url交给MainHandler处理
],
debug=True # 开启调试模式,与flask、Django类似,可以自动重启,打印错误栈
)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
二、tornado中为什么不能写同步的方法
如下示例代码:
import time
from tornado import web
import tornado
web.URLSpec
class MainHandler(web.RequestHandler):
async def get(self, *args, **kwargs):
time.sleep(5)
self.write("hello world")
class MainHandler2(web.RequestHandler):
async def get(self, *args, **kwargs):
self.write("hello world2")
if __name__ == "__main__":
app = web.Application([
("/", MainHandler),
("/2/", MainHandler2)
], debug=True)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
当我们先访问http://localhost:8888/ 然后立即 http://localhost:8888/2/,由于tornado的处理http请求是一种单线程的模式,http://localhost:8888/这个请求,如果在请求处理函数中使用同步IO(time.sleep(5)就属于同步IO)
它会阻塞其他的请求处理,直到该请求执行完毕返回”hello world“,然后http://localhost:8888/2/的处理函数才会执行并返回hello world2。
所以通过上述实例,我们一定要注意,千万不要在tornado中使用同步IO。
三、tornado中url的映射配置
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:pyy
# datetime:2018/12/24 10:29
# !/usr/bin/env python
# -*- coding:utf-8 -*-
# author:pyy
# datetime:2018/12/20 16:01
import time
from tornado import web
import tornado
class MainHandler(web.RequestHandler):
# 当客户端发起不同的http方法的时候, 只需要重载handler中的对应的方法即可
async def get(self, *args, **kwargs):
self.write("hello world")
class PeopleIdHandler(web.RequestHandler):
# 当客户端发起不同的http方法的时候, 只需要重载handler中的对应的方法即可
async def get(self, id, *args, **kwargs):
# 比如http://127.0.0.1:8888/people/9, 那么tornado框架会将9自动映射到id上,如果这里没有id,那么9这个数值,将会被放置到*args中,关键字参数则会被放入**kwargs中
# id=9
self.write("用户No:" + id)
class PeopleNameHandler(web.RequestHandler):
# 当客户端发起不同的http方法的时候, 只需要重载handler中的对应的方法即可
async def get(self, name, *args, **kwargs):
# 比如http://127.0.0.1:8888/people/vanness
# name=vanness
self.write("用户姓名:" + name)
class PeopleInfoHandler(web.RequestHandler):
async def get(self, name, age, gender, *args, **kwargs): #按顺序接收url中使用()的参数
# 比如http://127.0.0.1:8888/people/vanness/19/female/
# 那么name=vanness, age=19, gender=female
self.write("用户姓名:{}, 用户年龄:{}, 用户性别: {}".format(name, age, gender))
url = [
("/", MainHandler),
("/people/(\d+)/?", PeopleIdHandler),
("/people/(\w+)/?", PeopleNameHandler), # 使用正则表达式配置url
("/people/(\w+)/(\d+)/(\w+)/?", PeopleInfoHandler),
]
if __name__ == "__main__":
app = web.Application(url, debug = True)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
tornado也可以像Flask、Django一样,为url取别名,通过tornado.web.URLSpec(self, pattern, handler, kwargs=None, name=None)函数来实现,示例代码如下:
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:pyy
# datetime:2018/12/24 10:29
# !/usr/bin/env python
# -*- coding:utf-8 -*-
# author:pyy
# datetime:2018/12/20 16:01
import time
from tornado import web
import tornado
class MainHandler(web.RequestHandler):
async def get(self, *args, **kwargs):
self.write("hello world")
# 如何使用url的name
# self.redirect(self.reverse_url("people_name", "vanness"))
class PeopleIdHandler(web.RequestHandler):
# URLSpec传入的参数字典会直接映射到initialize方法的参数中,可以使用相同的key值进行接收。
def initialize(self, name):
self.db_name = name
async def get(self, id, *args, **kwargs):
self.write("用户No:" + id)
class PeopleNameHandler(web.RequestHandler):
async def get(self, name, *args, **kwargs):
self.write("用户姓名:" + name)
class PeopleInfoHandler(web.RequestHandler):
async def get(self, name, age, gender, *args, **kwargs):
self.write("用户姓名:{}, 用户年龄:{}, 用户性别: {}".format(name, age, gender))
# 关键字参数字典
people_db = {
"name": "people"
}
url = [
tornado.web.URLSpec("/", MainHandler, name="index"),
# URLSpec函数还可以传入关键字参数
tornado.web.URLSpec("/people/(\d+)/?", PeopleIdHandler, people_db, name="people_id"),
tornado.web.URLSpec("/people/(\w+)/?", PeopleNameHandler, name="people_name"),
tornado.web.URLSpec("/people/(\w+)/(\d+)/(\w+)/?", PeopleInfoHandler, name="people_info"),
]
if __name__ == "__main__":
app = web.Application(url, debug = True)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
以上代码介绍了tornado.web.URLSpec函数取别名的方式,也演示了一下,URLSpec给handler传入初始值的用法。
四、define、options、parse_comand_line
常量配置通过define()函数来定义,然后,通过options来获取
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# author:pyy
# datetime:2018/12/24 10:29
from tornado import web
import tornado
from tornado.options import define, options
# 定义port、debug常量
define("port", default=8888, help="run on the given port", type=int)
define("debug", default=True, help="open debug or not", type=bool)
# options.parse_command_line() 通过命令行读取port、debug常量
options.parse_config_file("conf.cfg") #通过配置文件conf.cfg来配置port、debug常量
class MainHandler(web.RequestHandler):
async def get(self, *args, **kwargs):
self.write("hello world")
# 如何使用url的name
# self.redirect(self.reverse_url("people_name", "vanness"))
class PeopleIdHandler(web.RequestHandler):
def initialize(self, name):
self.db_name = name
async def get(self, id, *args, **kwargs):
self.write("用户No:" + id)
class PeopleNameHandler(web.RequestHandler):
async def get(self, name, *args, **kwargs):
self.write("用户姓名:" + name)
class PeopleInfoHandler(web.RequestHandler):
async def get(self, name, age, gender, *args, **kwargs):
self.write("用户姓名:{}, 用户年龄:{}, 用户性别: {}".format(name, age, gender))
people_db = {
"name": "people"
}
url = [
tornado.web.URLSpec("/", MainHandler, name="index"),
tornado.web.URLSpec("/people/(\d+)/?", PeopleIdHandler, people_db, name="people_id"),
tornado.web.URLSpec("/people/(\w+)/?", PeopleNameHandler, name="people_name"),
tornado.web.URLSpec("/people/(\w+)/(\d+)/(\w+)/?", PeopleInfoHandler, name="people_info"),
]
if __name__ == "__main__":
app = web.Application(url, debug = options.debug) # 通过options读取配置的常量
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
五、RequestHandler常用方法和子类
构建一个tornado网站,必须包含一个或者多个handler,这些handler是RequestHandler的子类。每个请求都会被映射到handler中进行处理,处理后再将结果返回给客户端。所以,可以看到hanlder作为客户端请求跟业务服务逻辑间的桥梁,如果拿MVC的模式来类比的话,每个handler就相当于MVC中的Controller。
RequestHanlder作为所有hanlder的父类,我们看看他有哪些方法与接口,子类需要怎样继承?
构造函数
定义:
def _init_(self, application, request, **kwargs):
参数:
application: Application对象
request: request请求对象
kwargs:其他参数,在hanlder映射配置时,可以设置。
处理过程:
super(RequestHandler, self).__init__()
self.application = application
self.request = request
self._headers_written = False
self._finished = False
self._auto_finish = True
self._transforms = None # will be set in _execute
self._prepared_future = None
self.path_args = None
self.path_kwargs = None
self.ui = ObjectDict((n, self._ui_method(m)) for n, m in
application.ui_methods.items())
# UIModules are available as both `modules` and `_tt_modules` in the
# template namespace. Historically only `modules` was available
# but could be clobbered by user additions to the namespace.
# The template {% module %} directive looks in `_tt_modules` to avoid
# possible conflicts.
self.ui["_tt_modules"] = _UIModuleNamespace(self,
application.ui_modules)
self.ui["modules"] = self.ui["_tt_modules"]
self.clear()
self.request.connection.set_close_callback(self.on_connection_close)
self.initialize(**kwargs)
1、对外部属性跟内部属性(带下划线)的赋值。
2、然后对ui的处理,这个暂时不做深入细读。
3、 self.clear() 调用clear方法,初始化一些属性。
4、设置请求连接关闭时的回调函数。
self.request.connection.set_close_callback(self.on_connection_close)
5、调用初始化函数。 self.initialize(**kwargs)
这个被子类继承,针对每个hanlder实现自己的初始化过程。
入点函数
1、initialize方法
该方法被子类重写,实现初始化过程。参数的来源于配置Application时,传递的参数。举例如下:
from tornado.ioloop import IOLoop
from tornado.web import Application, RequestHandler
__author__ = 'Administrator'
class BookRequestHandler(RequestHandler):
def initialize(self, welcome):
self.welcome = welcome
def get(self, *args, **kwargs):
print(self.welcome)
self.write(self.welcome)
welcome = "**大学图书馆"
app = Application(handlers=[
(r"/book", BookRequestHandler, dict(welcome=welcome)),
])
def main():
app.listen(8888)
IOLoop.current().start()
if __name__ == "__main__":
main()
将welcome初始化的值传递到BookRequestHandler的self.welcome属性中。当访问http://localhost:8888/book时,打印出welcome的值。
结果输出2、prepare 、 on_finish方法
prepare方法用于当真正调用请求处理方法之前的初始化处理,比如get、post方法。而on_finish用于请求处理结束后的一些清理工作。这两个方法一个在处理前,一个在处理后,可以根据实际需要进行重写。比如用prepare方法做些初始化操作(比如赋值设置全局变量,比如打开I/O),而on_finish方法可以做一些清理对象占用的内存或者关闭数据库连接等等。举个例子,来证明他们的执行顺序。
class BookRequestHandler(RequestHandler):
def initialize(self, welcome,value2):
print("initialize method:initilize some variables....")
self.welcome = welcome
self.value2=value2
def get(self, *args, **kwargs):
#print(self.welcome + "\r\n" + "and value2 =" + self.value2)
print("get method: Processing get Method...........")
self.write(self.welcome + "\r\n" + "and value2 =" + self.value2)
def set_default_headers(self):
self._headers.add("custom_header1", "custom_header1")
self._headers.add("custom_header2", "custom_header2")
def prepare(self):
print("prepare method:initilize some variables....")
def on_finish(self):
print("on_finish method:clear some memory or close database connection")
执行的结果如下:
执行结果
所以得出执行的顺序为:
initialize > prepare > get > on_finish
如果有熟悉Java 的JUnit的话呢,prepare跟on_finish方法相当于before跟behind两个注解的功能。
获得输入的函数:
1、 get_argument、get_arguments方法
返回给定参数的值,get_argument获得单个值,而get_arguments是针对参数存在多个值得情况下使用,返回多个值的列表。看一get_arguments方法的源代码,如下:
get_argument源码
它的实现是调用了内部方法_get_arguments,注意传递的参数self.request.arguments,从request(HTTPServerRequest对象)的arguments属性中去查询给定名称的值。看看HTTPServerRequest源代码(位于tornado>httputil.py)对arguments的解释,如下截图:
arguments
大体意思就是说,这里存储的是客户端GET/POST方法提交上来的合并后的参数列表集合。也就是说RequestHanlder的get_arguments方法是能获得不管是Get还是POST得参数的值。举个GET提交参数的例子
修改BookRequestHanlder的get方法。如下:
def get(self, *args, **kwargs):
#print(self.welcome + "\r\n" + "and value2 =" + self.value2)
print("get method: Processing get Method...........")
#self.write(self.welcome + "\r\n" + "and value2 =" + self.value2)
self.write("参数name的值为:" + self.get_argument("name", "liaofei"))
向游览器中打印出参数name的值,游览器中访问:http://localhost:8888/book?name=brain,结果如下图所示:
参数name的值为:brain
在举个POST方式提交参数的例子,在BookRequestHanlder 中新增POST方法,如下:
def post(self, *args, **kwargs):
print(self.request.arguments)
print("POS age:" + self.get_argument("age"))
self.write("POS age:" + self.get_argument("age"))
这里使用postman模拟post请求
image.png
后台打印的结果:
结果
HTTPRequest 的arguments属性是一个字典。提一个问题?如果URL中带有age查询参数,而post过去也有age参数,这时HTTPRequest 的arguments中age的值会是什么???测试一下便知。按照如下访问:
image.png
后台答应结果:
结果
,age的值是一个列表集合,将POST提交方式age参数值跟GET提交方式age参数值合并啦,而且是GET在前,POST再后。而get_arguments获得最后一个。
2、get_query_argument、get_query_arguments方法
与get_argument、get_arguments功能类似,只是他仅仅是从URL查询参数中获取值。
这里测试一下get_query_argument、get _argument、get_body_argument的区别
def post(self, *args, **kwargs):
request_argument = self.get_argument("age")
request_arguments = self.get_arguments("age")
request_query_argument = self.get_query_argument("age")
request_query_arguments = self.get_query_arguments("age")
request_body_argument = self.get_body_argument('age')
request_body_arguments = self.get_body_arguments('age')
print(request_argument)
print(request_arguments)
print(request_query_argument)
print(request_query_arguments)
print(request_body_argument)
print(request_body_arguments)
self.write("POS age:" + self.get_argument("age"))
请求如下:
请求
通过断点调试,我们可以看到request对象的参数详情
image.png
程序执行结果输出:
chre
['113', 'chre']
113
['113']
chre
['chre']
所以得出结论就是,get _argument获取的值范围是get_query_argument与get_body_argument两者范围的合并。
输出响应函数:
1、clear方法
"""Resets all headers and content for this response."""
self._headers = httputil.HTTPHeaders({
"Server": "TornadoServer/%s" % tornado.version,
"Content-Type": "text/html; charset=UTF-8",
"Date": httputil.format_timestamp(time.time()),
})
self.set_default_headers()
self._write_buffer = []
self._status_code = 200
self._reason = httputil.responses[200]
处理过程
- 首先默认设置3个响应头部,分别是Server,Content_Type、Date.
- 调用set_default_headers 方法,实现自定义header的设置。
- 其他参数的默认设置。
2、set_default_headers方法
上面说了set_default_headers 方法是实现请求响应头部的自定义实现,被子类重写。举个例子。 在上面的BookRequestHandler中加入以下代码
def set_default_headers(self):
self._headers.add("custom_header1", "custom_header1")
self._headers.add("custom_header2", "custom_header2")
通过postman访问http://localhost:8888/book,查询响应头。结果如下:
3、write方法
将给定的块输出到输出缓存中。如果给定的块是一个字典,就会将这个快当成是JSON并且将Content_Type设置成application/json返回给客户端。但是,如果你想用不同的Content_Type发送JSON,可以在调用write方法后再调用set_header方法进行设置。
注意,list 列表集合是不会转化成JSON的,原因是考虑到跨域的安全。所有的JSON输出都必须用字典包装。具体源码说明如下:
def write(self, chunk):
"""Writes the given chunk to the output buffer.
To write the output to the network, use the flush() method below.
If the given chunk is a dictionary, we write it as JSON and set
the Content-Type of the response to be ``application/json``.
(if you want to send JSON as a different ``Content-Type``, call
set_header *after* calling write()).
Note that lists are not converted to JSON because of a potential
cross-site security vulnerability. All JSON output should be
wrapped in a dictionary. More details at
http://haacked.com/archive/2009/06/25/json-hijacking.aspx/ and
https://github.com/facebook/tornado/issues/1009
"""
if self._finished:
raise RuntimeError("Cannot write() after finish()")
#检查传入参数类型
if not isinstance(chunk, (bytes, unicode_type, dict)):
message = "write() only accepts bytes, unicode, and dict objects"
# 传入参数不能够为list类型,主要是考虑到跨域安全
if isinstance(chunk, list):
message += ". Lists not accepted for security reasons; see " + \
"http://www.tornadoweb.org/en/stable/web.html#tornado.web.RequestHandler.write"
raise TypeError(message)
#如果是字典类型,会转换为json类型,设置content-type为json,编码为utf-8
if isinstance(chunk, dict):
chunk = escape.json_encode(chunk)
self.set_header("Content-Type", "application/json; charset=UTF-8")
chunk = utf8(chunk)
self._write_buffer.append(chunk)
4、render方法
用给定的参数渲染模板。这个涉及到模板的概念,后续再学。
5、write_error方法
重写自定义错误页面的实现。
如果error是由没有捕获的异常(包括HTTPError)引起的,通过kwargs[|”exc_info”]能获取exc_info元组。实现代码如下:
def write_error(self, status_code, **kwargs):
"""Override to implement custom error pages.
``write_error`` may call `write`, `render`, `set_header`, etc
to produce output as usual.
If this error was caused by an uncaught exception (including
HTTPError), an ``exc_info`` triple will be available as
``kwargs["exc_info"]``. Note that this exception may not be
the "current" exception for purposes of methods like
``sys.exc_info()`` or ``traceback.format_exc``.
"""
if self.settings.get("serve_traceback") and "exc_info" in kwargs:
# in debug mode, try to send a traceback
self.set_header('Content-Type', 'text/plain')
for line in traceback.format_exception(*kwargs["exc_info"]):
self.write(line)
self.finish()
else:
self.finish("<html><title>%(code)d: %(message)s</title>"
"<body>%(code)d: %(message)s</body></html>" % {
"code": status_code,
"message": self._reason,
})
举个例子实现一个自定义的错误页面,在BookRequestHandler中添加如下代码:
def write_error(self, status_code, **kwargs):
self.write("oh,my god!出错啦!!!!请联系系统管理员。\n")
self.write("呵呵,也没关系,我已经讲错误信息记录在日志中去了,系统管理员会看到的。\r\n")
if "exc_info" in kwargs:
self.write("错误信息为:\r\n")
for line in traceback.format_exception(*kwargs["exc_info"]):
self.write(line)
self.finish()
并将get方法修改成:
def get(self, *args, **kwargs):
# print(self.welcome + "\r\n" + "and value2 =" + self.value2)
print("get method: Processing get Method...........")
# self.write(self.welcome + "\r\n" + "and value2 =" + self.value2)
# print(self.request.query_arguments)
# print(self.request.arguments)
# print(self.request.body_arguments)
# self.write("参数name的值为:" + self.get_argument("name", "liaofei"))
print(1/0)
print(1/0)语句,人为地制造一个错误。在游览器中访问http://localhost:8888/book,得到如下结果:
Cookie相关函数
1、get_cookie、set_cookie
获取与设置cookie的值。这个对熟悉web开发的人,一看就明白。就不多解释。
2、get_secure_cookie、set_secure_cookie
获取与设置安全cookie的值。与1 相比加了一个单词secure(安全),就说明是为cookie安全加密解密的作用。这个方法涉及到cookie_secret 的Application 设置选项。来举个列子说明他们之间的区别:
class CookieRequestHandler(RequestHandler):
def get(self, flag):
if flag == '0':
self.set_cookie("user", "liaofei")
elif flag == '1':
userCookie = self.get_cookie("user")
self.write(userCookie)
修改application的配置
settings = {
"debug":False,
"cookie_secret":"gagagaarwrslijwlrjoiajfoajgojowurwojrowjojgfoaguuuu9",
}
app = Application(handlers=[
(r"/book", BookRequestHandler, dict(welcome=welcome, value2=value2)),
(r"/cookie/([0-1]+)", CookieRequestHandler),
], **settings)
(r"/cookie/([0-1]+)", CookieRequestHandler),
], **settings)
注意settings中设置了cookie_secret的值,访问http://localhost:8888/cookie/0时,调用set_cookie
设置cookie中user的值为liaofei,访问http://localhost:8888/cookie/1时,调用set_secure_cookie
设置cookie 中usersecure的值同样为liaofei。用chrome浏览器查看这个相同值得cookie(都是liaofei),发现在游览器客户端的值是不一样的,一个加密过,一个未加密。具体结果如下图:
set_secure_cookie
子类用法:RedirectHandler、StaticFileHandler
from tornado.web import StaticFileHandler, RedirectHandler
#1. RedirectHandler
#1. 301是永久重定向, 302是临时重定向,获取用户个人信息, http://www.baidu.com https
#StaticFileHandler
import time
from tornado import web
import tornado
web.URLSpec
class MainHandler(web.RequestHandler):
async def get(self, *args, **kwargs):
time.sleep(5)
self.write("hello world")
class MainHandler2(web.RequestHandler):
async def get(self, *args, **kwargs):
self.write("hello world2")
settings = {
"static_path":"C:/projects/tornado_overview/chapter02/static",
"static_url_prefix":"/static2/"
}
if __name__ == "__main__":
app = web.Application([
("/", MainHandler),
("/2/", RedirectHandler, {"url":"/"}), #将/2/请求重定向到/下,
("/static/(.*)", StaticFileHandler, {"path": "C:/projects/tornado_overview/chapter02/static"})
], debug=True, **settings)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
RedirectHandler的详细介绍,推荐大家可以查看这篇文章,说的很详细(https://www.cnblogs.com/ShaunChen/p/6652567.html)
StaticFileHandler,以上实例配置了静态文件的访问路径,大家可以自行尝试。
六、 tornado的template
- 定义模板地址
首先我们需要定义一下模板所在地址,让tornado知道去哪里找模板,一般我们把地址写在入口文件中,详情可以看《tornado 1. 项目结构初入》。下列代码static_path的值就是模板的地址。
settings = {
template_path=os.path.join(os.path.dirname(__file__),"template"),
'debug' : True,
- 传递参数到模板中
当我们在handler处理好数据后,就可以把数据传递到相应的模板中去。
class MainHandler(tornado.web.RequestHandler):
def com(a):
return a
def post(self):
number = self.get_argument('num')
self.render('main.html', quantity=number,com=com())
上述代码中渲染模板路径下的main.html模板,其中的变量number的值传递到模板中去,在模板中我们可以使用quantity的获取变量值。你甚至可以将一个函数传到模板中去,如上面com()函数
- 填充及控制语句
在main.html文件中填充变量,可以使用下列方式获取值:
{{ quantity }}
{{ com(1) }}
同时tornado还支持一些简单的控制语句:
{%if 或者 for %}
... 这里是各种表达式
{%end%}
Tornado在所有模板中默认提供了一些便利的函数。它们包括:
- escape(s):替换字符串s中的&、<、>为他们对应的HTML字符。
- url_escape(s):使用urllib.quote_plus替换字符串s中的字符为URL编码形式。
- json_encode(val):将val编码成JSON格式。(在系统底层,这是一个对json库的dumps函数的调用。查阅相关的文档以获得更多关于该函数接收和返回参数的信息。)
- squeeze(s):过滤字符串s,把连续的多个空白字符替换成一个空格。
- 其他
在模板中设置变量:
{%set str='xxxx'%}
使用:{{str}
模板转义:转义是为了防止你的访客进行恶意攻击的,但当你不希望转移时,可以使用raw来阻止对变量转义。
{% raw mail %}