翻譯:introduce to tornado-Asynchronous Web Services

異步的web請求

迄今爲止,咱們經過tornado這個強大的框架的功能建立了不少web應用,它簡單,易用,它有足夠強大的理由讓咱們選擇它作爲一個web項目的框架.咱們最須要使用的功能是它在服務端的異步讀取功能.這是一個很是好的理由:tornado可讓咱們輕鬆地完成非阻塞請求的相應.幫助咱們更有效率地處理更多事情.在這一章節中,咱們將會討論tornado最基本的異步請求.同時也會涉及一些長鏈接輪詢的技術,咱們將會經過編寫一個簡單的web應用來演示服務器如何在較少資源的狀況下,處理更多的web請求. javascript

異步的web請求

大部分的web應用(包括以前的全部例子),實際上都是阻塞運行的.這意味着當接到請求以後,進程將會一直掛起,直到請求完成.根據實際的統計數據,tornado能夠更高效且快速地完成web請求,而不須要顧慮阻塞的問題.然而對於一些操做(例如大量的數據庫請求或調用外部的API)tornado能夠花費一些時間等待操做完成,這意味着這期間應用能夠有效地鎖定進程直到操做結束,很明顯,這樣將會解決不少大規模的問題. tornado給咱們提供了很是好的方法去分類地處理這樣的事情.去更改那些鎖死進程直到請求完成的場景.當請求開始時,應用能夠跳出這個I/O請求的循環,去打開另外一個客戶端的請求,當第一個請求完成以後,應用可使用callback去喚醒以前掛起的進程. 經過下面的例子,可以幫助你深刻了解tornado的異步功能,咱們經過調用一個twitter搜索的API去建立一個簡單的web應用.這個web應用將一個變量q將查詢字符串作爲搜索條件傳遞給twitter的API,這樣的操做將會獲得一個大概的響應時間,如圖片5-1展現的就是咱們應用想要實現的效果. html

圖片5-1

對於這個應用,咱們將會展現三個版本: 1. 第一個版本將會同步的方式處理http請求 2. 第二個版本將會使用tornado的異步處理http請求 3. 最後的版本咱們將會使用tornado2.1的新模塊 gen 讓異步處理http的請求更加簡潔和易用 html5

你不須要深刻了解例子中的twitter搜索API是如何工做的,固然你若是想要成爲這方面的專家,能夠經過閱讀這些開發文檔瞭解具體的實現:twitter search API java

開始同步處理

例子5-1的源代碼是咱們統計tweer響應時間例子的同步處理版本.請記住:咱們在最開始導入了tornado的 httpclient 模塊.咱們將會使用模塊中的 httpclient 類去之行咱們的http請求. 而後,咱們將會使用模塊中另外一個類 AsyncHTTPClient . 例子5-1:同步的HTTP請求:tweet_rate.py python

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import tornado . httpserver
import tornado . ioloop
import tornado . options
import tornado . web
import tornado . httpclient
import urllib
import json
import datetime
import time
from tornado . options import define , options
define ( "port" , default = 8000 , help = "run on the given port" , type = int )
 
class IndexHandler ( tornado . web . RequestHandler ) :
     def get ( self ) :
         query = self . get_argument ( 'q' )
         client = tornado . httpclient . HTTPClient ( )
         response = client . fetch ( "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) )
         body = json . loads ( response . body )
         result_count = len ( body [ 'results' ] )
         now = datetime . datetime . utcnow ( )
         raw_oldest_tweet_at = body [ 'results' ] [ - 1 ] [ 'created_at' ]
         oldest_tweet_at = datetime . datetime . strptime ( raw_oldest_tweet_at ,
             "%a, %d %b %Y %H:%M:%S +0000" )
         seconds_diff = time . mktime ( now . timetuple ( ) ) - \
             time . mktime ( oldest_tweet_at . timetuple ( ) )
         tweets_per_second = float ( result_count ) / seconds_diff
         self . write ( """
<div style="text-align: center">
  <div style="font-size: 72px">
    %s
  </div>
     <div style="font-size: 144px">
    %.02f
  </div>
  <div style="font-size: 24px">
    tweets per second
  </div>
</div>""" % ( query , tweets_per_second ) )
 
if __name__ == "__main__" :
     tornado . options . parse_command_line ( )
     app = tornado . web . Application ( handlers = [ ( r "/" , IndexHandler ) ] )
     http_server = tornado . httpserver . HTTPServer ( app )
     http_server . listen ( options . port )
     tornado . ioloop . IOLoop . instance ( ) . start ( )

這是目前爲止咱們最熟悉的程序結構:咱們有一個 RequestHandler 類, IndexHandler 類,來自於根目錄的應用請求將會被轉發到 IndexHandler 類的get方法中.咱們q變量將經過 get_argument 抓取請求的字符串,傳遞q變量給 twitter 的搜索API並執行.這裏是對應功能的代碼: jquery

1
2
3
4
client = tornado . httpclient . HTTPClient ( )
         response = client . fetch ( "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) )
         body = json . loads ( response . body )

在例子中咱們演示了tornado的HTTPClient類,它將會調用返回對象的 fetch .這個同步版本的fetch 方法將會取到一個 URL 變量的參數作爲參數. 咱們構造的這個 URL 變量與twitter 的搜索API 返回的結果關聯(這個 rpp 變量將會記錄咱們100個tweets 從第一個頁面獲取的結果, result_type 變量記錄了咱們最近一次匹配的搜索結果).這個 fetch 方法將會返回一個 HTTPResponse 對象, 它的內容包括:從遠程的 URL 返回結果的時間.twitter 返回的結果是 JSON 格式的,因此咱們可使用python的 json 模塊去建立一個python能夠處理的數據結構,獲取返回的數據. 「HTTPResponse對象的 fetch 方法容許你關聯全部 HTTP 響應的結果.不只僅是body,若是想要了解更多能夠閱讀這個文檔:official documentation git

剩下的代碼處理的是咱們關心的tweets每一秒處理的圖表.咱們使用全部tweets中最先的時間戳與當前時間比較,來肯定咱們有多少時間花費在搜索中.而後除以咱們的進行搜索操做的tweets數量,最後咱們將結果作成圖表,以最基本的HTML格式返回給客戶的瀏覽器. github

阻塞帶來的麻煩

咱們已經編寫了一個簡單的tornado應用來完成統計從twitterAPI中獲取搜索結果並顯示到瀏覽器的功能,然而應用並無如想象地那樣快速地將結果返回.它老是在每個twitter的搜索結果返回以前停滯.在同步(到目前爲止,咱們的全部操做都是單線程的)應用,這意味着,服務器一次只能處理一個請求,因此,若是咱們的應用包含兩個api的請求,你每一次(最多)只能處理一個請求.這不是一個高可用的應用,不利於分佈到多進程或多服務器上進行操做. 以這個程序作爲母版,你可使用任何工具對這個應用進行性能檢測,咱們將會經過一個很是傑出的工具 Siege utility 作爲咱們的測試例子.你能夠像這樣使用Siege Utility: web

1
$ siege http : / / localhost : 8000 / ? q = pants - c10 - t10s

在這個例子中, Siege 將會每十秒鐘對咱們的應用進行一次有十個併發的請求.這個輸出的結果能夠查看圖片5-2.你能夠快速地在這裏看到結果.這個API須要在等待中掛起,直到請求完成並返回處理結果才能繼續.它不考慮是一個仍是兩個請求,固然經過這一百個(每次十個併發)的模擬用戶,它全部的操做結果顯示會愈來愈慢. 圖5-2 能夠看到,每十秒一次,每次十個模擬用戶完成請求的平均響應時間是1.99秒.成功完成了29次查詢,請記住,這知識一個簡單的web響應頁面,若是咱們想要在添加更多調用數據庫或其它web server 的請求,這個結果將會更糟糕.若是這種類型的代碼被應用到生產環境中,到了必定程度的負載以後,請求的響應將會愈來愈慢,最後致使整個服務響應超時. ajax

基本的異步調用

很是幸運,tornado擁有一個叫作 AsyncHTTPClient 的類.它可以異步地處理 HTTP 請求,它的工做方式與例5-1同步工做的例子很像.下面咱們將重點討論其中差別的內容.例5-2的源代碼以下: 例5-2.tweet_rate_async.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import tornado . httpserver
import tornado . ioloop
import tornado . options
import tornado . web
import tornado . httpclient
 
import urllib
import json
import datetime
import time
 
from tornado . options import define , options
define ( "port" , default = 8000 , help = "run on the given port" , type = int )
 
class IndexHandler ( tornado . web . RequestHandler ) :
     @ tornado . web . asynchronous
     def get ( self ) :
         query = self . get_argument ( 'q' )
         client = tornado . httpclient . AsyncHTTPClient ( )
         client . fetch ( "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) ,
             callback = self . on_response )
 
     def on_response ( self , response ) :
         body = json . loads ( response . body )
         result_count = len ( body [ 'results' ] )
         now = datetime . datetime . utcnow ( )
         raw_oldest_tweet_at = body [ 'results' ] [ - 1 ] [ 'created_at' ]
         oldest_tweet_at = datetime . datetime . strptime ( raw_oldest_tweet_at ,
             "%a, %d %b %Y %H:%M:%S +0000" )
         seconds_diff = time . mktime ( now . timetuple ( ) ) - \
             time . mktime ( oldest_tweet_at . timetuple ( ) )
         tweets_per_second = float ( result_count ) / seconds_diff
         self . write ( """
<div style="text-align: center">
  <div style="font-size: 72px">
    %s
  </div>
  <div style="font-size: 144px">
    %.02f
  </div>
  <div style="font-size: 24px">
    tweets per second
  </div>
</div>""" % ( self . get_argument ( 'q' ) , tweets_per_second ) )
     self . finish ( )
if __name__ == "__main__" :
     tornado . options . parse_command_line ( )
     app = tornado . web . Application ( handlers = [ ( r "/" , IndexHandler ) ] )
     http_server = tornado . httpserver . HTTPServer ( app )
     http_server . listen ( options . port )
     tornado . ioloop . IOLoop . instance ( ) . start ( )

AsyncHTTPClient 中的fetch方法並不會直接返回調用的結果.作爲替代方案,它指定了一個回調函數,當咱們指定的方法或函數調用的 HTTP 請求有返回結果時,它將會把 HTTPResponse 對象作爲變量返回,此時回調函數將會從新調用咱們設定的方法或者函數,繼續執行.

1
2
3
4
client = tornado . httpclient . AsyncHTTPClient ( )
         client . fetch ( "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) ,
             callback = self . on_response )

在這個例子中,咱們指定了 on_response 作爲回調,咱們想傳送給 Twitter search API 的全部請求,都將放到 on_response 函數中.別忘了在前面使用 @tornado.web.asynchronous 修飾(在咱們定義的get 方法以前)並在這個回調的方法結束前調用 self.finish() ,咱們將在不久以後討論其中的細節.這個版本的應用與阻塞版本對外輸出的參數是同樣的,可是性能將會更好一些,會有多大的提高呢?讓咱們來看看實際的測試結果.正如你在圖片5-3中看到的同樣,咱們目前的版本測試結果爲在12.59秒內,平均每秒鐘發送3.20個請求.服務成功進行了118次一樣的查詢,性能有了很是明顯的提高!正如預想的,使用長鏈接能夠支持更多的併發用戶,而不會像阻塞版本的例子那樣逐漸變慢直到超時. 圖片5-3

異步函數的修飾

tornado默認狀況下會將函數處理結果返回以前與客戶端的鏈接關閉.在通常狀況下,這正是咱們想要使用的功能,可是在使用異步請求的回調功能時,咱們但願與客戶端的鏈接一直保持到回調執行完畢.你能夠在你想要保持與客戶端長鏈接的方法中使用 @tornado.web.asynchronous 修飾符告訴 tornado 保持這個鏈接.作爲 tweet rate 例子的異步版本,咱們將會在 IndexHandler 的 get 方法中使用這個功能,相關的代碼以下:

1
2
3
4
5
class IndexHandler ( tornado . web . RequestHandler ) :
     @ tornado . web . asynchronous
     def get ( self ) :
         query = self . get_argument ( 'q' )
         [ . . other request handler code here . . ]

請注意:在你使用 @tornado.web.asynchronous 修飾符的地方,tornado將不會關閉這個方法建立的鏈接.你必須明確地在 RequestHandler 對象的finish 方法中告訴 tornado 關閉這個請求建立的鏈接(不然,這個請求將會一直在後臺掛起,而且瀏覽器可能沒法正常顯示你發送給客戶端的數據).在目前這個異步的例子中,咱們將調用 finish 方法的操做寫到了 on_response 函數中.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[ . . other callback code . . ]
         self . write ( """
<div style="text-align: center">
  <div style="font-size: 72px">
    %s
  </div>
  <div style="font-size: 144px">
    %.02f
  </div>
  <div style="font-size: 24px">
    tweets per second
  </div>
</div>""" % ( self . get_argument ( 'q' ) , tweets_per_second ) )
     self . finish ( )

異步啓動器

如今這個版本的 tweet rate 程序是性能更加優越的異步版本.不幸的是,它的結構顯得稍微有些凌亂:咱們將會把處理請求的函數分離到兩個方法中.當咱們有兩個兩個或者多個相互依賴的異步請求須要執行的時候:你可能須要在回調函數中調用回調函數.這可能會讓代碼的編寫變得更加困難且難以維護.下面是一個很不科學(固然,它很是可能出現)的例子.

1
2
3
4
5
6
7
8
9
10
11
def get ( self ) :
     client = AsyncHTTPClient ( )
     client . fetch ( "http://example.com" , callback = on_response )
def on_response ( self , response ) :
     client = AsyncHTTPClient ( )
     client . fetch ( "http://another.example.com/" , callback = on_response2 )
def on_response2 ( self , response ) :
     client = AsyncHTTPClient ( )
     client . fetch ( "http://still.another.example.com/" , callback = on_response3 )
def on_response3 ( self , response ) :
     [ etc . , etc . ]

幸運的是,tornado2.1介紹了一個 tornado.gen 模塊,它提供了很是簡潔的範例給咱們演示瞭如何處理異步請求.例子5-3 是使用了 tornado.gen 到 tweet rate 應用進行異步處理的版本.請仔細閱讀,而後咱們將會對它是如何工做的展開討論. 例子5-3 tweet_rate_gen.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
import tornado . httpserver
import tornado . ioloop
import tornado . options
import tornado . web
import tornado . httpclient
import tornado . gen
 
import urllib
import json
import datetime
import time
 
from tornado . options import define , options
define ( "port" , default = 8000 , help = "run on the given port" , type = int )
 
class IndexHandler ( tornado . web . RequestHandler ) :
     @ tornado . web . asynchronous
     @ tornado . gen . engine
     def get ( self ) :
         query = self . get_argument ( 'q' )
         client = tornado . httpclient . AsyncHTTPClient ( )
         response = yield tornado . gen . Task ( client . fetch ,
             "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) )
         body = json . loads ( response . body )
         result_count = len ( body [ 'results' ] )
         now = datetime . datetime . utcnow ( )
         raw_oldest_tweet_at = body [ 'results' ] [ - 1 ] [ 'created_at' ]
         oldest_tweet_at = datetime . datetime . strptime ( raw_oldest_tweet_at ,
             "%a, %d %b %Y %H:%M:%S +0000" )
         seconds_diff = time . mktime ( now . timetuple ( ) ) - \
             time . mktime ( oldest_tweet_at . timetuple ( ) )
         tweets_per_second = float ( result_count ) / seconds_diff
         self . write ( """
<div style="text-align: center">
  <div style="font-size: 72px">
    %s
  </div>
  <div style="font-size: 144px">
    %.02f
  </div>
  <div style="font-size: 24px">
    tweets per second
  </div>
  
</div>""" % ( query , tweets_per_second ) )
         self . finish ( )
if __name__ == "__main__" :
     tornado . options . parse_command_line ( )
     app = tornado . web . Application ( handlers = [ ( r "/" , IndexHandler ) ] )
     http_server = tornado . httpserver . HTTPServer ( app )
     http_server . listen ( options . port )
     tornado . ioloop . IOLoop . instance ( ) . start ( )

正如你看到的,這個版本的代碼大部分都和前兩個版本的代碼一致.主要的差別在於咱們如何調用 AsyncHTTPClient 對象中的 fetch 方法,這裏是相關的代碼:

1
2
3
4
5
client = tornado . httpclient . AsyncHTTPClient ( )
         response = yield tornado . gen . Task ( client . fetch ,
             "http://search.twitter.com/search.json?" + \
             urllib . urlencode ( { "q" : query , "result_type" : "recent" , "rpp" : 100 } ) )
         body = json . loads ( response . body )

咱們使用了python的 yield 關鍵詞標記了一個實例化的 tornado.gen.task 對象,咱們想要經過這個功能將變量傳遞給須要調用的函數,在這裏咱們使用 yield 控制tornado 程序返回的結果,容許 tornado 在一個HTTP 請求執行的時候,去執行另一個任務.當 這個 HTTP 請求完成的時候, RequestHandler 方法會使用斷開以前保存的變量,從上一次斷開的地方繼續執行.這是一個很是完美的控制方式,在 HTTP 請求返回正確的結果以後,將其轉到正確的請求處理流程中,而不是放到回調函數中,這樣操做的好處在於,代碼變得更加容易理解:全部請求相關的邏輯代碼都被放到相同的地方.這樣的代碼仍然支持異步執行.並且性能與咱們使用了 tornado.gen 處理異步請求的回調函數是一致的,具體信息能夠查看圖片5-4. 圖片5-4

請記住, @tornado.gen.engine 修飾符要放到咱們定義的 get 方法前面,這樣纔可以在這個方法中使用 tornado 的 tornado.gen.task 類.這個 tornado.gen 模塊有許多類和功能,使用它們可讓咱們寫出更簡單且易於維護的 tornado 異步處理代碼.更多資料能夠查看這個文檔.

使用異步處理

咱們已經在本章節使用 tornado 的異步終端作爲例子演示如何對任務進行異步的處理.不一樣的開發人員可能會選擇不一樣的異步終端庫配合tornado解決問題,這裏已經整理出了相關的類庫在tornado wiki 上. bit.ly的 asyncmong 是其中最出名的例子.它可讓咱們異步地去調用 mongodb 服務.這對咱們來講是一個很是好的選擇,讓tornado 開發者能夠利用它去完成異步調用數據庫的操做,假如你但願使用別的數據庫,也能夠查看 tornado wiki 列表中提供的其它異步處理庫.

異步操做總結

正如咱們在前一個例子看到的,在tornado中使用異步的web服務超乎想象的簡單和強大.使用異步地處理一些須要長時間等待的API和數據庫請求,能夠減小阻塞等待的時間,使得最終的服務處理請求更加快速.雖然不是每個操做都適於使用異步處理,但 tornado 試圖讓咱們更快速地使用異步處理去完成一個完整的應用.在構建一個依賴於緩慢的查詢或外部服務的 web 應用時,tornado 非阻塞的功能會讓問題變得更加易於處理. 然而值得值得一提的是這個例子並非很是合適.若是你來設計一個相似的應用,不管它是什麼規模的應用,你也許更但願將這個 twitter 搜索請求放到客戶端的瀏覽器執行(使用javascript),讓 web server 能夠去處理其它的服務請求.在大部分的實例中,你也許還要考慮使用 cache 來緩存結果,這樣就不須要對全部的搜索請求調用外部的API,相同的請求能夠直接返回 cache 中緩存的結果.在通常狀況下,若是你只須要在後臺調用本身web 服務的 HTTP 請求,那麼你應該再思考一下如何配置你的應用.

tornado的長輪詢

tornado 異步架構的另一個優點是能夠更方便地處理 HTTP 的長輪詢.它能夠處理實時更新狀態:如一個很是簡單的用戶通知系統或複雜的多用戶聊天室. 開發一個具備實時更新狀態功能的 web 應用是全部 web 開發人員都必需要面對的挑戰.更新用戶狀態,發送新的提示消息,或其它任何從服務端發送到客戶端瀏覽器進行狀態變動和加載的狀態.早期一個相似的解決方案是按期讓客戶端瀏覽器發送一個更新狀態的請求到服務器.這個技術帶來的挑戰是:詢問的頻率必須足夠高才能保證通知能夠實時更新,可是太頻繁的 HTTP 請求在規模大的時候會面對更多挑戰,當有成百上千的客戶端時須要不斷地建立新鏈接,頻繁的輪詢操做致使 web 服務端由於關閉上千個請求而掛掉. 因此使用」服務端推送」技術使得 web 應用能夠合理地分配資源去保證明時分發狀態更新更高效.實際上服務端推送技術能夠更好地與現代瀏覽器結合起來.這個技術在由客戶端發起鏈接接收服務端推送的更新狀態的架構中很是流行.與短暫的 http 請求相對應,這項技術被成爲長輪詢或長鏈接請求. 長輪詢意味着瀏覽器只須要簡單地啓動並保持一個與服務器的鏈接.這樣瀏覽器只須要簡單地等待響應服務器隨時推送的更新狀態請求便可.在服務器發送更新完畢以後就能夠關閉鏈接(或這客戶端的瀏覽器請求時間超時),這個客戶端能夠很是簡單地建立新的鏈接,等待下一個更新信息. 這樣分段處理全部 HTTP 的長輪詢能夠簡單地完成實時更新的應用.tornado 的異步架構體系使得這樣的應用搭建變得更加簡單.

長輪詢的好處

HTTP 長輪詢最主要的好處是能夠顯著減小 web 服務器的負載.替換掉客戶端頻繁(服務器須要處理每個http 頭信息)的短鏈接請求以後,服務器只須要處理第一次發送的初始化鏈接請求和須要發送更新信息所建立的鏈接.在大多數的時間,是沒有新狀態須要更新的,這時候這個鏈接將不會消耗任何cpu資源. 瀏覽器將得到更大的兼容性,全部的瀏覽器都支持 AJAX 建立的長輪詢請求,不須要添加插件或使用額外的資源.相比其它服務端推送的技術, http 長輪詢是爲數很少被普遍應用的技術. 咱們已經接觸了一些使用長輪詢的技術,實際上前面提到的狀態更新,消息提示,聊天功能都是當前比較流行的 web 頁面功能. google Docs 使用長輪詢來實現同步協做的功能,兩我的能夠同時編輯同一個文檔,而且能夠實時地看到對方進行的修改,twitter使用長輪詢在瀏覽器上面實時地顯示狀態和消息的更新.facebook使用這個技術來構建它的聊天功能.長輪詢如此流行的一個重要緣由就是:用戶再也不須要反覆刷新網頁就能夠看到最新的內容和狀態.

例子:實時的庫存報表

這個例子演示了一個服務如何在多個購買者的瀏覽器中保持最新的零售產品庫存信息.這個應用服務將會在圖書信息頁面的 「添加購物車」 按鈕被按下時從新計算圖書的剩餘庫存,其餘購買者的瀏覽器將會看到庫存信息的減小. 要完成這個庫存更新的功能,咱們須要編寫一個 RequestHandler 的子類.在 HTTP 鏈接初始化方法被調用以後不要關閉鏈接.咱們須要使用 tornado 內置的 asynchronous 修飾符來完成這個功能.相關的代碼能夠查看例子5-4. 例子5-4: shopping-cart.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import tornado . web
import tornado . httpserver
import tornado . ioloop
import tornado . options
from uuid import uuid4
 
class ShoppingCart ( object ) :
     totalInventory = 10
     callbacks = [ ]
     carts = { }
     def register ( self , callback ) :
         self . callbacks . append ( callback )
 
     def moveItemToCart ( self , session ) :
         if session in self . carts :
             return
         self . carts [ session ] = True
         self . notifyCallbacks ( )
 
     def removeItemFromCart ( self , session ) :
         if session not in self . carts :
             return
         del ( self . carts [ session ] )
         self . notifyCallbacks ( )
 
     def notifyCallbacks ( self ) :
         for c in self . callbacks :
             self . callbackHelper ( c )
         self . callbacks = [ ]
    
     def callbackHelper ( self , callback ) :
         callback ( self . getInventoryCount ( ) )
 
     def getInventoryCount ( self ) :
         return self . totalInventory - len ( self . carts )
    
     class DetailHandler ( tornado . web . RequestHandler ) :
         def get ( self ) :
             session = uuid4 ( )
             count = self . application . shoppingCart . getInventoryCount ( )
             self . render ( "index.html" , session = session , count = count )
    
     class CartHandler ( tornado . web . RequestHandler ) :
         def post ( self ) :
             action = self . get_argument ( 'action' )
             session = self . get_argument ( 'session' )
             if not session :
                 self . set_status ( 400 )
                 return
             if action == 'add' :
                 self . application . shoppingCart . moveItemToCart ( session )
             elif action == 'remove' :
                 self . application . shoppingCart . removeItemFromCart ( session )
             else :
                 self . set_status ( 400 )
 
     class StatusHandler ( tornado . web . RequestHandler ) :
         @ tornado . web . asynchronous
         def get ( self ) :
             self . application . shoppingCart . register ( self . async_callback ( self . on_message ) )
         def on_message ( self , count ) :
             self . write ( '{"inventoryCount":"%d"}' % count )
             self . finish ( )
    
     class Application ( tornado . web . Application ) :
         def __init__ ( self ) :
             self . shoppingCart = ShoppingCart ( )
 
             handlers = [
                 ( r '/' , DetailHandler ) ,
                 ( r '/cart' , CartHandler ) ,
                 ( r '/cart/status' , StatusHandler )
             ]
             settings = {
                 'template_path' : 'templates' ,
                 'static_path' : 'static'
             }
             tornado . web . Application . __init__ ( self , handlers , * * settings )
 
if __name__ == '__main__' :
     tornado . options . parse_command_line ( )
 
     app = Application ( )
     server = tornado . httpserver . HTTPServer ( app )
     server . listen ( 8000 )
     tornado . ioloop . IOLoop . instance ( ) . start ( )

在咱們仔細查看 shopping_cart.py 代碼以前,瞭解一下 template 和 script 文件.咱們定義了一個 ShoppingCart 類來維護咱們的庫存信息並在購買者添加這本書到購物車時顯示實時的庫存信息.而後咱們定義 DetailHandler 來提供html頁面. CartHandler 提供了一個接口來維護購物車的信息.而後是 StatusHandler 咱們用它來通知咱們的最終庫存的清單的改變. 這個 DetailHandler 將會爲每個請求頁面生成惟一的標識符.爲每個查詢庫存的請求調用index.html 模板併發送到客戶端瀏覽器. CartHandler 提供了一個 API 給瀏覽器讓購買者進行書籍添加到購物車或者從購物車移除的操做,這個 javascript 將會在瀏覽器提交購物車操做的時候經過 POST 的方式運行.咱們來看看這個方法是如何與 StatusHandler 和 ShoppingCart 類共同影響實時庫存信息的.

1
2
3
4
class StatusHandler ( tornado . web . RequestHandler ) :
         @ tornado . web . asynchronous
         def get ( self ) :
             self . application . shoppingCart . register ( self . async_callback ( self . on_message ) )

咱們首先要注意的事情是 StatusHandler 在 get 方法前使用了 @tornado.web.asynchronous 修飾符.這將會通知 tornado 不要關閉這個 get 方法返回的鏈接.在這個方法中,咱們爲每個購物車操做註冊了一個 callback . 咱們將會爲要這個 callback 方法使用 self.async_callback 去保證這個 callback 不會被 RequestHandler 意外關閉.

在tornado1.1的版本中, callback 須要使用 self.async_callback() 方法來捕獲任何由 wrapped 功能拋出的錯誤,在tornado1.1或更新的版本中,這個操做是必須的:

1
2
3
def on_message ( self , count ) :
     self . write ( '{"inventoryCount":"%d"}' % count )
     self . finish ( )

每當用戶操做購物車的時候, ShoppingCart 控制器就會經過調用 on_message 方法來喚醒每個註冊的 callback. 這個方法會把當前的庫存信息發送給每個用而後關閉鏈接.(若是服務沒有關閉鏈接,瀏覽器將沒有辦法知道請求是否完成,也不可以通知 script 更新數據.)如今咱們的長輪詢鏈接將會被關閉,購物車控制器必須從註冊的 callback 列表中移除這個 callback . 在這個例子中咱們將會使用新的 callback 列表替換空的列表.當咱們調用並完成請求處理時將 callback 的註冊信息移除很是重要,若是使用 callback 喚醒以前已關閉的鏈接的並調用其對應的 finish() ,將會致使程序出錯. 最後, ShoppingCart 控制器將會維護全部庫存清單的分配狀況和 callback 的狀態. StatusHandler 將會經過 register 方法註冊全部的 callback 狀態,這些附加的功能都會被集成到 callback 列表中.

1
2
3
4
5
6
7
8
9
10
11
def moveItemToCart ( self , session ) :
         if session in self . carts :
             return
         self . carts [ session ] = True
         self . notifyCallbacks ( )
 
     def removeItemFromCart ( self , session ) :
         if session not in self . carts :
             return
         del ( self . carts [ session ] )
         self . notifyCallbacks ( )

ShoppingCart 控制器還爲 CartHandler 構造了 addItemOcart 和 removeItemFromCart 方法.在咱們調用 notifyCallbacks 以前 CartHandler 將會調用這些方法的給請求的頁面分配一個惟一的標識符( session 變量傳遞給這個方法)並使用它去給咱們的調用作標記.

1
2
3
4
5
6
7
def notifyCallbacks ( self ) :
         for c in self . callbacks :
             self . callbackHelper ( c )
         self . callbacks = [ ]
    
     def callbackHelper ( self , callback ) :
         callback ( self . getInventoryCount ( ) )

這些註冊的 callback 在被調用的時候將會帶上正確的庫存數量和一個已清空的 callback 列表以保證 callback 不會調用到已關閉的鏈接.

這裏是例子5-5對應的 html 模板,用於信息發生變動的書籍. 例子5-5 index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[ crayon - 5194d2915e9ac inline = "true"    class = "xml" ]
< html >
     < head >
         < title > Burt 's Books – Book Detail</title>
        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"
             type = "text/javascript" > </script>
        <script src="{{ static_url('scripts/inventory.js') }}"
             type = "application/javascript" > </script>
    </head>
    
    <body>
        <div>
            <h1>Burt' s Books < / h1 >
             < hr / >
             < p > < h2 > The Definitive Guide to the Internet < / h2 >
             < em > Anonymous < / em > < / p >
         < / div >
        
         < img src = "static/images/internet.jpg" alt = "The Definitive Guide to the Internet" / >
 
         < hr / >
         < input type = "hidden" id = "session" value = "{{ session }}" / >
         < div id = "add-to-cart" >
             < p > < span style = "color: red;" > Only < span id = "count" > { { count } } < / span >
             left in stock ! Order now ! < / span > < / p >
             < p > $ 20.00 < input type = "submit" value = "Add to Cart" id = "add-button" / > < / p >
         < / div >
         < div id = "remove-from-cart" style = "display: none;" >
             < p > < span style = "color: green;" > One copy is in your cart . < / p >
             < p > < input type = "submit" value = "Remove from Cart" id = "remove-button" / > < / p >
         < / div >
     < / body >
< / html >

[/crayon]

當 DetailHandler 返回這個 index.html 模板時.咱們能夠很簡單地將書籍的詳細信息和包含請求的 javascript 代碼返回給瀏覽器,這樣咱們就能夠動態地在頁面顯示惟一的 session id 和正確的庫存統計信息. 最後,咱們來深刻探討客戶端的 javascript 代碼,目前咱們使用的python 和 tornado 實現的圖書管理系統中客戶端代碼是一個很是重要的例子,因此你必需要了解其中的細節.在例子5-6,咱們將會使用 jQuery 庫對瀏覽器顯示頁面的效果進行操做. 例子5-6 inventory.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
$ ( document ) . ready ( function ( ) {
     document . session = $ ( '#session' ) . val ( ) ;
 
     setTimeout ( requestInventory , 100 ) ;
    
     $ ( '#add-button' ) . click ( function ( event ) {
         jQuery . ajax ( {
             url : '//localhost:8000/cart' ,
             type : 'POST' ,
             data : {
                 session : document . session ,
                 action : 'add'
             } ,
             dataType : 'json' ,
             beforeSend : function ( xhr , settings ) {
                 $ ( event . target ) . attr ( 'disabled' , 'disabled' ) ;
             } ,
             success : function ( data , status , xhr ) {
                 $ ( '#add-to-cart' ) . hide ( ) ;
                 $ ( '#remove-from-cart' ) . show ( ) ;
                 $ ( event . target ) . removeAttr ( 'disabled' ) ;
         }
     } ) ;
 
     $ ( '#remove-button' ) . click ( function ( event ) {
         jQuery . ajax ( {
             url : '//localhost:8000/cart' ,
             type : 'POST' ,
             data : {
                 session : document . session ,
                 action : 'remove'
             } ,
             dataType : 'json' ,
             beforeSend : function ( xhr , settings ) {
                 $ ( event . target ) . attr ( 'disabled' , 'disabled' ) ;
             } ,
             success : function ( data , status , xhr ) {
                 $ ( '#remove-from-cart' ) . hide ( ) ;
                 $ ( '#add-to-cart' ) . show ( ) ;
                 $ ( event . target ) . removeAttr ( 'disabled' ) ;
             }
         } ) ;
     } ) ;
} ) ;
 
function requestInventory ( ) {
     jQuery . getJSON ( '//localhost:8000/cart/status' , { session : document . session } ,
         function ( data , status , xhr ) {
             $ ( '#count' ) . html ( data [ 'inventoryCount' ] ) ;
             setTimeout ( requestInventory , 0 ) ;
         }
     ) ;
}

當文檔加載完成以後,咱們須要添加一個單擊事件的處理方法到 Add to Cart 按鈕和一個隱藏的 Remove from Cart 按鈕.這個事件處理的功能是讓關聯的 API 根據服務器的參數,顯示 Add-To-Cart 或 Remove-From-Cart 中的一個.

1
2
3
4
5
6
7
8
function requestInventory ( ) {
     jQuery . getJSON ( '//localhost:8000/cart/status' , { session : document . session } ,
         function ( data , status , xhr ) {
             $ ( '#count' ) . html ( data [ 'inventoryCount' ] ) ;
             setTimeout ( requestInventory , 0 ) ;
         }
     ) ;
}

這個 requestInventory 函數調用了一個很短的延時,等待頁面加載完成後執行.在函數的主體,咱們初始化了一個長輪詢鏈接經過 HTTP Get 請求 /cart/status 的資源. 這個延遲容許加載的進程指示器在瀏覽器加載頁面完成以後長輪詢請求不會被 Esc 鍵或 停止按鈕中斷.當請求返回成功後,當前頁面的數據將會被正確的統計數據替換. 圖片5-5 展現了兩個瀏覽器窗口中顯示的全部庫存清單. 圖片5-5

如今,讓咱們到服務器運行這個程序,你能夠經過輸入 URL 來查看書籍正確的庫存統計數據.同時打開多個瀏覽器窗口查看詳細信息的頁面,並單擊其中一個窗口的 Add to Cart 按鈕.剩餘書籍的數據將會馬上更新到其它窗口中,就像圖片5-6顯示的那樣. 這樣的購物車實現可能有些簡單,但能夠確定的是,它在邏輯上確保了咱們不會超量銷售.咱們尚未提到如何在同一臺服務器中的兩個 tornado 實例之間調用共享數據信息.這個實現咱們留給讀者作爲練習吧. 圖片5-6

長輪詢的缺陷

正如咱們看到的,HTTP 的長輪詢在高度交互的頁面反饋信息或用戶狀態上起着很是重要的做用.可是咱們仍然要注意其中存在的一些缺陷. 當咱們使用長輪詢開發應用時,請記住,服務器是否能夠控制瀏覽器請求超時的時間間隔,這是瀏覽器可能會出現異常中斷,從新啓動HTTP 鏈接,另外一種狀況是,瀏覽器會限制打開一個特定主機的併發請求數,當某一個鏈接出現異常時,其它下載網站內容的請求可能也會受到限制. 此外,你還應該留意請求是如何影響服務器性能的.一旦大量的庫存清單發生改變,全部的長鏈接請求須要同時響應變動並關閉鏈接時,服務器將會忽然接收到大量的瀏覽器從新創建鏈接的請求.對於應用程序來講,相似於用戶之間的聊天信息或通知,將會有少許的用戶鏈接被關閉.這樣的問題,如何處理.請查看下面的章節.

tornado 的 websockets

WebSockets 是html5中爲客戶端與服務端通訊提供的新協議.這個協議仍然是一個草案,因此至今只有一些最新版本的瀏覽器支持它.然而它擁有很是多的好處,以致於愈來愈多的瀏覽器開始支持它(對於web 開發來講,最謹慎的作法是保持務實的策略,信賴新功能的可用性,若是有特別需求時也能夠回滾到舊技術上) WebSockets 協議在客戶端和服務端之間提供了一個持續的雙向通訊鏈接.這個協議使用了一個新的 ws://URL 規則,可是其頭部仍然是標準的 http.而且使用標準的 http 和 https 端口,它避免了許多使用 web 代理的網絡鏈接到網站時出現的問題, html5 文檔不只描述了它的通訊協議,還提供了在使用 WebSockets 時必需要在寫入到瀏覽器客戶端頁面的 API 請求. 當 WebSockets 剛開始被一些新的瀏覽器支持的時候, tornado 已經提供了一個完整的模塊支持它. WebSockets 值得咱們花費一些精力去了解如何在應用程序中使用它.

tornado 的 websocket 模塊

tornado 的 websocket 模塊提供了一個 websockethandler 類.這個類提供瞭如何在客戶端鏈接中發起一個 websocket 事件並進行通訊. open 方法將會在客戶端收到一個新消息時調用並經過 on_message 開一個新的 websocket 鏈接,在客戶端關閉鏈接時調用 on_close關閉. 須要注意的是, WebSocktHandler 類提供了一個 write_message 方法發送信息到客戶端,還提供了一個 close 方法去關閉這個鏈接.讓咱們看看一個簡單的例子,複製客戶端發來的消息並回復給客戶端.

1
2
3
4
5
6
class EchoHandler ( tornado . websocket . WebSocketHandler ) :
     def on_open ( self ) :
         self . write_message ( 'connected!' )
 
     def on_message ( self , message ) :
         self . write_message ( message )

正如你在咱們實現的 EchoHandler 看到的.咱們只是使用了 websockethandler 的基類中的 write_message 方法,經過執行 on_open 方法簡單的發送了一個字符串 i」connected!」 返回給客戶端,這個 on_message 方法在每一次收到客戶端發來的新信息時都會執行. 而且咱們回覆了一個相同的信息給客戶端.它就是這樣工做的,讓咱們來看看如何經過這個協議去實現一個完整的例子.

例子:websockets實現的存貨清單

咱們將會在這一部分的內容看到如何使用 websockets 更新上一個 http 長輪詢的例子中的代碼.請記住! websockets 是一個新標準,因此只有最新版本的瀏覽器提供了支持.tornado 支持的 websockets 協議版本須要在 firefox6.0以上版本, safari5.0.1, chrome6及以上版本和 internet explorer 10 開發版才能正常工做. 暫且不理會這些,讓咱們來看看它的源代碼.大部分的代碼都沒有改變,只有一部分服務端應用須要修改,如 shoppingcart 類和 statushandler 類. 例子5-7 看起來很是熟悉:

例子5-7: shopping_cart.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import tornado . web
import tornado . websocket
import tornado . httpserver
import tornado . ioloop
import tornado . options
from uuid import uuid4
 
class ShoppingCart ( object ) :
     totalInventory = 10
     callbacks = [ ]
     carts = { }
 
     def register ( self , callback ) :
         self . callbacks . append ( callback )
     def unregister ( self , callback ) :
         self . callbacks . remove ( callback )
     def moveItemToCart ( self , session ) :
         if session in self . carts :
             return
         self . carts [ session ] = True
         self . notifyCallbacks ( )
     def removeItemFromCart ( self , session ) :
         if session not in self . carts :
             return
     del ( self . carts [ session ] )
         self . notifyCallbacks ( )
     def notifyCallbacks ( self ) :
         for callback in self . callbacks :
             callback ( self . getInventoryCount ( ) )
     def getInventoryCount ( self ) :
         return self . totalInventory - len ( self . carts )
 
class DetailHandler ( tornado . web . RequestHandler ) :
     def get ( self ) :
         session = uuid4 ( )
         count = self . application . shoppingCart . getInventoryCount ( )
         self . render ( "index.html" , session = session , count = count )
 
class CartHandler ( tornado . web . RequestHandler ) :
     def post ( self ) :
     action = self . get_argument ( 'action' )
     session = self . get_argument ( 'session' )
     if not session :
         self . set_status ( 400 )
         return
     if action == 'add' :
         self . application . shoppingCart . moveItemToCart ( session )
     elif action == 'remove' :
         self . application . shoppingCart . removeItemFromCart ( session )
     else :
         self . set_status ( 400 )
 
class StatusHandler ( tornado . websocket . WebSocketHandler ) :
     def open ( self ) :
         self . application . shoppingCart . register ( self . callback )
     def on_close ( self ) :
         self . application . shoppingCart . unregister ( self . callback )
     def on_message ( self , message ) :
         pass
     def callback ( self , count ) :
         self . write_message ( '{"inventoryCount":"%d"}' % count )
 
class Application ( tornado . web . Application ) :
     def __init__ ( self ) :
         self . shoppingCart = ShoppingCart ( )
         handlers = [
             ( r '/' , DetailHandler ) ,
             ( r '/cart' , CartHandler ) ,
             ( r '/cart/status' , StatusHandler )
         ]
         settings = {
             'template_path' : 'templates' ,
             'static_path' : 'static'
         }
         tornado . web . Application . __init__ ( self , handlers , * * settings )
if __name__ == '__main__' :
     tornado . options . parse_command_line ( )
     app = Application ( )
     server = tornado . httpserver . HTTPServer ( app )
     server . listen ( 8000 )
     tornado . ioloop . IOLoop . instance ( ) . start ( )

除了 import 導入的聲明外,咱們只須要改變 shoppingcart 和statushandler 類. 首先要注意的是 tornado.websocker 模塊必須用來獲取 websockethandler 的功能. 在shoppingcart 類中,咱們須要對通知系統的 callback 作一些改變. websockets 一旦打開就會一直保持開啓的狀態,不須要從 callback 列表中移除這個通知. 只須要反覆迭代這個列表,並調用 callback 獲取最新的庫存清單數據便可:

1
2
3
def notifyCallbacks ( self ) :
         for callback in self . callbacks :
             callback ( self . getInventoryCount ( ) )

另外一個改變是添加 unregister 方法, statushandler 將在 websocket 鏈接關閉時調用這個方法移除對應的 callback

1
2
def unregister ( self , callback ) :
         self . callbacks . remove ( callback )

在 statushandler 類中還有一個重要的改變.須要繼承 tornado.websocket.websockethandler ,替換掉處理每個 http 方法的功能. websocket 操做執行在 open 和 on_message 方法.當鏈接打開或收到關閉鏈接的消息時分別調用它們.此外, on_close 方法在遠程主機鏈接關閉時也會被調用.

1
2
3
4
5
6
7
8
9
class StatusHandler ( tornado . websocket . WebSocketHandler ) :
     def open ( self ) :
         self . application . shoppingCart . register ( self . callback )
     def on_close ( self ) :
         self . application . shoppingCart . unregister ( self . callback )
     def on_message ( self , message ) :
         pass
     def callback ( self , count ) :
         self . write_message ( '{"inventoryCount":"%d"}' % count )

在咱們的實現中,當 shoppingcart 開啓一個新的鏈接時註冊一個 callback 方法, 當鏈接關閉時, 註銷這個 callback . 固然,咱們依舊使用 http API 調用 cartHandler 類, 咱們不用監聽 websocket 鏈接是否有新的消息,由於 on_message 的實現是空的(咱們重載這個 on_message 實現是爲了不在收到消息時, tornado 的NotImplementedError 意外拋出).最後 callback 方法在存貨清單發生變動時,經過 websocket 鏈接通知客戶端最新的數據. javascript 代碼和上一個版本是相同的.咱們只須要改變 requestInventory 函數, 替換掉長輪詢的 AJAX 請求. 咱們使用 html5 websocket API, 請查看 例子5-8的代碼: 例子5-8 the new requestInventory function from inventory.js

1
2
3
4
5
6
7
8
9
function requestInventory ( ) {
     var host = 'ws://localhost:8000/cart/status' ;
     var websocket = new WebSocket ( host ) ;
     websocket . onopen = function ( evt ) { } ;
     websocket . onmessage = function ( evt ) {
         $ ( '#count' ) . html ( $ . parseJSON ( evt . data ) [ 'inventoryCount' ] ) ;
     } ;
     websocket . onerror = function ( evt ) { } ;
}

在經過URL ws://localhost:8000/cart/status建立一個新的 websocket 鏈接以後,咱們添加一個函數爲每個實現發送咱們設置的響應.在這個例子中咱們只關心一個事件 onmessage ,與前面修改的 requestInventory 函數相似,咱們用它向全部目錄更新相同的統計數據.(稍微不一樣的地方在於,咱們在服務器發送的 JSON 對象須要進行分析) 像前面的例子同樣,這個庫存清單會在購物者添加書籍到他們的購物車時動態地調整庫存清單.不一樣的地方在於,咱們只須要爲每個用戶維護一個固定的 websocket 鏈接,替換掉了原先須要反覆打開 http 請求的長輪詢更新.

websockets的將來

websockets協議還在草案階段,還須要進行許多修改才能定稿,然而,自從它提交到 IETF 進行終稿審覈之後,實際上它已經不可能發生太大的改動.使用 websockets最大的缺陷咱們在剛開始已經提到了,那就是到目前爲止,只有最新版本的瀏覽器才提供了對它的支持. 輕忽略掉以前的警告吧, websockets 在瀏覽器與服務器之間實現雙向通訊這一方式的前景是光明的.這個協議將會獲得充分的擴展和支持.咱們將會在愈來愈多優秀的應用上面看到它的實現.

這一章節翻譯的很是痛苦,由於博主對異步的概念還有一些理不清楚的地方,因此翻譯未必能完整的表達出做者想要表達的內容.本章依然求校訂,有任何錯誤的地方,歡迎隨時私信博主.

原創翻譯:發佈於http://blog.xihuan.de/tech/web/tornado/tornado_asynchronous_web_services.html

上一篇: 翻譯:introduce to tornado - Databases

下一篇: 翻譯: introduce to tornado - Writing Secure Applications

相關文章
相關標籤/搜索