<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                到目前為止,我們已經看到了許多使Tornado成為一個Web應用強有力框架的功能。它的簡單性、易用性和便捷性使其有足夠的理由成為許多Web項目的不錯的選擇。然而,Tornado受到最多關注的功能是其異步取得和提供內容的能力,它有著很好的理由:它使得處理非阻塞請求更容易,最終導致更高效的處理以及更好的可擴展性。在本章中,我們將看到Tornado異步請求的基礎,以及一些推送技術,這種技術可以使你使用更少的資源來提供更多的請求以編寫更簡單的Web應用。 [TOC=2,3] ## 5.1 異步Web請求 大部分Web應用(包括我們之前的例子)都是阻塞性質的,也就是說當一個請求被處理時,這個進程就會被掛起直至請求完成。在大多數情況下,Tornado處理的Web請求完成得足夠快使得這個問題并不需要被關注。然而,對于那些需要一些時間來完成的操作(像大數據庫的請求或外部API),這意味著應用程序被有效的鎖定直至處理結束,很明顯這在可擴展性上出現了問題。 不過,Tornado給了我們更好的方法來處理這種情況。應用程序在等待第一個處理完成的過程中,讓I/O循環打開以便服務于其他客戶端,直到處理完成時啟動一個請求并給予反饋,而不再是等待請求完成的過程中掛起進程。 為了實現Tornado的異步功能,我們構建一個向Twotter搜索API發送HTTP請求的簡單Web應用。這個Web應用有一個參數q作為查詢字符串,并確定多久會出現一條符合搜索條件的推文被發布在Twitter上("每秒推數")。確定這個數值的方法非常粗糙,但足以達到例子的目的。圖5-1展示了這個應用的界面。 ![圖5-1](https://box.kancloud.cn/2015-09-04_55e96e96df91e.jpg) 圖5-1 異步HTTP示例:推率 我們將展示這個應用的三個不同版本:首先,是一個使用同步HTTP請求的版本,然后是一個使用帶有回調函數的Tornado異步HTTP客戶端版本。最后,我們將展示如何使用Tornado 2.1版本新增的gen模塊來使異步HTTP請求更加清晰和易實現。為了理解這些例子,你不需要成為關于Twitter搜索API的專家,但一定的熟悉不會有害。你可以在[https://dev.twitter.com/docs/api/1/get/search](https://dev.twitter.com/docs/api/1/get/search)閱讀關于搜索API的開發者文檔。 ### 5.1.1 從同步開始 代碼清單5-1包含我們的推率計算器的同步版本的代碼。記住我們在頂部導入了Tornado的httpclient模塊:我們將使用這個模塊的HTTPClient類來執行HTTP請求。之后,我們將使用這個模塊的AsyncHTTPClient。 代碼清單5-1 同步HTTP請求:tweet_rate.py ~~~ 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,然后用它執行一個到Twitter搜索API的請求。下面是最相關的一部分代碼: ~~~ 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個推文,而result_type參數指定我們只想獲得匹配搜索的最近推文)。fetch方法會返回一個HTTPResponse對象,其?body屬性包含我們從遠端URL獲取的任何數據。Twitter將返回一個JSON格式的結果,所以我們可以使用Python的json模塊來從結果中創建一個Python數據結構。 fetch方法返回的HTTPResponse對象允許你訪問HTTP響應的任何部分,不只是body。可以在[官方文檔](http://www.tornadoweb.org/en/stable/httpclient.html)[1]閱讀更多相關信息。 處理函數的其余部分關注的是計算每秒推文數。我們使用搜索結果中最舊推文與最新推文時間戳之差來確定搜索覆蓋的時間,然后使用這個數值除以搜索取得的推文數來獲得我們的最終結果。最后,我們編寫了一個擁有這個結果的簡單HTML頁面給瀏覽器。 ### 5.1.2 阻塞的困擾 到目前為止,我們已經編寫了 一個請求Twitter API并向瀏覽器返回結果的簡單Tornado應用。盡管應用程序本身響應相當快,但是向Twitter發送請求到獲得返回的搜索數據之間有相當大的滯后。在同步(到目前為止,我們假定為單線程)應用,這意味著同時只能提供一個請求。所以,如果你的應用涉及一個2秒的API請求,你將每間隔一秒才能提供(最多!)一個請求。這并不是你所稱的高可擴展性應用,即便擴展到多線程和/或多服務器 。 為了更具體的看出這個問題,我們對剛編寫的例子進行基準測試。你可以使用任何基準測試工具來驗證這個應用的性能,不過在這個例子中我們使用優秀的[Siege utility](http://www.joedog.org/siege-home/)工具進行測試。它可以這樣使用: ~~~ $ siege http://localhost:8000/?q=pants -c10 -t10s ~~~ 在這個例子中,Siege對我們的應用在10秒內執行大約10個并發請求,輸出結果如圖5-2所示。我們可以很容易看出,這里的問題是無論每個請求自身返回多么快,API往返都會以至于產生足夠大的滯后,因為進程直到請求完成并且數據被處理前都一直處于強制掛起狀態。當一兩個請求時這還不是一個問題,但達到100個(甚至10個)用戶時,這意味著整體變慢。 ![圖5-2](https://box.kancloud.cn/2015-09-04_55e96e9752d9b.jpg) 圖5-2 同步推率獲取 此時,不到10秒時間10個相似用戶的平均響應時間達到了1.99秒,共計29次。請記住,這個例子只提供了一個非常簡單的網頁。如果你要添加其他Web服務或數據庫的調用的話,結果會更糟糕。這種代碼如果被 用到網站上,即便是中等強度的流量都會導致請求增長緩慢,甚至發生超時或失敗。 ### 5.1.3 基礎異步調用 幸運的是,Tornado包含一個AsyncHTTPClient類,可以執行異步HTTP請求。它和代碼清單5-1的同步客戶端實現有一定的相似性,除了一些我們將要討論的重要區別。代碼清單5-2是其源代碼。 代碼清單5-2 異步HTTP請求:tweet_rate_async.py ~~~ 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方法并不返回調用的結果。取而代之的是它指定了一個callback參數;你指定的方法或函數將在HTTP請求完成時被調用,并使用HTTPResponse作為其參數。 ~~~ 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搜索API請求到網頁中的所有邏輯被搬到了on_response函數中。還需要注意的是@tornado.web.asynchronous裝飾器的使用(在get方法的定義之前)以及在回調方法結尾處調用的self.finish()。我們稍后將簡要的討論他們的細節。 這個版本的應用擁有和之前同步版本相同的外觀,但其性能更加優越。有多好呢?讓我們看看基準測試的結果吧。 正如你在圖5-3中所看到的,我們從同步版本的每秒3.20個事務提升到了12.59,在相同的時間內總共提供了118次請求。這真是一個非常大的改善!正如你所想象的,當擴展到更多用戶和更長時間時,它將能夠提供更多連接,并且不會遇到同步版本遭受的變慢的問題。 ![圖5-3](https://box.kancloud.cn/2015-09-04_55e96e97cd23f.jpg) 圖5-3 異步推率獲取 ### 5.1.4 異步裝飾器和finish方法 Tornado默認在函數處理返回時關閉客戶端的連接。在通常情況下,這正是你想要的。但是當我們處理一個需要回調函數的異步請求時,我們需要連接保持開啟狀態直到回調函數執行完畢。你可以在你想改變其行為的方法上面使用@tornado.web.asynchronous裝飾器來告訴Tornado保持連接開啟,正如我們在異步版本的推率例子中IndexHandler的get方法中所做的。下面是相關的代碼片段: ~~~ class IndexHandler(tornado.web.RequestHandler): @tornado.web.asynchronous def get(self): query = self.get_argument('q') [... other request handler code here...] ~~~ 記住當你使用@tornado.web.asynchonous裝飾器時,Tornado永遠不會自己關閉連接。你必須在你的RequestHandler對象中調用finish方法來顯式地告訴Tornado關閉連接。(否則,請求將可能掛起,瀏覽器可能不會顯示我們已經發送給客戶端的數據。)在前面的異步示例中,我們在on_response函數的write后面調用了finish方法: ~~~ [... 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() ~~~ ### 5.1.5 異步生成器 現在,我們的推率程序的異步版本運轉的不錯并且性能也很好。不幸的是,它有點麻煩:為了處理請求 ,我們不得不把我們的代碼分割成兩個不同的方法。當我們有兩個或更多的異步請求要執行的時候,編碼和維護都顯得非常困難,每個都依賴于前面的調用:不久你就會發現自己調用了一個回調函數的回調函數的回調函數。下面就是一個構想出來的(但不是不可能的)例子: ~~~ 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.] ~~~ 幸運的是,Tornado 2.1版本引入了tornado.gen模塊,可以提供一個更整潔的方式來執行異步請求。代碼清單5-3就是使用了tornado.gen版本的推率應用源代碼。讓我們先來看一下,然后討論它是如何工作的。 代碼清單5-3 使用生成器模式的異步請求:tweet_rate_gen.py ~~~ 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() ~~~ 正如你所看到的,這個代碼和前面兩個版本的代碼非常相似。主要的不同點是我們如何調用Asynchronous對象的fetch方法。下面是相關的代碼部分: ~~~ 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的控制,允許在HTTP請求進行中執行其他任務。當HTTP請求完成時,RequestHandler方法在其停止的地方恢復。這種構建的美在于它在請求處理程序中返回HTTP響應,而不是回調函數中。因此,代碼更易理解:所有請求相關的邏輯位于同一個位置。而HTTP請求依然是異步執行的,所以我們使用tornado.gen可以達到和使用回調函數的異步請求版本相同的性能,正如我們在圖5-4中所看到的那樣。 ![圖5-4](https://box.kancloud.cn/2015-09-04_55e96e983f0fc.jpg) 圖5-4 使用tornado.gen的異步推率獲取 記住@tornado.gen.engine裝飾器的使用需要剛好在get方法的定義之前;這將提醒Tornado這個方法將使用tornado.gen.Task類。tornado.gen模塊還喲一些其他類和函數可以方便Tornado的異步編程。查閱一下[文檔](http://www.tornadoweb.org/en/stable/gen.html)[1]是非常值得的。 使一切異步 在本章中我們使用了Tornado的異步HTTP客戶端作為如何執行異步任務的實現。其他開發者也編寫了針對其他任務的異步客戶端庫。志愿者們在[Tornado wiki](https://github.com/facebook/tornado/wiki/Links)上維護了一個關于這些庫的相當完整的列表。 一個重要的例子是bit.ly的[asyncmongo](https://github.com/bitly/asyncmongo),它可以異步的調用MongoDB服務器。這個庫是我們的一個非常不錯的選擇,因為它是專門給Tornado開發者開發提供異步數據庫訪問的,不過對于使用其他數據庫的用戶而言,在這里也可以找到不錯的異步數據存儲庫的選擇。 ### 5.1.6 異步操作總結 正如我們在前面的例子中所看到的,Tornado異步Web發服務不僅容易實現也在實踐中有著不容小覷的能力。使用異步處理可以讓我們的應用在長時間的API和數據庫請求中免受阻塞之苦,最終更快地提供更多請求。盡管不是所有的處理都能從異步中受益--并且實際上嘗試整個程序非阻塞會迅速使事情變得復雜--但Tornado的非阻塞功能可以非常方便的創建依賴于緩慢查詢或外部服務的Web應用。 不過,值得注意的是,這些例子都非常的做作。如果你正在設計一個任何規模下帶有該功能的應用,你可能希望客戶端瀏覽器來執行Twitter搜索請求(使用JavaScript),而讓Web服務器轉向提供其他請求。在大多數情況下,你至少希望將結果緩存以便兩次相同搜索項的請求不會導致再次向遠程API執行完整請求。通常,如果你在后端執行HTTP請求提供網站內容,你可能希望重新思考如何建立你的應用。 考慮到這一點,在下一組示例中,我們將看看如何在前端使用像JavaScript這樣的工具處理異步應用,讓客戶端承擔更多工作,以提高你應用的擴展性。 ## 5.2 使用Tornado進行長輪詢 Tornado異步架構的另一個優勢是它能夠輕松處理HTTP長輪詢。這是一個處理實時更新的方法,它既可以應用到簡單的數字標記通知,也可以實現復雜的多用戶聊天室。 部署提供實時更新的Web應用對于Web程序員而言是一項長期的挑戰。更新用戶狀態、發送新消息提醒、或者任何一個需要在初始文檔完成加載后由服務器向瀏覽器發送消息方法的全局活動。一個早期的方法是瀏覽器以一個固定的時間間隔向服務器輪詢新請求。這項技術帶來了新的挑戰:輪詢頻率必須足夠快以便通知是最新的,但又不能太頻繁,當成百上千的客戶端持續不斷的打開新的連接會使HTTP請求面臨嚴重的擴展性挑戰。頻繁的輪詢使得Web服務器遭受"凌遲"之苦。 所謂的"服務器推送"技術允許Web應用實時發布更新,同時保持合理的資源使用以及確保可預知的擴展。對于一個可行的服務器推送技術而言,它必須在現有的瀏覽器上表現良好。最流行的技術是讓瀏覽器發起連接來模擬服務器推送更新。這種方式的HTTP連接被稱為長輪詢或Comet請求。 長輪詢意味著瀏覽器只需啟動一個HTTP請求,其連接的服務器會有意保持開啟。瀏覽器只需要等待更新可用時服務器"推送"響應。當服務器發送響應并關閉連接后,(或者瀏覽器端客戶請求超時),客戶端只需打開一個新的連接并等待下一個更新。 本節將包括一個簡單的HTTP長輪詢實時應用以及證明Tornado架構如何使這些應用更簡單。 ### 5.2.1 長輪詢的好處 HTTP長輪詢的主要吸引力在于其極大地減少了Web服務器的負載。相對于客戶端制造大量的短而頻繁的請求(以及每次處理HTTP頭部產生的開銷),服務器端只有當其接收一個初始請求和再次發送響應時處理連接。大部分時間沒有新的數據,連接也不會消耗任何處理器資源。 瀏覽器兼容性是另一個巨大的好處。任何支持AJAX請求的瀏覽器都可以執行推送請求。不需要任何瀏覽器插件或其他附加組件。對比其他服務器端推送技術,HTTP長輪詢最終成為了被廣泛使用的少數幾個可行方案之一。 我們已經接觸過長輪詢的一些使用。實際上,前面提到的狀態更新、消息通知以及聊天消息都是目前流行的網站功能。像Google Docs這樣的站點使用長輪詢同步協作,兩個人可以同時編輯文檔并看到對方的改變。Twitter使用長輪詢指示瀏覽器在新狀態更新可用時展示通知。Facebook使用這項技術在其聊天功能中。長輪詢如此流行的一個原因是它改善了應用的用戶體驗:訪客不再需要不斷地刷新頁面來獲取最新的內容。 ### 5.2.2 示例:實時庫存報告 這個例子演示了一個根據多個購物者瀏覽器更新的零售商庫存實時計數服務。這個應用提供一個帶有"Add to Cart"按鈕的HTML書籍細節頁面,以及書籍剩余庫存的計數。一個購物者將書籍添加到購物車之后,其他訪問這個站點的訪客可以立刻看到庫存的減少。 為了提供庫存更新,我們需要編寫一個在初始化處理方法調用后不會立即關閉HTTP連接的RequestHandler子類。我們使用Tornado內建的asynchronous裝飾器完成這項工作,如代碼清單5-4所示。 代碼清單5-4 長輪詢:shopping_cart.py ~~~ 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。我們定義了一個ShoppingCart類來維護我們的庫存中商品的數量,以及把商品加入購物車的購物者列表。然后,我們定義了DetailHandler用于渲染HTML;CartHandler用于提供操作購物車的接口;StatusHandler用于查詢全局庫存變化的通知。 DetailHandler為每個頁面請求產生一個唯一標識符,在每次請求時提供庫存數量,并向瀏覽器渲染index.html模板。CartHandler為瀏覽器提供了一個API來請求從訪客的購物車中添加或刪除物品。瀏覽器中運行的JavaScript提交POST請求來操作訪客的購物車。我們將在下面的StatusHandler和ShoppingCart類的講解中看到這些方法是如何作用域庫存數量查詢的。 ~~~ 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方法返回時不會關閉連接。在這個方法中,我們只是注冊了一個帶有購物車控制器的回調函數。我們使用self.async_callback包住回調函數以確保回調函數中引發的異常不會使RequestHandler關閉連接。 在Tornado 1.1之前的版本中,回調函數必須被包在self.async_callback()方法中來捕獲被包住的函數可能會產生的異常。不過,在Tornado 1.1或更新版本中,這不再是顯式必須的了。 ~~~ def on_message(self, count): self.write('{"inventoryCount":"%d"}' % count) self.finish() ~~~ 每當訪客操作購物車,ShoppingCart控制器為每個已注冊的回調函數調用on_message方法。這個方法將當前庫存數量寫入客戶端并關閉連接。(如果服務器不關閉連接的話,瀏覽器可能不會知道請求已經被完成,也不會通知腳本有過更新。)既然長輪詢連接已經關閉,購物車控制器必須刪除已注冊的回調函數列表中的回調函數。在這個例子中,我們只需要將回調函數列表替換為一個新的空列表。在請求處理中被調用并完成后刪除已注冊的回調函數十分重要,因為隨后在調用回調函數時將在之前已關閉的連接上調用finish(),這會產生一個錯誤。 最后,ShoppingCart控制器管理庫存分批和狀態回調。StatusHandler通過register方法注冊回調函數,即添加這個方法到內部的callbacks數組。 ~~~ 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中的addItemToCart和removeItemFromCart。當CartHandler調用這些方法,請求頁面的唯一標識符(傳給這些方法的session變量)被用于在調用notifyCallbacks之前標記庫存。[2] ~~~ def notifyCallbacks(self): for c in self.callbacks: self.callbackHelper(c) self.callbacks = [] def callbackHelper(self, callback): callback(self.getInventoryCount()) ~~~ 已注冊的回調函數被以當前可用庫存數量調用,并且回調函數列表被清空以確保回調函數不會在一個已經關閉的連接上調用。 代碼清單5-5是展示書籍列表變化的模板。 代碼清單5-5 長輪詢:index.html ~~~ <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.</span></p> <p><input type="submit" value="Remove from Cart" id="remove-button" /></p> </div> </body> </html> ~~~ 當DetailHandler渲染index.html模板時,我們只是渲染了圖書的詳細信息并包含了必需的的JavaScript代碼。此外,我們通過session變量動態地包含了一個唯一ID,并以count變量保存當前庫存值。 最后,我們將討論客戶端的JavaScript代碼。由于這是一本關于Tornado的書籍,因此我們直到現在一直使用的是Python,而這個例子中的客戶端代碼是至關重要的,我們至少要能夠理解它的要點。在代碼清單5-6中,我們使用了jQuery庫來協助定義瀏覽器的頁面行為。 代碼清單5-6 長輪詢:inventory.js ~~~ $(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 form Cart"按鈕。這些事件處理函數關聯服務器的API調用,并交換添加到購物車接口和從購物車移除接口。 ~~~ function requestInventory() { jQuery.getJSON('//localhost:8000/cart/status', {session: document.session}, function(data, status, xhr) { $('#count').html(data['inventoryCount']); setTimeout(requestInventory, 0); } ); } ~~~ requestInventory函數在頁面完成加載后經過一個短暫的延遲再進行調用。在函數主體中,我們通過到/cart/status的HTTP?GET請求初始化一個長輪詢。延遲允許在瀏覽器完成渲染頁面時使加載進度指示器完成,并防止Esc鍵或停止按鈕中斷長輪詢請求。當請求成功返回時,count的內容更新為當前的庫存量。圖5-5所示為展示全部庫存的兩個瀏覽器窗口。 ![圖5-5](https://box.kancloud.cn/2015-09-04_55e96e98b0e75.jpg) 圖5-5 長輪詢示例:全部庫存 現在,當你運行服務器,你將可以加載根URL并看到書籍的當前庫存數量。打開多個細節頁的瀏覽器窗口,并在其中一個窗口點擊"Add to Cart"按鈕。其余窗口的剩余庫存數量會立刻更新,如果5-6所示。 ![圖5-6](https://box.kancloud.cn/2015-09-04_55e96e990c690.jpg) 圖5-6 長輪詢示例:一個物品在購物車中 這是一個非常簡單的購物車實現,可以肯定的是--沒有邏輯確保我們不會跌破總庫存量,更不用說數據無法在Tornado應用的不同調用間或同一服務器并行的應用實例間保留。我們將這些改善作為練習留給讀者。 ### 5.2.3 長輪詢的缺陷 正如我們所看到的,HTTP長輪詢在站點或特定用戶狀態的高度交互反饋通信中非常有用。但我們也應該知道它的一些缺陷。 當使用長輪詢開發應用時,記住對于瀏覽器請求超時間隔無法控制是非常重要的。由瀏覽器決定在任何中斷情況下重新開啟HTTP連接。另一個潛在的問題是許多瀏覽器限制了對于打開的特定主機的并發請求數量。當有一個連接保持空閑時,剩下的用來下載網站內容的請求數量就會有限制。 此外,你還應該明白請求是怎樣影響服務器性能的。再次考慮購物車應用。由于在庫存變化時所有的推送請求*同時*應答和關閉,使得在瀏覽器重新建立連接時服務器受到了新請求的猛烈沖擊。對于像用戶間聊天或消息通知這樣的應用而言,只有少數用戶的連接會同時關閉,這就不再是一個問題了。 ## 5.3 Tornado與WebSockets WebSockets是HTML5規范中新提出的客戶-服務器通訊協議。這個協議目前仍是草案,只有最新的一些瀏覽器可以支持它。但是,它的好處是顯而易見的,隨著支持它的瀏覽器越來越多,我們將看到它越來越流行。(和以往的Web開發一樣,必須謹慎地堅持依賴可用的新功能并能在必要時回滾到舊技術的務實策略。) WebSocket協議提供了在客戶端和服務器間持久連接的雙向通信。協議本身使用新的ws://URL格式,但它是在標準HTTP上實現的。通過使用HTTP和HTTPS端口,它避免了從Web代理后的網絡連接站點時引入的各種問題。HTML5規范不只描述了協議本身,還描述了使用WebSockets編寫客戶端代碼所需要的瀏覽器API。 由于WebSocket已經在一些最新的瀏覽器中被支持,并且Tornado為之提供了一些有用的模塊,因此來看看如何使用WebSockets實現應用是非常值得的。 ### 5.3.1 Tornado的WebSocket模塊 Tornado在websocket模塊中提供了一個WebSocketHandler類。這個類提供了和已連接的客戶端通信的WebSocket事件和方法的鉤子。當一個新的WebSocket連接打開時,open方法被調用,而on_message和on_close方法分別在連接接收到新的消息和客戶端關閉時被調用。 此外,WebSocketHandler類還提供了write_message方法用于向客戶端發送消息,close方法用于關閉連接。 ~~~ class EchoHandler(tornado.websocket.WebSocketHandler): def open(self): self.write_message('connected!') def on_message(self, message): self.write_message(message) ~~~ 正如你在我們的EchoHandler實現中所看到的,open方法只是使用WebSocketHandler基類提供的write_message方法向客戶端發送字符串"connected!"。每次處理程序從客戶端接收到一個新的消息時調用on_message方法,我們的實現中將客戶端提供的消息原樣返回給客戶端。這就是全部!讓我們通過一個完整的例子看看實現這個協議是如何簡單的吧。 ### 5.3.2 示例:使用WebSockets的實時庫存 在本節中,我們可以看到把之前使用HTTP長輪詢的例子更新為使用WebSockets是如何簡單。但是,請記住,WebSockets還是一個新標準,只有最新的瀏覽器版本可以支持它。Tornado支持的特定版本的WebSocket協議版本只在Firefox 6.0或以上、Safari 5.0.1或以上、Chrome 6或以上、IE 10預覽版或以上版本的瀏覽器中可用。 不去管免責聲明,讓我們先看看源碼吧。除了服務器應用需要在ShoppingCart和StatusHandler類中做一些修改外,大部分代碼保持和之前一樣。代碼清單5-7看起來會很熟悉。 代碼清單5-7 WebSockets:shopping_cart.py ~~~ 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() ~~~ 除了額外的導入語句外,我們只需要改變ShoppingCart和StatusHandler類。首先需要注意的是,為了獲得WebSocketHandler的功能,需要使用tornado.websocket模塊。 在ShoppingCart類中,我們只需要在通知回調函數的方式上做一個輕微的改變。因為WebSOckets在一個消息發送后保持打開狀態,我們不需要在它們被通知后移除內部的回調函數列表。我們只需要迭代列表并調用帶有當前庫存量的回調函數: ~~~ def notifyCallbacks(self): for callback in self.callbacks: callback(self.getInventoryCount()) ~~~ 另一個改變是添加了unregisted方法。StatusHandler會在WebSocket連接關閉時調用該方法移除一個回調函數。 ~~~ def unregister(self, callback): self.callbacks.remove(callback) ~~~ 大部分改變是在繼承自tornado.websocket.WebSocketHandler的StatusHandler類中的。WebSocket處理函數實現了open和on_message方法,分別在連接打開和接收到消息時被調用,而不是為每個HTTP方法實現處理函數。此外,on_close方法在連接被遠程主機關閉時被調用。 ~~~ 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方法,并在連接關閉時注銷了這個回調函數。因為我們依然使用了CartHandler類的HTTP API調用,因此不需要監聽WebSocket連接中的新消息,所以on_message實現是空的。(我們覆寫了on_message的默認實現以防止在我們接收消息時Tornado拋出NotImplementedError異常。)最后,callback方法在庫存改變時向WebSocket連接寫消息內容。 這個版本的JavaScript代碼和之前的非常相似。我們只需要改變其中的requestInventory函數。我們使用HTML5 WebSocket API取代長輪詢資源的AJAX請求。參見代碼清單5-8. 代碼清單5-8 WebSockets:inventory.js中新的requestInventory函數 ~~~ 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) { }; } ~~~ 在創建了一個到ws://localhost:8000/cart/status的心得WebSocket連接后,我們為每個希望響應的事件添加了處理函數。在這個例子中我們唯一關心的事件是onmessage,和之前版本的requestInventory函數一樣更新count的內容。(輕微的不同是我們必須手工解析服務器送來的JSON對象。) 就像前面的例子一樣,在購物者添加書籍到購物車時庫存量會實時更新。不同之處在于一個持久的WebSocket連接取代了每次長輪詢更新中重新打開的HTTP請求。 ### 5.3.3 WebSockets的未來 WebSocket協議目前仍是草案,在它完成時可能還會修改。然而,因為這個規范已經被提交到IETF進行最終審查,相對而言不太可能會再面臨重大的改變。正如本節開頭所提到的那樣,WebSocket的主要缺陷是目前只支持最新的一些瀏覽器。 盡管有上述警告,WebSockets仍然是在瀏覽器和服務器之間實現雙向通信的一個有前途的新方法。當協議得到了廣泛的支持后,我們將開始看到更加著名的應用的實現。 [1] 書中網頁已不存在,替換為當前網址。 [2] 下面的這組代碼書中使用的不是前面的代碼,這里為了保持一致修改為和前面的代碼一樣。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看