Python Tornado系列(甩鍋版)

tornado簡介

   tornadoPython界中很是出名的一款Web框架,和Flask同樣它也屬於輕量級的Web框架。javascript

   可是從性能而言tornado因爲其支持異步非阻塞的特性因此對於一些高併發的場景顯得更爲適用。css

   tornado簡潔,高效,可以支持WebSocket,其I/O多路複用採用epoll模式來實現異步,而且還有Future期程對象來實現非阻塞。html

   tornadoDjangoFlask等基於WSGI的框架有一個根本的區別,就是它實現socket的模塊是本身寫的,並非用其餘模塊。前端

  A : socket部分 B: 路由與視圖函數對應關係(路由匹配) C: 模版語法
django 別人的wsgiref模塊 本身寫 本身的(沒有jinja2好用 可是也很方便)
flask 別人的werkzeug(內部仍是wsgiref模塊) 本身寫 別人的(jinja2)
tornado 本身寫的 本身寫 本身寫

  

起步介紹

   如何編寫一個最簡單的tornadojava

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>

HTTP請求處理方式

   下面未來探究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的異步

   要想了解tornado的異步,就結合前面請求處理方式來看。

   同步Web框架與異步Web框架的區別,這裏有一篇文章寫的不錯:

   同步與異步 Python 有何不一樣?

   上面文章的一句話來歸納就是說同步大多數都是監聽一個socket對象(服務器),當服務器對象的描述符狀態(可讀)發生改變後,就會建立一個新的線程來處理本次請求,Django/Flask內部其實都是經過wsgiref模塊實現,而且wsgiref依賴於socketserver模塊。若是想了解他們如何啓動多線程進行服務監聽,可參照早期文章(調用方式如出一轍):

   socketserver使用及源碼分析

   而對於tornado來講,它不會建立多線程,而是將conn雙向鏈接對象放入事件循環中予以監聽。

   得益於epoll的主動性,tornado的速度很是快,而在處理完conn(本次會話後),則會將connSocket)進行斷開。 (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或者autoreloadTrue時,修改源文件代碼將會自動重啓服務,至關於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_cookieset_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中不支持.的深度查詢訪問,這點與DTLJinJa2不一樣:

<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內容,如&lt;&gt;等。

   關於模板轉義的方式有如下幾種。

   1.單變量去除轉義:

{{'<b>你好</b>'}}   	 # &lt;b&gt;你好&lt;/b&gt;
{% 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就有這個組件了:

  image-20210119234518053

   若是想在home頁面中也加入這個組件,直接使用{% module AD() %}便可。

模板原理

  • 使用內置的open函數讀取Html文件中的內容
  • 根據模板語言的標籤分割Html文件的內容,例如:{{}} 或 {%%}
  • 將分割後的部分數據塊格式化成特殊的字符串(表達式)
  • 經過python的內置函數執行字符串表達式,即:將html文件的內容和嵌套的數據整合
  • 將數據返回給請求客戶端

   也就是說,它會將一個文件進行讀取,以{{}}或者{%%}做爲分隔符,拿到其中的內容並進行替換。

   舉個例子:

<h1>你們好,我是{{username}}</h1>

   它內部會這樣進行拆分:

["<h1>你們好,我是","username","</h1>"]

   而後將username這一部分替換爲控制器視圖中的對應變量,假設username='yunya',最後就會變爲:

["<h1>你們好,我是","yunya","</h1>"]

   固然在替換的時候也會進行模板字符串轉義的檢測,若是檢測出有字符<或者>等,則特換爲&lt;&gt;等。

   因此最後的結果就是:

<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_userNone

   要在應用程序實現用戶認證的功能,你須要複寫請求處理中 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內部集成了對第三方認證形式的支持,好比GoogleOAuth

   參閱 auth 模塊 的代碼文檔以瞭解更多信息。 for more details. Checkauth 模塊以瞭解更多的細節。在Tornado的源碼中有一個 Blog的例子,你也能夠從那裏看到 用戶認證的方法(以及如何在 MySQL 數據庫中保存用戶數據)。

內部原理

   tornadocookies操做相關原理很是簡單,以加密cookies爲例:

   寫cookie過程:

   將值進行base64加密

   對除值之外的內容進行簽名,哈希算法(沒法逆向解析)

   拼接 簽名 + 加密值

   讀cookie過程:

  讀取 簽名 + 加密值

  對簽名進行驗證

  base64解密,獲取值內容

XSRF跨域請求僞造

基本介紹

   跨域請求僞造被稱爲CSRF或者是XSRFDjango中稱其爲CSRFtornado中稱其爲XSRF)。

   Django中間件與CSRF跨域請求僞造

   如何防止跨域請求僞造就是對該站發出的網頁添加上一個隨機字符串(隨機的cookie),全部的向本網站後端提交的POST/PUT/PATCH/DELETE請求都須要帶上這一隨機字符串,若是隨機字符串不是本網站後端發出或者壓根沒有,就認爲該次提交是一個僞造的請求。

   驗證時tornado會檢查這兩個點,知足任意一個點便可:

  1. 請求頭中有沒有X-XSRFToken的請求頭,若是有就檢查值
  2. 攜帶的參數中,有沒有_xsrf命名的鍵值對,若是有就檢查值

   tornado中如何開啓跨域請求僞造呢?只須要在application的配置項中打開便可:

settings = {
    "xsrf_cookies": True,
}

   若是提交數據時沒有攜帶這個xsrf_cookies,就會提示異常:

  image-20210120154712426

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>

   它實際上是會返回給你一個hiddeninput,在表單提交的時候也一塊兒發送了過去:

<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響應,這是一個很是關鍵的點,用下面這張圖進行一下說明,如何實現異步非阻塞(其實仍是有一些問題的):

  image-20210127231705066

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模塊,因此咱們只須要使用asyncawait便可開啓異步編程。

   按照第一個協程模式裝飾器的示例,其實咱們能夠對其進行簡寫:

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等。

   tornadoHttpClient官方文檔

   如下是基本使用方式:

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必需要瀏覽器支持。

  img

握手流程

   如下是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、數據體),咱們要研究的是如何區分這三部分。

  img

   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

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操做,可是咱們能夠本身寫一個:

  image-20210127220319215

  

  如下是最基礎的示例,將Session放置在內存中。(Session存儲時未進行加密,可對此作優化)

  若是想放置在RedisFile等地方,原理也是同樣的。

#!/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探究

  1.   普通同步(單線程)阻塞服務器框架原理

      經過selectsocket咱們能夠開發一個微型的框架,使用select實現I/O多路複用監聽本地服務端socket

      當有客戶端發送請求時,select(Linux下爲epoll)監聽的本地socket發生變化,經過socket.accept()獲得客戶端發送來的conn(也是一個socket),並將conn也添加到select監聽列表裏。當客戶端經過conn發送數據時,服務端select監聽列表的conn發生變化,咱們將conn發送的數據(請求數據)接收保存並處理獲得request_headerrequest_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()

tornado源碼流程圖示

  

  img

寫在最後

   本文內容主要來源於網絡、一些代碼等都是手動測一遍結合本身想法就寫上去了。

   另外,不少知識點都摘自武Sir博客。

   歡迎訪問武Sir博客地址

   其實從異步非阻塞開始,我寫的就有點心虛了,由於大多數資料都是從網上找的自己也沒翻看過tornado源碼,因此有一些地方深刻解讀會有一些衝突。

   若是想了解底層可能會有偏差,甩個鍋先,可是基本上新版tornado你要單純使用異步就簡單粗暴的async await便可。

   之後有空再看源碼回來填坑吧、^(* ̄(oo) ̄)^,感謝您的閱讀。

   2021年1月28日凌晨12.01

相關文章
相關標籤/搜索