学习Tornado,主要就是整理查询出别人整理的资料,感谢大佬们的分享~~
简介
Tornado全称Tornado Web Server,是一个用Python语言写成的Web服务器兼Web应用框架,由FriendFeed公司在自己的网站FriendFeed中使用,被Facebook收购以后框架在2009年9月以开源软件形式开放给大众。
1. 特点:
作为Web框架,是一个轻量级的Web框架,其拥有异步非阻塞IO的处理方式。作为Web服务器,Tornado有较为出色的抗负载能力,官方用nginx反向代理的方式部署Tornado和其它Python web应用框架进行对比,结果最大浏览量超过第二名近40%。
2. 性能:
Tornado有着优异的性能。它试图解决C10k问题,即处理大于或等于一万的并发。
Tornado框架和服务器一起组成一个WSGI的全栈替代品。单独在WSGI容器中使用tornado网络框架或者tornaod http服务器,有一定的局限性,为了最大化的利用tornado的性能,推荐同时使用tornaod的网络框架和HTTP服务器。
3. 应用场景
用户量大,高并发。如秒杀抢购、双十一某宝购物、春节抢火车票
大量的HTTP持久连接。使用同一个TCP连接来发送和接收多个HTTP请求/应答,而不是为每一个新的请求/应答打开新的连接的方法。对于HTTP 1.0,可以在请求的包头(Header)中添加Connection: Keep-Alive。对于HTTP 1.1,所有的连接默认都是持久连接。
4. Tornado与Django
Django
Django是走大而全的方向,注重的是高效开发,它最出名的是其全自动化的管理后台:只需要使用起ORM,做简单的对象定义,它就能自动生成数据库结构、以及全功能的管理后台。
Django提供的方便,也意味着Django内置的ORM跟框架内的其他模块耦合程度高,应用程序必须使用Django内置的ORM,否则就不能享受到框架内提供的种种基于其ORM的便利。
session功能
后台管理
ORM
Tornado
Tornado走的是少而精的方向,注重的是性能优越,它最出名的是异步非阻塞的设计方式。
HTTP服务器
异步编程
WebSockets
5. 简单的Tornado案例
```
import tornado.web
```
import tornado.ioloop
class IndexHandler(tornado.web.RequestHandler):
"""主路由处理类"""
def get(self):
"""对应http的get请求方式"""
self.write("Hello Python!")
if __name__ == "__main__":
app = tornado.web.Application([
(r"/", IndexHandler),
])
app.listen(8000)
tornado.ioloop.IOLoop.current().start()
以上代码解释说明:
tornado的基础web框架模块
RequestHandler:封装了对应一个请求的所有信息和方法,write(响应信息)就是写响应信息的一个方法;对应每一种http请求方式(get、post等),把对应的处理逻辑写进同名的成员方法中(如对应get请求方式,就将对应的处理逻辑写在get()方法中),当没有对应请求方式的成员方法时,会返回“405: Method Not Allowed”错误。
Application:Tornado Web框架的核心应用类,是与服务器对接的接口,里面保存了路由信息表,其初始化接收的第一个参数就是一个路由信息映射元组的列表;其listen(端口)方法用来创建一个http服务器实例,并绑定到给定端口(注意:此时服务器并未开启监听)
Tornado核心IOLoop循环模块:tornado的核心io循环模块,封装了Linux的epoll和BSD的kqueue,tornado高性能的基石。
IOLoop.current()返回当前线程的IOLoop实例。
IOLoop.start()启动IOLoop实例的I/O循环,同时服务器监听被打开。
6. Tornado Web程序编写思路
创建web应用实例对象,第一个初始化参数为路由映射列表。
定义实现路由映射列表中的handler类。
创建服务器实例,绑定服务器端口。
启动当前线程的IOLoop。
7. 引入httpserver模块
修改上篇文章简单tornado案例的代码如下:
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)正是对这一过程的简写。
关于app.listen()这个方法只能在单进程模式中使用。
对于app.listen()与手动创建HTTPServer实例
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(8000)
这两种方式,建议大家先使用后者即创建HTTPServer实例的方式,因为其对于理解tornado web应用工作流程的完整性有帮助,便于大家记忆tornado开发的模块组成和程序结构;在熟练使用后,可以改为简写。
8. 单进程
修改上面httpserver版本代码如下:
import tornado.web
import tornado.ioloop
import tornado.httpserver
class IndexHandler(tornado.web.RequestHandler):
"""主路由处理类"""
def get(self):
"""对应http的get请求方式"""
self.write("Hello Itcast!")
if __name__ == "__main__":
app = tornado.web.Application([
(r"/", IndexHandler),
])
http_server = tornado.httpserver.HTTPServer(app)
# -----------修改----------------
http_server.bind(8000)
http_server.start(0)
# ------------------------------
tornado.ioloop.IOLoop.current().start()
http_server.bind(port)方法是将服务器绑定到指定端口。
http_server.start(num_processes=1)方法指定开启几个进程,参数num_processes默认值为1,即默认仅开启一个进程;如果num_processes为None或者<=0,则自动根据机器硬件的cpu核芯数创建同等数目的子进程;如果num_processes>0,则创建num_processes个子进程。
我们在前面写的http_server.listen(8000)实际上就等同于:
http_server.bind(8000)
http_server.start(1)
9. 多进程
虽然tornado给我们提供了一次开启多个进程的方法,但是由于:
每个子进程都会从父进程中复制一份IOLoop实例,如果在创建子进程前我们的代码动了IOLoop实例,那么会影响到每一个子进程,势必会干扰到子进程IOLoop的工作;
所有进程是由一个命令一次开启的,也就无法做到在不停服务的情况下更新代码;
所有进程共享同一个端口,想要分别单独监控每一个进程就很困难。
不建议使用这种多进程的方式,而是建议手动开启多个进程,并且绑定不同的端口。
10. 引入options模块
在前面的示例中我们都是将服务端口的参数写死在程序中,很不灵活。tornado为我们提供了一个便捷的工具,tornado.options模块——全局参数定义、存储、转换。
name选项变量名,须保证全局唯一性,否则会报“Option 'xxx' already defined in ...”的错误;
default 选项变量的默认值,如不传默认为None;
type选项变量的类型,从命令行或配置文件导入参数的时候tornado会根据这个类型转换输入的值,转换不成功时会报错,可以是str、float、int、datetime、timedelta中的某个,若未设置则根据default的值自动推断,若default也未设置,那么不再进行转换。可以通过利用设置type类型字段来过滤不正确的输入。
multiple选项变量的值是否可以为多个,布尔类型,默认值为False,如果multiple为True,那么设置选项变量时值与值之间用英文逗号分隔,而选项变量则是一个list列表(若默认值和输入均未设置,则为空列表[])。
help选项变量的帮助提示信息,在命令行启动tornado时,通过加入命令行参数--help 可以查看所有选项变量的信息(注意,代码中需要加入tornado.options.parse_command_line())。
关于tornado.options.options:全局的options对象,所有定义的选项变量都会作为该对象的属性。
关于tornado.options.parse_command_line():转换命令行参数,并将转换后的值对应的设置到全局options对象相关属性上。追加命令行参数的方式是--myoption=myvalue
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.") # 定义服务器监听端口选项
class IndexHandler(tornado.web.RequestHandler):
"""主路由处理类"""
def get(self):
"""对应http的get请求方式"""
self.write("Hello Python Tornado!")
if __name__ == "__main__":
tornado.options.parse_command_line()
app = tornado.web.Application([
(r"/", IndexHandler),
])
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(tornado.options.options.port)
tornado.ioloop.IOLoop.current().start()
关于tornado.options.parse_config_file(path):
从配置文件导入option,配置文件中的选项格式如下:
myoption = "myvalue"
myotheroption = "myothervalue"
在使用prase_config_file()的时候,配置文件的书写格式仍需要按照python的语法要求,其优势是可以直接将配置文件的参数转换设置到全局对象tornado.options.options中;然而,其不方便的地方在于需要在代码中调用tornado.options.define()来定义选项,而且不支持字典类型,故而在实际应用中大都不使用这种方法。
在使用配置文件的时候,通常会新建一个python文件(如config.py),然后在里面直接定义python类型的变量(可以是字典类型);在需要配置文件参数的地方,将config.py作为模块导入,并使用其中的变量参数。
关于日志:
当我们在代码中调用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()
11. 引入Application模块
settings:在创建tornado.web.Application的对象时,传入了第一个参数——路由映射列表。实际上Application类的构造函数还接收很多关于tornado web应用的配置参数。
debug:设置tornado是否工作在调试模式,默认为False即工作在生产模式。当设置debug=True后,tornado会工作在调试/开发模式,在此种模式下,tornado为方便我们开发而提供了几种特性:
自动重启,tornado应用会监控我们的源代码文件,当有改动保存后便会重启程序,这可以减少我们手动重启程序的次数。需要注意的是,一旦我们保存的更改有错误,自动重启会导致程序报错而退出,从而需要我们保存修正错误后手动启动程序。这一特性也可单独通过autoreload=True设置;
取消缓存编译的模板,可以单独通过compiled_template_cache=False来设置;
取消缓存静态文件hash值,可以单独通过static_hash_cache=False来设置;
提供追踪信息,当RequestHandler或者其子类抛出一个异常而未被捕获后,会生成一个包含追踪信息的页面,可以单独通过serve_traceback=True来设置。
使用debug参数的方法:
import tornado.web
app = tornado.web.Application([], debug=True)
路由映射:先前我们在构建路由映射列表的时候,使用的是二元元组,如:
[(r"/", IndexHandler),]
对于这个映射列表中的路由,实际上还可以传入多个信息,如:
[
(r"/", Indexhandler),
(r"/python", XiaokeHandler, {"subject":"python"}),
url(r"/python", XiaokeHandler, {"subject":"python"}, name="python_url")
]
对于路由中的字典,会传入到对应的RequestHandler的initialize()方法中:
from tornado.web import RequestHandler
class XiaokeHandler(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。
小案例:
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 XiaokeHandler(RequestHandler):
def get(self):
python_url = self.reverse_url("python_url")
self.write('xiaoke python' %
python_url)
if __name__ == "__main__":
tornado.options.parse_command_line()
app = tornado.web.Application([
url(r"/python", XiaokeHandler, {"subject":"python"}, name="python_url")
],
debug = True)
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(options.port)
tornado.ioloop.IOLoop.current().start()
12. 引入RequestHandler模块
利用HTTP协议向服务器传参几种途径:
查询字符串(query string),形如key1=value1&key2=value2;
请求体(body)中发送的数据,比如表单数据、json、xml;
提取uri的特定部分,如/blogs/2016/09/0001,可以在服务器端的路由中用正则表达式截取;
在http报文的头(header)中增加自定义字段,如X-XSRFToken=xiaoke
获取查询字符串参数:
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同前,不再赘述。
说明:对于请求体中数据的要求同前。 这两个方法最常用。注意:以上方法返回的都是unicode字符串
关于requestHandler.request请求信息:RequestHandler.request对象存储了关于请求的相关信息,具体属性有:
method HTTP的请求方式,如GET或POST;
host被请求的主机名;
uri请求的完整资源标示,包括路径和查询字符串;
path请求的路径部分;
query请求的查询字符串部分;
version使用的HTTP版本;
headers请求的协议头,是类字典型的对象,支持关键字索引的方式获取特定协议头信息,例如:request.headers["Content-Type"]
body请求体数据;
remote_ip客户端的IP地址;
files用户上传的文件,为字典类型,型如:
{
"form_filename1":[
, ], "form_filename2":[
,], ...
}
关于tornado.httputil.HTTPFile:是接收到的文件对象,它有三个属性:
filename文件的实际名字,与form_filename1不同,字典中的键名代表的是表单对应项的名字;
body文件的数据实体;
content_type文件的类型。
这三个对象属性可以像字典一样支持关键字索引,如request.files["form_filename1"][0]["body"]。
关于正则提取uri:
tornado中对于路由映射也支持正则提取uri,提取出来的参数会作为RequestHandler中对应请求方式的成员方法参数。若在正则表达式中定义了名字,则参数按名传递;若未定义名字,则参数按顺序传递。提取出来的参数会作为对应请求方式的成员方法的参数。
关于write方法: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)属性:利用set_header(name, value)方法,可以手动设置一个名为name、值为value的响应头header字段。
例如:
self.set_header("Content-Type", "application/json; charset=UTF-8")
关于set_default_headers()方法:该方法会在进入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")
# 设置一个名为name、值为python的header
self.set_header("name", "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("name", "i love python") # 注意此处重写了header中的name字段,会覆盖掉在set_default_headers()方法中设置的同名header。
关于set_status(status_code, reason=None):为响应设置状态码。
status_code int类型,状态码
reason string类型,描述状态码的词组,若为None,则会被自动填充为标准的状态码。
非标准状态码:self.set_status(210, "python error") #非标准状态码,必须设置了reason。否则报错
关于redirect(url):告知浏览器重定向到url。
关于send_error(status_code=500, **kwargs):抛出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错误"是没有意义的。要想处理这个错误信息,必须重写write_error()方法
使用send_error()方法后就不要再使用self.write(),再向输出缓冲区写内容了!
关于write_error(status_code, **kwargs):用来处理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"
出错了,程序员GG正在赶过来!
")self.write(u"
错误名:%s
" % kwargs["title"])self.write(u"
错误详情:%s
" % kwargs["content"])
关于接口与调用顺序:
class IndexHandler(RequestHandler):
# 对应每个请求的处理类Handler在构造一个实例后首先执行initialize()方法。
#路由映射中的第三个字典型参数会作为该方法的命名参数传递,
#作用:初始化参数(对象属性)
def initialize(self):
print "调用了initialize()"
# 预处理,即在执行对应请求方式的HTTP方法(如get、post等)前先执行,
#注意:不论以何种HTTP方式请求,都会执行prepare()方法。
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) # 注意此出抛出了错误
# 在请求处理结束后调用,即在调用HTTP方法后调用。
#通常该方法用来进行资源清理释放或处理日志等。
#注意:请尽量不要在此方法中进行响应输出。
def on_finish(self):
print "调用了on_finish()"
在正常情况未抛出错误时,调用顺序为:
set_defautl_headers()
initialize()
prepare()
HTTP方法(get或post方法)
on_finish()
在有错误抛出时,调用顺序为:
set_default_headers()
initialize()
prepare()
HTTP方法
set_default_headers()
write_error()
on_finish()