这篇我们将学习Tornado
的web基础。
用tornado写个hello world
我们使用tornado
编写一个简单的web页面。
from tornado import web
import tornado
# 这里的类名随意起,用于和路由一一对应
# 类要继承 RequestHandler 来实现 web请求
class MainHandler(web.RequestHandler):
# 当客户端发起不同的http方法的时候, 只需要重载handler中的对应的方法即可
# 下面的 async 写的话将其变成一个协程
# 当不存在await 的情况下是可以不写 async 的
# tornado的核心是一个单线程的模式 尽量不要在方法中写一些阻塞代码
async def get(self, *args, **kwargs):
# write 将字符串回写到客户端或者 socket
self.write("hello world")
if __name__ == "__main__":
# 我们真正启动的是这个 app
# 创建实例的时候将路由和对应的Handler输出进去
# 传入的是一个 list
# 设置 debug 为 True 开启调试模式
app = web.Application([
("/", MainHandler)
], debug=True)
# 监听的端口
app.listen(8888)
# 核心是 ioloop
tornado.ioloop.IOLoop.current().start()
这段简洁的代码特别像Flask
的。
tornado中为什么不能写同步的方法
这是因为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 world1")
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()
上面是两个url
,我们在第一个请求中做了一个延时的io
操作,现在我们看下直接访问第二个url
的耗时。
当我们先访问第一个阻塞url
的时候,再看下耗时。
tornado中的url配置
上面我们已经配置了url
,对于包含变量的url
我们可以使用下面的方式。
class PeopleInfoHandler(web.RequestHandler):
async def get(self, name, age, gender, *args, **kwargs):
self.write("用户姓名:{},用户年龄:{},用户性别:{},".format(name, age, gender))
# 最后的 ? 可以确保访问的时候不加 / 的时候自动跳转到加了 / 的url
urls = [
("/people/(\w+)/(\d+)/(\w+)/?", PeopleInfoHandler),
]
# 通过正则表达式的形式来指代变量
if __name__ == "__main__":
app = web.Application(urls, debug=True)
app.listen(8888)
tornado.ioloop.IOLoop.current().start()
使用URLSpec
进行一些配置,我们先看下源码:
class URLSpec(Rule):
"""Specifies mappings between URLs and handlers.
.. versionchanged: 4.5
`URLSpec` is now a subclass of a `Rule` with `PathMatches` matcher and is preserved for
backwards compatibility.
"""
def __init__(self, pattern, handler, kwargs=None, name=None):
"""Parameters:
* ``pattern``: Regular expression to be matched. Any capturing
groups in the regex will be passed in to the handler's
get/post/etc methods as arguments (by keyword if named, by
position if unnamed. Named and unnamed capturing groups
may not be mixed in the same rule).
* ``handler``: `~.web.RequestHandler` subclass to be invoked.
* ``kwargs`` (optional): A dictionary of additional arguments
to be passed to the handler's constructor.
* ``name`` (optional): A name for this handler. Used by
`~.web.Application.reverse_url`.
"""
我们可以为每个url
配置一个名字,方便内部访问。
urls = [
# 下面这种方式可以给每个变量命名 增加可读性 不够参数名字要和上面Handler的一致
tornado.web.URLSpec("/people/(?P<name>\w+)/(?P<age>\d+)/(?P<gender>\w+)/?", PeopleInfoHandler, name="people_info"),
# 配置如/people/name/age/gender/
]
比如我们可以在访问一个url
的时候直接进行跳转
class MainHandler(web.RequestHandler):
async def get(self, *args, **kwargs):
# self.write("hello world")
# 跳转到名字为 people_name 的url 如果存在可变参数 依次传入可变参数
self.redirect(self.reverse_url("people_name", "bobby"))
class PeopleNameHandler(web.RequestHandler):
async def get(self, name, *args, **kwargs):
self.write("用户姓名:{}".format(name))
urls = [
tornado.web.URLSpec("/", MainHandler, name="index"),
tornado.web.URLSpec("/people/(\w+)/?", PeopleNameHandler, name="people_name"),
]
我们可以在url
请求的时候传入一些初始化参数,再处理请求之前获得这些初始化参数。
class PeopleIdHandler(web.RequestHandler):
# 这里参数名要和下面传进去的数据一致
def initialize(self, name, key):
self.db_name = name
async def get(self, id, *args, **kwargs):
self.write("用户id:{}".format(id))
people_db = {
"name": "people",
"key": "value"
}
urls = [
tornado.web.URLSpec("/people/(\d+)/?", PeopleIdHandler, people_db, name="people_id"),
]
我们在配置路由的时候传入初始化数据people_db
。
这种访问初始化的一个应用场景:针对不同的url
访问不同的数据库。
小节知识点:
- 使用
reverse_url
通过参数名获得内部url
可以进行可变参数拼接,类比flask
的url_for
。 - 使用
redirect
重定向 - 可以给
Handler
传一个初始值
关于 define
,options
,parse_comand_line
这个小节我们学习下如何在启动python
模块的时候通过tornado
的option
模块传递一些参数。
from tornado.options import define, options, parse_command_line
看下define
的作用:
定义一些可以在命令行中传递的参数以及类型。
define('port', default=8008, help="run on the given port", type=int)
define('debug', default=True, help="set tornado debug mode", type=bool)
完成上面配置之后,需要调用parse_command_line
:
options.parse_command_line()
将运行python
模块的命令进行解析,获得define
中定义的各个参数,以属性值的形式放到options
中。
注意:
options
是一个类,全局只有一个
直接通过属性值获得参数值
if __name__ == "__main__":
app = web.Application([
("/", MainHandler),
], debug=options.debug)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()
完成上面配置可以直接运行脚本,访问服务。
python options_test.py --port=8002 --debug=True
除了从命令行获取到参数值,还可以通过parse_config_file
从文件中获得:
我们新建一个文件conf.fg
,输入我们要传递的参数。
port=8002
debug=True
通过parse_config_file
指定文件名:
options.parse_config_file("conf.cfg")
这样直接运行脚本即可访问服务。
RequestHandler常用方法
我们看下官方文档已经将常用方法进行了分类:常用方法分类
我们先看下入口方法:官方文档
第一个是initialize
方法,是初始化Handler
类的时候定义的方法,用于接受定义url
传入的参数。
class ProfileHandler(RequestHandler):
def initialize(self, database):
self.database = database
def get(self, username):
...
app = Application([
(r'/user/(.*)', ProfileHandler, dict(database=database)),
])
通过这个初始化方法之后将一些数据保存起来,供其他方法使用。
第二个方法是prepare
,是在所有真正请求处理方法调用之前调用的方法,initialize
是在prepare
之前调用的。我们可以在prepare
方法中打印日志,打开文件等操作。
Decorate this method with
gen.coroutine
or useasync def
to make it asynchronous
第三个方法是on_finish
,这个方法和prepare
是相反的,是所有请求结束之后调用的。我们可以在方法中进行关闭句柄,清理缓存等操作。
上面两个方法可以理解为Flask
的请求钩子。
还有就是一些 HTTP方法:
def get(self, *args, **kwargs):
pass
def post(self, *args, **kwargs):
pass
def delete(self, *args, **kwargs):
pass
def patch(self, *args, **kwargs):
pass
def put(self, *args, **kwargs):
pass
看下输入方法有哪些:官方文档
输入方法主要处理传入的一些参数。
先看下get_query_argument
和get_query_arguments
两个方法。
get_query_argument
:
从请求的query string
返回给定name的参数的值.
如果没有提供默认值, 这个参数将被视为必须的, 并且当找不到这个 参数的时候我们会抛出一个 MissingArgumentError
异常.
如果这个参数在url中多次出现, 我们将返回最后一次的值.
返回值永远是unicode
.
get_query_arguments
:
返回指定name的参数列表.
如果参数不存在, 将返回空列表.
返回值永远是unicode
.
这两个方法获得的都是通过
url
传参。
接着我们看下get_argument
和get_arguments
两个方法。
这两个方法和上面两个类似,不过除了能够获得通过url
传递的参数外,还能获得post
方式的传参。
get_argument
:
返回指定的name参数的值.
如果没有提供默认值, 那么这个参数将被视为是必须的, 并且当 找不到这个参数的时候我们会抛出一个 MissingArgumentError
.
如果一个参数在url上出现多次, 我们返回最后一个值.
返回值永远是unicode.
get_arguments
:
返回指定name的参数列表.
如果url
传参外还通过post
方式传参,都会将参数放到列表。
如果参数不存在, 返回一个空列表.
返回值永远是unicode.
我们可以直接获取到所有的参数通过
self.request.arguments
想要获得参数我们还可以使用get_body_argument
和get_body_arguments
:
下面我们发送一个带有json
数据的post
请求:
requests.post("http://127.0.0.1:8888/?name=tornado", json={
"name": "hongshaorou",
"age": 26
})
我们通过url
传递的参数是在arguments
,query
中,body
存的是byte
类型而body_arguments
为空。
这时候直接通过get_body_argument
会获得
我们看下get_body_argument
的源码:
def get_body_argument(self, name, default=_ARG_DEFAULT, strip=True):
"""Returns the value of the argument with the given name
from the request body.
"""
return self._get_argument(name, default, self.request.body_arguments, strip)
从源码看到是从request.body_arguments
获得参数。
在传递json
数据的时候并没有将数据放到body_arguments
中,而是放到body
中。
我们现在从request.body
中获得数据。
那我们怎么使用get_body_argument
方法获得数据呢?
我们需要指定传输数据的form
类型。
import requests
headers = {
"Content-Type": "application/x-www-form-urlencoded;",
}
requests.post("http://127.0.0.1:8888/?name=tornado", headers=headers, data={
"name": "hongshaorou",
"age": 26
})
输出方法:官方文档
使用set_status
方法设置返回的状态码:
-
status_code (int) – 响应状态码. 如果
reason
是None
, 它必须存在于httplib.responses
. -
reason (string) – 用人类可读的原因短语来描述状态码. 如果是
None
, 它会由来自httplib.responses
的reason填满.
使用write
返回数据:
我们可以调用多个write
:
self.write("hello")
self.write("world")
返回的数据将是helloworld
。这是因为RequestHandler
是一个长连接如果不关闭会一直写。每调用一次将数据写入到缓冲区,最后将数据统一发送出去。
使用finish
方法结束HTTP
请求,结束响应:
我们在调用finish
方法之后就不能再使用write
了。因为连接已经被关闭了。
我们可以在finish
中传入一个字典 ,将会自动返回一个json
数据。
self.finish({
"name":"hongshaorou"
})
我们使用redirect
进行重定向:
重定向到给定的URL(可以选择相对路径).
如果指定了 status
参数, 这个值将作为HTTP状态码; 否则 将通过 permanent
参数选择301 (永久) 或者 302 (临时). 默认是 302 (临时重定向).
我们使用write_error
进行错误页面的重写:
write_error
可能调用 write
, render
, set_header
,等 来产生一般的输出.