Tornado應(yīng)用筆記02-Web框架

索引

本節(jié)內(nèi)容圍繞Tornado的Web框架部分展開, 主要介紹Tornado在Web框架部分中使用頻率最高的RequestHandler, 同時也包括Application等其余相關(guān)內(nèi)容.

RequestHandler

作為每一個HTTP請求的"必經(jīng)之地", 一個請求在RequestHandler內(nèi)的大致處理流程如下:

  1. 根據(jù)正則匹配創(chuàng)建相應(yīng)RequestHandler
  2. .initialize()初始化
  3. .prepare()準備
  4. 根據(jù)請求的http verb method進入相應(yīng)入口, 如.get() .post()
  5. .finish()完成請求
  6. .on_finish()后續(xù)操作

(注: 這個流程是不夠嚴謹?shù)? 只是希望讀者對此能先有個大概的認識)

RequestHandler內(nèi)的方法可以劃分成以下幾類: 入口, 輸入, 輸出, Cookie和其他, 這里只分析其中最常用的方法, 如果想要了解全部內(nèi)容則需要查閱官方文檔.

入口(參考鏈接):

.initialize()

進行初始化工作, 可以接收來自注冊路由時傳遞的參數(shù). 雖然這里也可以做輸出操作, 但是并不建議這么做, 輸出操作放到.prepare()會使邏輯更清晰.


.prepare()

可以理解為一個請求"真正"的開始, 主要用來處理一些請求的準備工作, 比如預(yù)處理請求, 也可以做輸出操作. 完成以后進入到.get() .post()等. 需要注意的是, 如果在這里結(jié)束請求, 如調(diào)用.finish()等, 那就不會執(zhí)行.get() .post()等. 有一個比較有意思的點是.prepare()是可以"異步"的, 更準確的說法應(yīng)該是可以"協(xié)程化", 通過@gen.coroutine@return_future可以實現(xiàn)(不能使用@asynchronous). 關(guān)于Tornado實現(xiàn)協(xié)程和異步的方法, 后續(xù)會有文章深入探討, 這里就不展開說了.


.on_finish()

請求完成后自動調(diào)用(實際上是由.finish()調(diào)用的), 可以根據(jù)需要做一些釋放資源或?qū)懭罩镜炔僮? 注意, 這里是不能進行輸出操作的.

默認支持的http verb method
.get() .post() .put() .patch() .delete() .head() .options()


跑一個例子能更好的理解這個流程

# -*- coding: utf-8 -*-
# file: request_entry_point.py

import tornado.ioloop
import tornado.web


class BaseHandler(tornado.web.RequestHandler):
    # 擴展默認http方法的辦法
    SUPPORTED_METHODS = tornado.web.RequestHandler.SUPPORTED_METHODS + ('PROPFIND',)

    def initialize(self, **kwargs):
        print 'into initialize'
        self._id = kwargs.get('id', -1)

    def prepare(self):
        print 'into prepare'

    def on_finish(self):
        print 'into finish'
        self.release_resource()

    def release_resource(self):
        pass

    def get(self, *args, **kwargs):
        print 'into get'
        arg = args[0] or None
        self.write('a get request, the re arg is |%s|' % arg)

    def propfind(self, *args, **kwargs):
        print 'into extra method propfind'
        self.write('a propfind request')


def make_app():
    return tornado.web.Application([
        # 正則匹配的參數(shù)會傳入http方法中
        (r"/(.*)", BaseHandler, {'id': '123456'}),
    ])


if __name__ == "__main__":
    app = make_app()
    app.listen(8888)
    print 'Tornado server is running at localhost:8888'
    tornado.ioloop.IOLoop.current().start()

輸入(參考鏈接):

請求參數(shù)
.get_argument() .get_arguments()

bodyurl中獲取參數(shù)(參數(shù)都是unicode編碼的), 兩者不同點在于.get_arguments()返回的是參數(shù)列表, 而.get_argument()返回參數(shù)列表的最后一個參數(shù), 并且.get_argument()會在目標參數(shù)不存在的時候拋出MissingArgumentError異常.


.get_query_argument() .get_query_arguments()

url中獲取參數(shù), 區(qū)別參考.get_argument() .get_arguments()


.get_body_argument() .get_body_arguments()

body中獲取參數(shù), 區(qū)別參考.get_argument() .get_arguments()


.get_json()

實際上, Tornado并未直接提供獲取json格式數(shù)據(jù)的方法, 如果有需要的話, 可以參考下面這段代碼

def get_json(self):
    import json
    content_type = self.request.headers.get('Content-Type')
    if content_type and content_type.lower().startswith('application/json'):
        try:
            return json.loads(self.request.body)
        except ValueError:
            pass
    raise Exception('get json fail, please check content-type |%s| & body |%s|' % (content_type, body))
請求信息
.request

.request實際上是一個HTTPServerRequest對象, 包含method uri query version headers body remote_ip protocol host arguments query_arguments body_arguments files connection cookies full_url() request_time().

這里只介紹headersfiles(cookies放在后面與相關(guān)方法一起介紹), 其余的可以參考官方文檔, 又或是print出來看看是什么.

在上傳文件時(Content-Type: multipart/form-data; boundary=----WebKitFormBoundary*random_string*), 文件變?yōu)?a target="_blank" rel="nofollow">HTTPFile對象

# .request.files的結(jié)構(gòu)
{
    'arg_name': [
        {
            'body': '********',
            'content_type': 'image/png',
            'filename': 'picture.png',
        },
    ]
}

headers 是一個HTTPHeaders對象, 使用方法參考:

# 獲取元素
print self.request.headers.get('Content-Type')
print self.request.headers['Content-Type']
# 可以直接轉(zhuǎn)字典
print json.dumps(dict(self.request.headers), indent=2)

輸出(參考鏈接):

HTTP status
.set_status()

設(shè)置響應(yīng)HTTP狀態(tài)碼


.send_error() .write_error()

.send_error()用于發(fā)送HTTP錯誤頁(狀態(tài)碼). 該操作會調(diào)用.clear() .set_status() .write_error()用于清除headers, 設(shè)置狀態(tài)碼, 發(fā)送錯誤頁. 重寫.write_error()可以自定義錯誤頁.

HTTP header
.add_header() .set_header() .set_default_headers()

設(shè)置響應(yīng)HTTP頭, 前兩者的不同點在于多次設(shè)置同一個項時, .add_header()會"疊加"參數(shù), 而.set_header()則以最后一次為準.

# add_header
self.add_header('Foo', 'one')
self.add_header('Foo', 'two')
# set_header
self.set_header('Bar', 'one')
self.set_header('Bar', 'two')

# HTTP頭的設(shè)置結(jié)果
# Foo → one, two
# Bar → two

.set_default_headers()比較特殊, 是一個空方法, 可根據(jù)需要重寫, 作用是在每次請求初始化RequestHandler時設(shè)置默認headers.

.clear_header() .clear()

.clear_header()清除指定的headers, 而.clear()清除.set_default_headers()以外所有的headers設(shè)置.

數(shù)據(jù)流
.write()

將數(shù)據(jù)寫入輸出緩沖區(qū). 如果直接傳入dict, 那Tornado會自動將其識別為json, 并把Content-Type設(shè)置為application/json, 如果你不想要這個Content-Type, 那么在.write()之后, 調(diào)用.set_header()重新設(shè)置就好了. 需要注意的是, 如果直接傳入的是list, 考慮到安全問題(json數(shù)組會被認為是一段可執(zhí)行的JavaScript腳本, 且<script src="*/secret.json">可以繞過跨站限制), list將不會被轉(zhuǎn)換成json.


.flush()

將輸出緩沖區(qū)的數(shù)據(jù)寫入socket. 如果設(shè)置了callback, 會在完成數(shù)據(jù)寫入后回調(diào). 需要注意的是, 同一時間只能有一個"等待"的flush callback, 如果"上一次"的flush callback還沒執(zhí)行, 又來了新的flush, 那么"上一次"的flush callback會被忽略掉.


.finish()

完成響應(yīng), 結(jié)束本次請求. 通常情況下, 請求會在return時自動調(diào)用.finish(), 只有在使用了異步裝飾器@asynchronous或其他將._auto_finish設(shè)置為False的操作, 才需要手動調(diào)用.finish().

頁面
.render()

返回渲染完成的html. 調(diào)用后不能再進行輸出操作.

.redirect()

重定向, 可以指定3xx重定向狀態(tài)碼. 調(diào)用后不能再進行輸出操作.

# 臨時重定向 301
self.redirect('/foo')
# 永久重定向 302
self.redirect('/foo', permanent=True)
# 指定狀態(tài)碼, 會忽略參數(shù) permanent
self.redirect('/foo', status=304)

Cookie(參考鏈接):

獲取
.cookies

.request.cookies的別名, Cookie.SimpleCookie()對象(了解更多).

# 如果你想查看字典形式的 cookies, 可以用下面的方法
cookies = self.cookies
print json.dumps({k: cookies[k].value for k in cookies}, indent=2)
設(shè)置和解析
.set_cookie() .set_secure_cookie() .get_cookie() .get_secure_cookie()

設(shè)置和解析cookies. 兩組方法的用法基本一致, 不過使用.set_secure_cookie() .get_secure_cookie()前需要在Application中設(shè)置cookie_secret.

import time
# 設(shè)置 a=aa; httponly; Path=/ 3600秒后過期
self.set_cookie('a', 'aa', httponly=True, expires=time.time() + 3600)
# 設(shè)置 b=bb; secure; Path=/ 1天后過期
self.set_cookie('b', 'bb', secure=True, expires_days=1)
# 獲取 cookie 值
self.get_cookie('a', 'default value')
清除
.clear_cookie() .clear_all_cookies()

清除cookie. 前者清除指定值, 后者清除所有.

安全簽名
.create_signed_value()

這個方法比較特殊, 作用是生成一個難以被偽造的帶時間戳的加密字符串, 這是.set_secure_cookie()之所以"secure"的關(guān)鍵. 同樣也需要先在Application中設(shè)置cookie_secret.

# 生成安全簽名
secure_sign = self.create_signed_value('foo', '123')
# 同樣也是用 get_secure_cookie 解密, 不過需要傳入可選參數(shù) value
result = self.get_secure_cookie('foo', secure_sign)

其他(參考鏈接)

Application
.application

獲取處理這個請求的Application對象. 可以用來訪問Application內(nèi)部的變量.


.setting

.application.setting 的別名, 用于獲取Application當前配置(dict格式).


.require_setting()

查詢Application是否有配置此選項, 如果沒有會觸發(fā)異常.

用戶驗證
.current_user .get_current_user()

獲取當前用戶. 只有第一次在請求內(nèi)調(diào)用.current_user時, 才會通過.get_current_user()獲取當前用戶, 所以.current_user相當于當前用戶的緩存. .get_current_user()是一個需要復(fù)寫的空方法, 用于獲取當前用戶.


.get_login_url()

獲取登錄頁面鏈接. Tornado內(nèi)置的身份驗證是由@authenticated .current_user .get_login_url()實現(xiàn)的. 使用@authenticated后, 會在.current_userNone時跳轉(zhuǎn)到login_url. 默認情況下, 使用.get_login_url()需要先在Application設(shè)置login_url, 當然也可以通過復(fù)寫.get_login_url()免去配置, 同時也能更加靈活的配置登錄鏈接.

防御跨站請求偽造
.xsrf_form_html()

內(nèi)置的防御跨站請求偽造功能, 需要放在html里面, 使用前要在Application設(shè)置cookie_secret xsrf_cookies. 實現(xiàn)原理是給把兩個由同一token簽名過的字符串分別放置在cookiehtml中, 然后在"正式"處理請求前, 解密這兩個字符串然后比對token是否相同.

<!-- `.xsrf_form_html()`參考用法 -->
 <form action="/login" method="post">
    {% raw xsrf_form_html() %}
    <input type="text" name="message"/>
    <input type="submit" value="Post1"/>
</form>

有意思的是token的比較并不是簡單采用a == b這種方式, 而是使用了一個叫_time_independent_equals的函數(shù). 為什么要繞一大圈呢? 實際上是出于安全的考慮, 常規(guī)的比較方法如a == b, 一旦發(fā)現(xiàn)兩者的不同點, 就會立即退出比較, 這樣好像確實也沒什么不妥的, 從頭到尾比較兩個字符串確實太低效. 不過既然考慮到了安全, 就不能以常規(guī)的角度去看.

現(xiàn)在我們假設(shè)比較一個字符串的時間是1s(當然這是極度夸張放大的耗時), 此時我們需要匹配一個長度為3的字符串, 那么按照a == b比較法, 在命中第一個字符后繼續(xù)比較第二個字符, 那么此次比較耗時肯定是大于1s的, 如果沒有命中第一個字符, 那么耗時是1s. 這樣的話, 現(xiàn)在我不就能根據(jù)耗時"猜出"我給的第一個字符是否匹配了嗎. 當然在實際情況下, 不可能有如此夸張的時間差, 但倘若攻擊者能夠發(fā)起大量請求并分析其結(jié)果的話, 這也并不是"mission impossible", 所以做一個"恒時"匹配還是有比要的.

# 這種比較方法, 只有兩種"常量"耗時
# 一是比較兩者長度的耗時,  二是在一的基礎(chǔ)上疊加完全匹配兩者的耗時
# 需要注意的是, 這里的"常量"指的是它的耗時幾乎只受字符串長度的影響
def _time_independent_equals(a, b):
    if len(a) != len(b):
        return False
    result = 0
    # a, b = 'abc', 'def'
    # zip(a, b) => [('a', 'd'), ('b', 'e'), ('c', 'f')]
    for x, y in zip(a, b):
        result |= ord(x) ^ ord(y)
    return result == 0

Application(參考鏈接)

Application的初始化可以考慮采用下面的方法. Application中的可配置項有很多, 這里只挑了其中最常用的做解釋, 想了解更多關(guān)于配置的內(nèi)容可以進入到上方的參考鏈接查看.

class Application(tornado.web.Application):
    def __init__(self):
        """
            以 /xxxx/([\d]*)/ 為例
            "([\d]*)"會以參數(shù)的形式傳遞給路由,
            在下面的情況, "([\d]*)", 就是 yyyy
            def MyHandler(BaseHandler):
                get(self, yyyy):
                    print yyyy

            對于有多組匹配的路由, 參數(shù)會按從左到右的順序傳遞給路由
            以 /xxxx/([\w\-]+)/([\d]*)/ 為例,
            "([\w\-]+)" 對應(yīng) yyyy, "([\d]*)" 對應(yīng) zzzz,
            def MyHandler(BaseHandler):
                get(self, yyyy, zzzz):
                    print yyyy, zzzz
        """
        handlers = [
            (r'/skill/([\w\-]+)/', SkillHandler),
        ]

        tornado_settings = dict(
            # debug 模式開關(guān), 開啟 debug 文件變化時自動重載
            debug=False,

            # 自帶防跨站腳本, 開啟后將驗證每次請求的 _xsrf 參數(shù)
            xsrf_cookies=True,

            # 模板和靜態(tài)文件路徑配置
            template_path=os.path.join(os.path.dirname(__file__), 'templates'),
            static_path=os.path.join(os.path.dirname(__file__), 'static'),

            # sercure_cookie 秘鑰
            cookie_secret=config.COOKIE_SECRET,

            # 與tornado自帶的驗證配合, 未登錄下用戶重定向 uri
            login_url='/login/',

            # log_function 可用于記錄完整的請求信息, 復(fù)寫可自定義日志輸出
            log_function=my_log,
        )

        # 添加 application 配置, 追加到父類 __init__
        super(Application, self).__init__(handlers, **tornado_settings)

        # application的變量, 可在 handler 中通過 self.application.db 調(diào)用
        self.db = torndb.Connection(**config.mysql_settings)

另外需要注意的是, Tornado支持通過 x-real-ip 或 x-forwarded-for來獲取IP, 但前提是需要在你的HTTPServer實例中增加xheaders=True參數(shù), 如:

http_server = tornado.httpserver.HTTPServer(Application(), xheaders=True)
http_server.listen(8888)
tornado.ioloop.IOLoop.current().start()

否則通過RequestHandler.request.remote_ip取到的IP只能是127.0.0.1

本節(jié)內(nèi)容就是這些了, 涵蓋了Tornado的Web框架部分中日常開發(fā)能使用到的絕大部分方法, 看起來內(nèi)容不少, 實際上來來回回都是這幾樣套路. 下節(jié)內(nèi)容將開始介紹Tornado異步和協(xié)程的內(nèi)容.

NEXT ===> Tornado應(yīng)用筆記03-協(xié)程與異步示例

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容