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
2. 第一個tornado程序
#!/usr/bin/env python
#-*- coding:utf-8 -*-
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時補充}
Tornado原生支持二級域名,方法application.add_handlers(二級域名,[url匹配規則])
假如我們的主頁地址是www.tornado.com
, 我們可以通過application.add_handlers
,添加一個“admin.torando.com”
,當我們訪問admin.tornado.com
時,我們就可以訪問到這個www.tornado.com
的二級域名中。
如果匹配的是admin.tornado.com
, 他會去admin.tornado.com
里去找對應關系,如果沒有匹配默認就去.*,他這個就類似Django中的URL分類。
application = tornado.web.Application([
(r"/index", MainHandler),
])
application.add_handlers("admin.tornado.com",([
(r"/index", MainHandler),
])
)
路由系統其實就是 url 和 類 的對應關系,這里不同於其他框架,其他很多框架均是 url 對應 函數,Tornado中每個url對應的是一個類。
#!/usr/bin/env python
#-*- coding:utf-8 -*-
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.write("Hello, world")
class AdminHandler(tornado.web.RedirectHandler):
def get(self):
self.write("This is tornado admin web site,hello!")
application = tornado.web.Application([
(r"/index", MainHandler), # localhost:8888/index
])
application.add_handlers("admin.tornado.com",([
(r"/index", AdminHandler), # admin.tornado.8888:/index
])
)
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
當然,如果你要本地瀏覽器支持配置訪問二級域名,你不可能去專門去買一個域名做實驗,沒有必要。可以通過修改本地電腦的 hosts
文件的配置來模擬這一個二級域名的實驗。
- windows:**C:\Windows\System32\drivers\etc\hosts **
- mac:vim /etc/hosts
文件配置內容:
127.0.0.1 www.tornado.com
127.0.0.1 admin.tornado.com
127.0.0.1 web.tornado.com
通過定制二級域名制作Django式分層路由
上面好像是做到了 url 分開管理,但是還遠遠沒有達到Django
那樣讓人稱心合意。即每個 APP
下有自己的 url
文件來專門管理自己的 url
匹配規則。並且下次增加二級域名時,只要修改配置文件就是了,那么如何處理呢。
Tornado實現二級域名的函數,是要傳入兩個參數,一個是二級域名名稱,一個是url映射規則。
其中url映射規則,也就是一個列表,可以直接放在各個app的url文件,
而二級域名名稱,則放在系統下配置文件里,每個二級域名,對應url文件路徑,和url文件下的列表變量為一個組合存放
url 文件
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from .Controllers import Account
from .Controllers import Home
patterns = [
(r"/Login.html$", Account.LoginHandler),
(r"/CheckCode.html$", Account.CheckCodeHandler),
(r"/Register.html$", Account.RegisterHandler),
(r"/Index.html$", Home.IndexHandler),
(r"/Detail-(?P<product_id>\d+)-(?P<price_id>\d+).html$", Home.DetailHandler),
(r"/Pay.html$", Home.PayHandler),
(r"/", Home.IndexHandler),
]
config配置文件
routes = (
{
'host_pattern': 'www.tornado.com',
'route_path': 'UIWeb.Urls',
'route_name': 'patterns'
},
{
'host_pattern': 'admin.tornado.com',
'route_path': 'UIAdmin.Urls',
'route_name': 'patterns'},
{
'host_pattern': 'dealer.tornado.com',
'route_path': 'UIDealer.Urls',
'route_name': 'patterns'
}
)
做好上面的路由分離后,最后一步就是定一個 url
的加載方法,在程序啟動時,將這些 url
規則加載。
url 加載函數
def load_routes(app):
for route in Config.routes:
host_pattern = route['host_pattern']
route_path = route['route_path']
route_name = route['route_name']
m = __import__(route_path, fromlist=True)
pattern_list = getattr(m, route_name)
app.add_handlers(host_pattern, pattern_list)
啟動程序
application = tornado.web.Application([
#(r"/index", home.IndexHandler),
], **settings)
load_routes(application)
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
附另一個方法
該方法來自於此鏈接,點擊可觀看詳情,下面附上主要代碼
自定義一個ulr處理的.py文件
# coding: utf-8
from __future__ import unicode_literals
from importlib import import_module
def include(module):
res = import_module(module)
urls = getattr(res, 'urls', res)
return urls
def url_wrapper(urls):
wrapper_list = []
for url in urls:
path, handles = url
if isinstance(handles, (tuple, list)):
for handle in handles:
pattern, handle_class = handle
wrap = ('{0}{1}'.format(path, pattern), handle_class)
wrapper_list.append(wrap)
else:
wrapper_list.append((path, handles))
return wrapper_list
app的url.py文件
# coding: utf-8
from __future__ import unicode_literals
from test.views import TestWriteHandle, TestHandle
urls = [
(r'write', TestWriteHandle),
(r'', TestHandle),
]
main.py 啟動文件
# coding: utf-8
from __future__ import unicode_literals
import tornado.httpserver
import tornado.ioloop
import tornado.web
from url_router import include, url_wrapper
application = tornado.web.Application(url_wrapper([
(r"/test/", include('test.urls')), # 主要在此加載,十分像django了
(r"/other", XXXHandle),
]))
if __name__ == "__main__":
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8888)
tornado.ioloop.IOLoop.instance().start()
4. 模板
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(base.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 'base.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 'base.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
的別名
自定義模板函數,字段,類供模板使用
Tornado
默認提供的上面這些功能其實本質上就是 UIMethod
和 UIModule
,我們也可以自定義從而實現類似於Django
的simple_tag
的功能:
1. 定義
# uimethods.py
def tab(self):
return "UIMethod"
# uimodules.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>tornado</h1>")
2. 注冊
#!/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
3. 使用
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<link href="{{static_url("commons.css")}}" rel="stylesheet" />
</head>
<body>
<h1>hello</h1>
{% module custom(123) %}
{{ tab() }}
</body>
index.html
5. 實用功能
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再來發送請求
3. cookie
Tornado 中可以對 cookie 進行操作,並且可以對 cookie進行簽名以防止偽造。
基本操作
class MainHandler(tornado.web.RequestHandler):
def get(self):
if not self.get_cookie("mycookie"):
self.set_cookie("mycookie", "myvalue")
self.write("Your cookie was not set yet!")
else:
self.write("Your cookie was set!")
簽名
Cookie 很容易被惡意的客戶端偽造。加入你想在 cookie 中保存當前登陸用戶的 id 之類的信息,你需要對 cookie 作簽名以防止偽造。Tornado 通過 set_secure_cookie 和 get_secure_cookie 方法直接支持了這種功能。 要使用這些方法,你需要在創建應用時提供一個密鑰,名字為 cookie_secret。 你可以把它作為一個關鍵詞參數傳入應用的設置中:
class MainHandler(tornado.web.RequestHandler):
def get(self):
if not self.get_secure_cookie("mycookie"):
self.set_secure_cookie("mycookie", "myvalue")
self.write("Your cookie was not set yet!")
else:
self.write("Your cookie was set!")
application = tornado.web.Application([
(r"/", MainHandler),
], 'cookie_secret': base64.b64encode(uuid.uuid3(uuid.NAMESPACE_DNS, 'jiayan').bytes))
其內部實現算法,可下載tornado源碼自行查看
簽名Cookie的本質是:
寫cookie過程:
- 將值進行base64加密
- 對除值以外的內容進行簽名,哈希算法(無法逆向解析)
- 拼接 簽名 + 加密值
讀cookie過程:
- 讀取 簽名 + 加密值
- 對簽名進行驗證
- base64解密,獲取值內容
注:許多API驗證機制和安全cookie的實現機制相同。
# 普通方式
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import tornado.ioloop
import tornado.web
class MainHandler(tornado.web.RequestHandler):
def get(self):
login_user = self.get_secure_cookie("login_user", None)
if login_user:
self.write(login_user)
else:
self.redirect('/login')
class LoginHandler(tornado.web.RequestHandler):
def get(self):
self.current_user()
self.render('login.html', **{'status': ''})
def post(self, *args, **kwargs):
username = self.get_argument('name')
password = self.get_argument('pwd')
if username == 'tornado' and password == '123':
self.set_secure_cookie('login_user', 'tornado')
self.redirect('/')
else:
self.render('login.html', **{'status': '用戶名或密碼錯誤'})
settings = {
'template_path': 'template',
'static_path': 'static',
'static_url_prefix': '/static/',
'cookie_secret': base64.b64encode(uuid.uuid3(uuid.NAMESPACE_DNS, 'jiayan').bytes)
}
application = tornado.web.Application([
(r"/index", MainHandler),
(r"/login", LoginHandler),
], **settings)
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
# Demo-Toando內部提供基於cookie進行用戶驗證 基於源碼更改
# https://www.zhihu.com/question/21030844
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import tornado.ioloop
import tornado.web
class BaseHandler(tornado.web.RequestHandler):
def get_current_user(self):
return self.get_secure_cookie("login_user")
class MainHandler(BaseHandler):
@tornado.web.authenticated
def get(self):
login_user = self.current_user # 此時會調取 get_current_user
self.write(login_user)
class LoginHandler(tornado.web.RequestHandler):
def get(self):
self.current_user()
self.render('login.html', **{'status': ''})
def post(self, *args, **kwargs):
username = self.get_argument('name')
password = self.get_argument('pwd')
if username == 'tornado' and password == '123':
self.set_secure_cookie('login_user', 'tornado')
self.redirect('/')
else:
self.render('login.html', **{'status': '用戶名或密碼錯誤'})
settings = {
'template_path': 'template',
'static_path': 'static',
'static_url_prefix': '/static/',
'cookie_secret': base64.b64encode(uuid.uuid3(uuid.NAMESPACE_DNS, 'jiayan').bytes),
'login_url': '/login'
}
application = tornado.web.Application([
(r"/index", MainHandler),
(r"/login", LoginHandler),
], **settings)
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
4. Ajax 上傳文件
Html 文件
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
</head>
<body>
<input type="file" id="img" />
<input type="button" onclick="UploadFile();" />
<script>
function UploadFile(){
var fileObj = document.getElementById("img").files[0];
var form = new FormData();
form.append("k1", "v1");
form.append("fff", fileObj);
var xhr = new XMLHttpRequest();
xhr.open("post", '/index', true);
xhr.send(form);
}
</script>
</body>
</html>
python文件
#!/usr/bin/env python
# -*- coding:utf-8 -*-
#!/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')
def post(self, *args, **kwargs):
file_metas = self.request.files["fff"]
# print(file_metas)
for meta in file_metas:
file_name = meta['filename']
with open(file_name,'wb') as up:
up.write(meta['body'])
settings = {
'template_path': 'template',
}
application = tornado.web.Application([
(r"/index", MainHandler),
], **settings)
if __name__ == "__main__":
application.listen(8000)
tornado.ioloop.IOLoop.instance().start()
Ajax
var fileObj = $("#img")[0].files[0];
var form = new FormData();
form.append("k1", "v1");
form.append("fff", fileObj);
$.ajax({
type:'POST',
url: '/index',
data: form,
processData: false, // tell jQuery not to process the data
contentType: false, // tell jQuery not to set contentType
success: function(arg){
console.log(arg);
}
})
jQuery Ajax Upload
6. 自定義Session
Tornado是沒有Session的,Cookie和Session的關系可以自己去了解一下。簡單來說,cookie是把基本信息存到瀏覽器中,每次請求,瀏覽器帶上cookie,服務端從cookie來辨明用戶身份。session是根據用戶信息生成一段特有信息放到隨機生成的字符串 A 中,然后把 A 加密放到cookie中,讓用戶每次帶上cookie訪問時,讀取驗證 A,並到 A 對應的存儲中讀取用戶數據。Session 把用戶信息放到服務端較Cookie把用戶信息放到客戶端要安全些。
現在我們來自己實現一個Tornado 的 Session。
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. session實現機制代碼
#!/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):
'''
初始函數,tornado 視圖函數的執行順序中第一個執行的函數
'''
self.my_session = Session(self) # 執行Session的構造方法並且把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實現
7. 分布式session框架
通過上述 Session 的設置,我們可以得知,session主要存放的位置是 session_container={}
這個字典中。並沒有使用相應的數據庫。如果當前進程掛了,數據容易丟失,並且不容易擴展。
基於以上考慮,我們可以使用分布式來解決這一個問題。
假使我們有三台機器,分別為 A,B,C。
當用戶訪問過來的時候,通過權重和hash計算把 session 加入到 hash 環中的服務上,實現session在不同的主機上存儲,當第二次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
"""
from hashlib import sha1
import os, time
create_session_id = lambda: sha1('%s%s' % (os.urandom(16), time.time())).hexdigest()
class Session(object):
session_id = "__sessionId__"
def __init__(self, request):
session_value = request.get_cookie(Session.session_id)
if not session_value:
self._id = create_session_id()
else:
self._id = session_value
request.set_cookie(Session.session_id, self._id)
def __getitem__(self, key):
# 根據 self._id ,在一致性哈西中找到其對應的服務器IP
# 找到相對應的redis服務器,如: r = redis.StrictRedis(host='localhost', port=6379, db=0)
# 使用python redis api 鏈接
# 獲取數據,即:
# return self._redis.hget(self._id, name)
def __setitem__(self, key, value):
# 根據 self._id ,在一致性哈西中找到其對應的服務器IP
# 使用python redis api 鏈接
# 設置session
# self._redis.hset(self._id, name, value)
def __delitem__(self, key):
# 根據 self._id 找到相對應的redis服務器
# 使用python redis api 鏈接
# 刪除,即:
return self._redis.hdel(self._id, name)
8. 自定義模型框架(form框架)
模型綁定有兩個主要功能:
- 自動生成html表單
- 用戶輸入驗證
不同於django,tornado許多功能需要自定義才能實現。
html文件
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<link href="{{static_url("commons.css")}}" rel="stylesheet" />
</head>
<body>
<h1>hello</h1>
<form action="/index" method="post">
<p>hostname: <input type="text" name="host" /> </p>
<p>ip: <input type="text" name="ip" /> </p>
<p>port: <input type="text" name="port" /> </p>
<p>phone: <input type="text" name="phone" /> </p>
<input type="submit" />
</form>
</body>
</html>
python文件
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import tornado.ioloop
import tornado.web
from hashlib import sha1
import os, time
import re
class MainForm(object):
def __init__(self):
self.host = "(.*)"
self.ip = "^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$"
self.port = '(\d+)'
self.phone = '^1[3|4|5|8][0-9]\d{8}$'
def check_valid(self, request):
form_dict = self.__dict__
for key, regular in form_dict.items():
post_value = request.get_argument(key)
# 讓提交的數據 和 定義的正則表達式進行匹配
ret = re.match(regular, post_value)
print key,ret,post_value
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render('index.html')
def post(self, *args, **kwargs):
obj = MainForm()
result = obj.check_valid(self)
self.write('ok')
settings = {
'template_path': 'template',
'static_path': 'static',
'static_url_prefix': '/static/',
'cookie_secret': 'aiuasdhflashjdfoiuashdfiuh',
'login_url': '/login'
}
application = tornado.web.Application([
(r"/index", MainHandler),
], **settings)
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()
python文件升級版
增加代碼邏輯的復用。
#!/usr/bin/env python
# -*- coding:utf-8 -*-
import tornado.ioloop
import tornado.web
import re
class Field(object):
def __init__(self, error_msg_dict, required):
self.id_valid = False
self.value = None
self.error = None
self.name = None
self.error_msg = error_msg_dict
self.required = required
def match(self, name, value):
self.name = name
if not self.required:
self.id_valid = True
self.value = value
else:
if not value:
if self.error_msg.get('required', None):
self.error = self.error_msg['required']
else:
self.error = "%s is required" % name
else:
ret = re.match(self.REGULAR, value)
if ret:
self.id_valid = True
self.value = ret.group()
else:
if self.error_msg.get('valid', None):
self.error = self.error_msg['valid']
else:
self.error = "%s is invalid" % name
class IPField(Field):
REGULAR = "^(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}$"
def __init__(self, error_msg_dict=None, required=True):
error_msg = {} # {'required': 'IP不能為空', 'valid': 'IP格式錯誤'}
if error_msg_dict:
error_msg.update(error_msg_dict)
super(IPField, self).__init__(error_msg_dict=error_msg, required=required)
class IntegerField(Field):
REGULAR = "^\d+$"
def __init__(self, error_msg_dict=None, required=True):
error_msg = {'required': '數字不能為空', 'valid': '數字格式錯誤'}
if error_msg_dict:
error_msg.update(error_msg_dict)
super(IntegerField, self).__init__(error_msg_dict=error_msg, required=required)
class CheckBoxField(Field):
def __init__(self, error_msg_dict=None, required=True):
error_msg = {} # {'required': 'IP不能為空', 'valid': 'IP格式錯誤'}
if error_msg_dict:
error_msg.update(error_msg_dict)
super(CheckBoxField, self).__init__(error_msg_dict=error_msg, required=required)
def match(self, name, value):
self.name = name
if not self.required:
self.id_valid = True
self.value = value
else:
if not value:
if self.error_msg.get('required', None):
self.error = self.error_msg['required']
else:
self.error = "%s is required" % name
else:
if isinstance(name, list):
self.id_valid = True
self.value = value
else:
if self.error_msg.get('valid', None):
self.error = self.error_msg['valid']
else:
self.error = "%s is invalid" % name
class FileField(Field):
REGULAR = "^(\w+\.pdf)|(\w+\.mp3)|(\w+\.py)$"
def __init__(self, error_msg_dict=None, required=True):
error_msg = {} # {'required': '數字不能為空', 'valid': '數字格式錯誤'}
if error_msg_dict:
error_msg.update(error_msg_dict)
super(FileField, self).__init__(error_msg_dict=error_msg, required=required)
def match(self, name, value):
self.name = name
self.value = []
if not self.required:
self.id_valid = True
self.value = value
else:
if not value:
if self.error_msg.get('required', None):
self.error = self.error_msg['required']
else:
self.error = "%s is required" % name
else:
m = re.compile(self.REGULAR)
if isinstance(value, list):
for file_name in value:
r = m.match(file_name)
if r:
self.value.append(r.group())
self.id_valid = True
else:
self.id_valid = False
if self.error_msg.get('valid', None):
self.error = self.error_msg['valid']
else:
self.error = "%s is invalid" % name
break
else:
if self.error_msg.get('valid', None):
self.error = self.error_msg['valid']
else:
self.error = "%s is invalid" % name
def save(self, request, upload_path=""):
file_metas = request.files[self.name]
for meta in file_metas:
file_name = meta['filename']
with open(file_name,'wb') as up:
up.write(meta['body'])
class Form(object):
def __init__(self):
self.value_dict = {}
self.error_dict = {}
self.valid_status = True
def validate(self, request, depth=10, pre_key=""):
self.initialize()
self.__valid(self, request, depth, pre_key)
def initialize(self):
pass
def __valid(self, form_obj, request, depth, pre_key):
"""
驗證用戶表單請求的數據
:param form_obj: Form對象(Form派生類的對象)
:param request: Http請求上下文(用於從請求中獲取用戶提交的值)
:param depth: 對Form內容的深度的支持
:param pre_key: Html中name屬性值的前綴(多層Form時,內部遞歸時設置,無需理會)
:return: 是否驗證通過,True:驗證成功;False:驗證失敗
"""
depth -= 1
if depth < 0:
return None
form_field_dict = form_obj.__dict__
for key, field_obj in form_field_dict.items():
print key,field_obj
if isinstance(field_obj, Form) or isinstance(field_obj, Field):
if isinstance(field_obj, Form):
# 獲取以key開頭的所有的值,以參數的形式傳至
self.__valid(field_obj, request, depth, key)
continue
if pre_key:
key = "%s.%s" % (pre_key, key)
if isinstance(field_obj, CheckBoxField):
post_value = request.get_arguments(key, None)
elif isinstance(field_obj, FileField):
post_value = []
file_list = request.request.files.get(key, None)
for file_item in file_list:
post_value.append(file_item['filename'])
else:
post_value = request.get_argument(key, None)
print post_value
# 讓提交的數據 和 定義的正則表達式進行匹配
field_obj.match(key, post_value)
if field_obj.id_valid:
self.value_dict[key] = field_obj.value
else:
self.error_dict[key] = field_obj.error
self.valid_status = False
class ListForm(object):
def __init__(self, form_type):
self.form_type = form_type
self.valid_status = True
self.value_dict = {}
self.error_dict = {}
def validate(self, request):
name_list = request.request.arguments.keys() + request.request.files.keys()
index = 0
flag = False
while True:
pre_key = "[%d]" % index
for name in name_list:
if name.startswith(pre_key):
flag = True
break
if flag:
form_obj = self.form_type()
form_obj.validate(request, depth=10, pre_key="[%d]" % index)
if form_obj.valid_status:
self.value_dict[index] = form_obj.value_dict
else:
self.error_dict[index] = form_obj.error_dict
self.valid_status = False
else:
break
index += 1
flag = False
class MainForm(Form):
def __init__(self):
# self.ip = IPField(required=True)
# self.port = IntegerField(required=True)
# self.new_ip = IPField(required=True)
# self.second = SecondForm()
self.fff = FileField(required=True)
super(MainForm, self).__init__()
#
# class SecondForm(Form):
#
# def __init__(self):
# self.ip = IPField(required=True)
# self.new_ip = IPField(required=True)
#
# super(SecondForm, self).__init__()
class MainHandler(tornado.web.RequestHandler):
def get(self):
self.render('index.html')
def post(self, *args, **kwargs):
# for i in dir(self.request):
# print i
# print self.request.arguments
# print self.request.files
# print self.request.query
# name_list = self.request.arguments.keys() + self.request.files.keys()
# print name_list
# list_form = ListForm(MainForm)
# list_form.validate(self)
#
# print list_form.valid_status
# print list_form.value_dict
# print list_form.error_dict
# obj = MainForm()
# obj.validate(self)
#
# print "驗證結果:", obj.valid_status
# print "符合驗證結果:", obj.value_dict
# print "錯誤信息:"
# for key, item in obj.error_dict.items():
# print key,item
# print self.get_arguments('favor'),type(self.get_arguments('favor'))
# print self.get_argument('favor'),type(self.get_argument('favor'))
# print type(self.get_argument('fff')),self.get_argument('fff')
# print self.request.files
# obj = MainForm()
# obj.validate(self)
# print obj.valid_status
# print obj.value_dict
# print obj.error_dict
# print self.request,type(self.request)
# obj.fff.save(self.request)
# from tornado.httputil import HTTPServerRequest
# name_list = self.request.arguments.keys() + self.request.files.keys()
# print name_list
# print self.request.files,type(self.request.files)
# print len(self.request.files.get('fff'))
# obj = MainForm()
# obj.validate(self)
# print obj.valid_status
# print obj.value_dict
# print obj.error_dict
# obj.fff.save(self.request)
self.write('ok')
settings = {
'template_path': 'template',
'static_path': 'static',
'static_url_prefix': '/static/',
'cookie_secret': 'aiuasdhflashjdfoiuashdfiuh',
'login_url': '/login'
}
application = tornado.web.Application([
(r"/index", MainHandler),
], **settings)
if __name__ == "__main__":
application.listen(8888)
tornado.ioloop.IOLoop.instance().start()