为tornado自定义session
cookie和session
在自定義session前,我們需要先了解cookie和session是什么,可以參考我之前的博客:http://blog.csdn.net/ayhan_huang/article/details/78032097
簡單來說:
- cookie是保存在瀏覽器的鍵值對
- session是保存在服務端的鍵值對
- session依賴于cookie
在Django框架中,我們可以直接操作cookie和session,但是tornado只支持cookie,那如果要使用session怎么辦呢?自己定義session
思路和實現(xiàn)
我們知道,在tornado中,所有的請求都是由RequestHandler對象來處理(以下簡稱handler對象)。在RequestHandler源碼中,預留了一個鉤子方法initialize,該方法會在實例化Handler對象時執(zhí)行。因此,如果我們繼承RequestHandler類并重寫initialize,就可以完成一些自定義操作。
代碼實現(xiàn):
import tornado.ioloop import tornado.web from hashlib import sha1 import os import time# 隨機生成session_id create_session_id = lambda: sha1(bytes('%s%s' % (os.urandom(16), time.time()), encoding='utf-8')).hexdigest()class Session:"""自定義session"""info_container = {# session_id: {'user': info} --> 通過session保存用戶信息,權限等}def __init__(self, handler):"""初始化時傳入RequestHandler對象,通過它進行cookie操作self.handler.set_cookie()self.handler.get_cookie():param handler: """self.handler = handler# 從 cookie 中獲取作為 session_id 的隨機字符串,如果沒有或不匹配則生成 session_idrandom_str = self.handler.get_cookie('session_id')if (not random_str) or (random_str not in self.info_container):random_str = create_session_id()self.info_container[random_str] = {}self.random_str = random_str# 每次請求進來都會執(zhí)行set_cookie,保證每次重置過期時間為當前時間以后xx秒以后self.handler.set_cookie('session_id', random_str, max_age=60)def __getitem__(self, item):return self.info_container[self.random_str].get(item)def __setitem__(self, key, value):self.info_container[self.random_str][key] = valuedef __delitem__(self, key):if self.info_container[self.random_str].get(key):del self.info_container[self.random_str][key]def delete(self):"""從大字典刪除session_id"""del self.info_container[self.random_str]class SessionHandler:def initialize(self):self.session = Session(self) # handler增加session屬性class LoginHandler(SessionHandler, tornado.web.RequestHandler):def get(self):self.render('login.html')def post(self):user = self.get_argument('user')pwd = self.get_argument('pwd')if user == 'lena' and pwd == '123':self.session['user'] = userself.redirect('/index')class IndexHandler(SessionHandler, tornado.web.RequestHandler):def get(self):user = self.session['user'] # 注意這里不能用session.get('user')if not user:self.redirect('/login')returnself.write('你好啊,%s'% user) # 返回響應settings = {'template_path': 'templates','cookie_secret': 'asdfasdfasd', # 簽名cookie加鹽 }application = tornado.web.Application([(r'/login', LoginHandler),(r'/index', IndexHandler), ], **settings)if __name__ == '__main__':application.listen(8080)tornado.ioloop.IOLoop.instance().start()說明:
- 定義一個Session類,其實例化時接收handler對象
- 在Session類中定義一個靜態(tài)字段(大字典),用來存儲session_id和對應的用戶信息;所有的session對象都可以訪問這個大字典。
- 在Session的構造方法中,獲取和設置cookie:
- 調用handler對象get_cookie()方法獲取session_id,如果沒有,則生成一段隨機字符串random_str作為session_id
- 將session_id寫入大字典
- 調用handler對象的set_cookie()方法,通知瀏覽器設置cookie:set-cookie: {session_id: random_str}
- 在Session類中,定義__getitem__, __setitem__, __delitem__方法來實現(xiàn)通過字典的方式操作session對象(面向對象內置方法參考這里)
- 在initialize方法中為handler對象增加session屬性,其值是Session對象:self.session=Session(self);在每個路由對應的視圖中都重寫initialize方法太麻煩了,利用面向對象的多繼承,將這一步單獨寫在一個類SessionHandler,所以視圖類先繼承這個類即可。
- 每次請求進來,都會執(zhí)行SessionHandler中的initialize方法,并實例化Session對象,從而獲取session_id
- 操作session:
- 通過self.session[key] = value 即可調用session對象的__setitem__方法來寫session;
- 通過self.session[key] 即可調用session對象的__getitem__方法來獲取session
- 通過del self.session[key] 即可調用session對象的__delitem__方法來刪除session
- 通過self.session.delete(),即可調用session對象的delete方法,刪除整個session_id
一致性哈希和分布式session
將session保存在redis緩存服務器中,可以獲得更高的性能。如果有多臺緩存服務器,就需要對服務器作負載均衡,將session分發(fā)到每臺服務器上。實現(xiàn)負載均衡的算法有很多,最常用的是哈希算法,它的基本邏輯是,對session_id(隨機字符串)進行哈希,哈希結果再按服務器數(shù)量進行取模運算,得到的余數(shù)i就是第i個服務器。
一致性哈希(Consistent Hashing)是分布式負載均衡的首選算法。python中有實現(xiàn)模塊hash_ring,不需要安裝,直接將其中的單文件hash_ring.py拿來用即可,也就100多行代碼。一致性哈希除了可以用在這里,也可以用于作分布式爬蟲。
hash_ring.py
# -*- coding: utf-8 -*- """hash_ring~~~~~~~~~~~~~~Implements consistent hashing that can be used whenthe number of server nodes can increase or decrease (like in memcached).Consistent hashing is a scheme that provides a hash table functionalityin a way that the adding or removing of one slotdoes not significantly change the mapping of keys to slots.More information about consistent hashing can be read in these articles:"Web Caching with Consistent Hashing":http://www8.org/w8-papers/2a-webserver/caching/paper2.html"Consistent hashing and random trees:Distributed caching protocols for relieving hot spots on the World Wide Web (1997)":http://citeseerx.ist.psu.edu/legacymapper?did=38148Example of usage::memcache_servers = ['192.168.0.246:11212','192.168.0.247:11212','192.168.0.249:11212']ring = HashRing(memcache_servers)server = ring.get_node('my_key'):copyright: 2008 by Amir Salihefendic.:license: BSD """import math import sys from bisect import bisectif sys.version_info >= (2, 5):import hashlibmd5_constructor = hashlib.md5 else:import md5md5_constructor = md5.newclass HashRing(object):def __init__(self, nodes=None, weights=None):"""`nodes` is a list of objects that have a proper __str__ representation.`weights` is dictionary that sets weights to the nodes. The defaultweight is that all nodes are equal."""self.ring = dict()self._sorted_keys = []self.nodes = nodesif not weights:weights = {}self.weights = weightsself._generate_circle()def _generate_circle(self):"""Generates the circle."""total_weight = 0for node in self.nodes:total_weight += self.weights.get(node, 1)for node in self.nodes:weight = 1if node in self.weights:weight = self.weights.get(node)factor = math.floor((40*len(self.nodes)*weight) / total_weight)for j in range(0, int(factor)):b_key = self._hash_digest( '%s-%s' % (node, j) )for i in range(0, 3):key = self._hash_val(b_key, lambda x: x+i*4)self.ring[key] = nodeself._sorted_keys.append(key)self._sorted_keys.sort()def get_node(self, string_key):"""Given a string key a corresponding node in the hash ring is returned.If the hash ring is empty, `None` is returned."""pos = self.get_node_pos(string_key)if pos is None:return Nonereturn self.ring[ self._sorted_keys[pos] ]def get_node_pos(self, string_key):"""Given a string key a corresponding node in the hash ring is returnedalong with it's position in the ring.If the hash ring is empty, (`None`, `None`) is returned."""if not self.ring:return Nonekey = self.gen_key(string_key)nodes = self._sorted_keyspos = bisect(nodes, key)if pos == len(nodes):return 0else:return posdef iterate_nodes(self, string_key, distinct=True):"""Given a string key it returns the nodes as a generator that can hold the key.The generator iterates one time through the ringstarting at the correct position.if `distinct` is set, then the nodes returned will be unique,i.e. no virtual copies will be returned."""if not self.ring:yield None, Nonereturned_values = set()def distinct_filter(value):if str(value) not in returned_values:returned_values.add(str(value))return valuepos = self.get_node_pos(string_key)for key in self._sorted_keys[pos:]:val = distinct_filter(self.ring[key])if val:yield valfor i, key in enumerate(self._sorted_keys):if i < pos:val = distinct_filter(self.ring[key])if val:yield valdef gen_key(self, key):"""Given a string key it returns a long value,this long value represents a place on the hash ring.md5 is currently used because it mixes well."""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.encode('utf-8'))# return map(ord, m.digest()) # python 2 return list(m.digest()) # pyhton 3注意,在python3中,list方法可以直接將字符轉為ASC碼,但是在python 2中需要利用內置函數(shù)ord來實現(xiàn)。
>>> import hashlib >>> m = hashlib.md5(b'hello world') >>> list(m.digest()) [94, 182, 59, 187, 224, 30, 238, 208, 147, 203, 34, 187, 143, 90, 205, 195]將自定義session改為分布式
實例化hash_ring對象,改寫session中的幾個內置方法,其它不變:
import tornado.ioloop import tornado.web from hashlib import sha1 import os import time import redis from hash_ring import HashRing# 緩存服務器列表 cache_servers = ['192.168.0.246:11212','192.168.0.247:11212','192.168.0.249:11212' ]# 配置權重 weights = {'192.168.0.246:11212': 2,'192.168.0.247:11212': 2,'192.168.0.249:11212': 1 }ring = HashRing(cache_servers, weights) # 實例化HashRing對象# 隨機生成session_id create_session_id = lambda: sha1(bytes('%s%s' % (os.urandom(16), time.time()), encoding='utf-8')).hexdigest()class Session:"""自定義session"""info_container = {# session_id: {'user': info} --> 通過session保存用戶信息,權限等}def __init__(self, handler):"""初始化時傳入RequestHandler對象,通過它進行cookie操作self.handler.set_cookie()self.handler.get_cookie():param handler: """self.handler = handler# 從 cookie 中獲取作為 session_id 的隨機字符串,如果沒有或不匹配則生成 session_idrandom_str = self.handler.get_cookie('session_id')if (not random_str) or (random_str not in self.info_container):random_str = create_session_id()self.info_container[random_str] = {}self.random_str = random_str# 每次請求進來都會執(zhí)行set_cookie,保證每次重置過期時間為當前時間以后xx秒以后self.handler.set_cookie('session_id', random_str, max_age=60)def __getitem__(self, item):# get_node()根據(jù)隨機字符串哈希取模的結果,來選取服務器;再通過split方式提取服務器hotst和porthost, port = ring.get_node(self.random_str).split(':')conn = redis.Redis(host=host, port=port)return conn.hget(self.random_str, item)def __setitem__(self, key, value):host, port = ring.get_node(self.random_str).split(':')conn = redis.Redis(host=host, port=port)conn.hset(self.random_str, key, value)def __delitem__(self, key):host, port = ring.get_node(self.random_str).split(':')conn = redis.Redis(host=host, port=port)conn.hdel(self.random_str, key)def delete(self):"""從大字典刪除session_id"""del self.info_container[self.random_str]總結
以上是生活随笔為你收集整理的为tornado自定义session的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python3之对象垃圾收集机制浅析
- 下一篇: I/O线程