用tornado做網(wǎng)站(7)

2018-02-24 15:48 更新

到上一節(jié)結(jié)束,其實(shí)讀者已經(jīng)能夠做一個網(wǎng)站了,但是,僅僅用前面的技術(shù)來做的網(wǎng)站,僅能算一個小網(wǎng)站,在《為做網(wǎng)站而準(zhǔn)備》中,說明之所以選tornado,就是因?yàn)樗軌蚪鉀Qc10k問題,即能夠?qū)崿F(xiàn)大用戶量訪問。

要實(shí)現(xiàn)大用戶量訪問,必須要做的就是:異步。除非你是很土的土豪。

相關(guān)概念

同步和異步

有不少資料對這兩個概念做了不同角度和層面的解釋。在我來看,一個最典型的例子就是打電話和發(fā)短信。

  • 打電話就是同步。張三給李四打電話,張三說:“是李四嗎?”。當(dāng)這個信息被張三發(fā)出,提交給李四,就等待李四的響應(yīng)(一般會聽到“是”,或者“不是”),只有得到了李四返回的信息之后,才能進(jìn)行后續(xù)的信息傳送。
  • 發(fā)短信是異步。張三給李四發(fā)短信,編輯了一句話“今晚一起看老齊的零基礎(chǔ)學(xué)python”,發(fā)送給李四。李四或許馬上回復(fù),或許過一段時間,這段時間多長也不定,才回復(fù)??傊?,李四不管什么時候回復(fù),張三會以聽到短信鈴聲為提示查看短信。

以上方式理解“同步”和“異步”不是很精準(zhǔn),有些地方或有牽強(qiáng)。要嚴(yán)格理解,需要用嚴(yán)格一點(diǎn)的定義表述(以下表述參照了知乎上的回答):

同步和異步關(guān)注的是消息通信機(jī)制 (synchronous communication/ asynchronous communication)

所謂同步,就是在發(fā)出一個“調(diào)用”時,在沒有得到結(jié)果之前,該“調(diào)用”就不返回。但是一旦調(diào)用返回,就得到返回值了。 換句話說,就是由“調(diào)用者”主動等待這個“調(diào)用”的結(jié)果。

而異步則是相反,“調(diào)用”在發(fā)出之后,這個調(diào)用就直接返回了,所以沒有返回結(jié)果。換句話說,當(dāng)一個異步過程調(diào)用發(fā)出后,調(diào)用者不會立刻得到結(jié)果。而是在“調(diào)用”發(fā)出后,“被調(diào)用者”通過狀態(tài)、通知來通知調(diào)用者,或通過回調(diào)函數(shù)處理這個調(diào)用。

可能還是前面的打電話和發(fā)短信更好理解。

阻塞和非阻塞

“阻塞和非阻塞”與“同步和異步”常常被換為一談,其實(shí)它們之間還是有差別的。如果按照一個“差不多”先生的思維方法,你也可以不那么深究它們之間的學(xué)理上的差距,反正在你的程序中,會使用就可以了。不過,必要的嚴(yán)謹(jǐn)還是需要的,特別是我寫這個教程,要裝扮的讓別人看來自己懂,于是就再引用知乎上的說明(我個人認(rèn)為,別人已經(jīng)做的挺好的東西,就別重復(fù)勞動了,“拿來主義”,也不錯。或許你說我抄襲和山寨,但是我明確告訴你來源了):

阻塞和非阻塞關(guān)注的是程序在等待調(diào)用結(jié)果(消息,返回值)時的狀態(tài).

阻塞調(diào)用是指調(diào)用結(jié)果返回之前,當(dāng)前線程會被掛起。調(diào)用線程只有在得到結(jié)果之后才會返回。非阻塞調(diào)用指在不能立刻得到結(jié)果之前,該調(diào)用不會阻塞當(dāng)前線程。

按照這個說明,發(fā)短信就是顯然的非阻塞,發(fā)出去一條短信之后,你利用手機(jī)還可以干別的,乃至于再發(fā)一條“老齊的課程沒意思,還是看PHP刺激”也是可以的。

關(guān)于這兩組基本概念的辨析,不是本教程的重點(diǎn),讀者可以參閱這篇文章:http://www.cppblog.com/converse/archive/2009/05/13/82879.html,文章作者做了細(xì)致入微的辨析。

tornado的同步

此前,在tornado基礎(chǔ)上已經(jīng)完成的web,就是同步的、阻塞的。為了更明顯的感受這點(diǎn),不妨這樣試一試。

在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做網(wǎng)站(1)》中所講述的網(wǎng)站框架熟悉,應(yīng)該知道如何做了,不熟悉,請回頭復(fù)習(xí)。

sleep.html和see.html是兩個簡單的模板,內(nèi)容可以自己寫。別忘記修改url.py中的目錄。

然后的測試稍微復(fù)雜一點(diǎn)點(diǎn),就是打開瀏覽器之后,打開兩個標(biāo)簽,分別在兩個標(biāo)簽中輸入localhost:8000/sleep(記為標(biāo)簽1)和localhost:8000/see(記為標(biāo)簽2),注意我用的是8000端口。輸入之后先不要點(diǎn)擊回車去訪問。做好準(zhǔn)備,記住切換標(biāo)簽可以用“ctrl-tab”組合鍵。

  1. 執(zhí)行標(biāo)簽1,讓它訪問網(wǎng)站;
  2. 馬上切換到標(biāo)簽2,訪問網(wǎng)址。
  3. 注意觀察,兩個標(biāo)簽頁面,是不是都在顯示正在訪問,請等待。
  4. 當(dāng)標(biāo)簽1不呈現(xiàn)等待提示(比如一個正在轉(zhuǎn)的圓圈)時,標(biāo)簽2的表現(xiàn)如何?幾乎同時也訪問成功了。

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

當(dāng)然,這是比較笨拙的方法,本來是可以通過測試工具完成上述操作比較的。怎奈要用別的工具,還要進(jìn)行介紹,又多了一個分散精力的東西,故用如此笨拙的方法,權(quán)當(dāng)有一個體會。

異步設(shè)置

tornado本來就是一個異步的服務(wù)框架,體現(xiàn)在tornado的服務(wù)器和客戶端的網(wǎng)絡(luò)交互的異步上,起作用的是tornado.ioloop.IOLoop。但是如果的客戶端請求服務(wù)器之后,在執(zhí)行某個方法的時候,比如上面的代碼中執(zhí)行g(shù)et()方法的時候,遇到了time.sleep(17)這個需要執(zhí)行時間比較長的操作,耗費(fèi)時間,就會使整個tornado服務(wù)器的性能受限了。

為了解決這個問題,tornado提供了一套異步機(jī)制,就是異步裝飾器@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服務(wù)器本身默認(rèn)的設(shè)置_auto_fininsh值修改為false。如果不用這個裝飾器,客戶端訪問服務(wù)器的get()方法并得到返回值之后,兩只之間的連接就斷開了,但是用了@tornado.web.asynchronous之后,這個連接就不關(guān)閉,直到執(zhí)行了self.finish()才關(guān)閉這個連接。

tornado.ioloop.IOLoop.instance().add_timeout()也是一個實(shí)現(xiàn)異步的函數(shù),time.time()+17是給前面函數(shù)提供一個參數(shù),這樣實(shí)現(xiàn)了相當(dāng)于time.sleep(17)的功能,不過,還沒有完成,當(dāng)這個操作完成之后,就執(zhí)行回調(diào)函數(shù)on_response()中的self.render("sleep.html"),并關(guān)閉連接self.finish()。

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

讀者看這個代碼,或許感覺有點(diǎn)不是很舒服。如果有這么一點(diǎn)感覺,是正常的。因?yàn)樗锩娉搜b飾器之外,用到了一個回調(diào)函數(shù),它讓代碼的邏輯不是平鋪下去,而是被分割為了兩段。第一段是tornado.ioloop.IOLoop.instance().add_timeout(time.time() + 17, callback=self.on_response),用callback=self.on_response來使用回調(diào)函數(shù),并沒有如同改造之前直接self.render("sleep.html");第二段是回調(diào)函數(shù)on_response(self),要在這個函數(shù)里面執(zhí)行self.render("sleep.html"),并且以self.finish()`結(jié)尾以關(guān)閉連接。

這還是執(zhí)行簡單邏輯,如果復(fù)雜了,不斷地要進(jìn)行“回調(diào)”,無法讓邏輯順利延續(xù),那面會“眩暈”了。這種現(xiàn)象被業(yè)界成為“代碼邏輯拆分”,打破了原有邏輯的順序性。為了讓代碼邏輯不至于被拆分的七零八落,于是就出現(xiàn)了另外一種常用的方法:

#!/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")

從整體上看,這段代碼避免了回調(diào)函數(shù),看著順利多了。

再看細(xì)節(jié)部分。

首先使用的是@tornado.gen.coroutine裝飾器,所以要在前面有import tornado.gen。跟這個裝飾器類似的是@tornado.gen.engine裝飾器,兩者功能類似,有一點(diǎn)細(xì)微差別。請閱讀官方對此的解釋

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是古時候用的,現(xiàn)在我們都使用@tornado.gen.corroutine了,這個是在tornado 3.0以后開始。在網(wǎng)上查閱資料的時候,會遇到一些使用@tornado.gen.engine的,但是在你使用或者借鑒代碼的時候,就勇敢地將其修改為@tornado.gen.coroutine好了。有了這個裝飾器,就能夠控制下面的生成器的流程了。

然后就看到get()方法里面的yield了,這是一個生成器(參閱本教程《生成器》)。yield tornado.gen.Task(tornado.ioloop.IOLoop.instance().add_timeout, time.time() + 17)的執(zhí)行過程,應(yīng)該先看括號里面,跟前面的一樣,是來替代time.sleep(17)的,然后是tornado.gen.Task()方法,其作用是“Adapts a callback-based asynchronous function for use in coroutines.”(由于怕翻譯后遺漏信息,引用原文)。返回后,最后使用yield得到了一個生成器,先把流程掛起,等完全完畢,再喚醒繼續(xù)執(zhí)行。要提醒讀者,生成器都是異步的。

其實(shí),上面啰嗦一對,可以用代碼中注釋了的一句話來代替yield tornado.gen.sleep(17),之所以擴(kuò)所,就是為了順便看到tornado.gen.Task()方法,因?yàn)槿绻x者在看古老的代碼時候,會遇到。但是,后面你寫的時候,就不要那么啰嗦了,請用yield tornado.gen.sleep()

至此,基本上對tornado的異步設(shè)置有了概覽,不過,上面的程序在實(shí)際中沒有什么價值。在工程中,要讓tornado網(wǎng)站真正異步起來,還要做很多事情,不僅僅是如上面的設(shè)置,因?yàn)楹芏鄸|西,其實(shí)都不是異步的。

實(shí)踐中的異步

以下各項(xiàng)同步(阻塞)的,如果在tornado中按照之前的方式只用它們,就是把tornado的非阻塞、異步優(yōu)勢削減了。

  • 數(shù)據(jù)庫的所有操作,不管你的數(shù)據(jù)是SQL還是noSQL,connect、insert、update等
  • 文件操作,打開,讀取,寫入等
  • time.sleep,在前面舉例中已經(jīng)看到了
  • smtplib,發(fā)郵件的操作
  • 一些網(wǎng)絡(luò)操作,比如tornado的httpclient以及pycurl等

除了以上,或許在編程實(shí)踐中還會遇到其他的同步、阻塞實(shí)踐。僅僅就上面幾項(xiàng),就是編程實(shí)踐中經(jīng)常會遇到的,怎么解決?

聰明的大牛程序員幫我們做了擴(kuò)展模塊,專門用來實(shí)現(xiàn)異步/非阻塞的。

  • 在數(shù)據(jù)庫方面,由于種類繁多,不能一一說明,比如mysql,可以使用adb模塊來實(shí)現(xiàn)python的異步mysql庫;對于mongodb數(shù)據(jù)庫,有一個非常優(yōu)秀的模塊,專門用于在tornado和mongodb上實(shí)現(xiàn)異步操作,它就是motor。特別貼出它的logo,我喜歡。官方網(wǎng)站:http://motor.readthedocs.org/en/stable/上的安裝和使用方法都很詳細(xì)。

  • 文件操作方面也沒有替代模塊,只能盡量控制好IO,或者使用內(nèi)存型(Redis)及文檔型(MongoDB)數(shù)據(jù)庫。
  • time.sleep()在tornado中有替代:tornado.gen.sleep()或者tornado.ioloop.IOLoop.instance().add_timeout,這在前面代碼已經(jīng)顯示了。
  • smtp發(fā)送郵件,推薦改為tornado-smtp-client。
  • 對于網(wǎng)絡(luò)操作,要使用tornado.httpclient.AsyncHTTPClient。

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

教程到這里,讀者是不是要思考一個問題,既然對于mongodb有專門的motor庫來實(shí)現(xiàn)異步,前面對于tornado的異步,不管是哪個裝飾器,都感覺麻煩,有沒有專門的庫來實(shí)現(xiàn)這種異步呢?這不是異想天開,還真有。也應(yīng)該有,因?yàn)檫@才體現(xiàn)python的特點(diǎn)。比如greenlet-tornado,就是一個不錯的庫。讀者可以瀏覽官方網(wǎng)站深入了解(為什么對mysql那么不積極呢?按理說應(yīng)該出來好多支持mysql異步的庫才對)。

必須聲明,前面演示如何在tornado中設(shè)置異步的代碼,僅僅是演示理解設(shè)置方法。在工程實(shí)踐中,那個代碼的意義不到。為此,應(yīng)該有一個近似于實(shí)踐的代碼示例。是的,的確應(yīng)該有。當(dāng)我正要寫這樣的代碼時候,在網(wǎng)上發(fā)現(xiàn)一篇文章,這篇文章阻止了我寫,因?yàn)槲乙獙懙哪瞧恼碌淖髡咴缇蛯懞昧?,而且我認(rèn)為表述非常到位,示例也詳細(xì)。所以,我不得不放棄,轉(zhuǎn)而推薦給讀者這篇好文章:

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

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號