Tornado项目实战:高并发技术论坛网站

05、tornado的web基础

2018-12-26  本文已影响17人  vannesspeng

项目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]

处理过程

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,查询响应头。结果如下:

image.png

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,得到如下结果:

image.png

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_cookie
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

  1. 定义模板地址

首先我们需要定义一下模板所在地址,让tornado知道去哪里找模板,一般我们把地址写在入口文件中,详情可以看《tornado 1. 项目结构初入》。下列代码static_path的值就是模板的地址。

settings = {
     template_path=os.path.join(os.path.dirname(__file__),"template"),
     'debug' : True,
  1. 传递参数到模板中

当我们在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()函数

  1. 填充及控制语句
    在main.html文件中填充变量,可以使用下列方式获取值:

{{ quantity }}
{{ com(1) }}

同时tornado还支持一些简单的控制语句:

{%if 或者 for %}
... 这里是各种表达式
{%end%}

Tornado在所有模板中默认提供了一些便利的函数。它们包括:

  1. 其他
    在模板中设置变量:

{%set str='xxxx'%}
使用:{{str}

模板转义:转义是为了防止你的访客进行恶意攻击的,但当你不希望转移时,可以使用raw来阻止对变量转义。

{% raw mail %}

七、 tornado的settings

推荐参考官方文档

上一篇下一篇

猜你喜欢

热点阅读