赞
踩
Django是走大而全的方向,注重的是高效开发,它最出名的是其全自动化的管理后台:只需要使用起ORM,做简单的对象定义,它就能自动生成数据库结构、以及全功能的管理后台。
Django提供的方便,也意味着Django内置的ORM跟框架内的其他模块耦合程度高,应用程序必须使用Django内置的ORM,否则就不能享受到框架内提供的种种基于其ORM的便利。
Tornado走的是少而精的方向,注重的是性能优越,它最出名的是异步非阻塞的设计方式。
pip install tornado
tornado的基础web框架模块
RequestHandler
封装了对应一个请求的所有信息和方法,write(响应信息)就是写响应信息的一个方法;对应每一种http请求方式(get、post等),把对应的处理逻辑写进同名的成员方法中(如对应get请求方式,就将对应的处理逻辑写在get()方法中),当没有对应请求方式的成员方法时,会返回“405: Method Not Allowed”错误。
我们将代码中定义的get()方法更改为post()后,再用浏览器重新访问(浏览器地址栏中输入网址访问的方式为get请求方式),演示如下:
- # coding:utf-8
-
- import tornado.web
- import tornado.ioloop
-
- class IndexHandler(tornado.web.RequestHandler):
- """主路由处理类"""
- def post(self): # 我们修改了这里
- """对应http的post请求方式"""
- self.write("Hello Itcast!")
-
- if __name__ == "__main__":
- app = tornado.web.Application([
- (r"/", IndexHandler),
- ])
- app.listen(8000)
- tornado.ioloop.IOLoop.current().start()
Application
Tornado Web框架的核心应用类,是与服务器对接的接口,里面保存了路由信息表,其初始化接收的第一个参数就是一个路由信息映射元组的列表;其listen(端口)方法用来创建一个http服务器实例,并绑定到给定端口(注意:此时服务器并未开启监听)。
tornado的核心io循环模块,封装了Linux的epoll和BSD的kqueue,tornado高性能的基石。 以Linux的epoll为例,
IOLoop.current()
返回当前线程的IOLoop实例。
IOLoop.start()
启动IOLoop实例的I/O循环,同时服务器监听被打开。
- # coding:utf-8
-
- import tornado.web
- import tornado.ioloop
- import tornado.httpserver # 新引入httpserver模块
-
- class IndexHandler(tornado.web.RequestHandler):
- """主路由处理类"""
- def get(self):
- """对应http的get请求方式"""
- self.write("Hello Itcast!")
-
- if __name__ == "__main__":
- app = tornado.web.Application([
- (r"/", IndexHandler),
- ])
- # ------------------------------
- # 我们修改这个部分
- # app.listen(8000)
- http_server = tornado.httpserver.HTTPServer(app)
- http_server.listen(8000)
- # ------------------------------
- tornado.ioloop.IOLoop.current().start()
在这一修改版本中,我们引入了tornado.httpserver模块,顾名思义,它就是tornado的HTTP服务器实现。
我们创建了一个HTTP服务器实例http_server,因为服务器要服务于我们刚刚建立的web应用,将接收到的客户端请求通过web应用中的路由映射表引导到对应的handler中,所以在构建http_server对象的时候需要传出web应用对象app。http_server.listen(8000)将服务器绑定到8000端口。
实际上一版代码中app.listen(8000)正是对这一过程的简写。
http_server.bind(port)方法是将服务器绑定到指定端口。
http_server.start(num_processes=1)方法指定开启几个进程,参数num_processes默认值为1,即默认仅开启一个进程;如果num_processes为None或者<=0,则自动根据机器硬件的cpu核芯数创建同等数目的子进程;如果num_processes>0,则创建num_processes个子进程。
1.关于app.listen()
app.listen()这个方法只能在单进程模式中使用。
对于app.listen()与手动创建HTTPServer实例
- http_server = tornado.httpserver.HTTPServer(app)
- http_server.listen(8000)
这两种方式,建议大家先使用后者即创建HTTPServer实例的方式,因为其对于理解tornado web应用工作流程的完整性有帮助,便于大家记忆tornado开发的模块组成和程序结构;在熟练使用后,可以改为简写。
2.关于多进程
虽然tornado给我们提供了一次开启多个进程的方法,但是由于:
不建议使用这种多进程的方式,而是手动开启多个进程,并且绑定不同的端口。
在前面的示例中我们都是将服务端口的参数写死在程序中,很不灵活。
tornado为我们提供了一个便捷的工具,tornado.options模块——全局参数定义、存储、转换。
用来定义options选项变量的方法,定义的变量可以在全局的tornado.options.options中获取使用,传入参数:
全局的options对象,所有定义的选项变量都会作为该对象的属性。
转换命令行参数,并将转换后的值对应的设置到全局options对象相关属性上。追加命令行参数的方式是--myoption=myvalue
新建opt.py,我们用代码来看一下如何使用:
- # coding:utf-8
-
- import tornado.web
- import tornado.ioloop
- import tornado.httpserver
- import tornado.options # 新导入的options模块
-
- tornado.options.define("port", default=8000, type=int, help="run server on the given port.") # 定义服务器监听端口选项
- tornado.options.define("itcast", default=[], type=str, multiple=True, help="itcast subjects.") # 无意义,演示多值情况
-
- class IndexHandler(tornado.web.RequestHandler):
- """主路由处理类"""
- def get(self):
- """对应http的get请求方式"""
- self.write("Hello Itcast!")
-
- if __name__ == "__main__":
- tornado.options.parse_command_line()
- print tornado.options.options.itcast # 输出多值选项
- app = tornado.web.Application([
- (r"/", IndexHandler),
- ])
- http_server = tornado.httpserver.HTTPServer(app)
- http_server.listen(tornado.options.options.port)
- tornado.ioloop.IOLoop.current().start()
从配置文件导入option,配置文件中的选项格式如下:
- myoption = "myvalue"
- myotheroption = "myothervalue"
我们用代码来看一下如何使用,新建配置文件config,注意字符串和列表按照python的语法格式:
- port = 8000
- itcast = ["python","c++","java","php","ios"]
修改opt.py文件:
- # coding:utf-8
-
- import tornado.web
- import tornado.ioloop
- import tornado.httpserver
- import tornado.options # 新导入的options模块
-
- tornado.options.define("port", default=8000, type=int, help="run server on the given port.") # 定义服务器监听端口选项
- tornado.options.define("itcast", default=[], type=str, multiple=True, help="itcast subjects.") # 无意义,演示多值情况
-
- class IndexHandler(tornado.web.RequestHandler):
- """主路由处理类"""
- def get(self):
- """对应http的get请求方式"""
- self.write("Hello Itcast!")
-
- if __name__ == "__main__":
- tornado.options.parse_config_file("./config") # 仅仅修改了此处
- print tornado.options.options.itcast # 输出多值选项
- app = tornado.web.Application([
- (r"/", IndexHandler),
- ])
- http_server = tornado.httpserver.HTTPServer(app)
- http_server.listen(tornado.options.options.port)
- tornado.ioloop.IOLoop.current().start()
1. 日志
当我们在代码中调用parse_command_line()或者parse_config_file()的方法时,tornado会默认为我们配置标准logging模块,即默认开启了日志功能,并向标准输出(屏幕)打印日志信息。
如果想关闭tornado默认的日志功能,可以在命令行中添加--logging=none 或者在代码中执行如下操作:
- from tornado.options import options, parse_command_line
- options.logging = None
- parse_command_line()
2. 配置文件
我们看到在使用prase_config_file()的时候,配置文件的书写格式仍需要按照python的语法要求,其优势是可以直接将配置文件的参数转换设置到全局对象tornado.options.options中;然而,其不方便的地方在于需要在代码中调用tornado.options.define()来定义选项,而且不支持字典类型,故而在实际应用中大都不使用这种方法。
在使用配置文件的时候,通常会新建一个python文件(如config.py),然后在里面直接定义python类型的变量(可以是字典类型);在需要配置文件参数的地方,将config.py作为模块导入,并使用其中的变量参数。
如config.py文件:
- # conding:utf-8
-
- # Redis配置
- redis_options = {
- 'redis_host':'127.0.0.1',
- 'redis_port':6379,
- 'redis_pass':'',
- }
-
- # Tornado app配置
- settings = {
- 'template_path': os.path.join(os.path.dirname(__file__), 'templates'),
- 'static_path': os.path.join(os.path.dirname(__file__), 'statics'),
- 'cookie_secret':'0Q1AKOKTQHqaa+N80XhYW7KCGskOUE2snCW06UIxXgI=',
- 'xsrf_cookies':False,
- 'login_url':'/login',
- 'debug':True,
- }
-
- # 日志
- log_path = os.path.join(os.path.dirname(__file__), 'logs/log')
使用config.py的模块中导入config,如下:
- # conding:utf-8
-
- import tornado.web
- import config
-
- if __name__ = "__main__":
- app = tornado.web.Application([], **config.settings)
前面的学习中,我们在创建tornado.web.Application的对象时,传入了第一个参数——路由映射列表。实际上Application类的构造函数还接收很多关于tornado web应用的配置参数,在后面的学习中我们用到的地方会为大家介绍。
我们先来学习一个参数:
debug,设置tornado是否工作在调试模式,默认为False即工作在生产模式。当设置debug=True 后,tornado会工作在调试/开发模式,在此种模式下,tornado为方便我们开发而提供了几种特性:
使用debug参数的方法:
- import tornado.web
- app = tornado.web.Application([], debug=True)
先前我们在构建路由映射列表的时候,使用的是二元元组,如:
[(r"/", IndexHandler),]
对于这个映射列表中的路由,实际上还可以传入多个信息,如:
- [
- (r"/", Indexhandler),
- (r"/cpp", ItcastHandler, {"subject":"c++"}),
- url(r"/python", ItcastHandler, {"subject":"python"}, name="python_url")
- ]
对于路由中的字典,会传入到对应的RequestHandler的initialize()方法中:
- from tornado.web import RequestHandler
- class ItcastHandler(RequestHandler):
- def initialize(self, subject):
- self.subject = subject
-
- def get(self):
- self.write(self.subject)
对于路由中的name字段,注意此时不能再使用元组,而应使用tornado.web.url来构建。name是给该路由起一个名字,可以通过调用RequestHandler.reverse_url(name)来获取该名子对应的url。
- # coding:utf-8
-
- import tornado.web
- import tornado.ioloop
- import tornado.httpserver
- import tornado.options
- from tornado.options import options, define
- from tornado.web import url, RequestHandler
-
- define("port", default=8000, type=int, help="run server on the given port.")
-
- class IndexHandler(RequestHandler):
- def get(self):
- python_url = self.reverse_url("python_url")
- self.write('<a href="%s">itcast</a>' %
- python_url)
-
- class ItcastHandler(RequestHandler):
- def initialize(self, subject):
- self.subject = subject
-
- def get(self):
- self.write(self.subject)
-
- if __name__ == "__main__":
- tornado.options.parse_command_line()
- app = tornado.web.Application([
- (r"/", Indexhandler),
- (r"/cpp", ItcastHandler, {"subject":"c++"}),
- url(r"/python", ItcastHandler, {"subject":"python"}, name="python_url")
- ],
- debug = True)
- http_server = tornado.httpserver.HTTPServer(app)
- http_server.listen(options.port)
- tornado.ioloop.IOLoop.current().start()
回想一下,利用HTTP协议向服务器传参有几种途径?
get_query_argument(name, default=_ARG_DEFAULT, strip=True)
从请求的查询字符串中返回指定参数name的值,如果出现多个同名参数,则返回最后一个的值。
default为设值未传name参数时返回的默认值,如若default也未设置,则会抛出tornado.web.MissingArgumentError异常。
strip表示是否过滤掉左右两边的空白字符,默认为过滤。
get_query_arguments(name, strip=True)
从请求的查询字符串中返回指定参数name的值,注意返回的是list列表(即使对应name参数只有一个值)。若未找到name参数,则返回空列表[]。
strip同前,不再赘述。
get_body_argument(name, default=_ARG_DEFAULT, strip=True)
从请求体中返回指定参数name的值,如果出现多个同名参数,则返回最后一个的值。
default与strip同前,不再赘述。
get_body_arguments(name, strip=True)
从请求体中返回指定参数name的值,注意返回的是list列表(即使对应name参数只有一个值)。若未找到name参数,则返回空列表[]。
strip同前,不再赘述。
对于请求体中的数据要求为字符串,且格式为表单编码格式(与url中的请求字符串格式相同),即key1=value1&key2=value2,HTTP报文头Header中的"Content-Type"为application/x-www-form-urlencoded 或 multipart/form-data。对于请求体数据为json或xml的,无法通过这两个方法获取。
get_argument(name, default=_ARG_DEFAULT, strip=True)
从请求体和查询字符串中返回指定参数name的值,如果出现多个同名参数,则返回最后一个的值。
default与strip同前,不再赘述。
get_arguments(name, strip=True)
从请求体和查询字符串中返回指定参数name的值,注意返回的是list列表(即使对应name参数只有一个值)。若未找到name参数,则返回空列表[]。
strip同前,不再赘述。
说明
对于请求体中数据的要求同前。 这两个方法最常用。
RequestHandler.request 对象存储了关于请求的相关信息,具体属性有:
tornado.httputil.HTTPFile是接收到的文件对象,它有三个属性:
tornado中对于路由映射也支持正则提取uri,提取出来的参数会作为RequestHandler中对应请求方式的成员方法参数。若在正则表达式中定义了名字,则参数按名传递;若未定义名字,则参数按顺序传递。提取出来的参数会作为对应请求方式的成员方法的参数。
建议:提取多个值时最好用命名方式。
将chunk数据写到输出缓冲区。如我们在之前的示例代码中写的:
- class IndexHandler(RequestHandler):
- def get(self):
- self.write("hello itcast!")
想一想,可不可以在同一个处理方法中多次使用write方法?
下面的代码会出现什么效果?
- class IndexHandler(RequestHandler):
- def get(self):
- self.write("hello itcast 1!")
- self.write("hello itcast 2!")
- self.write("hello itcast 3!")
write方法是写到缓冲区的,我们可以像写文件一样多次使用write方法不断追加响应内容,最终所有写到缓冲区的内容一起作为本次请求的响应输出。
想一想,如何利用write方法写json数据?
- import json
-
- class IndexHandler(RequestHandler):
- def get(self):
- stu = {
- "name":"zhangsan",
- "age":24,
- "gender":1,
- }
- stu_json = json.dumps(stu)
- self.write(stu_json)
实际上,我们可以不用自己手动去做json序列化,当write方法检测到我们传入的chunk参数是字典类型后,会自动帮我们转换为json字符串。
- class IndexHandler(RequestHandler):
- def get(self):
- stu = {
- "name":"zhangsan",
- "age":24,
- "gender":1,
- }
- self.write(stu)
两种方式有什么差异?
对比一下两种方式的响应头header中Content-Type
字段,自己手动序列化时为Content-Type:text/html; charset=UTF-8
,而采用write方法时为Content-Type:application/json; charset=UTF-8
。
write方法除了帮我们将字典转换为json字符串之外,还帮我们将Content-Type
设置为application/json; charset=UTF-8
。
利用set_header(name, value)方法,可以手动设置一个名为name、值为value的响应头header字段。
用set_header方法来完成上面write所做的工作。
- import json
-
- class IndexHandler(RequestHandler):
- def get(self):
- stu = {
- "name":"zhangsan",
- "age":24,
- "gender":1,
- }
- stu_json = json.dumps(stu)
- self.write(stu_json)
- self.set_header("Content-Type", "application/json; charset=UTF-8")
该方法会在进入HTTP处理方法前先被调用,可以重写此方法来预先设置默认的headers。
注意:在HTTP处理方法中使用set_header()方法会覆盖掉在set_default_headers()方法中设置的同名header。
- class IndexHandler(RequestHandler):
- def set_default_headers(self):
- print "执行了set_default_headers()"
- # 设置get与post方式的默认响应体格式为json
- self.set_header("Content-Type", "application/json; charset=UTF-8")
- # 设置一个名为itcast、值为python的header
- self.set_header("itcast", "python")
-
- def get(self):
- print "执行了get()"
- stu = {
- "name":"zhangsan",
- "age":24,
- "gender":1,
- }
- stu_json = json.dumps(stu)
- self.write(stu_json)
- self.set_header("itcast", "i love python") # 注意此处重写了header中的itcast字段
-
- def post(self):
- print "执行了post()"
- stu = {
- "name":"zhangsan",
- "age":24,
- "gender":1,
- }
- stu_json = json.dumps(stu)
- self.write(stu_json)
为响应设置状态码。
参数说明:
告知浏览器跳转到url。
- class IndexHandler(RequestHandler):
- """对应/"""
- def get(self):
- self.write("主页")
-
- class LoginHandler(RequestHandler):
- """对应/login"""
- def get(self):
- self.write('<form method="post"><input type="submit" value="登陆"></form>')
-
- def post(self):
- self.redirect("/")
抛出HTTP错误状态码status_code,默认为500,kwargs为可变命名参数。使用send_error抛出错误后tornado会调用write_error()方法进行处理,并返回给浏览器处理后的错误页面。
- class IndexHandler(RequestHandler):
- def get(self):
- self.write("主页")
- self.send_error(404, content="出现404错误")
注意:默认的write\_error()
方法不会处理send\_error
抛出的kwargs参数,即上面的代码中content="出现404错误"
是没有意义的。
用来处理send_error抛出的错误信息并返回给浏览器错误信息页面。可以重写此方法来定制自己的错误显示页面。
- class IndexHandler(RequestHandler):
- def get(self):
- err_code = self.get_argument("code", None) # 注意返回的是unicode字符串,下同
- err_title = self.get_argument("title", "")
- err_content = self.get_argument("content", "")
- if err_code:
- self.send_error(err_code, title=err_title, content=err_content)
- else:
- self.write("主页")
-
- def write_error(self, status_code, **kwargs):
- self.write(u"<h1>出错了,程序员GG正在赶过来!</h1>")
- self.write(u"<p>错误名:%s</p>" % kwargs["title"])
- self.write(u"<p>错误详情:%s</p>" % kwargs["content"])
下面的接口方法是由tornado框架进行调用的,我们可以选择性的重写这些方法。
对应每个请求的处理类Handler在构造一个实例后首先执行initialize()方法。
路由映射中的第三个字典型参数会作为该方法的命名参数传递,如:
- class ProfileHandler(RequestHandler):
- def initialize(self, database):
- self.database = database
-
- def get(self):
- ...
-
- app = Application([
- (r'/user/(.*)', ProfileHandler, dict(database=database)),
- ])
此方法通常用来初始化参数(对象属性),很少使用。
预处理,即在执行对应请求方式的HTTP方法(如get、post等)前先执行
注意:不论以何种HTTP方式请求,都会执行prepare()方法。
以预处理请求体中的json数据为例:
- import json
-
- class IndexHandler(RequestHandler):
- def prepare(self):
- if self.request.headers.get("Content-Type").startswith("application/json"):
- self.json_dict = json.loads(self.request.body)
- else:
- self.json_dict = None
-
- def post(self):
- if self.json_dict:
- for key, value in self.json_dict.items():
- self.write("<h3>%s</h3><p>%s</p>" % (key, value))
-
- def put(self):
- if self.json_dict:
- for key, value in self.json_dict.items():
- self.write("<h3>%s</h3><p>%s</p>" % (key, value))
方法 | 描述 |
---|---|
get | 请求指定的页面信息,并返回实体主体。 |
head | 类似于get请求,只不过返回的响应中没有具体的内容,用于获取报头 |
post | 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。POST请求可能会导致新的资源的建立和/或已有资源的修改。 |
delete | 请求服务器删除指定的内容。 |
patch | 请求修改局部数据。 |
put | 从客户端向服务器传送的数据取代指定的文档的内容。 |
options | 返回给定URL支持的所有HTTP方法。 |
在请求处理结束后调用,即在调用HTTP方法后调用。通常该方法用来进行资源清理释放或处理日志等。注意:请尽量不要在此方法中进行响应输出。
我们通过一段程序来看上面这些接口的调用顺序。
- class IndexHandler(RequestHandler):
-
- def initialize(self):
- print "调用了initialize()"
-
- def prepare(self):
- print "调用了prepare()"
-
- def set_default_headers(self):
- print "调用了set_default_headers()"
-
- def write_error(self, status_code, **kwargs):
- print "调用了write_error()"
-
- def get(self):
- print "调用了get()"
-
- def post(self):
- print "调用了post()"
- self.send_error(200) # 注意此出抛出了错误
-
- def on_finish(self):
- print "调用了on_finish()"
在正常情况未抛出错误时,调用顺序为:
在有错误抛出时,调用顺序为:
我们可以通过向web.Application类的构造函数传递一个名为static_path的参数来告诉Tornado从文件系统的一个特定位置提供静态文件,如:
- app = tornado.web.Application(
- [(r'/', IndexHandler)],
- static_path=os.path.join(os.path.dirname(__file__), "statics"),
- )
在这里,我们设置了一个当前应用目录下名为statics的子目录作为static_path的参数。现在应用将以读取statics目录下的filename.ext来响应诸如/static/filename.ext的请求,并在响应的主体中返回。
对于静态文件目录的命名,为了便于部署,建议使用static
对于我们提供的静态文件资源,可以通过http://127.0.0.1/static/html/index.html
来访问。而且在index.html中引用的静态资源文件,我们给定的路径也符合/static/...的格式,故页面可以正常浏览。
我们再看刚刚访问页面时使用的路径http://127.0.0.1/static/html/index.html
,这中url显然对用户是不友好的,访问很不方便。我们可以通过tornado.web.StaticFileHandler来自由映射静态文件与其访问路径url。
tornado.web.StaticFileHandler是tornado预置的用来提供静态资源文件的handler。
- import os
-
- current_path = os.path.dirname(__file__)
- app = tornado.web.Application(
- [
- (r'^/()$', StaticFileHandler, {"path":os.path.join(current_path, "statics/html"), "default_filename":"index.html"}),
- (r'^/view/(.*)$', StaticFileHandler, {"path":os.path.join(current_path, "statics/html")}),
- ],
- static_path=os.path.join(current_path, "statics"),
- )
现在,对于静态文件statics/html/index.html,可以通过三种方式进行访问:
使用模板,需要仿照静态文件路径设置一样,向web.Application类的构造函数传递一个名为template_path的参数来告诉Tornado从文件系统的一个特定位置提供模板文件,如:
- app = tornado.web.Application(
- [(r'/', IndexHandler)],
- static_path=os.path.join(os.path.dirname(__file__), "statics"),
- template_path=os.path.join(os.path.dirname(__file__), "templates"),
- )
在这里,我们设置了一个当前应用目录下名为templates的子目录作为template_path的参数。在handler中使用的模板将在此目录中寻找。
与Django框架相比,Tornado没有自带ORM,对于数据库需要自己去适配。我们使用MySQL数据库。
pip install torndb
query为要执行的sql语句,parameters与kwparameters为要绑定的参数,如:
- db.execute("insert into houses(title, position, price, score, comments) values(%s, %s, %s, %s, %s)", "独立装修小别墅", "紧邻文津街", 280, 5, 128)
- 或
- db.execute("insert into houses(title, position, price, score, comments) values(%(title)s, %(positi
- class InsertHandler(RequestHandler):
- def post(self):
- title = self.get_argument("title")
- position = self.get_argument("position")
- price = self.get_argument("price")
- score = self.get_argument("score")
- comments = self.get_argument("comments")
- try:
- ret = self.application.db.execute("insert into houses(title, position, price, score, comments) values(%s, %s, %s, %s, %s)", title, position, price, score, comments)
- except Exception as e:
- self.write("DB error:%s" % e)
- else:
- self.write("OK %d" % ret)
以上一章节模板中的案例来演示,先修改一下index.html模板,将
- <span class="house-title">{{title_join(house["titles"])}}</span>
改为
<span class="house-title">{{house["title"]}}</span>
添加两个新的handler:
- class GetHandler(RequestHandler):
- def get(self):
- """访问方式为http://127.0.0.1/get?id=111"""
- hid = self.get_argument("id")
- try:
- ret = self.application.db.get("select title,position,price,score,comments from houses where id=%s", hid)
- except Exception as e:
- self.write("DB error:%s" % e)
- else:
- print type(ret)
- print ret
- print ret.title
- print ret['title']
- self.render("index.html", houses=[ret])
-
-
- class QueryHandler(RequestHandler):
- def get(self):
- """访问方式为http://127.0.0.1/query"""
- try:
- ret = self.application.db.query("select title,position,price,score,comments from houses limit 10")
- except Exception as e:
- self.write("DB error:%s" % e)
- else:
- self.render("index.html", houses=ret)
set_cookie(name, value, domain=None, expires=None, path='/', expires_days=None)
参数说明:
参数名 | 说明 |
---|---|
name | cookie名 |
value | cookie值 |
domain | 提交cookie时匹配的域名 |
path | 提交cookie时匹配的路径 |
expires | cookie的有效期,可以是时间戳整数、时间元组或者datetime类型,为UTC时间 |
expires_days | cookie的有效期,天数,优先级低于expires |
get_cookie(name, default=None)
获取名为name的cookie,可以设置默认值。
- class IndexHandler(RequestHandler):
- def get(self):
- n3 = self.get_cookie("n3")
- self.write(n3)
clear_cookie(name, path='/', domain=None)
删除名为name,并同时匹配domain和path的cookie。
clear_all_cookies(path='/', domain=None)
删除同时匹配domain和path的所有cookie。
- class ClearOneCookieHandler(RequestHandler):
- def get(self):
- self.clear_cookie("n3")
- self.write("OK")
-
- class ClearAllCookieHandler(RequestHandler):
- def get(self):
- self.clear_all_cookies()
- self.write("OK")
注意:执行清除cookie操作后,并不是立即删除了浏览器中的cookie,而是给cookie值置空,并改变其有效期使其失效。真正的删除cookie是由浏览器去清理的。
Cookie是存储在客户端浏览器中的,很容易被篡改。Tornado提供了一种对Cookie进行简易加密签名的方法来防止Cookie被恶意篡改。
使用安全Cookie需要为应用配置一个用来给Cookie进行混淆的秘钥cookie_secret,将其传递给Application的构造函数。我们可以使用如下方法来生成一个随机字符串作为cookie_secret的值。
将生成的cookie_secret传入Application构造函数:
- app = tornado.web.Application(
- [(r"/", IndexHandler),],
- cookie_secret = "2hcicVu+TqShDpfsjMWQLZ0Mkq5NPEWSk9fi0zsSt3A="
- )
set_secure_cookie(name, value, expires_days=30)
设置一个带签名和时间戳的cookie,防止cookie被伪造。
get_secure_cookie(name, value=None, max_age_days=31)
如果cookie存在且验证通过,返回cookie的值,否则返回None。max_age_day不同于expires_days,expires_days是设置浏览器中cookie的有效期,而max_age_day是过滤安全cookie的时间戳。
- class IndexHandler(RequestHandler):
- def get(self):
- cookie = self.get_secure_cookie("count")
- count = int(cookie) + 1 if cookie else 1
- self.set_secure_cookie("count", str(count))
- self.write(
- '<html><head><title>Cookie计数器</title></head>'
- '<body><h1>您已访问本页%d次。</h1>' % count +
- '</body></html>'
- )
我们看签名后的cookie值:
"2|1:0|10:1476412069|5:count|4:NQ==|cb5fc1d4434971de6abf87270ac33381c686e4ec8c6f7e62130a0f8cbe5b7609"
字段说明:
注意:Tornado的安全cookie只是一定程度的安全,仅仅是增加了恶意修改的难度。Tornado的安全cookies仍然容易被窃听,而cookie值是签名不是加密,攻击者能够读取已存储的cookie值,并且可以传输他们的数据到任意服务器,或者通过发送没有修改的数据给应用伪造请求。因此,避免在浏览器cookie中存储敏感的用户数据是非常重要的。
先建立一个网站127.0.0.1:8000,使用上一节中的Cookie计数器:
- class IndexHandler(RequestHandler):
- def get(self):
- cookie = self.get_secure_cookie("count")
- count = int(cookie) + 1 if cookie else 1
- self.set_secure_cookie("count", str(count))
- self.write(
- '<html><head><title>Cookie计数器</title></head>'
- '<body><h1>您已访问本页%d次。</h1>' % count +
- '</body></html>'
- )
再建立一个网站127.0.0.1:9000,
- class IndexHandler(RequestHandler):
- def get(self):
- self.write('<html><head><title>被攻击的网站</title></head>'
- '<body><h1>此网站的图片链接被修改了</h1>'
- '<img alt="这应该是图片" src="http://127.0.0.1:8000/?f=9000/">'
- '</body></html>'
- )
在9000网站我们模拟攻击者修改了我们的图片源地址为8000网站的Cookie计数器页面网址。当我们访问9000网站的时候,在我们不知道、未授权的情况下8000网站的Cookie被使用了,以至于让8000网址认为是我们自己调用了8000网站的逻辑。这就是CSRF(Cross-site request forgery)跨站请求伪造(跨站攻击或跨域攻击的一种),通常缩写为CSRF或者XSRF。
我们刚刚使用的是GET方式模拟的攻击,为了防范这种方式的攻击,任何会产生副作用的HTTP请求,比如点击购买按钮、编辑账户设置、改变密码或删除文档,都应该使用HTTP POST方法(或PUT、DELETE)。但是,这并不足够:一个恶意站点可能会通过其他手段来模拟发送POST请求,保护POST请求需要额外的策略。
浏览器有一个很重要的概念——同源策略(Same-Origin Policy)。 所谓同源是指,域名,协议,端口相同。 不同源的客户端脚本(javascript、ActionScript)在没明确授权的情况下,不能读写对方的资源。
由于第三方站点没有访问cookie数据的权限(同源策略),所以我们可以要求每个请求包括一个特定的参数值作为令牌来匹配存储在cookie中的对应值,如果两者匹配,我们的应用认定请求有效。而第三方站点无法在请求中包含令牌cookie值,这就有效地防止了不可信网站发送未授权的请求。
要开启XSRF保护,需要在Application的构造函数中添加xsrf_cookies参数:
- app = tornado.web.Application(
- [(r"/", IndexHandler),],
- cookie_secret = "2hcicVu+TqShDpfsjMWQLZ0Mkq5NPEWSk9fi0zsSt3A=",
- xsrf_cookies = True
- )
当这个参数被设置时,Tornado将拒绝请求参数中不包含正确的_xsrf值的POST、PUT和DELETE请求。
- class IndexHandler(RequestHandler):
- def post(self):
- self.write("hello itcast")
用不带_xsrf的post请求时,报出了HTTP 403: Forbidden ('_xsrf' argument missing from POST)
的错误。
在模板中使用XSRF保护,只需在模板中添加
{% module xsrf_form_html() %}
如新建一个模板index.html
- <!DOCTYPE html>
- <html>
- <head>
- <title>测试XSRF</title>
- </head>
- <body>
- <form method="post">
- {% module xsrf_form_html() %}
- <input type="text" name="message"/>
- <input type="submit" value="Post"/>
- </form>
- </body>
- </html>
后端
- class IndexHandler(RequestHandler):
- def get(self):
- self.render("index.html")
-
- def post(self):
- self.write("hello itcast")
模板中添加的语句帮我们做了两件事:
渲染后的页面原码如下:
- <!DOCTYPE html>
- <html>
- <head>
- <title>测试XSRF</title>
- </head>
- <body>
- <form method="post">
- <input type="hidden" name="_xsrf" value="2|543c2206|a056ff9e49df23eaffde0a694cde2b02|1476443353"/>
- <input type="text" name="message"/>
- <input type="submit" value="Post"/>
- </form>
- </body>
- </html>
对于不使用模板的应用来说,首先要设置_xsrf的Cookie值,可以在任意的Handler中通过获取self.xsrf_token的值来生成_xsrf并设置Cookie。
下面两种方式都可以起到设置_xsrf Cookie的作用。
- class XSRFTokenHandler(RequestHandler):
- """专门用来设置_xsrf Cookie的接口"""
- def get(self):
- self.xsrf_token
- self.write("Ok")
-
- class StaticFileHandler(tornado.web.StaticFileHandler):
- """重写StaticFileHandler,构造时触发设置_xsrf Cookie"""
- def __init__(self, *args, **kwargs):
- super(StaticFileHandler, self).__init__(*args, **kwargs)
- self.xsrf_token
对于请求携带_xsrf参数,有两种方式:
1. 请求体携带_xsrf参数
新建一个页面xsrf.html:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title>测试XSRF</title>
- </head>
- <body>
- <a href="javascript:;" onclick="xsrfPost()">发送POST请求</a>
- <script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
- <script type="text/javascript">
- //获取指定Cookie的函数
- function getCookie(name) {
- var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
- return r ? r[1] : undefined;
- }
- //AJAX发送post请求,表单格式数据
- function xsrfPost() {
- var xsrf = getCookie("_xsrf");
- $.post("/new", "_xsrf="+xsrf+"&key1=value1", function(data) {
- alert("OK");
- });
- }
- </script>
- </body>
- </html>
2. HTTP头X-XSRFToken
新建一个页面json.html:
- <!DOCTYPE html>
- <html>
- <head>
- <meta charset="utf-8">
- <title>测试XSRF</title>
- </head>
- <body>
- <a href="javascript:;" onclick="xsrfPost()">发送POST请求</a>
- <script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>
- <script type="text/javascript">
- //获取指定Cookie的函数
- function getCookie(name) {
- var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
- return r ? r[1] : undefined;
- }
- //AJAX发送post请求,json格式数据
- function xsrfPost() {
- var xsrf = getCookie("_xsrf");
- var data = {
- key1:1,
- key1:2
- };
- var json_data = JSON.stringify(data);
- $.ajax({
- url: "/new",
- method: "POST",
- headers: {
- "X-XSRFToken":xsrf,
- },
- data:json_data,
- success:function(data) {
- alert("OK");
- }
- })
- }
- </script>
- </body>
- </html>
用户验证是指在收到用户请求后进行处理前先判断用户的认证状态(如登陆状态),若通过验证则正常处理,否则强制用户跳转至认证页面(如登陆页面)。
为了使用Tornado的认证功能,我们需要对登录用户标记具体的处理函数。我们可以使用@tornado.web.authenticated装饰器完成它。当我们使用这个装饰器包裹一个处理方法时,Tornado将确保这个方法的主体只有在合法的用户被发现时才会调用。
- class ProfileHandler(RequestHandler):
- @tornado.web.authenticated
- def get(self):
- self.write("这是我的个人主页。")
装饰器@tornado.web.authenticated的判断执行依赖于请求处理类中的self.current_user属性,如果current_user值为假(None、False、0、""等),任何GET或HEAD请求都将把访客重定向到应用设置中login_url指定的URL,而非法用户的POST请求将返回一个带有403(Forbidden)状态的HTTP响应。
在获取self.current_user属性的时候,tornado会调用get_current_user()方法来返回current_user的值。也就是说,我们验证用户的逻辑应写在get_current_user()方法中,若该方法返回非假值则验证通过,否则验证失败。
- class ProfileHandler(RequestHandler):
- def get_current_user(self):
- """在此完成用户的认证逻辑"""
- user_name = self.get_argument("name", None)
- return user_name
-
- @tornado.web.authenticated
- def get(self):
- self.write("这是我的个人主页。")
当用户验证失败时,将用户重定向到login_url上,所以我们还需要在Application中配置login_url。
- class LoginHandler(RequestHandler):
- def get(self):
- """在此返回登陆页面"""
- self.write("登陆页面")
-
- app = tornado.web.Application(
- [
- (r"/", IndexHandler),
- (r"/profile", ProfileHandler),
- (r"/login", LoginHandler),
- ],
- "login_url":"/login"
- )
Tornado实现异步的机制不是线程,而是epoll,即将异步过程交给epoll执行并进行监视回调。
Tornado利用epoll来实现异步,程序的挂起与唤醒始终在一个线程上,由Tornado自己来调度,属于真正意义上的协程。
因为epoll主要是用来解决网络IO的并发问题,所以Tornado的异步编程也主要体现在网络IO的异步上,即异步Web请求。
Tornado提供了一个异步Web请求客户端tornado.httpclient.AsyncHTTPClient用来进行异步Web请求。
用于执行一个web请求request,并异步返回一个tornado.httpclient.HTTPResponse响应。
request可以是一个url,也可以是一个tornado.httpclient.HTTPRequest对象。如果是url,fetch会自己构造一个HTTPRequest对象。
HTTP请求类,HTTPRequest的构造函数可以接收众多构造参数,最常用的如下:
HTTP响应类,其常用属性如下:
此装饰器用于回调形式的异步方法,并且应该仅用于HTTP的方法上(如get、post等)。
此装饰器不会让被装饰的方法变为异步,而只是告诉框架被装饰的方法是异步的,当方法返回时响应尚未完成。只有在request handler调用了finish方法后,才会结束本次请求处理,发送响应。
不带此装饰器的请求在get、post等方法返回时自动完成结束请求处理。
网站基本都会有数据库操作,而Tornado是单线程的,这意味着如果数据库查询返回过慢,整个服务器响应会被堵塞。
数据库查询,实质上也是远程的网络调用;理想情况下,是将这些操作也封装成为异步的;但Tornado对此并没有提供任何支持。
这是Tornado的设计,而不是缺陷。
一个系统,要满足高流量;是必须解决数据库查询速度问题的!
数据库若存在查询性能问题,整个系统无论如何优化,数据库都会是瓶颈,拖慢整个系统!
异步并不能从本质上提到系统的性能;它仅仅是避免多余的网络响应等待,以及切换线程的CPU耗费。
如果数据库查询响应太慢,需要解决的是数据库的性能问题;而不是调用数据库的前端Web应用。
对于实时返回的数据查询,理想情况下需要确保所有数据都在内存中,数据库硬盘IO应该为0;这样的查询才能足够快;而如果数据库查询足够快,那么前端web应用也就无将数据查询封装为异步的必要。
就算是使用协程,异步程序对于同步程序始终还是会提高复杂性;需要衡量的是处理这些额外复杂性是否值得。
如果后端有查询实在是太慢,无法绕过,Tornaod的建议是将这些查询在后端封装独立封装成为HTTP接口,然后使用Tornado内置的异步HTTP客户端进行调用。
WebSocket是HTML5规范中新提出的客户端-服务器通讯协议,协议本身使用新的ws://URL格式。
WebSocket 是独立的、创建在 TCP 上的协议,和 HTTP 的唯一关联是使用 HTTP 协议的101状态码进行协议切换,使用的 TCP 端口是80,可以用于绕过大多数防火墙的限制。
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端直接向客户端推送数据而不需要客户端进行请求,两者之间可以创建持久性的连接,并允许数据进行双向传送。
目前常见的浏览器如 Chrome、IE、Firefox、Safari、Opera 等都支持 WebSocket,同时需要服务端程序支持 WebSocket。
Tornado提供支持WebSocket的模块是tornado.websocket,其中提供了一个WebSocketHandler类用来处理通讯。
WebSocketHandler.open()
当一个WebSocket连接建立后被调用。
WebSocketHandler.on_message(message)
当客户端发送消息message过来时被调用,注意此方法必须被重写。
WebSocketHandler.on_close()
当WebSocket连接关闭后被调用。
WebSocketHandler.write_message(message, binary=False)
向客户端发送消息messagea,message可以是字符串或字典(字典会被转为json字符串)。若binary为False,则message以utf8编码发送;二进制模式(binary=True)时,可发送任何字节码。
WebSocketHandler.close()
关闭WebSocket连接。
WebSocketHandler.check_origin(origin)
判断源origin,对于符合条件(返回判断结果为True)的请求源origin允许其连接,否则返回403。可以重写此方法来解决WebSocket的跨域请求(如始终return True)。
在前端JS中使用WebSocket与服务器通讯的常用方法如下:
- var ws = new WebSocket("ws://127.0.0.1:8888/websocket"); // 新建一个ws连接
- ws.onopen = function() { // 连接建立好后的回调
- ws.send("Hello, world"); // 向建立的连接发送消息
- };
- ws.onmessage = function (evt) { // 收到服务器发送的消息后执行的回调
- alert(evt.data); // 接收的消息内容在事件参数evt的data属性中
- };
为了充分利用多核CPU,并且为了减少同步代码中的阻塞影响,在部署Tornado的时候需要开启多个进程(最好为每个CPU核心开启一个进程)
因为Tornado自带的服务器性能很高,所以我们只需开启多个Tornado进程。为了对外有统一的接口,并且可以分发用户的请求到不同的Tornado进程上,我们用Nginx来进行代理。
为了统一管理Tornado的多个进程,我们可以借助supervisor工具。
sudo pip install supervisor
运行echo_supervisord_conf命令输出默认的配置项,可以如下操作将默认配置保存到文件中
echo_supervisord_conf > supervisord.conf
vim 打开编辑supervisord.conf文件,修改
- [include]
- files = relative/directory/*.ini
为
- [include]
- files = /etc/supervisor/*.conf
include选项指明包含的其他配置文件。
将编辑后的supervisord.conf文件复制到/etc/目录下
sudo cp supervisord.conf /etc/
然后我们在/etc目录下新建子目录supervisor(与配置文件里的选项相同),并在/etc/supervisor/中新建tornado管理的配置文件tornado.conf。
- [group:tornadoes]
- programs=tornado-8000,tornado-8001,tornado-8002,tornado-8003
-
- [program:tornado-8000]
- command=/home/python/.virtualenvs/tornado_py2/bin/python /home/python/Documents/demo/chat /server.py --port=8000
- directory=/home/python/Documents/demo/chat
- user=python
- autorestart=true
- redirect_stderr=true
- stdout_logfile=/home/python/tornado.log
- loglevel=info
-
- [program:tornado-8001]
- command=/home/python/.virtualenvs/tornado_py2/bin/python /home/python/Documents/demo/chat /server.py --port=8001
- directory=/home/python/Documents/demo/chat
- user=python
- autorestart=true
- redirect_stderr=true
- stdout_logfile=/home/python/tornado.log
- loglevel=info
-
- [program:tornado-8002]
- command=/home/python/.virtualenvs/tornado_py2/bin/python /home/python/Documents/demo/chat /server.py --port=8002
- directory=/home/python/Documents/demo/chat
- user=python
- autorestart=true
- redirect_stderr=true
- stdout_logfile=/home/python/tornado.log
- loglevel=info
-
- [program:tornado-8003]
- command=/home/python/.virtualenvs/tornado_py2/bin/python /home/python/Documents/demo/chat /server.py --port=8003
- directory=/home/python/Documents/demo/chat
- user=python
- autorestart=true
- redirect_stderr=true
- stdout_logfile=/home/python/tornado.log
- loglevel=info
supervisord -c /etc/supervisord.conf
查看 supervisord 是否在运行:
ps aux | grep supervisord
我们可以利用supervisorctl来管理supervisor。
- supervisorctl
-
- > status # 查看程序状态
- > stop tornadoes:* # 关闭 tornadoes组 程序
- > start tornadoes:* # 启动 tornadoes组 程序
- > restart tornadoes:* # 重启 tornadoes组 程序
- > update # 重启配置文件修改过的程序
执行status命令时,显示如下信息说明tornado程序运行正常:
- supervisor> status
- tornadoes:tornado-8000 RUNNING pid 32091, uptime 00:00:02
- tornadoes:tornado-8001 RUNNING pid 32092, uptime 00:00:02
- tornadoes:tornado-8002 RUNNING pid 32093, uptime 00:00:02
- tornadoes:tornado-8003 RUNNING pid 32094, uptime 00:00:02
对于使用ubuntu apt-get 安装nginx,其配置文件位于/etc/nginx/sites-available中,修改default文件如下:
- upstream tornadoes {
- server 127.0.0.1:8000;
- server 127.0.0.1:8001;
- server 127.0.0.1:8002;
- server 127.0.0.1:8003;
- }
-
- upstream websocket {
- server 127.0.0.1:8000;
- }
-
- server {
- listen 80 default_server;
- listen [::]:80 default_server;
- server_name _;
- location /static/ {
- root /home/python/Documents/demo/chat;
- if ($query_string) {
- expires max;
- }
- }
-
- location /chat {
- proxy_pass http://websocket/chat;
- proxy_http_version 1.1;
- proxy_set_header Upgrade $http_upgrade;
- proxy_set_header Connection "upgrade";
- }
-
- location / {
- proxy_pass_header Server;
- proxy_set_header Host $http_host;
- proxy_redirect off;
- proxy_set_header X-Real-IP $remote_addr;
- proxy_set_header X-Scheme $scheme; # 协议 http https
- proxy_pass http://tornadoes;
- }
- }
启动nginx
- service nginx start # 启动
- service nginx stop # 停止
- service nginx restart # 重启
源码安装版本
- 启动:sudo sbin/nginx
- 停止:sudo sbin/nginx -s stop
- 重启:sudo sbin/nginx -s reload
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。