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

这篇我们将学习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访问不同的数据库。

小节知识点:

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

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

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常用方法

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

image.png

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

第一个是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 use async 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_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请求:

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) – 响应状态码. 如果 reasonNone, 它必须存在于 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,等 来产生一般的输出.

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容