Tornado学习笔记第三篇-tornado的web基础上篇

这篇我们将学习Tornado的web基础。

用tornado写个hello world

我们使用tornado编写一个简单的web页面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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核心是一个单线程,当我们在一个请求中使用了同步代码的时候,会阻塞其他请求的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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的耗时。

image-20181102154607193

当我们先访问第一个阻塞url的时候,再看下耗时:

image-20181102154714643

tornado中的url配置

上面我们已经配置了url,对于包含变量的url我们可以使用下面的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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()

image-20181102160944357

使用URLSpec进行一些配置,我们先看下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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配置一个名字,方便内部访问。

1
2
3
4
5
urls = [
# 下面这种方式可以给每个变量命名 增加可读性 不够参数名字要和上面Handler的一致
tornado.web.URLSpec("/people/(?P<name>\w+)/(?P<age>\d+)/(?P<gender>\w+)/?", PeopleInfoHandler, name="people_info"),
# 配置如/people/name/age/gender/
]

比如我们可以在访问一个url的时候直接进行跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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请求的时候传入一些初始化参数,再处理请求之前获得这些初始化参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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

image-20181102162914368

这种访问初始化的一个应用场景:针对不同的url访问不同的数据库。

小节知识点:

  1. 使用reverse_url通过参数名获得内部url可以进行可变参数拼接,类比flaskurl_for
  2. 使用redirect重定向
  3. 可以给Handler传一个初始值
关于 define,options,parse_comand_line

这个小节我们学习下如何在启动python模块的时候通过tornadooption模块传递一些参数。

1
from tornado.options import define, options, parse_command_line

看下define的作用:

定义一些可以在命令行中传递的参数以及类型。

1
2
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:

1
options.parse_command_line()

将运行python模块的命令进行解析,获得define中定义的各个参数,以属性值的形式放到options中。

注意:options是一个类,全局只有一个

直接通过属性值获得参数值

1
2
3
4
5
6
if __name__ == "__main__":
app = web.Application([
("/", MainHandler),
], debug=options.debug)
app.listen(options.port)
tornado.ioloop.IOLoop.current().start()

完成上面配置可以直接运行脚本,访问服务。

1
python options_test.py --port=8002 --debug=True

除了从命令行获取到参数值,还可以通过parse_config_file从文件中获得:

我们新建一个文件conf.fg,输入我们要传递的参数。

1
2
port=8002
debug=True

通过parse_config_file指定文件名:

1
options.parse_config_file("conf.cfg")

这样直接运行脚本即可访问服务。

RequestHandler常用方法

我们看下官方文档已经将常用方法进行了分类:常用方法分类

image-20181103110750513

我们先看下入口方法:官方文档

第一个是initialize方法,是初始化Handler类的时候定义的方法,用于接受定义url传入的参数。

1
2
3
4
5
6
7
8
9
10
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 use async def to make it asynchronous

第三个方法是on_finish,这个方法和prepare是相反的,是所有请求结束之后调用的。我们可以在方法中进行关闭句柄,清理缓存等操作。

上面两个方法可以理解为Flask的请求钩子。

还有就是一些 HTTP方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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_argumentget_query_arguments两个方法。

get_query_argument:

从请求的query string返回给定name的参数的值.

如果没有提供默认值, 这个参数将被视为必须的, 并且当找不到这个 参数的时候我们会抛出一个 MissingArgumentError 异常.

如果这个参数在url中多次出现, 我们将返回最后一次的值.

返回值永远是unicode.

get_query_arguments

返回指定name的参数列表.

如果参数不存在, 将返回空列表.

返回值永远是unicode.

这两个方法获得的都是通过url传参。

接着我们看下get_argumentget_arguments两个方法。

这两个方法和上面两个类似,不过除了能够获得通过url传递的参数外,还能获得post方式的传参。

get_argument

返回指定的name参数的值.

如果没有提供默认值, 那么这个参数将被视为是必须的, 并且当 找不到这个参数的时候我们会抛出一个 MissingArgumentError.

如果一个参数在url上出现多次, 我们返回最后一个值.

返回值永远是unicode.

get_arguments

返回指定name的参数列表.

如果url传参外还通过post方式传参,都会将参数放到列表。

如果参数不存在, 返回一个空列表.

返回值永远是unicode.

我们可以直接获取到所有的参数通过 self.request.arguments

想要获得参数我们还可以使用get_body_argumentget_body_arguments:

下面我们发送一个带有json数据的post请求:

1
2
3
4
requests.post("http://127.0.0.1:8888/?name=tornado", json={
"name": "hongshaorou",
"age": 26
})

image-20181103150128077

我们通过url传递的参数是在arguments,query中,body存的是byte类型而body_arguments为空。

这时候直接通过get_body_argument会获得:image-20181103150956448

我们看下get_body_argument的源码:

1
2
3
4
5
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中获得数据。

image-20181103151727189

那我们怎么使用get_body_argument方法获得数据呢?

我们需要指定传输数据的form类型。

1
2
3
4
5
6
7
8
9
10
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
})

image-20181103152322710

输出方法:官方文档

使用set_status方法设置返回的状态码:

  • status_code (int) – 响应状态码. 如果 reasonNone, 它必须存在于 httplib.responses.
  • reason (string) – 用人类可读的原因短语来描述状态码. 如果是 None, 它会由来自httplib.responses 的reason填满.

使用write返回数据:

我们可以调用多个write

1
2
self.write("hello")
self.write("world")

返回的数据将是helloworld。这是因为RequestHandler是一个长连接如果不关闭会一直写。每调用一次将数据写入到缓冲区,最后将数据统一发送出去。

使用finish方法结束HTTP请求,结束响应:

我们在调用finish方法之后就不能再使用write了。因为连接已经被关闭了。

我们可以在finish中传入一个字典 ,将会自动返回一个json数据。

1
2
3
self.finish({
"name":"hongshaorou"
})

我们使用redirect进行重定向:

重定向到给定的URL(可以选择相对路径).

如果指定了 status 参数, 这个值将作为HTTP状态码; 否则 将通过 permanent 参数选择301 (永久) 或者 302 (临时). 默认是 302 (临时重定向).

我们使用write_error进行错误页面的重写:

write_error 可能调用 write, render, set_header,等 来产生一般的输出.

知识就是财富
如果您觉得文章对您有帮助, 欢迎请我喝杯水!