tornado
是Python
界中很是出名的一款Web
框架,和Flask
同樣它也屬於輕量級的Web
框架。javascript
可是從性能而言tornado
因爲其支持異步非阻塞的特性因此對於一些高併發的場景顯得更爲適用。css
tornado
簡潔,高效,可以支持WebSocket
,其I/O
多路複用採用epoll
模式來實現異步,而且還有Future
期程對象來實現非阻塞。html
tornado
與Django
和Flask
等基於WSGI
的框架有一個根本的區別,就是它實現socket
的模塊是本身寫的,並非用其餘模塊。前端
A : socket部分 | B: 路由與視圖函數對應關係(路由匹配) | C: 模版語法 | |
---|---|---|---|
django | 別人的wsgiref模塊 | 本身寫 | 本身的(沒有jinja2好用 可是也很方便) |
flask | 別人的werkzeug(內部仍是wsgiref模塊) | 本身寫 | 別人的(jinja2) |
tornado | 本身寫的 | 本身寫 | 本身寫 |
如何編寫一個最簡單的tornado
:java
import os import tornado.ioloop import tornado.web BASE_DIR = os.path.dirname(__file__) class IndexHandler(tornado.web.RequestHandler): def get(self): self.render("index.html") settings = { "debug": True, "template_path": os.path.join(BASE_DIR, "views"), # 存放模板的文件夾 "static_path": os.path.join(BASE_DIR, "static"), # 存放靜態文件的文件夾 } application = tornado.web.Application( [ (r"/index", IndexHandler), # 正則匹配,路由規則 ], **settings) # 配置項 if __name__ == '__main__': # 1.新增socket Server端,並將fp描述符添加至select或者epoll中 application.listen(8888) # 2.循環epoll進行監聽 tornado.ioloop.IOLoop.instance().start()
模板文件:python
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>INDEX</title> <link rel="stylesheet" href="{{static_url('common.css')}}"> <!-- <link rel="stylesheet" href="/static/common.css">--> </head> <body> <p>INDEX</p> </body> </html>
下面未來探究Django/Flask/tornado
如何處理一次HTTP
請求:mysql
Django
中處理一次HTTP
請求默認是以多線程模式完成。jquery
在DJnago1.x版本以後,默認啓動都是多線程形式,若是要啓用單線程模式:git
python manage.py runserver --nothreadinggithub
from django.shortcuts import HttpResponse from threading import get_ident def api1(request): print(get_ident()) # 13246 return HttpResponse("api1") def api2(request): print(get_ident()) # 13824 return HttpResponse("api2")
Flask
的底層其實也是wsgiref
模塊實現,因此處理一次HTTP
請求也是以多線程。
import flask from threading import get_ident app = flask.Flask(__name__) @app.route('/api1') def api1(): print(get_ident()) # 15952 return "api1" @app.route('/api2') def api2(): print(get_ident()) # 15236 return "api2" if __name__ == '__main__': app.run()
tornado
的處理方式是單線程+I/O
多路複用,這意味着必須挨個挨個排隊處理每個HTTP
請求:
import tornado.ioloop import tornado.web from threading import get_ident class Api1Handler(tornado.web.RequestHandler): def get(self): print(get_ident()) # 10168 self.write("api1") class Api2Handler(tornado.web.RequestHandler): def get(self): print(get_ident()) # 10168 self.write("api2") application = tornado.web.Application([ (r"/api1",Api1Handler), (r"/api2",Api2Handler), ]) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
要想了解tornado
的異步,就結合前面請求處理方式來看。
同步Web
框架與異步Web
框架的區別,這裏有一篇文章寫的不錯:
上面文章的一句話來歸納就是說同步大多數都是監聽一個socket
對象(服務器),當服務器對象的描述符狀態(可讀)發生改變後,就會建立一個新的線程來處理本次請求,Django/Flask
內部其實都是經過wsgiref
模塊實現,而且wsgiref
依賴於socketserver
模塊。若是想了解他們如何啓動多線程進行服務監聽,可參照早期文章(調用方式如出一轍):
而對於tornado
來講,它不會建立多線程,而是將conn
雙向鏈接對象放入事件循環中予以監聽。
得益於epoll
的主動性,tornado
的速度很是快,而在處理完conn
(本次會話後),則會將conn
(Socket
)進行斷開。 (HTTP
短連接)
拿Django
的單線程舉例,當一個HTTP
請求到來並未完成時,下一個HTTP
請求將會被阻塞。
python manage.py runserver --nothreading # 嘗試以單線程的方式運行...對比tornado的單線程
代碼以下:
from django.shortcuts import HttpResponse import time def api1(request): time.sleep(5) return HttpResponse("api1") def api2(request): return HttpResponse("api2")
而若是是tornado
的非阻塞方式,單線程模式下即便第一個視圖阻塞了,第二個視圖依舊可以進行訪問.
import time import tornado.ioloop import tornado.web from tornado import gen from tornado.concurrent import Future class Api1Handler(tornado.web.RequestHandler): @gen.coroutine def get(self): future = Future() # 方式一:添加回調 五秒後執行該異步任務 tornado.ioloop.IOLoop.current().add_timeout(time.time() + 5, self.done) # 方式二:添加future # tornado.ioloop.IOLoop.current().add_future(future,self.done) # 方式三:添加回調 # future.add_done_callback(self.doing) yield future def done(self, *args, **kwargs): self.write('api1') self.finish() # 完成本次HTTP請求,將future的result狀態改變 class Api2Handler(tornado.web.RequestHandler): def get(self): self.write("api2") application = tornado.web.Application([ (r"/api1", Api1Handler), (r"/api2", Api2Handler), ]) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
有關於Future
對象如何實現異步,下面會進行詳細的探討。
tornado
實現異步的根本技術點:I/O
多路複用的epoll
模式
tornado
實現非阻塞的根本技術點:Future
期程(將來)對象
仔細看起步介紹中,tornado
的配置,它是做爲關鍵字傳參傳入Application
這個類中。
因此咱們可使用**{k1:v1}
的方式來設定配置項,下面舉例一些常見的配置項。
常規配置項:
設置項 | 描述 |
---|---|
autoreload | 若是True,任何源文件更改時服務器進程將從新啓動,如調試模式和自動從新加載中所述。此選項是Tornado 3.2中的新選項; 之前此功能由debug設置控制 |
debug | 幾種調試模式設置的簡寫,在調試模式和自動從新加載中描述。設置debug=True至關於autoreload=True,compiled_template_cache=False,static_hash_cache=False,serve_traceback=True |
default_handler_class|default_handler_args | 若是沒有找到其餘匹配項,將使用此處理程序; 使用它來實現自定義404頁面(Tornado 3.2中的新增功能) |
compress_response | 若是True,文本格式的響應將自動壓縮。Tornado 4.0的新功能 |
gzip | compress_response自Tornado 4.0以來已棄用的別名 |
log_function | 此函數將在每一個記錄結果的請求結束時調用(使用一個參數,即RequestHandler 對象)。默認實現將寫入logging模塊的根記錄器。也能夠經過覆蓋來定製Application.log_request。 |
serve_traceback | 若是True,默認錯誤頁面將包含錯誤的回溯。此選項是Tornado 3.2中的新選項; 之前此功能由debug設置控制 |
ui_modules | ui_methods | 能夠設置爲UIModule可用於模板的映射或UI方法。能夠設置爲模塊,字典或模塊和/或dicts列表。有關詳細信息,請參閱UI模塊。 |
websocket_ping_interval | 若是設置爲數字,則每n秒鐘將對全部websockets進行ping操做。這有助於經過關閉空閒鏈接的某些代理服務器保持鏈接活動,而且它能夠檢測websocket是否在未正確關閉的狀況下發生故障。 |
websocket_ping_timeout | 若是設置了ping間隔,而且服務器在這麼多秒內沒有收到「pong」,它將關閉websocket。默認值是ping間隔的三倍,最少30秒。若是未設置ping間隔,則忽略。 |
說點人話,debug
或者autoreload
爲True
時,修改源文件代碼將會自動重啓服務,至關於Django
的重啓功能。
而log_function
則能夠自定製日誌的輸出格式,以下所示:
def log_func(handler): if handler.get_status() < 400: log_method = access_log.info elif handler.get_status() < 500: log_method = access_log.warning else: log_method = access_log.error request_time = 1000.0 * handler.request.request_time() log_method("%d %s %s (%s) %s %s %.2fms", handler.get_status(), handler.request.method, handler.request.uri, handler.request.remote_ip, handler.request.headers["User-Agent"], handler.request.arguments, settings = {"log_function":log_func}
關於身份、驗證、安全的配置項:
設置項 | 描述 |
---|---|
cookie_secret | 用於RequestHandler.get_secure_cookie 和set_secure_cookie簽署cookie |
key_version | set_secure_cooki 當cookie_secret是密鑰字典時,requestHandler 使用特定密鑰對cookie進行簽名 |
login_url | authenticated若是用戶未登陸,裝飾器將重定向到此URL。能夠經過覆蓋進一步自定義RequestHandler.get_login_url |
xsrf_cookies | 若是True ,將啓用跨站點請求僞造保護 |
xsrf_cookie_version | 控制此服務器生成的新XSRF cookie的版本。一般應該保留默認值(它始終是支持的最高版本),但能夠在版本轉換期間臨時設置爲較低的值。Tornado 3.2.2中的新功能,它引入了XSRF cookie版本2 |
xsrf_cookie_kwargs | 能夠設置爲要傳遞給RequestHandler.set_cookie XSRF cookie 的其餘參數的字典 |
twitter_consumer_key | 所用的 tornado.auth模塊來驗證各類API,如檢測這些種類帳號是否登陸等... |
twitter_consumer_secret | 同上.. |
friendfeed_consumer_key | 同上.. |
friendfeed_consumer_secret | 同上.. |
google_consumer_key | 同上.. |
google_consumer_secret | 同上.. |
facebook_api_key | 同上.. |
facebook_secret | 同上.. |
模板設置項:
設置項 | 描述 |
---|---|
autoescape | 控制模板的自動轉義。能夠設置爲None禁用轉義,或者設置 應該傳遞全部輸出的函數的名稱。默認爲"xhtml_escape"。可使用該指令在每一個模板的基礎上進行更改。{% autoescape %} |
compiled_template_cache | 默認是True; 若是False每一個請求都會從新編譯模板。此選項是Tornado 3.2中的新選項; 之前此功能由debug設置控制 |
template_path | 包含模板文件的目錄。能夠經過覆蓋進一步定製RequestHandler.get_template_path |
template_loader | 分配給tornado.template.BaseLoader自定義模板加載的實例 。若是使用此 設置,則忽略template_path和autoescape設置。能夠經過覆蓋進一步定製RequestHandler.create_template_loader |
template_whitespace | 控制模板中空格的處理; 查看tornado.template.filter_whitespace容許的值。Tornado 4.3中的新功能 |
靜態文件相關設置:
設置項 | 描述 |
---|---|
static_hash_cache | 默認是True; 若是False 每次請求都會從新計算靜態網址。此選項是Tornado 3.2中的新選項; 之前此功能由debug設置控制 |
static_path | 將從中提供靜態文件的目錄 |
static_url_prefix | 靜態文件的Url前綴,默認爲/static/ |
static_handler_class | static_handler_args | 能夠設置爲靜態文件而不是默認文件使用不一樣的處理程序 tornado.web.StaticFileHandler。 static_handler_args若是設置,則應該是要傳遞給處理程序initialize方法的關鍵字參數的字典。 |
在tornado
中,一個url
對應一個類。
匹配方式爲正則匹配,所以要注意使用^
與$
的使用。
因爲匹配行爲是從上至下,因此在定義時必定要注意順序。
import tornado.ioloop import tornado.web # http://127.0.0.1:8888/admin class APIHandler(tornado.web.RequestHandler): def get(self): self.write("...") settings = {"debug": True} application = tornado.web.Application([ (r"^/a.{4}$", APIHandler), ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
使用正則分組()
解析出資源請求地址的一些參數。
匹配到的參數會經過位置傳參的形式傳遞給控制器處理函數,因此接收參數能夠任意命名,所以你能夠經過*args
接收到全部匹配的參數:
import tornado.ioloop import tornado.web # http://127.0.0.1:8888/register/yunya class RegisterHandler(tornado.web.RequestHandler): def get(self,*args): self.write(str(args)) # ('yunya',) settings = {"debug": True} application = tornado.web.Application([ (r"^/register/(\w+)", RegisterHandler), ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
若是肯定這一被捕捉參數將被使用,則可指定形參進行接收:
import tornado.ioloop import tornado.web # http://127.0.0.1:8888/register/yunya class RegisterHandler(tornado.web.RequestHandler): def get(self,params): self.write(params) # 'yunya' settings = {"debug": True} application = tornado.web.Application([ (r"^/register/(\w+)", RegisterHandler), ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
使用正則的有命分組(?P<組名>規則)
解析出資源請求地址的一些參數。
匹配到的參數會經過關鍵字傳參的形式傳遞給控制器處理函數,因此接收參數必須與組名相同,所以你能夠經過**kwargs
接收到全部匹配的參數:
import tornado.ioloop import tornado.web # http://127.0.0.1:8888/register/yunya class RegisterHandler(tornado.web.RequestHandler): def get(self,**kwargs): self.write(str(kwargs)) # {'username': 'yunya'} settings = {"debug": True} application = tornado.web.Application([ (r"^/register/(?P<username>\w+)", RegisterHandler), ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
若是肯定這一被捕捉參數將被使用,則可指定形參進行接收(形參命名必須與正則匹配的組名相同):
import tornado.ioloop import tornado.web # http://127.0.0.1:8888/register/yunya class RegisterHandler(tornado.web.RequestHandler): def get(self,username): self.write(username) # 'yunya' settings = {"debug": True} application = tornado.web.Application([ (r"^/register/(?P<username>\w+)", RegisterHandler), ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
在tornado
中,路由匹配的參數捕捉不容許無名分組和有名分組的混合使用,這會引起一個異常:
application = tornado.web.Application([ (r"^/register/(/d+)/(?P<username>\w+)", RegisterHandler), ], **settings)
拋出的錯誤:
AssertionError: groups in url regexes must either be all named or all positional: '^/register/(/d+)/(?P<username>\\w+)$'
分組必須所有使用位置、或者使用命名。
反向解析要與路由命名同時使用:
import tornado.ioloop import tornado.web # http://127.0.0.1:8888/register class RegisterHandler(tornado.web.RequestHandler): def get(self): print(self.reverse_url("reg")) # /register self.write("註冊頁面") settings = {"debug": True} # 使用tornado.web.url來進行添加路由規則 application = tornado.web.Application([ tornado.web.url(r'/register', RegisterHandler, name="reg") ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
前端模板中的反向解析(必須註冊名字):
{{reverse_url('reg')}}
在MCV
模型中,C
表明Controller
即爲控制器,相似於Django
中的views
。
咱們能夠看見在tornado
中,控制器處理函數都絕不意外的繼承了一個叫作tornado.web.RequestHandler
的對象,全部的方法都是從self
中進行調用,因此你能夠查看它的源碼獲取全部方法,或者使用help()
函數得到它的DOC
。
下面我將例舉出一些經常使用的方法。
如下是經常使用的獲取相關屬性以及方法,基本是RequestHandler
中的屬性、方法與self.request
對象中封裝的方法和屬性:
屬性/方法 | 描述 |
---|---|
self.request | 獲取用戶請求相關信息 |
self._headers | 獲取請求頭信息,基本請求頭被過濾 |
self.request.headers | 獲取請求頭信息,包含基本請求頭 |
slef.request.body | 獲取請求體信息,bytes格式 |
self.request.remote_ip | 獲取客戶端IP |
self.request.method | 獲取請求方式 |
self.request.version | 獲取所使用的HTTP版本 |
self.get_query_argument() | 獲取單個GET中傳遞過來的參數,若是多個參數同名,獲取最後一個 |
slef.get_query_arguments() | 獲取全部GET中傳遞過來的參數,返回列表的形式 |
self.get_body_argument() | 獲取單個POST中傳遞過來的參數,若是多個參數同名,獲取最後一個 |
self.get_body_arguments() | 獲取全部POST中傳遞過來的參數,返回列表的形式 |
self.get_argument() | 獲取單個GET/POST中傳遞過來的參數,若是多個參數同名,獲取最後一個 |
self.get_arguments() | 獲取全部GET/POST中傳遞過來的參數,返回列表的形式 |
self.request.files | 獲取全部經過 multipart/form-data POST 請求上傳的文件 |
self.request.host | 獲取主機名 |
self.request.uri | 獲取請求的完整資源標識,包括路徑和查詢字符串 |
self.request.query | 獲取查詢字符串的部分 |
self.request.path | 獲取請求的路徑( ?以前的全部內容) |
示例演示:
import tornado.ioloop import tornado.web # http://127.0.0.1:8888/register?name=yunya&hobby=%E7%AF%AE%E7%90%83&hobby=%E8%B6%B3%E7%90%83 class RegisterHandler(tornado.web.RequestHandler): def get(self): # 獲取客戶端IP print(self.request.remote_ip) # 127.0.0.1 # 查看請求方式 print(self.request.method) # GET # 獲取單個GET/POST傳遞的參數 print(self.get_query_argument("name")) # yunya # 獲取多個GET/POST傳遞的參數、list形式 print(self.get_query_arguments("hobby")) # ['籃球', '足球'] print(self.request.host) # 127.0.0.1:8888 print(self.request.uri) # register?name=yunya&hobby=%E7%AF%AE%E7%90%83&hobby=%E8%B6%B3%E7%90%83 print(self.request.path) # /register print(self.request.query) # name=yunya&hobby=%E7%AF%AE%E7%90%83&hobby=%E8%B6%B3%E7%90%83 self.write("OK") settings = {"debug":True} application = tornado.web.Application([ tornado.web.url(r'/register', RegisterHandler, name="reg") ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
文件上傳案例:
import tornado.ioloop import tornado.web class APIHandler(tornado.web.RequestHandler): def post(self): # step01:獲取全部名爲img的文件對象,返回一個列表 [img,img,img] file_obj_list = self.request.files.get("img") # step02:獲取第一個對象 file_obj = file_obj_list[0] # step03:獲取文件名稱 file_name = file_obj.filename # step04:獲取文件數據 file_body = file_obj.body # step05:獲取文件類型 file_type = file_obj.content_type with open(f"./{file_name}",mode="wb") as f: f.write(file_body) self.write("OK") settings = {"debug":True} application = tornado.web.Application([ tornado.web.url(r'/api', APIHandler) ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
響應通常就分爲如下幾種,返回單純的字符串,返回一個模板頁面,返回JSON
格式數據,返回一個錯誤,以及重定向:
返回單純字符串:
self.write("OK")
返回一個模板頁面:
self.render("templatePath",**kwargs) # 傳遞給模板的數據
返回JSON
格式數據(手動JSON
):
import json json_data = json.dumps({"k1":"v1"}) self.write(json_data)
返回一個錯誤(直接raise
引起異常便可):
raise tornado.web.HTTPError(403)
重定向:
self.redirect("/",status=301)
響應頭相關的操做:
self.set_header("k1", 1) self.add_header("k2", 2) self.clear_header("k1")
咱們能夠在控制器中定義一個鉤子函數initialize()
,而且能夠在url
中對他進行一些參數傳遞:
import tornado.ioloop import tornado.web class APIHandler(tornado.web.RequestHandler): def initialize(self, *args, **kwargs) -> None: print(kwargs) # {k1:v1} self.data = "某個數據" def post(self): print(self.data) # 某個數據 self.write("ok") settings = {"debug": True} application = tornado.web.Application([ tornado.web.url(r'/api', APIHandler, {"k1": "v1"}), # dict -> **kwargs ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
全部的鉤子函數:
class APIHandler(tornado.web.RequestHandler): def set_default_headers(self): print("first--設置headers") def initialize(self): print("second--初始化") def prepare(self): print("third--準備工做") def get(self): print("fourth--處理get請求") def post(self): print('fourth--處理post請求') def write_error(self, status_code, **kwargs): print("fifth--處理錯誤") def on_finish(self): print("sixth--處理結束,釋放資源--")
在settings
中指定模板所在目錄,如不指定,默認在當前文件夾下:
import tornado.ioloop import tornado.web class APIHandler(tornado.web.RequestHandler): def get(self): # 找當前目錄下的views文件夾,到views下去找api.html模板文件 self.render("api.html") settings = { "debug": True, "template_path": "views", # 指定模板目錄 "static_path": "static", # 指定靜態文件目錄 } application = tornado.web.Application([ tornado.web.url(r'/api', APIHandler), ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
tornado
中的模板傳參與Flask
相同。
模板傳參能夠經過k=v
的方式傳遞,也能夠經過**dict
的方式進行解包傳遞:
class APIHandler(tornado.web.RequestHandler): def get(self): context = { "name": "雲崖", "age": 18, "hobby": ["籃球", "足球"] } self.render("api.html",**context) # self.render("api.html",name="雲崖",age=18,hobby=["籃球", "足球"])
渲染,經過{{}}
進行,注意:trdtmp
中不支持.
的深度查詢訪問,這點與DTL
和JinJa2
不一樣:
<body> <p>{{name}}</p> <p>{{age}}</p> <p>{{hobby[0]}}-{{hobby[1]}}</p> </body>
模板中有兩種表現形式,一種是表達式形式以{{}}
進行包裹,另外一種是命令形式以{% 命令 %}
包裹。
注意:tornado的模板語法的結束標識是{% end %},不是Django或jinja2的{% endblock %}
舉例表達式形式:
# 渲染控制器函數傳入的變量 <body> 歡迎{{ username }}登陸 </body> # 進行Python表達式 {{ 1 + 1 }} # 導入模塊並使用 {{ time.time() }}
舉例命令形式:
{% if 1 %} this is if {% end %}
若是想對命令進行註釋,則可使用{# #}
,若是不想執行內容,則可使用{{! {%! {#
爲前綴,以下示例:
{{! 1 + 1}} {%! if 1 %} this is if {%! end %} {#! time.time() #}}
tornado
中的模板語言容許導入Python
包、模塊:
{% import time %} {{ time.time() }} {% from util.modify import mul %} {{mul(6,6)}}
模板中提供一些功能,能夠在{{}}
或者{% %}
中進行使用:
模板調用的方法/功能/模塊 | 描述 |
---|---|
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 | for handler.static_url 的別名 |
xsrf_form_html | handler.xsrf_form_html 的別名 |
reverse_url | Application.reverse_url 的別名 |
Application | 設置中ui_methods和 ui_modules下面的全部項目 |
模板中的if
判斷:
{% if username != 'no' %} 歡迎{{ username }}登陸 {% else %} 請登陸 {% end %}
for
循環:
<body> {% for item in range(10) %} {% if item == 0%} <p>start</p> {% elif item == len(range(10))-1 %} <p>end</p> {% else %} <p>{{item}}</p> {% end %} {% end %} </body>
while
循環:
{% set a = 0 %} {% while a<5 %} {{ a }}<br> {% set a += 1 %} {% end %}
默認的模板在渲染時都會將<
與>
以及空格等特殊字符替換爲HTML
內容,如<
,>
等。
關於模板轉義的方式有如下幾種。
1.單變量去除轉義:
{{'<b>你好</b>'}} # <b>你好</b> {% raw '<b>你好</b>' %} # <b>你好</b>
2.當前模板全局去除轉義:
{% autoescape None %} # 模板首行加入
3.整個項目去掉轉義,爲當前的application
進行配置:
settings = { "autoescape":None, # 禁用轉義 }
使用{% extends %}
引入一個定義好的模板。
使用{% blocak %}
和{% end %}
定義並替換塊。
定義主模板:
# views/base.html <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>{% block title %}Document{% end %}</title> {% block css %} {% end %} </head> <body> {% block main %} {% end %} </body> {% block js %} {% end %} </html>
繼承與使用模板:
{% extends 'base.html'%} {% block title %} API {% end %} {% block css %} <style> body{ background-color: red; } </style> {% end %} {% block main %} <p>HELLO</p> {% end %} {% block js %} <script> alert("HELLO") </script> {% end %}
若是一個地方須要一塊完整的模板文件,則使用模板引入便可:
{include 'templateName'}
定義公用模板:
# views/common.html <h1>公用內容</h1>
引入公用模板:
{% extends 'base.html'%} {% block main %} {% include 'common.html' %} <!-- 模板引入 --> {% end %}
模板中訪問靜態資源方式有兩種,但首先你須要在application
的配置項中對其進行配置:
settings = { 'template_path': 'views', 'static_path': 'static', # 指定靜態文件目錄 'static_url_prefix': '/static/', # 若是使用靜態導入,則這個是前綴 }
推薦動態導入的方式:
<head lang="en"> <link href={{static_url("common.css")}} rel="stylesheet" /> </head>
也可進行靜態導入:
<head lang="en"> <link href="/static/common.css" rel="stylesheet" /> </head>
容許定義全局可用的方法,以便在模板中進行調用。
第一步,建立獨立的一個.py
文件,而且書寫函數便可:
# ./templates_methods # 全部模板公用函數必定有self def add(self, x, y): return x + y # 若是方法中返回的是html字符串,則會被轉義掉 def input(self, type, name): return f"<input type={type} name={name}>"
第二步,在appliction
註冊ui_methods
:
import tornado.ioloop import tornado.web # 導入自定義的py文件 import templates_methods class APIHandler(tornado.web.RequestHandler): def get(self): self.render("api.html") settings = { "debug": True, "template_path": "views", # 指定模板目錄 "static_path": "static", # 指定靜態文件目錄 "ui_methods": templates_methods, # 註冊 } application = tornado.web.Application([ tornado.web.url(r'/api', APIHandler), ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
第三步,在模板中使用:
{{ add(1,2) }} {% raw input('text','username') %} # 受限於ui_methods中返回字符串會通過轉義的設定,因此在前端上咱們選擇用raw來作轉義
ui_modules
定義一些公用的組件,在這裏返回的字符串將默認關閉HTML
字符轉義功能。
好比有一個多頁面網站,咱們但願對該網站中每個頁面都加上一則廣告窗口,就可使用ui_modules
。
首先第一步,廣告彈窗確定是一個獨立的組件,須要有HTML
代碼,CSS
樣式,JS
腳本,因此咱們能夠看一下ui_modules
中到底提供了什麼方法讓咱們對其進行實現。
簡單的作一下說明:
須要覆寫的方法 | 描述 |
---|---|
render() | 覆寫該方法,返回該UI模塊的輸出 |
embedded_javascript() | 覆寫該方法,返回一段JavaScript代碼字符串,它將會在模板中自動添加script標籤,而且該script標籤內部會填入該方法返回的JavaScript代碼字符串 |
javascript_files() | 覆寫該方法,返回值應當是str,它將會在模板中自動添加script標籤而且該script標籤的src屬性會指向該方法所返回的字符串,若是返回值是一個相對路徑,則會去application的settings中尋找靜態資源的path作拼接 |
embedded_css() | 覆寫該方法,返回一段CSS代碼字符串,它將會在模板中自動添加style標籤,而且該style標籤內部會填入該方法返回的CSS代碼字符串 |
css_files() | 覆寫該方法,返回值應當是str,它將會在模板中自動添加link標籤而且該link標籤的href屬性會指向該方法所返回的字符串,若是返回值是一個相對路徑,則會去application的settings中尋找靜態資源的path作拼接 |
html_head() | 重寫該方法,返回值將放置在<head />元素中的HTML字符串。 |
html_body() | 重寫該方法,返回值將放置在<body />元素末尾的HTML字符串。 |
render_string() | 渲染模板並將其做爲字符串返回。 |
下面咱們來寫一個很是簡單的廣告組件,新建一個叫ui_modules.py
的文件:
from tornado.web import UIModule class AD(UIModule): def render(self, *args, **kwargs): """ 模板調用時傳遞的參數分別放入args以及kwargs中 """ return "<div id='ad'>這是一段廣告</div>" def embedded_css(self): """ 注意:配置的CSS或者JS代碼是全局生效的,因此咱們應該只對AD組件作一個約束 """ return """ #ad{ width: 200px; height: 200px; position: fixed; left: 25%; line-height: 200px; text-align: center; background: red; } """
須要爲application
註冊一下這個ui_modules
:
import tornado.ioloop import tornado.web # 導入 import ui_modules class IndexHandler(tornado.web.RequestHandler): def get(self): self.render("index.html") class HomeHandler(tornado.web.RequestHandler): def get(self): self.render("home.html") settings = { "debug": True, "template_path": "views", # 指定模板目錄 "static_path": "static", # 指定靜態文件目錄 "ui_modules":ui_modules, # 註冊 } application = tornado.web.Application([ tornado.web.url(r'^/index/', IndexHandler, name="index"), tornado.web.url(r'^/home/', HomeHandler, name="home"), ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
而後只須要在模板中進行使用便可:
# views/index.html {% extends 'base.html' %} {% block title %} 主頁 {% end %} {% block main %} <h1>歡迎來到主頁</h1> {% module AD() %} {% end %}
如今咱們的index
就有這個組件了:
若是想在home
頁面中也加入這個組件,直接使用{% module AD() %}
便可。
也就是說,它會將一個文件進行讀取,以{{}}
或者{%%}
做爲分隔符,拿到其中的內容並進行替換。
舉個例子:
<h1>你們好,我是{{username}}</h1>
它內部會這樣進行拆分:
["<h1>你們好,我是","username","</h1>"]
而後將username
這一部分替換爲控制器視圖中的對應變量,假設username='yunya'
,最後就會變爲:
["<h1>你們好,我是","yunya","</h1>"]
固然在替換的時候也會進行模板字符串轉義的檢測,若是檢測出有字符<
或者>
等,則特換爲<
與>
等。
因此最後的結果就是:
<h1>你們好,我是yunya</h1>
操做cookie
有兩個最基本的方法:
方法 | 描述 |
---|---|
self.get_cookie() | 獲取cookie鍵值對 |
self.set_cookie() | 設置cookie鍵值對,參數expires可設置過時時間(日期:datetime/time,默認30天),expires_day設置過時天數(優先級更高),示例:expirse = time.time() + 60 * 60 * 24 * 7 |
簡單示例:
class APIHandler(tornado.web.RequestHandler): def get(self): if self.get_cookie("access"): self.write("你來訪問過了") else: self.set_cookie("access","yes") self.write("七天內可再次訪問")
cookies
是明文存儲在了用戶的瀏覽器中,所以可能會產生不安全的因素。
使用加密的cookies
來讓用戶隱私更加安全,你須要在application
的配置項中設定一個加密的鹽cookie_secret
,而後使用下面的兩個方法進行加密cookies
的操做:
方法 | 描述 |
---|---|
self.get_secure_cookie() | 獲取cookie鍵值對,並對其進行解密 |
self.set_secure_cookie() | 設置cookie鍵值對,將其與cookie_secret進行結合加密 |
簡單示例:
class APIHandler(tornado.web.RequestHandler): def get(self): if self.get_secure_cookie("access"): self.write("你來訪問過了") else: self.set_secure_cookie("access", "yes") self.write("七天內可再次訪問") settings = { "debug": True, "template_path": "views", # 指定模板目錄 "static_path": "static", # 指定靜態文件目錄 "cookie_secret": "0dsa0D9d0a%39433**das9))|ddsa", # cookie加密的鹽 } application = tornado.web.Application([ tornado.web.url(r'/api', APIHandler), ], **settings)
當前已經認證的用戶信息被保存在每個請求處理器的 self.current_user
當中, 同時在模板的 current_user
中也是。默認狀況下,current_user
爲 None
。
要在應用程序實現用戶認證的功能,你須要複寫請求處理中 get_current_user()
這 個方法,在其中斷定當前用戶的狀態,好比經過 cookie
。下面的例子讓用戶簡單地使用一個 nickname
登錄應用,該登錄信息將被保存到 cookie
中:
class BaseHandler(tornado.web.RequestHandler): def get_current_user(self): return self.get_secure_cookie("user") class MainHandler(BaseHandler): def get(self): if not self.current_user: self.redirect("/login") return name = tornado.escape.xhtml_escape(self.current_user) self.write("Hello, " + name) class LoginHandler(BaseHandler): def get(self): self.write('<html><body><form action="/login" method="post">' 'Name: <input type="text" name="name">' '<input type="submit" value="Sign in">' '</form></body></html>') def post(self): self.set_secure_cookie("user", self.get_argument("name")) self.redirect("/") application = tornado.web.Application([ (r"/", MainHandler), (r"/login", LoginHandler), ], cookie_secret="61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=")
對於那些必需要求用戶登錄的操做,可使用裝飾器 tornado.web.authenticated
。 若是一個方法套上了這個裝飾器,可是當前用戶並無登錄的話,頁面會被重定向到 login_url
(應用配置中的一個選項),上面的例子能夠被改寫成:
class MainHandler(BaseHandler): @tornado.web.authenticated def get(self): name = tornado.escape.xhtml_escape(self.current_user) self.write("Hello, " + name) settings = { "cookie_secret": "61oETzKXQAGaYdkL5gEmGeJJFuYh7EQnp2XdTP1o/Vo=", "login_url": "/login", } application = tornado.web.Application([ (r"/", MainHandler), (r"/login", LoginHandler), ], **settings)
若是你使用 authenticated
裝飾器來裝飾 post()
方法,那麼在用戶沒有登錄的狀態下, 服務器會返回 403 錯誤。
Tornado
內部集成了對第三方認證形式的支持,好比Google
的OAuth
。
參閱 auth
模塊 的代碼文檔以瞭解更多信息。 for more details. Checkauth
模塊以瞭解更多的細節。在Tornado
的源碼中有一個 Blog
的例子,你也能夠從那裏看到 用戶認證的方法(以及如何在 MySQL 數據庫中保存用戶數據)。
tornado
的cookies
操做相關原理很是簡單,以加密cookies
爲例:
寫cookie
過程:
將值進行base64加密
對除值之外的內容進行簽名,哈希算法(沒法逆向解析)
拼接 簽名 + 加密值
讀cookie
過程:
讀取 簽名 + 加密值
對簽名進行驗證
base64解密,獲取值內容
跨域請求僞造被稱爲CSRF
或者是XSRF
(Django
中稱其爲CSRF
,tornado
中稱其爲XSRF
)。
如何防止跨域請求僞造就是對該站發出的網頁添加上一個隨機字符串(隨機的cookie
),全部的向本網站後端提交的POST/PUT/PATCH/DELETE
請求都須要帶上這一隨機字符串,若是隨機字符串不是本網站後端發出或者壓根沒有,就認爲該次提交是一個僞造的請求。
驗證時tornado
會檢查這兩個點,知足任意一個點便可:
- 請求頭中有沒有X-XSRFToken的請求頭,若是有就檢查值
- 攜帶的參數中,有沒有_xsrf命名的鍵值對,若是有就檢查值
tornado
中如何開啓跨域請求僞造呢?只須要在application
的配置項中打開便可:
settings = { "xsrf_cookies": True, }
若是提交數據時沒有攜帶這個xsrf_cookies
,就會提示異常:
若是是form
表單提交,只須要在表單中添加{{ xsrf_form_html() }}
便可,這樣會知足第二種檢查機制:
<form action="/api" method="post"> {% raw xsrf_form_html() %} <p><input type="text" name="username"></p> <p><button type="submit">提交</button></p> </form>
它實際上是會返回給你一個hidden
的input
,在表單提交的時候也一塊兒發送了過去:
<input type="hidden" name="_xsrf" value="2|5a04ca78|fe6c8cdc75a4b2e304a9b2e3da98c7a4|1611128917">
表單發送數據時的參數數據是會進行url
編碼的,因此它的編碼格式就會變成下面這個樣子,而tornado
就檢查有沒有_xsrf
這個鍵值對以及值是否正確:
usernmae=xxx&_xsrf=xxx
若是是AJAX
異步提交POST/PUT/PATCH/DELETE
請求,則你須要在提交數據的請求頭中添加X-XSRFToken
的一組鍵值對,這樣會知足第一種檢查機制:
{% block main %} <form id="form"> <p><input type="text" name="username"></p> <p> <button type="button">提交</button> </p> </form> {% end %} {% block js %} <script src="https://code.jquery.com/jquery-3.1.1.min.js"></script> <script> // step01:定義獲取_xsrf值的函數 function getCookie(name) { var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); return r ? r[1] : undefined; } // step02:進行賦值 let xsrf = getCookie("_xsrf") // step03:在請求頭中設置X-XSRFToken的請求頭 $("button").on("click", function () { $.ajax({ type: "post", url:"/api", headers: {"X-XSRFToken": xsrf}, data: $("#form").serialize(), dataType: "text", success: ((res) => { console.log(res) }) }) }) </script> {% end %}
或者你也能夠按照第二種檢查機制來作,$.(form).serialize()
實際上會將數據進行url
編碼,你須要添加後面的_xsrf
與其對應值便可:
// step01:獲取_xsrf的值 function getCookie(name) { var r = document.cookie.match("\\b" + name + "=([^;]*)\\b"); return r ? r[1] : undefined; } // step02:在提交的數據中添加_xsrf與其值 $("button").on("click", function () { $.ajax({ type: "post", url:"/api", // url編碼中,&表明與,你能夠理解爲分割符,後面就是一組新的鍵值對 data: $("#form").serialize() + `&_xsrf=${getCookie('_xsrf')}`, dataType: "text", success: ((res) => { console.log(res) }) }) })
在tornado
中,異步非阻塞是其代言詞。
咱們知道tornado
處理一次HTTP
請求是以單線程加I/O
多路複用的epoll
模式實現的,因此在一次HTTP
請求中,你能夠發現Tornado
的控制器處理函數裏並無return
響應的寫法(固然你也能夠手動進行return None
),它內部會本身進行return
響應,這是一個很是關鍵的點,用下面這張圖進行一下說明,如何實現異步非阻塞(其實仍是有一些問題的):
tornado.gen.coroutine
協程模式裝飾器,使用yield
關鍵字來將任務包裝成協程任務。
1.這種方式必定要確保協程任務中不存在同步方法
2.若是控制器函數沒有進行gen.coroutine裝飾器進行修飾,單純使用yield時將不會具備非阻塞的效果
3.究其內部緣由,yield的一個控制器處理函數頭頂上若是具備gen.coroutine裝飾器,則內部會建立Future對象用於實現非阻塞,若是不具備gen.coroutine則不會建立Future對象
4.一言以蔽之,tornado.gen.coroutine必須與yield同時出現
以下所示,使用request
同步庫對谷歌發起請求(不可能成功返回),將產生阻塞:
import tornado.ioloop import tornado.web from tornado import gen import requests class Api1Handler(tornado.web.RequestHandler): # 經過async和await進行異步網絡請求 @gen.coroutine def get(self): result = yield self.callback() self.write(result) def callback(self): response = requests.request(method="GET",url="http://www.google.com") response_text = response.text return response_text class Api2Handler(tornado.web.RequestHandler): def get(self): self.write("api2") application = tornado.web.Application([ (r"/api1", Api1Handler), (r"/api2", Api2Handler), ]) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
若是採用異步網絡庫aiohttp
,就不會出現這樣的問題了,其仍是異步調用:
@gen.coroutine def get(self): response_text = yield self.callback() self.write(response_text) async def callback(self): async with aiohttp.ClientSession() as session: async with await session.get("http://www.google.com") as response: response_text = await response.text() return response_text
新版的tornado
全面依賴於asyncio
模塊,因此咱們只須要使用async
與await
便可開啓異步編程。
按照第一個協程模式裝飾器的示例,其實咱們能夠對其進行簡寫:
import tornado.ioloop import tornado.web import aiohttp class Api1Handler(tornado.web.RequestHandler): # 經過async和await進行異步網絡請求,若是替換成http://www.google.com # 這依舊不影響api2的訪問 async def get(self): async with aiohttp.ClientSession() as session: async with await session.get("http://www.cnblogs.com") as response: result = await response.text() self.write(result) class Api2Handler(tornado.web.RequestHandler): def get(self): self.write("api2") application = tornado.web.Application([ (r"/api1", Api1Handler), (r"/api2", Api2Handler), ]) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
tornado.web.asynchronous
裝飾器,將本次HTTP
請求轉變爲長鏈接的方式。
若是要斷開本次長鏈接,則必須使用self.finish()
方法:
經測試、6.0版本已移除
import tornado.ioloop import tornado.web class Api1Handler(tornado.web.RequestHandler): @tornado.web.asynchronous # step01:添加該裝飾器,本次HTTP請求將變爲長鏈接狀態 def get(self): with open(file="testDocument.text", mode="rb") as f: file_context = f.read() self.write(file_context) self.finish() # 手動結束本次長鏈接 class Api2Handler(tornado.web.RequestHandler): def get(self): self.write("api2") application = tornado.web.Application([ (r"/api1", Api1Handler), (r"/api2", Api2Handler), ]) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
tornado
的協程任務中必須是異步方法,所以tornado
內置一些異步組件。
好比自帶的異步發送網絡請求的HttpClient
庫。
另外還有一些第三方的異步模塊,如tornado-mysql
等。
如下是基本使用方式:
import tornado.ioloop import tornado.web import tornado.gen from tornado import httpclient class Api1Handler(tornado.web.RequestHandler): @tornado.gen.coroutine def get(self): client = httpclient.AsyncHTTPClient() response = yield client.fetch("http://www.cnblogs.com") self.write(response.body) application = tornado.web.Application([ (r"/api1", Api1Handler), ]) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
若是tornado
是新版,則也可使用下面的方式:
class Api1Handler(tornado.web.RequestHandler): # 新版:6.01測試經過 async def get(self): client = httpclient.AsyncHTTPClient() try: response = await client.fetch("http://www.cnblogs.com") except Exception as e: print(e) else: self.write(response.body)
咱們都知道,HTTP/WebSocket
都屬於應用層的協議,其自己是對TCP
協議的封裝。
那麼WebSocket
對於HTTP
協議來講有什麼不一樣呢?首先要從HTTP
協議特性入手。
HTTP協議是一種單主動協議,即只能由Browser端主動發送請求,而Server端只能被動迴應Browser端的請求,沒法主動向Browser端發送請求
WebSocket則是一種雙主動協議,Server端也可以主動向Browser端發送請求,該請求通常被稱之爲推送
WebSocket
必需要瀏覽器支持。
如下是WebSocket
的握手流程:
1.服務端開啓監聽 2.客戶端發送請求企圖創建鏈接 3.服務端進行三次握手,確認鏈接創建 4.客戶端生成一個隨機字符串,在請求頭中添加隨機字符串,超服務器發送過去(請求頭名字:Sec-WebSocket-Key) 5.服務端接收到請求,decode解碼出隨機字符串,經過sha1進行加密,而且把魔法字符串當鹽添加進去,而後經過base64編碼,將編碼完成後的數據超客戶端發送回去 6.客戶端進行驗證,將服務端返回的內容首先經過base64解碼,而後進行sha1+本地隨機字符串+魔法字符串進行比對,若是相同則表明websocket服務創建完成
魔法字符串是一個固定的值,以下:
258EAFA5-E914-47DA-95CA-C5AB0DC85B11
看Server
端代碼,理解上面的步驟:
import socket import base64 import hashlib def get_headers(data): """ 將請求頭格式化成字典 :param data: :return: """ header_dict = {} data = str(data, encoding='utf-8') for i in data.split('\r\n'): print(i) header, body = data.split('\r\n\r\n', 1) header_list = header.split('\r\n') for i in range(0, len(header_list)): if i == 0: if len(header_list[i].split(' ')) == 3: header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ') else: k, v = header_list[i].split(':', 1) header_dict[k] = v.strip() return header_dict sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8002)) sock.listen(5) conn, address = sock.accept() data = conn.recv(1024) headers = get_headers(data) # 提取請求頭信息 # 對請求頭中的sec-websocket-key進行加密 response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \ "WebSocket-Location: ws://%s%s\r\n\r\n" magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' value = headers['Sec-WebSocket-Key'] + magic_string ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url']) # 響應【握手】信息 conn.send(bytes(response_str, encoding='utf-8'))
當一個WebSocket
請求到來時,接收到請求的一方(server/browser)要對他的數據進行解析,如何進行數據解析是一個很是有趣的話題。
首先,一個WebSocket
的數據幀會大致上分爲三部分,(頭部、MASK
、數據體),咱們要研究的是如何區分這三部分。
1.將接收到的數據第1個字節進行提取,而且與b00001111作&運算(與),獲得一個數值。
2.若是該數值等於126,則頭部信息佔2個字節(第2字節和第3字節),MASK數據則是第4字節至第7字節,從第8字節開始均爲數據體部分。
3.若是該數值等於127,則頭部信息佔8個字節(第2字節至第9字節),MASK數據則是第10字節至第13字節,從第14字節開始均爲數據體部分。
4.若是該數值等於125,則無頭部信息,Mask數據是第2字節至第5字節,從第6字節開始均爲數據體部分。
而對數據體進行解析時,則會用到^
異或運算。
^ 按位異或運算符:當兩對應的二進位相異時,結果爲1
簡單的示例:
a = 0011 1100 b = 0000 1101 a^b = 0011 0001
下面是官網中提供的Js
解析數據體示例:
var DECODED = ""; for (var i = 0; i < ENCODED.length; i++) { DECODED[i] = ENCODED[i] ^ MASK[i % 4]; }
用Python
代碼進行實現 (Broswer
端已自動作好,可是若是咱們手動寫服務端,這些邏輯都須要本身搞明白):
info = conn.recv(8096) # 讀取數據 # step01:提取第一個字節的數據,與00001111(十進制127)進行與運算 payload_len = info[1] & 127 # step02:解析頭部、MASK部、體部信息 if payload_len == 126: extend_payload_len = info[2:4] mask = info[4:8] decoded = info[8:] elif payload_len == 127: extend_payload_len = info[2:10] mask = info[10:14] decoded = info[14:] else: extend_payload_len = None mask = info[2:6] decoded = info[6:] # step03:讀取數據體信息(官網示例) bytes_list = bytearray() for i in range(len(decoded)): # 核心代碼,數據體解析,異或運算 chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) body = str(bytes_list, encoding='utf-8') print(body)
其餘的一些知識點:
FIN:1bit Websocket不可一次接收過長的消息。因此用FIN來區分是否分片接收一條長消息。 若是是1表明這是單條消息,沒有後續分片了。而若是是0表明,表明此數據幀是否是一個完整的消息,而是一個消息的分片,而且不是最後一個分片後面還有其餘分片 RSV1, RSV2, RSV3: 1 bit each 必須是0,除非客戶端和服務端使用WS擴展時,能夠爲非0。 Opcode: 4bit 這個爲操做碼,表示對後面的有效數據荷載的具體操做,若是未知接收端須要斷開鏈接 %x0:表示連續幀 %x1:表示文本幀 %x2:表示二進制幀 %x3-7:保留用於其餘非控制幀 %x8:表示鏈接關閉 %x9:表示ping操做 %xA:表示pong操做 %xB-F:保留用於其餘控制幀 Mask: 1bit 是否進行過掩碼,好比客戶端給服務端發送消息,須要進行掩碼操做。而服務端到客戶端不須要 Payload Length: 7 bits, 7+16 bits, or 7+64 bits(上面已經寫過了) 「有效載荷數據」的長度(以字節爲單位):若是爲0-125,則爲有效載荷長度。 若是爲126,則如下2個字節解釋爲16位無符號整數是有效載荷長度。 若是是127,如下8個字節解釋爲64位無符號整數(最高有效位必須爲0)是有效載荷長度。 多字節長度數量以網絡字節順序表示。 注意在全部狀況下,必須使用最小字節數進行編碼長度,例如124字節長的字符串的長度不能編碼爲序列12六、0、124。有效載荷長度是「擴展數據」的長度+「應用程序數據」。 「擴展數據」的長度能夠是零,在這種狀況下,有效負載長度是 「應用程序數據」。 Masking-key: 0 or 4 bytes (32bit) 全部從客戶端傳送到服務端的數據幀,數據載荷都進行了掩碼操做,Mask爲1,且攜帶了4字節的Masking-key。若是Mask爲0,則沒有Masking-key。 Payload data: (x+y) bytes 「有效載荷數據」定義爲串聯的「Extension data」與「Application data」。 Extension data: x bytes 若是沒有協商使用擴展的話,擴展數據數據爲0字節。全部的擴展都必須聲明擴展數據的長度,或者能夠如何計算出擴展數據的長度。此外,擴展如何使用必須在握手階段就協商好。若是擴展數據存在,那麼載荷數據長度必須將擴展數據的長度包含在內。 Application data: y bytes 任意的應用數據,在擴展數據以後(若是存在擴展數據),佔據了數據幀剩餘的位置。載荷數據長度 減去 擴展數據長度,就獲得應用數據的長度。
當發送一個請求時,咱們須要對數據進行封裝。
如下是WebSocket
協議規定:
def send_msg(conn, msg_bytes): import struct token = b"\x81" # 協議規定,第一個字節必須是x81 length = len(msg_bytes) # 判斷長度 if length < 126: token += struct.pack("B", length) elif length <= 0xFFFF: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length) msg = token + msg_bytes conn.send(msg) return True
在JavaScript
中,啓用WebSocket
很是簡單,而且它已經將數據解析、數據發送都作好了。
直接用便可:
var ws = new WebSocket('ws://localhost:8080');
webSocket.readyState
用於查看當前的鏈接狀態:
switch (ws.readyState) { case WebSocket.CONNECTING: // do something 值是0,未鏈接 break; case WebSocket.OPEN: // do something 值爲1,表示鏈接成功,能夠通訊了。 break; case WebSocket.CLOSING: // do something 值爲2,表示鏈接正在關閉。 break; case WebSocket.CLOSED: // do something 值爲3,表示鏈接已經關閉,或者打開鏈接失敗。 break; default: // this never happens break; }
回調函數系列:
函數名稱 | 描述 |
---|---|
onopen | 用於指定鏈接成功後的回調函數 |
onclose | 用於指定鏈接關閉後的回調函數 |
onmessage | 用於指定收到服務器數據後的回調函數 |
onerror | 用於指定報錯時的回調函數 |
兩個基本方法:
方法名稱 | 描述 |
---|---|
send() | 用於向服務器發送數據 |
close() | 關閉鏈接 |
基本演示:
var ws = new WebSocket("ws://localhost:8080"); //申請一個WebSocket對象,參數是服務端地址,同http協議使用http://開頭同樣,WebSocket協議的url使用ws://開頭,另外安全的 WebSocket協議使用wss://開頭 ws.onopen = function(){ //當WebSocket建立成功時,觸發onopen事件 console.log("open"); ws.send("hello"); //將消息發送到服務端 } ws.onmessage = function(e){ //當客戶端收到服務端發來的消息時,觸發onmessage事件,參數e.data包含server傳遞過來的數據 console.log(e.data); } ws.onclose = function(e){ //當客戶端收到服務端發送的關閉鏈接請求時,觸發onclose事件 console.log("close"); } ws.onerror = function(e){ //若是出現鏈接、處理、接收、發送數據失敗的時候觸發onerror事件 console.log(error); }
onmessage
回調函數之接收二進制數據或字符串:
ws.onmessage = function(event){ if(typeOf event.data === String) { // 字符串 console.log("Received data string"); } if(event.data instanceof ArrayBuffer){ // 二進制 var buffer = event.data; console.log("Received arraybuffer"); } }
send()
方法之發送文本、發送文件、發送二進制數據:
// 發送文本 ws.send('your message'); // 發送文件 var file = document .querySelector('input[type="file"]') .files[0]; ws.send(file); // ArrayBuffer 二進制數據 // Sending canvas ImageData as ArrayBuffer var img = canvas_context.getImageData(0, 0, 400, 320); var binary = new Uint8Array(img.data.length); for (var i = 0; i < img.data.length; i++) { binary[i] = img.data[i]; } ws.send(binary.buffer);
手動用socket
實現websocket
服務端:
#!/usr/bin/env python # -*- coding:utf-8 -*- import socket import base64 import hashlib def get_headers(data): """ 將請求頭格式化成字典 :param data: :return: """ header_dict = {} data = str(data, encoding='utf-8') header, body = data.split('\r\n\r\n', 1) header_list = header.split('\r\n') for i in range(0, len(header_list)): if i == 0: if len(header_list[i].split(' ')) == 3: header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ') else: k, v = header_list[i].split(':', 1) header_dict[k] = v.strip() return header_dict def send_msg(conn, msg_bytes): """ WebSocket服務端向客戶端發送消息 :param conn: 客戶端鏈接到服務器端的socket對象,即: conn,address = socket.accept() :param msg_bytes: 向客戶端發送的字節 :return: """ import struct token = b"\x81" length = len(msg_bytes) if length < 126: token += struct.pack("B", length) elif length <= 0xFFFF: token += struct.pack("!BH", 126, length) else: token += struct.pack("!BQ", 127, length) msg = token + msg_bytes conn.send(msg) return True def run(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8003)) sock.listen(5) conn, address = sock.accept() data = conn.recv(1024) headers = get_headers(data) response_tpl = "HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection:Upgrade\r\n" \ "Sec-WebSocket-Accept:%s\r\n" \ "WebSocket-Location:ws://%s%s\r\n\r\n" value = headers['Sec-WebSocket-Key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) response_str = response_tpl % (ac.decode('utf-8'), headers['Host'], headers['url']) conn.send(bytes(response_str, encoding='utf-8')) while True: try: info = conn.recv(8096) except Exception as e: info = None if not info: break payload_len = info[1] & 127 if payload_len == 126: extend_payload_len = info[2:4] mask = info[4:8] decoded = info[8:] elif payload_len == 127: extend_payload_len = info[2:10] mask = info[10:14] decoded = info[14:] else: extend_payload_len = None mask = info[2:6] decoded = info[6:] bytes_list = bytearray() for i in range(len(decoded)): chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) body = str(bytes_list, encoding='utf-8') send_msg(conn,body.encode('utf-8')) sock.close() if __name__ == '__main__': run()
tornado
服務端:
import tornado.ioloop import tornado.web import tornado.websocket class WsHandler(tornado.websocket.WebSocketHandler): # 該類繼承RequestHandler類 def open(self): """ 鏈接成功後、自動執行 :return: """ # 超客戶端發送信息 self.write_message("鏈接成功") def on_message(self, message): """ 客戶端發送消息時,自動執行 :return: """ print(message) def on_close(self): """ 客戶端關閉鏈接時,,自動執行 :return: """ print("鏈接已關閉") class IndexHandler(tornado.web.RequestHandler): def get(self): self.render("index.html") settings = { "template_path": "views", } application = tornado.web.Application([ (r"/ws/", WsHandler), (r"/index/", IndexHandler), ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
模板文件:
<script> let ws = new WebSocket("ws://127.0.0.1:8888/ws/"); ws.onmessage = Event=>{ console.log(Event.data); ws.send("你好"); ws.close(); } </script>
tornado
自己支持WebSocket
,(Django&Flask
原生不支持)。
利用WebSocket
,構建網絡聊天室:
後端代碼:
#!/usr/bin/env python # -*- coding:utf-8 -*- import uuid import json import tornado.ioloop import tornado.web import tornado.websocket class IndexHandler(tornado.web.RequestHandler): def get(self): self.render('index.html') class ChatHandler(tornado.websocket.WebSocketHandler): # 用戶存儲當前聊天室用戶 waiters = set() # 用於存儲歷時消息 messages = [] def open(self): """ 客戶端鏈接成功時,自動執行,加載聊天記錄 :return: """ ChatHandler.waiters.add(self) uid = str(uuid.uuid4()) self.write_message(uid) for msg in ChatHandler.messages: self.write_message(msg) def on_message(self, message): """ 客戶端連發送消息時,自動執行,羣轉發消息 :param message: :return: """ msg = message ChatHandler.messages.append(msg) for client in ChatHandler.waiters: client.write_message(msg) def on_close(self): """ 客戶端關閉鏈接時,,自動執行 :return: """ ChatHandler.waiters.remove(self) def run(): settings = { 'template_path': 'views', 'static_path': 'static', } application = tornado.web.Application([ (r"/", IndexHandler), (r"/chat", ChatHandler), ], **settings) application.listen(8888) tornado.ioloop.IOLoop.instance().start() if __name__ == "__main__": run()
模板:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Python聊天室</title> </head> <body> <div> <input type="text" id="txt"> <input type="button" id="btn" value="提交" onclick="sendMsg();"/> <input type="button" id="close" value="關閉鏈接" onclick="closeConn();"/> </div> <div id="container" style="border: 1px solid #dddddd;margin: 20px;min-height: 500px;"> </div> <script type="text/javascript"> window.onload = () => { wsUpdater.start(); } var wsUpdater = { socket: null, uid: null, start: function () { var url = "ws://127.0.0.1:8888/chat"; wsUpdater.socket = new WebSocket(url); wsUpdater.socket.onmessage = function (event) { if (wsUpdater.uid) { // 解析信息 wsUpdater.showMessage(event.data); } else { // 第一次,獲取uid wsUpdater.uid = event.data; } } }, showMessage: function (content) { content = JSON.parse(content); let article = document.createElement("article"); let p_name = document.createElement("p"); let p_context = document.createElement("p") article.append(p_name); article.append(p_context); p_name.append(`${content.uid}`) p_name.style.textIndent = "2rem"; p_context.append(`${content.message}`) p_context.style.textIndent = "2rem"; document.querySelector("#container").append(article); } }; function sendMsg() { var msg = { uid: wsUpdater.uid, message: document.querySelector("#txt").value, }; wsUpdater.socket.send(JSON.stringify(msg)); } </script> </body> </html>
Session
是將用戶存儲的信息保存在服務器上,而後發送給用戶一段隨機字符串。
當用戶下次來時若是帶有該隨機字符串,則能獲取到保存的信息(表明已登陸),不然就獲取不到保存的信息(表明未登陸)。
其實本質仍是對cookie
的一次升級操做。
原生tronado
中未提供Seesion
操做,可是咱們能夠本身寫一個:
如下是最基礎的示例,將Session
放置在內存中。(Session
存儲時未進行加密,可對此作優化)
若是想放置在Redis
、File
等地方,原理也是同樣的。
#!/usr/bin/env python # -*- coding:utf-8 -*- import uuid import tornado.ioloop import tornado.web class Session(object): container = { # 用戶1-nid : {} } def __init__(self, handler): # 獲取用戶cookie,若是有,不操做,不然,給用戶生成隨即字符串 # - 寫給用戶 # - 保存在session nid = handler.get_cookie('session_id') if nid: if nid in Session.container: pass else: nid = str(uuid.uuid4()) Session.container[nid] = {} else: nid = str(uuid.uuid4()) Session.container[nid] = {} handler.set_cookie('session_id', nid) # nid當前訪問用戶的隨即字符串 self.nid = nid # 封裝了全部用戶請求信息 self.handler = handler def __setitem__(self, key, value): self.set(key, value) def __getitem__(self, item): return self.get(item) def __delitem__(self, key): self.delete(key) def get(self, item): return Session.container[self.nid].get(item) def set(self, key, value): Session.container[self.nid][key] = value def delete(self, key): del Session.container[self.nid][key] class MyHandler(tornado.web.RequestHandler): def initialize(self): self.session = Session(self) class IndexHandler(MyHandler): def get(self): if self.session.get("access"): self.write("你來訪問過了") else: self.session.set("access", "yes") self.write("七天內可再次訪問") settings = { 'template_path': 'views', 'static_path': 'statics', } application = tornado.web.Application([ (r'/index', IndexHandler), ], **settings) if __name__ == '__main__': application.listen(8888) tornado.ioloop.IOLoop.instance().start()
普通同步(單線程)阻塞服務器框架原理
經過select
與socket
咱們能夠開發一個微型的框架,使用select
實現I/O
多路複用監聽本地服務端socket
。
當有客戶端發送請求時,select
(Linux下爲epoll
)監聽的本地socket
發生變化,經過socket.accept()
獲得客戶端發送來的conn
(也是一個socket),並將conn
也添加到select
監聽列表裏。當客戶端經過conn
發送數據時,服務端select
監聽列表的conn
發生變化,咱們將conn
發送的數據(請求數據)接收保存並處理獲得request_header
與request_body
,而後能夠根據request_header
中的url
來匹配本地路由中的url
,而後獲得對應的控制器處理函數,而後將控制器處理函數的返回值(通常爲字符串)經過conn
發送回請求客戶端,而後將conn
關閉,而且移除select
監聽列表中的conn
,這樣一次網絡I/O
請求便算結束。
import socket import select class HttpRequest(object): """ 用戶封裝用戶請求信息 """ def __init__(self, content): """ :param content:用戶發送的請求數據:請求頭和請求體 """ self.content = content self.header_bytes = bytes() self.body_bytes = bytes() self.header_dict = {} self.method = "" self.url = "" self.protocol = "" self.initialize() self.initialize_headers() def initialize(self): temp = self.content.split(b'\r\n\r\n', 1) if len(temp) == 1: self.header_bytes += temp else: h, b = temp self.header_bytes += h self.body_bytes += b @property def header_str(self): return str(self.header_bytes, encoding='utf-8') def initialize_headers(self): headers = self.header_str.split('\r\n') first_line = headers[0].split(' ') if len(first_line) == 3: self.method, self.url, self.protocol = headers[0].split(' ') for line in headers: kv = line.split(':') if len(kv) == 2: k, v = kv self.header_dict[k] = v # class Future(object): # def __init__(self): # self.result = None def main(request): return "main" def index(request): return "indexasdfasdfasdf" routers = [ ('/main/',main), ('/index/',index), ] def run(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("127.0.0.1", 9999,)) sock.setblocking(False) sock.listen(128) inputs = [] inputs.append(sock) while True: rlist,wlist,elist = select.select(inputs,[],[],0.05) for r in rlist: if r == sock: """新請求到來""" conn,addr = sock.accept() conn.setblocking(False) inputs.append(conn) else: """客戶端發來數據""" data = b"" while True: try: chunk = r.recv(1024) data = data + chunk except Exception as e: chunk = None if not chunk: break # data進行處理:請求頭和請求體 request = HttpRequest(data) # 1. 請求頭中獲取url # 2. 去路由中匹配,獲取指定的函數 # 3. 執行函數,獲取返回值 # 4. 將返回值 r.sendall(b'alskdjalksdjf;asfd') import re flag = False func = None for route in routers: if re.match(route[0],request.url): flag = True func = route[1] break if flag: result = func(request) r.sendall(bytes(result,encoding='utf-8')) else: r.sendall(b"404") inputs.remove(r) r.close() if __name__ == '__main__': run()
二、Tornado
異步非阻塞實現原理
tornado
經過裝飾器 + Future
從而實現異步非阻塞。在控制器處理函數中若是加上gen.coroutine
且進行yield
時,會產生一個Future
對象,此時控制函數的類型是一個生成器,若是是self.write()
等操做將會直接返回,若是是Future
生成器對象的話將會把返回來的Future
對象添加到async_request_dict
中,先不給客戶端返回響應數據(此時能夠處理其餘客戶端的鏈接請求),等Future
對象的result
有值時再返回,還能夠設置超時時間,在規定的時間事後返回響應數據。 !! 關鍵是future
對象,future
對象裏有result
屬性,默認爲None
,當result
有值時再返回數據。
咱們看一下gen.coroutine
裝飾器的源碼,註釋裏有句話寫的很明瞭:
Functions with this decorator return a `.Future`. # 使用此函數做爲裝飾器將返回一個Future
雖然使用gen.coroutine
裝飾器會自動生成Future
,可是你任然能夠手動建立一個Future
並進行返回。
如下示例將展現Future
是依賴於result
,若是result
未設置值,則HTTP
請求不結束。
import tornado.ioloop import tornado.web from tornado import gen from tornado.concurrent import Future future = None # 全局變量 class MainHandler(tornado.web.RequestHandler): @gen.coroutine def get(self): global future future = Future() future.add_done_callback(self.done) # 本身返回future yield future def done(self, *args, **kwargs): self.write('Main') # 立馬寫入 self.finish() # 該請求完成! class IndexHandler(tornado.web.RequestHandler): def get(self): global future # 改變結果, future.set_result(None) self.write("Index") application = tornado.web.Application([ (r"/main", MainHandler), (r"/index", IndexHandler), ]) if __name__ == "__main__": application.listen(8888) tornado.ioloop.IOLoop.instance().start()
下面是手動實現異步非阻塞框架(自我感受仍是和tornado
有一些差別,下面這個代碼是必需要有HTTP
請求來纔會循環檢測Future
任務列表,tornado
中實際上是任務完成後自動就返回了,暫時也沒往深處研究....)
import socket import select import time class HttpRequest(object): """ 用戶封裝用戶請求信息 """ def __init__(self, content): """ :param content:用戶發送的請求數據:請求頭和請求體 """ self.content = content self.header_bytes = bytes() self.body_bytes = bytes() self.header_dict = {} self.method = "" self.url = "" self.protocol = "" self.initialize() self.initialize_headers() def initialize(self): temp = self.content.split(b'\r\n\r\n', 1) if len(temp) == 1: self.header_bytes += temp else: h, b = temp self.header_bytes += h self.body_bytes += b @property def header_str(self): return str(self.header_bytes, encoding='utf-8') def initialize_headers(self): headers = self.header_str.split('\r\n') first_line = headers[0].split(' ') if len(first_line) == 3: self.method, self.url, self.protocol = headers[0].split(' ') for line in headers: kv = line.split(':') if len(kv) == 2: k, v = kv self.header_dict[k] = v class Future(object): def __init__(self,timeout=0): self.result = None self.timeout = timeout self.start = time.time() def main(request): f = Future(5) return f def index(request): return "indexasdfasdfasdf" routers = [ ('/main/',main), ('/index/',index), ] def run(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("127.0.0.1", 9999,)) sock.setblocking(False) sock.listen(128) inputs = [] inputs.append(sock) async_request_dict = { # 'socket': futrue } while True: rlist,wlist,elist = select.select(inputs,[],[],0.05) for r in rlist: if r == sock: """新請求到來""" conn,addr = sock.accept() conn.setblocking(False) inputs.append(conn) else: """客戶端發來數據""" data = b"" while True: try: chunk = r.recv(1024) data = data + chunk except Exception as e: chunk = None if not chunk: break # data進行處理:請求頭和請求體 request = HttpRequest(data) # 1. 請求頭中獲取url # 2. 去路由中匹配,獲取指定的函數 # 3. 執行函數,獲取返回值 # 4. 將返回值 r.sendall(b'alskdjalksdjf;asfd') import re flag = False func = None for route in routers: if re.match(route[0],request.url): flag = True func = route[1] break if flag: result = func(request) if isinstance(result,Future): async_request_dict[r] = result else: r.sendall(bytes(result,encoding='utf-8')) inputs.remove(r) r.close() else: r.sendall(b"404") inputs.remove(r) r.close() for conn in async_request_dict.keys(): future = async_request_dict[conn] start = future.start timeout = future.timeout ctime = time.time() if (start + timeout) <= ctime : future.result = b"timeout" if future.result: conn.sendall(future.result) conn.close() del async_request_dict[conn] inputs.remove(conn) if __name__ == '__main__': run()
本文內容主要來源於網絡、一些代碼等都是手動測一遍結合本身想法就寫上去了。
另外,不少知識點都摘自武Sir
博客。
其實從異步非阻塞開始,我寫的就有點心虛了,由於大多數資料都是從網上找的自己也沒翻看過tornado
源碼,因此有一些地方深刻解讀會有一些衝突。
若是想了解底層可能會有偏差,甩個鍋先,可是基本上新版tornado
你要單純使用異步就簡單粗暴的async await
便可。
之後有空再看源碼回來填坑吧、^(* ̄(oo) ̄)^,感謝您的閱讀。
2021年1月28日凌晨12.01