tornado簡介
tornado
是Python
界中非常出名的一款Web
框架,和Flask
一樣它也屬於輕量級的Web
框架。
但是從效能而言tornado
由於其支援非同步非阻塞的特性所以對於一些高併發的場景顯得更為適用。
tornado
簡潔,高效,能夠支援WebSocket
,其I/O
多路複用採用epoll
模式來實現非同步,並且還有Future
期程物件來實現非阻塞。
tornado
與Django
和Flask
等基於WSGI
的框架有一個根本的區別,就是它實現socket
的模組是自己寫的,並不是用其他模組。
A : socket部分 | B: 路由與檢視函式對應關係(路由匹配) | C: 模版語法 | |
---|---|---|---|
django | 別人的wsgiref模組 | 自己寫 | 自己的(沒有jinja2好用 但是也很方便) |
flask | 別人的werkzeug(內部還是wsgiref模組) | 自己寫 | 別人的(jinja2) |
tornado | 自己寫的 | 自己寫 | 自己寫 |
起步介紹
如何編寫一個最簡單的tornado
:
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()
模板檔案:
<!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>
HTTP請求處理方式
下面將來探究Django/Flask/tornado
如何處理一次HTTP
請求:
Django
中處理一次HTTP
請求預設是以多執行緒模式完成。
在DJnago1.x版本之後,預設啟動都是多執行緒形式,如果要啟用單執行緒模式:
python manage.py runserver --nothreading
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的非同步
要想了解tornado
的非同步,就結合前面請求處理方式來看。
同步Web
框架與非同步Web
框架的區別,這裡有一篇文章寫的不錯:
上面文章的一句話來概括就是說同步大多數都是監聽一個socket
物件(伺服器),當伺服器物件的描述符狀態(可讀)發生改變後,就會建立一個新的執行緒來處理本次請求,Django/Flask
內部其實都是通過wsgiref
模組實現,並且wsgiref
依賴於socketserver
模組。如果想了解他們如何啟動多執行緒進行服務監聽,可參照早期文章(呼叫方式一模一樣):
而對於tornado
來說,它不會建立多執行緒,而是將conn
雙向連線物件放入事件迴圈中予以監聽。
得益於epoll
的主動性,tornado
的速度非常快,而在處理完conn
(本次會話後),則會將conn
(Socket
)進行斷開。 (HTTP
短連結)
tornado的非阻塞
拿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
tornado
實現非同步的根本技術點:I/O
多路複用的epoll
模式
tornado
實現非阻塞的根本技術點:Future
期程(未來)物件
tornado配置項
仔細看起步介紹中,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方法的關鍵字引數的字典。 |
url與路由
正則匹配
在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>
ui_methods
允許定義全域性可用的方法,以便在模板中進行呼叫。
第一步,建立獨立的一個.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
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() %}
即可。
模板原理
- 使用內建的open函式讀取Html檔案中的內容
- 根據模板語言的標籤分割Html檔案的內容,例如:{{}} 或 {%%}
- 將分割後的部分資料塊格式化成特殊的字串(表示式)
- 通過python的內建函式執行字串表示式,即:將html檔案的內容和巢狀的資料整合
- 將資料返回給請求客戶端
也就是說,它會將一個檔案進行讀取,以{{}}
或者{%%}
作為分隔符,拿到其中的內容並進行替換。
舉個例子:
<h1>大家好,我是{{username}}</h1>
它內部會這樣進行拆分:
["<h1>大家好,我是","username","</h1>"]
然後將username
這一部分替換為控制器檢視中的對應變數,假設username='yunya'
,最後就會變為:
["<h1>大家好,我是","yunya","</h1>"]
當然在替換的時候也會進行模板字串轉義的檢測,如果檢測出有字元<
或者>
等,則特換為<
與>
等。
所以最後的結果就是:
<h1>大家好,我是yunya</h1>
cookies
基本操作
操作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
是明文儲存在了使用者的瀏覽器中,因此可能會產生不安全的因素。
使用加密的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解密,獲取值內容
XSRF跨域請求偽造
基本介紹
跨域請求偽造被稱為CSRF
或者是XSRF
(Django
中稱其為CSRF
,tornado
中稱其為XSRF
)。
如何防止跨域請求偽造就是對該站發出的網頁新增上一個隨機字串(隨機的cookie
),所有的向本網站後端提交的POST/PUT/PATCH/DELETE
請求都需要帶上這一隨機字串,如果隨機字串不是本網站後端發出或者壓根沒有,就認為該次提交是一個偽造的請求。
驗證時tornado
會檢查這兩個點,滿足任意一個點即可:
- 請求頭中有沒有X-XSRFToken的請求頭,如果有就檢查值
- 攜帶的引數中,有沒有_xsrf命名的鍵值對,如果有就檢查值
tornado
中如何開啟跨域請求偽造呢?只需要在application
的配置項中開啟即可:
settings = {
"xsrf_cookies": True,
}
如果提交資料時沒有攜帶這個xsrf_cookies
,就會提示異常:
form表單提交
如果是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請求提交
如果是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
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
async await
新版的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
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()
HttpClient庫
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)
websocket
基礎介紹
我們都知道,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位元組長的字串的長度不能編碼為序列126、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
js演示
在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服務端
手動用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示例
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聊天室
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>
tornado其他
自定義Session
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()
Futer探究
-
普通同步(單執行緒)阻塞伺服器框架原理
通過
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()
2、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()
tornado原始碼流程圖示
寫在最後
本文內容主要來源於網路、一些程式碼等都是手動測一遍結合自己想法就寫上去了。
另外,很多知識點都摘自武Sir
部落格。
其實從非同步非阻塞開始,我寫的就有點心虛了,因為大多數資料都是從網上找的本身也沒翻看過tornado
原始碼,所以有一些地方深入解讀會有一些衝突。
如果想了解底層可能會有誤差,甩個鍋先,但是基本上新版tornado
你要單純使用非同步就簡單粗暴的async await
即可。
以後有空再看原始碼回來填坑吧、^(* ̄(oo) ̄)^,感謝您的閱讀。
2021年1月28日凌晨12.01