用 tornado 作網站 (7)

轉自:http://wiki.jikexueyuan.com/project/start-learning-python/309.html

用 tornado 作網站 (7)

到上一節結束,其實讀者已經可以作一個網站了,可是,僅僅用前面的技術來作的網站,僅能算一個小網站,在《爲作網站而準備》中,說明之因此選 tornado,就是由於它可以解決 c10k 問題,即可以實現大用戶量訪問。html

要實現大用戶量訪問,必需要作的就是:異步。除非你是很土的土豪。python

相關概念

同步和異步

有很多資料對這兩個概念作了不一樣角度和層面的解釋。在我來看,一個最典型的例子就是打電話和發短信。mysql

  • 打電話就是同步。張三給李四打電話,張三說:「是李四嗎?」。當這個信息被張三發出,提交給李四,就等待李四的響應(通常會聽到「是」,或者「不是」),只有獲得了李四返回的信息以後,才能進行後續的信息傳送。
  • 發短信是異步。張三給李四發短信,編輯了一句話「今晚一塊兒看老齊的零基礎學 Python」,發送給李四。李四或許立刻回覆,或許過一段時間,這段時間多長也不定,纔回復。總之,李四無論何時回覆,張三會以聽到短信鈴聲爲提示查看短信。

以上方式理解「同步」和「異步」不是很精準,有些地方或有牽強。要嚴格理解,須要用嚴格一點的定義表述(如下表述參照了知乎上的回答):git

同步和異步關注的是消息通訊機制 (synchronous communication/ asynchronous communication)程序員

所謂同步,就是在發出一個「調用」時,在沒有獲得結果以前,該「調用」就不返回。可是一旦調用返回,就獲得返回值了。 換句話說,就是由「調用者」主動等待這個「調用」的結果。github

而異步則是相反,「調用」在發出以後,這個調用就直接返回了,因此沒有返回結果。換句話說,當一個異步過程調用發出後,調用者不會馬上獲得結果。而是在「調用」發出後,「被調用者」經過狀態、通知來通知調用者,或經過回調函數處理這個調用。web

可能仍是前面的打電話和發短信更好理解。sql

阻塞和非阻塞

「阻塞和非阻塞」與「同步和異步」經常被換爲一談,其實它們之間仍是有差異的。若是按照一個「差很少」先生的思惟方法,你也能夠不那麼深究它們之間的學理上的差距,反正在你的程序中,會使用就能夠了。不過,必要的嚴謹仍是須要的,特別是我寫這個教程,要裝扮的讓別人看來本身懂,因而就再引用知乎上的說明(我我的認爲,別人已經作的挺好的東西,就別重複勞動了,「拿來主義」,也不錯。或許你說我抄襲和山寨,可是我明確告訴你來源了):mongodb

阻塞和非阻塞關注的是程序在等待調用結果(消息,返回值)時的狀態.數據庫

阻塞調用是指調用結果返回以前,當前線程會被掛起。調用線程只有在獲得結果以後纔會返回。非阻塞調用指在不能馬上獲得結果以前,該調用不會阻塞當前線程。

按照這個說明,發短信就是顯然的非阻塞,發出去一條短信以後,你利用手機還能夠幹別的,乃至於再發一條「老齊的課程沒意思,仍是看 PHP 刺激」也是能夠的。

關於這兩組基本概念的辨析,不是本教程的重點,讀者能夠參閱這篇文章:http://www.cppblog.com/converse/archive/2009/05/13/82879.html,文章做者作了細緻入微的辨析。

tornado 的同步

此前,在 tornado 基礎上已經完成的 web,就是同步的、阻塞的。爲了更明顯的感覺這點,不妨這樣試一試。

在 handlers 文件夾中創建一個文件,命名爲 sleep.py

#!/usr/bin/env python # coding=utf-8 from base import BaseHandler import time class SleepHandler(BaseHandler): def get(self): time.sleep(17) self.render("sleep.html") class SeeHandler(BaseHandler): def get(self): self.render("see.html")

其它的事情,若是讀者對我在《用 tornado 作網站 (1)》中所講述的網站框架熟悉,應該知道如何作了,不熟悉,請回頭複習。

sleep.html 和 see.html 是兩個簡單的模板,內容能夠本身寫。別忘記修改 url.py 中的目錄。

而後的測試稍微複雜一點點,就是打開瀏覽器以後,打開兩個標籤,分別在兩個標籤中輸入localhost:8000/sleep(記爲標籤 1)和 localhost:8000/see(記爲標籤 2),注意我用的是 8000 端口。輸入以後先不要點擊回車去訪問。作好準備,記住切換標籤能夠用「ctrl-tab」組合鍵。

  1. 執行標籤 1,讓它訪問網站;
  2. 立刻切換到標籤 2,訪問網址。
  3. 注意觀察,兩個標籤頁面,是否是都在顯示正在訪問,請等待。
  4. 當標籤 1 不呈現等待提示(好比一個正在轉的圓圈)時,標籤 2 的表現如何?幾乎同時也訪問成功了。

建議讀者修改 sleep.py 中的 time.sleep(17) 這個值,多試試。很好玩的吧。

固然,這是比較笨拙的方法,原本是能夠經過測試工具完成上述操做比較的。怎奈要用別的工具,還要進行介紹,又多了一個分散精力的東西,故用如此笨拙的方法,權當有一個體會。

異步設置

tornado 原本就是一個異步的服務框架,體如今 tornado 的服務器和客戶端的網絡交互的異步上,起做用的是 tornado.ioloop.IOLoop。可是若是的客戶端請求服務器以後,在執行某個方法的時候,好比上面的代碼中執行 get() 方法的時候,遇到了 time.sleep(17) 這個須要執行時間比較長的操做,耗費時間,就會使整個 tornado 服務器的性能受限了。

爲了解決這個問題,tornado 提供了一套異步機制,就是異步裝飾器 @tornado.web.asynchronous

#!/usr/bin/env Python # coding=utf-8 import tornado.web from base import BaseHandler import time class SleepHandler(BaseHandler): @tornado.web.asynchronous def get(self): tornado.ioloop.IOLoop.instance().add_timeout(time.time() + 17, callback=self.on_response) def on_response(self): self.render("sleep.html") self.finish()

將 sleep.py 的代碼如上述同樣改造,即在 get() 方法前面增長了裝飾器 @tornado.web.asynchronous,它的做用在於將 tornado 服務器自己默認的設置_auto_fininsh 值修改成 false。若是不用這個裝飾器,客戶端訪問服務器的 get() 方法並獲得返回值以後,兩隻之間的鏈接就斷開了,可是用了 @tornado.web.asynchronous 以後,這個鏈接就不關閉,直到執行了 self.finish() 才關閉這個鏈接。

tornado.ioloop.IOLoop.instance().add_timeout() 也是一個實現異步的函數,time.time()+17 是給前面函數提供一個參數,這樣實現了至關於 time.sleep(17) 的功能,不過,尚未完成,當這個操做完成以後,就執行回調函數on_response() 中的 self.render("sleep.html"),並關閉鏈接 self.finish()

過程清楚了。所謂異步,就是要解決原來的 time.sleep(17) 形成的服務器處理時間長,性能降低的問題。解決方法如上描述。

讀者看這個代碼,或許感受有點不是很舒服。若是有這麼一點感受,是正常的。由於它裏面除了裝飾器以外,用到了一個回調函數,它讓代碼的邏輯不是平鋪下去,而是被分割爲了兩段。第一段是tornado.ioloop.IOLoop.instance().add_timeout(time.time() + 17, callback=self.on_response),用callback=self.on_response 來使用回調函數,並無如同改造以前直接 self.render("sleep.html");第二段是回調函數 on_response(self),要在這個函數裏面執行self.render("sleep.html"),而且以self.finish()`結尾以關閉鏈接。

這仍是執行簡單邏輯,若是複雜了,不斷地要進行「回調」,沒法讓邏輯順利延續,那面會「眩暈」了。這種現象被業界成爲「代碼邏輯拆分」,打破了原有邏輯的順序性。爲了讓代碼邏輯不至於被拆分的七零八落,因而就出現了另一種經常使用的方法:

#!/usr/bin/env Python # coding=utf-8 import tornado.web import tornado.gen from base import BaseHandler import time class SleepHandler(tornado.web.RequestHandler): @tornado.gen.coroutine def get(self): yield tornado.gen.Task(tornado.ioloop.IOLoop.instance().add_timeout, time.time() + 17) #yield tornado.gen.sleep(17) self.render("sleep.html")

從總體上看,這段代碼避免了回調函數,看着順利多了。

再看細節部分。

首先使用的是 @tornado.gen.coroutine 裝飾器,因此要在前面有 import tornado.gen。跟這個裝飾器相似的是@tornado.gen.engine 裝飾器,二者功能相似,有一點細微差異。請閱讀官方對此的解釋

This decorator(指 engine) is similar to coroutine, except it does not return a Future and the callback argument is not treated specially.

@tornado.gen.engine 是古時候用的,如今咱們都使用 @tornado.gen.corroutine 了,這個是在 tornado 3.0 之後開始。在網上查閱資料的時候,會遇到一些使用 @tornado.gen.engine 的,可是在你使用或者借鑑代碼的時候,就勇敢地將其修改成 @tornado.gen.coroutine 好了。有了這個裝飾器,就可以控制下面的生成器的流程了。

而後就看到 get() 方法裏面的 yield 了,這是一個生成器(參閱本教程《生成器》)。yield tornado.gen.Task(tornado.ioloop.IOLoop.instance().add_timeout, time.time() + 17) 的執行過程,應該先看括號裏面,跟前面的同樣,是來替代 time.sleep(17) 的,而後是 tornado.gen.Task() 方法,其做用是「Adapts a callback-based asynchronous function for use in coroutines.」(因爲怕翻譯後遺漏信息,引用原文)。返回後,最後使用 yield 獲得了一個生成器,先把流程掛起,等徹底完畢,再喚醒繼續執行。要提醒讀者,生成器都是異步的。

其實,上面囉嗦一對,能夠用代碼中註釋了的一句話來代替 yield tornado.gen.sleep(17),之因此擴所,就是爲了順便看到 tornado.gen.Task() 方法,由於若是讀者在看古老的代碼時候,會遇到。可是,後面你寫的時候,就不要那麼囉嗦了,請用 yield tornado.gen.sleep()

至此,基本上對 tornado 的異步設置有了概覽,不過,上面的程序在實際中沒有什麼價值。在工程中,要讓 tornado 網站真正異步起來,還要作不少事情,不只僅是如上面的設置,由於不少東西,其實都不是異步的。

實踐中的異步

如下各項同步(阻塞)的,若是在 tornado 中按照以前的方式只用它們,就是把 tornado 的非阻塞、異步優點削減了。

  • 數據庫的全部操做,無論你的數據是 SQL 仍是 noSQL,connect、insert、update 等
  • 文件操做,打開,讀取,寫入等
  • time.sleep,在前面舉例中已經看到了
  • smtplib,發郵件的操做
  • 一些網絡操做,好比 tornado 的 httpclient 以及 pycurl 等

除了以上,或許在編程實踐中還會遇到其餘的同步、阻塞實踐。僅僅就上面幾項,就是編程實踐中常常會遇到的,怎麼解決?

聰明的大牛程序員幫咱們作了擴展模塊,專門用來實現異步/非阻塞的。

  • 在數據庫方面,因爲種類繁多,不能一一說明,好比 mysql,可使用adb模塊來實現 python 的異步 mysql 庫;對於 mongodb 數據庫,有一個很是優秀的模塊,專門用於在 tornado 和 mongodb 上實現異步操做,它就是 motor。特別貼出它的 logo,我喜歡。官方網站:http://motor.readthedocs.org/en/stable/上的安裝和使用方法都很詳細。

  • 文件操做方面也沒有替代模塊,只能儘可能控制好 IO,或者使用內存型(Redis)及文檔型(MongoDB)數據庫。
  • time.sleep() 在 tornado 中有替代:tornado.gen.sleep() 或者tornado.ioloop.IOLoop.instance().add_timeout,這在前面代碼已經顯示了。
  • smtp 發送郵件,推薦改成 tornado-smtp-client。
  • 對於網絡操做,要使用 tornado.httpclient.AsyncHTTPClient。

其它的解決方法,只能看到問題具體說了,甚至沒有很好的解決方法。不過,這裏有一個列表,列出了足夠多的庫,供使用者選擇:Async Client Libraries built on tornado.ioloop,同時這個頁面裏面還有不少別的連接,都是很好的資源,建議讀者多看看。

教程到這裏,讀者是否是要思考一個問題,既然對於 mongodb 有專門的 motor 庫來實現異步,前面對於 tornado 的異步,無論是哪一個裝飾器,都感受麻煩,有沒有專門的庫來實現這種異步呢?這不是異想天開,還真有。也應該有,由於這才體現python的特色。好比greenlet-tornado,就是一個不錯的庫。讀者能夠瀏覽官方網站深刻了解(爲何對 mysql 那麼不積極呢?按理說應該出來好多支持 mysql 異步的庫纔對)。

必須聲明,前面演示如何在 tornado 中設置異步的代碼,僅僅是演示理解設置方法。在工程實踐中,那個代碼的意義不到。爲此,應該有一個近似於實踐的代碼示例。是的,的確應該有。當我正要寫這樣的代碼時候,在網上發現一篇文章,這篇文章阻止了我寫,由於我要寫的那篇文章的做者早就寫好了,並且我認爲表述很是到位,示例也詳細。因此,我不得不放棄,轉而推薦給讀者這篇好文章:

舉例:http://emptysqua.re/blog/refactoring-tornado-coroutines/

相關文章
相關標籤/搜索