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

2018-02-24 15:48 更新

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

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

相關(guān)概念

同步和異步

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

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

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

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

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

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

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

阻塞和非阻塞

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

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

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

按照這個(gè)說(shuō)明,發(fā)短信就是顯然的非阻塞,發(fā)出去一條短信之后,你利用手機(jī)還可以干別的,乃至于再發(fā)一條“老齊的課程沒(méi)意思,還是看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文件夾中建立一個(gè)文件,命名為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")

其它的事情,如果讀者對(duì)我在《用tornado做網(wǎng)站(1)》中所講述的網(wǎng)站框架熟悉,應(yīng)該知道如何做了,不熟悉,請(qǐng)回頭復(fù)習(xí)。

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

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

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

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

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

異步設(shè)置

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

為了解決這個(gè)問(wèn)題,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。如果不用這個(gè)裝飾器,客戶端訪問(wèn)服務(wù)器的get()方法并得到返回值之后,兩只之間的連接就斷開了,但是用了@tornado.web.asynchronous之后,這個(gè)連接就不關(guān)閉,直到執(zhí)行了self.finish()才關(guān)閉這個(gè)連接。

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

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

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

這還是執(zhí)行簡(jiǎn)單邏輯,如果復(fù)雜了,不斷地要進(jìn)行“回調(diào)”,無(wú)法讓邏輯順利延續(xù),那面會(huì)“眩暈”了。這種現(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。跟這個(gè)裝飾器類似的是@tornado.gen.engine裝飾器,兩者功能類似,有一點(diǎn)細(xì)微差別。請(qǐng)閱讀官方對(duì)此的解釋

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

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

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

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

實(shí)踐中的異步

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

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

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

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

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

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

其它的解決方法,只能看到問(wèn)題具體說(shuō)了,甚至沒(méi)有很好的解決方法。不過(guò),這里有一個(gè)列表,列出了足夠多的庫(kù),供使用者選擇:Async Client Libraries built on tornado.ioloop,同時(shí)這個(gè)頁(yè)面里面還有很多別的鏈接,都是很好的資源,建議讀者多看看。

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

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

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

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

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)