Tornado
Tornado是使用Python编写的一个强大的、可扩展的Web服务器。它在处理严峻的网络流量时表现得足够强健,但却在创建和编写时有着足够的轻量级,并能够被用在大量的应用和工具中。
我们现在所知道的Tornado是基于Bret Taylor和其他人员为FriendFeed所开发的网络服务框架,当FriendFeed被Facebook收购后得以开源。不同于那些最多只能达到10,000个并发连接的传统网络服务器,Tornado在设计之初就考虑到了性能因素,旨在解决C10K问题,这样的设计使得其成为一个拥有非常高性能的框架。此外,它还拥有处理安全性、用户验证、社交网络以及与外部服务(如数据库和网站API)进行异步交互的工具。
C10K问题 基于线程的服务器,如Apache,为了传入的连接,维护了一个操作系统的线程池。Apache会为每个HTTP连接分配线程池中的一个线程,如果所有的线程都处于被占用的状态并且尚有内存可用时,则生成一个新的线程。尽管不同的操作系统会有不同的设置,大多数Linux发布版中都是默认线程堆大小为8MB。Apache的架构在大负载下变得不可预测,为每个打开的连接维护一个大的线程池等待数据极易迅速耗光服务器的内存资源。 大多数社交网络应用都会展示实时更新来提醒新消息、状态变化以及用户通知,这就要求客户端需要保持一个打开的连接来等待服务器端的任何响应。这些长连接或推送请求使得Apache的最大线程池迅速饱和。一旦线程池的资源耗尽,服务器将不能再响应新的请求。 异步服务器在这一场景中的应用相对较新,但他们正是被设计用来减轻基于线程的服务器的限制的。当负载增加时,诸如Node.js,lighttpd和Tornodo这样的服务器使用协作的多任务的方式进行优雅的扩展。也就是说,如果当前请求正在等待来自其他资源的数据(比如数据库查询或HTTP请求)时,一个异步服务器可以明确地控制以挂起请求。异步服务器用来恢复暂停的操作的一个常见模式是当合适的数据准备好时调用回调函数。
快速入手
1、安装Tornado
pip install tornado 源码安装:https://pypi.python.org/packages/source/t/tornado/tornado-4.3.tar.gz
2、第一个Tornado程序
#!/usr/bin/env python #-*- coding:utf-8 -*- __author__ = ‘luotianshuai‘ import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def get(self): self.write("Hello, world") application = tornado.web.Application([ (r"/index", MainHandler), ]) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()
基本上所有的WEB框架都有以下的流程(以Tornado为例):
准备阶段
加载配置文件
加载路由映射 application = tornado.web.Application([(r"/index", MainHandler),])
创建socket sk = socket
循环阶段
类似socket Server不断的循环监听文件句柄,当有请求过来的时候,根据用户的请求方法来来判断是什么请求,在通过反射来执行相应的函数或类
运行流程:
第一步:执行脚本,监听 8888 端口 第二步:浏览器客户端访问 /index --> http://127.0.0.1:8888/index 第三步:服务器接受请求,并交由对应的类处理该请求 第四步:类接受到请求之后,根据请求方式(post / get / delete ...)的不同调用并执行相应的方法 第五步:方法返回值的字符串内容发送浏览器
3、application
application = tornado.web.Application([ (r"/index", MainHandler), ])
内部在执行的时候执行了两个方法__init__方法和self.add_handlers(".*$", handlers)方法{源码后期解析Tornado时补充}
这个add_handlers默认传输的".*$" 就是www,他内部生成的路由映射的时候相当于(二级域名的方式)下图:
我们可以通过application.add_handlers,添加一个“shuaige.com”,他会生成一个类似下面的对应关系
shuaige
.*
如果匹配的是shuaige他会去"shuaige"里去找对应关系,如果没有匹配默认就去.*,他这个就类似Django中的URL分类!~~
application = tornado.web.Application([ (r"/index", MainHandler), ]) application.add_handlers("shuaige.com",([ (r"/index", MainHandler), ]) )
路由系统其实就是 url 和 类 的对应关系,这里不同于其他框架,其他很多框架均是 url 对应 函数,Tornado中每个url对应的是一个类。
#!/usr/bin/env python #-*- coding:utf-8 -*- __author__ = ‘luotianshuai‘ import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def get(self): self.write("Hello, world") class Shuaige(tornado.web.RedirectHandler): def get(self): self.write("This is shuaige web site,hello!") application = tornado.web.Application([ (r"/index", MainHandler), ]) application.add_handlers("shuaige.com",([ (r"/index", Shuaige), ]) ) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()
模板
Tornao中的模板语言和django中类似,模板引擎将模板文件载入内存,然后将数据嵌入其中,最终获取到一个完整的字符串,再将字符串返回给请求者。
Tornado 的模板支持“控制语句”和“表达语句”,控制语句是使用 {%
和 %}
包起来的 例如 {% if len(items) > 2 %}
。表达语句是使用 {{
和 }}
包起来的,例如 {{ items[0] }}
。
控制语句和对应的 Python 语句的格式基本完全相同。我们支持 if
、for
、while
和 try
,这些语句逻辑结束的位置需要用 {% end %}
做标记。还通过 extends
和 block
语句实现了模板继承。这些在 template
模块 的代码文档中有着详细的描述。
目录结构:
Tornado主文件:
#!/usr/bin/env python # -*- coding:utf-8 -*- import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def get(self): self.render(‘index.html‘) settings = { ‘template_path‘: ‘template‘, ‘static_path‘: ‘static‘, ‘static_url_prefix‘: ‘/static/‘, } application = tornado.web.Application([ (r"/index", MainHandler), ], **settings) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()
主模板html(layout.html)
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>shuaige</title> {% block css%} {% end %} </head> <body> <div><h1>TEST</h1></div> {% block htmlbody %}{% end %} <script src="{{static_url(‘js/jquery-1.8.2.min.js‘)}}"></script> {% block JavaScript %}{% end %} </body> </html>
子版(index.html)
{% extends ‘layout.html‘ %} {% block css %} <link href="{{static_url(‘css/index.css‘)}}" rel="stylesheet" /> {% end %} {% block htmlbody %} <h1 id="shuaige" class="tim">没有取值,就先这样吧,循环就先不写了.</h1> {% end %} {% block JavaScript %} {% end %}
for循环使用如下:
{% extends ‘layout.html‘%} {% block CSS %} <link href="{{static_url("css/index.css")}}" rel="stylesheet" /> {% end %} {% block RenderBody %} <h1>Index</h1> <ul> {% for item in li %} <li>{{item}}</li> {% end %} </ul> {% end %} {% block JavaScript %} {% end %}
在模板中默认提供了一些函数、字段、类以供模板使用:
escape
:tornado.escape.xhtml_escape
的別名xhtml_escape
:tornado.escape.xhtml_escape
的別名url_escape
:tornado.escape.url_escape
的別名json_encode
:tornado.escape.json_encode
的別名squeeze
:tornado.escape.squeeze
的別名linkify
:tornado.escape.linkify
的別名datetime
: Python 的datetime
模组handler
: 当前的RequestHandler
对象request
:handler.request
的別名current_user
:handler.current_user
的別名locale
:handler.locale
的別名_
:handler.locale.translate
的別名static_url
: forhandler.static_url
的別名xsrf_form_html
:handler.xsrf_form_html
的別名
2、自定义模板
Tornado默认提供的这些功能其实本质上就是 UIMethod 和 UIModule,我们也可以自定义从而实现类似于Django的simple_tag的功能:
1、定义
def tab(self): return ‘UIMethod‘ #文件名 uimethods.py
#!/usr/bin/env python # -*- coding:utf-8 -*- from tornado.web import UIModule from tornado import escape class custom(UIModule): def render(self, *args, **kwargs): return escape.xhtml_escape(‘<h1>wupeiqi</h1>‘) #return escape.xhtml_escape(‘<h1>wupeiqi</h1>‘) #文件名 uimodules.py
2、注册
#!/usr/bin/env python # -*- coding:utf-8 -*- #!/usr/bin/env python # -*- coding:utf-8 -*- import tornado.ioloop import tornado.web from tornado.escape import linkify import uimodules as md import uimethods as mt class MainHandler(tornado.web.RequestHandler): def get(self): self.render(‘index.html‘) settings = { ‘template_path‘: ‘template‘, ‘static_path‘: ‘static‘, ‘static_url_prefix‘: ‘/static/‘, ‘ui_methods‘: mt, ‘ui_modules‘: md, } application = tornado.web.Application([ (r"/index", MainHandler), ], **settings) if __name__ == "__main__": application.listen(8009) tornado.ioloop.IOLoop.instance().start() main.py
实用功能
1、静态文件
#!/usr/bin/env python # -*- coding:utf-8 -*- import tornado.ioloop import tornado.web class MainHandler(tornado.web.RequestHandler): def get(self): self.render(‘index.html‘) settings = { ‘template_path‘: ‘template‘, ‘static_path‘: ‘static‘, ‘static_url_prefix‘: ‘/static/‘, } application = tornado.web.Application([ (r"/index", MainHandler), ], **settings) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()
在html中引用的时候
<link href="{{static_url(‘css/index.css‘)}}" rel="stylesheet" />
目录结构:
静态文件缓存:
<link href="{{static_url(‘css/index.css‘)}}" rel="stylesheet" /> #这里static_url 为什么不写路径呢? ‘‘‘ 在Django中,我们写成变量形式的主要是为了以后扩展方便。但是在Tornado中不仅有这个功能还有一个缓存的功能 ‘‘‘
原理:
拿一个静态文件来说:/static/commons.js如果用Tornado封装后,类似于给他加了一个版本号/static/commons.js?v=sldkfjsldf123
当客户端访问过来的时候会携带这个值,如果发现没变客户端缓存的静态文件直接渲染就行了,不必再在服务器上下载一下静态文件了。
Tornado静态文件实现缓存源代码:
def get_content_version(cls, abspath): """Returns a version string for the resource at the given path. This class method may be overridden by subclasses. The default implementation is a hash of the file‘s contents. .. versionadded:: 3.1 """ data = cls.get_content(abspath) hasher = hashlib.md5() if isinstance(data, bytes): hasher.update(data) else: for chunk in data: hasher.update(chunk) return hasher.hexdigest()
2、CSRF
Tornado中的夸张请求伪造和Django中的相似,跨站伪造请求(Cross-site request forgery)
首先在index.py中启用
settings = { "xsrf_cookies": True, } application = tornado.web.Application([ (r"/", MainHandler), (r"/login", LoginHandler), ], **settings)
在form表单提交的时候加上
<form action="/new_message" method="post"> {{ xsrf_form_html() }} <input type="text" name="message"/> <input type="submit" value="Post"/> </form>
Ajax在使用的时候如下
function getCookie(name) { var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); return r ? r[1] : undefined; } jQuery.postJSON = function(url, args, callback) { args._xsrf = getCookie("_xsrf"); $.ajax({url: url, data: $.param(args), dataType: "text", type: "POST", success: function(response) { callback(eval("(" + response + ")")); }}); };
注:Ajax使用时,本质上就是去获取本地的cookie,携带cookie再来发送请求
自定义Session
对Session来说Tornado是没有的,Cookie和Session的关系可以参考我之前写的博客:http://www.cnblogs.com/luotianshuai/p/5278175.html
简单回顾下:
1、自动生成一段字符串
2、将字符串发送到客户端的浏览器,同时把字符串当做key放在session里。(可以理解为session就是一个字典)
3、在用户的session对应的value里设置任意值
操作session
- 获取session:request.session[key]
- 设置session:reqeust.session[key] = value
- 删除session:del request[key]
request.session.set_expiry(value) * 如果value是个整数,session会在些秒数后失效。 * 如果value是个datatime或timedelta,session就会在这个时间后失效。 * 如果value是0,用户关闭浏览器session就会失效。 * 如果value是None,session会依赖全局session失效策略。
2、上面是Django的原理是一样的,那么我们来自己写一个Session
2.1、储备知识点
#!/usr/bin/env python # -*- coding:utf-8 -*- class Foo(object): def __getitem__(self, key): print ‘__getitem__‘,key def __setitem__(self, key, value): print ‘__setitem__‘,key,value def __delitem__(self, key): print ‘__delitem__‘,key
2.2、代码
#!/usr/bin/env python # -*- coding:utf-8 -*- import tornado.ioloop import tornado.web from hashlib import sha1 import os, time session_container = {} create_session_id = lambda: sha1(‘%s%s‘ % (os.urandom(16), time.time())).hexdigest() class Session(object): session_id = "__sessionId__" def __init__(self, request): #当你请求过来的时候,我先去get_cookie看看有没有cookie!目的是看看有没有Cookie如果有的话就不生成了,没有就生成! session_value = request.get_cookie(Session.session_id) #如果没有Cookie生成Cookie[创建随机字符串] if not session_value: self._id = create_session_id() else: #如果有直接将客户端的随机字符串设置给_id这个字段,随机字符串封装到self._id里了 self._id = session_value #在给它设置一下 request.set_cookie(Session.session_id, self._id) def __getitem__(self, key): ret = None try: ret = session_container[self._id][key] except Exception,e: pass return ret def __setitem__(self, key, value): #判断是否有这个随机字符串 if session_container.has_key(self._id): session_container[self._id][key] = value else: #如果没有就生成一个字典 ‘‘‘ 类似:随机字符串:{‘IS_LOGIN‘:‘True‘} ‘‘‘ session_container[self._id] = {key: value} def __delitem__(self, key): del session_container[self._id][key] class BaseHandler(tornado.web.RequestHandler): def initialize(self): ‘‘‘ 这里initialize的self是谁? obj = LoginHandler() obj.initialize() ==>这里LoginHandler这个类里没有initialize这个方法,在他父类里有 所以initialize得self就是LoginHandler的对象 ‘‘‘ self.my_session = Session(self) #执行Session的构造方法并且把LoginHandler的对象传过去 ‘‘‘ 这个self.my_session = Session() 看这个例子: self.xxx = ‘123‘ 在这里创建一个对象,在LoginHandler中是否可以通过self.xxx去访问123这个值? 可以,因为self.xxx 是在get之前执行的,他们俩的对象都是LoginHandler对象 ‘‘‘ class MainHandler(BaseHandler): def get(self): ret = self.my_session[‘is_login‘] if ret: self.write(‘index‘) else: self.redirect("/login") class LoginHandler(BaseHandler): def get(self): ‘‘‘ 当用户访登录的时候我们就得给他写cookie了,但是这里没有写在哪里写了呢? 在哪里呢?之前写的Handler都是继承的RequestHandler,这次继承的是BaseHandler是自己写的Handler 继承自己的类,在类了加扩展initialize! 在这里我们可以在这里做获取用户cookie或者写cookie都可以在这里做 ‘‘‘ ‘‘‘ 我们知道LoginHandler对象就是self,我们可不可以self.set_cookie()可不可以self.get_cookie() ‘‘‘ # self.set_cookie() # self.get_cookie() self.render(‘login.html‘, **{‘status‘: ‘‘}) def post(self, *args, **kwargs): #获取用户提交的用户名和密码 username = self.get_argument(‘username‘) password = self.get_argument(‘pwd‘) if username == ‘wupeiqi‘ and password == ‘123‘: #如果认证通过之后就可以访问这个self.my_session对象了!然后我就就可以吧Cookie写入到字典中了,NICE self.my_session[‘is_login‘] = ‘true‘ ‘‘‘ 这里用到知识点是类里的: class Foo(object): def __getitem__(self,key): print ‘__getitem__‘,key def __setitem__(self,key,value): print ‘__setitem__‘,key,value def __delitem__(self,key): print ‘__delitem__‘,key obj = Foo() result = obj[‘k1‘] #自动触发执行 __getitem__ obj[‘k2‘] = ‘wupeiqi‘ #自动触发执行 __setitem__ del obj[‘k1‘] #自动触发执行 __delitme__ ‘‘‘ self.redirect(‘/index‘) else: self.render(‘login.html‘, **{‘status‘: ‘用户名或密码错误‘}) settings = { ‘template_path‘: ‘template‘, ‘static_path‘: ‘static‘, ‘static_url_prefix‘: ‘/static/‘, ‘cookie_secret‘: ‘aiuasdhflashjdfoiuashdfiuh‘, ‘login_url‘: ‘/login‘ } application = tornado.web.Application([ #创建两个URL 分别对应 MainHandler LoginHandler (r"/index", MainHandler), (r"/login", LoginHandler), ], **settings) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()
分布式Session框架
上面通过Tornado预留扩展和Class中使用的知识实现了session框架,那么来看下如何实现分布式Session框架!
首先来看下,上面面的这个session,默认是保存在:session_container = {} 这个可不可使是Redis,memcache,mysql?如果们把session都存储到这台机器上不管以后去增加、删除、查询的时候都得去这台机器上去拿
一旦这台数据挂掉,或者数据量大的时候这是这台机器就满足不了了!
这是我们就需要考虑分布式了!比如有3台机器,然后一部分放在A机器上、一部分放在B机器上、一部分放在C机器上:如下图:
当用户访问过来的时候通过权重和hash计算把session加入到hash环中的服务上,实现分不到不同的主机上存储,当第二次user携带Cookie过来的时候通过计算找到Session所在的服务器取出并认证!
#!/usr/bin/env python #coding:utf-8 import sys import math from bisect import bisect if sys.version_info >= (2, 5): import hashlib md5_constructor = hashlib.md5 else: import md5 md5_constructor = md5.new class HashRing(object): """一致性哈希""" def __init__(self,nodes): ‘‘‘初始化 nodes : 初始化的节点,其中包含节点已经节点对应的权重 默认每一个节点有32个虚拟节点 对于权重,通过多创建虚拟节点来实现 如:nodes = [ {‘host‘:‘127.0.0.1:8000‘,‘weight‘:1}, {‘host‘:‘127.0.0.1:8001‘,‘weight‘:2}, {‘host‘:‘127.0.0.1:8002‘,‘weight‘:1}, ] ‘‘‘ self.ring = dict() self._sorted_keys = [] self.total_weight = 0 self.__generate_circle(nodes) def __generate_circle(self,nodes): for node_info in nodes: self.total_weight += node_info.get(‘weight‘,1) for node_info in nodes: weight = node_info.get(‘weight‘,1) node = node_info.get(‘host‘,None) virtual_node_count = math.floor((32*len(nodes)*weight) / self.total_weight) for i in xrange(0,int(virtual_node_count)): key = self.gen_key_thirty_two( ‘%s-%s‘ % (node, i) ) if self._sorted_keys.__contains__(key): raise Exception(‘该节点已经存在.‘) self.ring[key] = node self._sorted_keys.append(key) def add_node(self,node): ‘‘‘ 新建节点 node : 要添加的节点,格式为:{‘host‘:‘127.0.0.1:8002‘,‘weight‘:1},其中第一个元素表示节点,第二个元素表示该节点的权重。 ‘‘‘ node = node.get(‘host‘,None) if not node: raise Exception(‘节点的地址不能为空.‘) weight = node.get(‘weight‘,1) self.total_weight += weight nodes_count = len(self._sorted_keys) + 1 virtual_node_count = math.floor((32 * nodes_count * weight) / self.total_weight) for i in xrange(0,int(virtual_node_count)): key = self.gen_key_thirty_two( ‘%s-%s‘ % (node, i) ) if self._sorted_keys.__contains__(key): raise Exception(‘该节点已经存在.‘) self.ring[key] = node self._sorted_keys.append(key) def remove_node(self,node): ‘‘‘ 移除节点 node : 要移除的节点 ‘127.0.0.1:8000‘ ‘‘‘ for key,value in self.ring.items(): if value == node: del self.ring[key] self._sorted_keys.remove(key) def get_node(self,string_key): ‘‘‘获取 string_key 所在的节点‘‘‘ pos = self.get_node_pos(string_key) if pos is None: return None return self.ring[ self._sorted_keys[pos]].split(‘:‘) def get_node_pos(self,string_key): ‘‘‘获取 string_key 所在的节点的索引‘‘‘ if not self.ring: return None key = self.gen_key_thirty_two(string_key) nodes = self._sorted_keys pos = bisect(nodes, key) return pos def gen_key_thirty_two(self, key): m = md5_constructor() m.update(key) return long(m.hexdigest(), 16) def gen_key_sixteen(self,key): b_key = self.__hash_digest(key) return self.__hash_val(b_key, lambda x: x) def __hash_val(self, b_key, entry_fn): return (( b_key[entry_fn(3)] << 24)|(b_key[entry_fn(2)] << 16)|(b_key[entry_fn(1)] << 8)| b_key[entry_fn(0)] ) def __hash_digest(self, key): m = md5_constructor() m.update(key) return map(ord, m.digest()) """ nodes = [ {‘host‘:‘127.0.0.1:8000‘,‘weight‘:1}, {‘host‘:‘127.0.0.1:8001‘,‘weight‘:2}, {‘host‘:‘127.0.0.1:8002‘,‘weight‘:1}, ] ring = HashRing(nodes) result = ring.get_node(‘98708798709870987098709879087‘) print result """
更多参考:http://www.cnblogs.com/wupeiqi/articles/5341480.html