1. 程式人生 > >python基於tornade的高併發介面程式設計實戰學習

python基於tornade的高併發介面程式設計實戰學習

第一章:引言
在過去的五年裡,Web開發人員的可用工具實現了跨越式地增長。當技術專家不斷推動極限,使Web應用無處不在時,我們也不得不升級我們的工具、建立框架以保證構建更好的應用。我們希望能夠使用新的工具,方便我們寫出更加整潔、可維護的程式碼,使部署到世界各地的使用者時擁有高效的可擴充套件性。
這就讓我們談論到Tornado,一個編寫易建立、擴充套件和部署的強力Web應用的夢幻選擇。我們三個都因為Tornado的速度、簡單和可擴充套件性而深深地愛上了它,在一些個人專案中嘗試之後,我們將其運用到日常工作中。我們已經看到,Tornado在很多大型或小型的專案中提升了開發者的速度(和樂趣!),同時,其魯棒性和輕量級也給開發者一次又一次留下了深刻的印象。
本書的目的是對Tornado Web伺服器進行一個概述,通過框架基礎、一些示例應用和真實世界使用的最佳實踐來引導讀者。我們將使用示例來詳細講解Tornado如何工作,你可以用它做什麼,以及在構建自己第一個應用時要避免什麼。
在本書中,我們假定你對Python已經有了粗略的瞭解,知道Web服務如何運作,對資料庫有一定的熟悉。有一些不錯的書籍可以為你深入瞭解這些提供參考(比如Learning Python,Restful Web Service和MongoDB: The Definitive Guide)。
你可以在Github上獲得本書中示例的程式碼。如果你有關於這些示例或其他方面的任何思想,歡迎在那裡告訴我們。
所以,事不宜遲,讓我們開始深入瞭解吧!
1.1 Tornado是什麼?¶
Tornado是使用Python編寫的一個強大的、可擴充套件的Web伺服器。它在處理嚴峻的網路流量時表現得足夠強健,但卻在建立和編寫時有著足夠的輕量級,並能夠被用在大量的應用和工具中。
我們現在所知道的Tornado是基於Bret Taylor和其他人員為FriendFeed所開發的網路服務框架,當FriendFeed被Facebook收購後得以開源。不同於那些最多隻能達到10,000個併發連線的傳統網路伺服器,Tornado在設計之初就考慮到了效能因素,旨在解決C10K問題,這樣的設計使得其成為一個擁有非常高效能的框架。此外,它還擁有處理安全性、使用者驗證、社交網路以及與外部服務(如資料庫和網站API)進行非同步互動的工具。
延伸閱讀:C10K問題
基於執行緒的伺服器,如Apache,為了傳入的連線,維護了一個作業系統的執行緒池。Apache會為每個HTTP連線分配執行緒池中的一個執行緒,如果所有的執行緒都處於被佔用的狀態並且尚有記憶體可用時,則生成一個新的執行緒。儘管不同的作業系統會有不同的設定,大多數Linux釋出版中都是預設執行緒堆大小為8MB。Apache的架構在大負載下變得不可預測,為每個開啟的連線維護一個大的執行緒池等待資料極易迅速耗光伺服器的記憶體資源。
大多數社交網路應用都會展示實時更新來提醒新訊息、狀態變化以及使用者通知,這就要求客戶端需要保持一個開啟的連線來等待伺服器端的任何響應。這些長連線或推送請求使得Apache的最大執行緒池迅速飽和。一旦執行緒池的資源耗盡,伺服器將不能再響應新的請求。
非同步伺服器在這一場景中的應用相對較新,但他們正是被設計用來減輕基於執行緒的伺服器的限制的。當負載增加時,諸如Node.js,lighttpd和Tornodo這樣的伺服器使用協作的多工的方式進行優雅的擴充套件。也就是說,如果當前請求正在等待來自其他資源的資料(比如資料庫查詢或HTTP請求)時,一個非同步伺服器可以明確地控制以掛起請求。非同步伺服器用來恢復暫停的操作的一個常見模式是當合適的資料準備好時呼叫回撥函式。我們將會在第五章講解回撥函式模式以及一系列Tornado非同步功能的應用。
自從2009年9月10日釋出以來,TornadoTornado已經獲得了很多社群的支援,並且在一系列不同的場合得到應用。除FriendFeed和Facebook外,還有很多公司在生產上轉向Tornado,包括Quora、Turntable.fm、Bit.ly、Hipmunk以及MyYearbook等。
總之,如果你在尋找你那龐大的CMS或一體化開發框架的替代品,Tornado可能並不是一個好的選擇。Tornado並不需要你擁有龐大的模型建立特殊的方式,或以某種確定的形式處理表單,或其他類似的事情。它所做的是讓你能夠快速簡單地編寫高速的Web應用。如果你想編寫一個可擴充套件的社交應用、實時分析引擎,或RESTful API,那麼簡單而強大的Python,以及Tornado(和這本書)正是為你準備的!
1.1.1 Tornado入門¶
在大部分*nix系統中安裝Tornado非常容易–你既可以從PyPI獲取(並使用easy_install或pip安裝),也可以從Github上下載原始碼編譯安裝,如下所示[1]:
$ curl -L -O

https://github.com/facebook/tornado/archive/v3.1.0.tar.gz
tarxvzfv3.1.0.tar.gz cd tornado-3.1.0
pythonsetup.pybuild sudo python setup.py install
Tornado官方並不支援Windows,但你可以通過ActivePython的PyPM包管理器進行安裝,類似如下所示:
C:> pypm install tornado
一旦Tornado在你的機器上安裝好,你就可以很好的開始了!壓縮包中包含很多demo,比如建立部落格、整合Facebook、執行聊天服務等的示例程式碼。我們稍後會在本書中通過一些示例應用逐步講解,不過你也應該看看這些官方demo。
本書中的程式碼假定你使用的是基於Unix的系統,並且使用的是Python2.6或2.7版本。如果是這樣,你就不需要任何除了Python標準庫之外的東西。如果你的Python版本是2.5或更低,在安裝pycURL、simpleJSON和Python開發標頭檔案後可以執行Tornado。[2]
1.1.2 社群和支援¶
對於問題、示例和一般的指南,Tornado官方文件是個不錯的選擇。在tornadoweb.org上有大量的例子和功能缺陷,更多細節和變更可以在Tornado在Github上的版本庫中看到。而對於更具體的問題,可以到Tornado的Google Group中諮詢,那裡有很多活躍的日常使用Tornado的開發者。
1.2 簡單的Web服務¶
既然我們已經知道了Tornado是什麼了,現在讓我們看看它能做什麼吧。我們首先從使用Tornado編寫一個簡單的Web應用開始。
1.2.1 Hello Tornado¶
Tornado是一個編寫對HTTP請求響應的框架。作為程式設計師,你的工作是編寫響應特定條件HTTP請求的響應的handler。下面是一個全功能的Tornado應用的基礎示例:
程式碼清單1-1 基礎:hello.py
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

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):
greeting = self.get_argument(‘greeting’, ‘Hello’)
self.write(greeting + ‘, friendly user!’)

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()
編寫一個Tornado應用中最多的工作是定義類繼承Tornado的RequestHandler類。在這個例子中,我們建立了一個簡單的應用,在給定的埠監聽請求,並在根目錄(”/”)響應請求。
你可以在命令列裡嘗試執行這個程式以測試輸出:
$ python hello.py –port=8000
現在你可以在瀏覽器中開啟http://localhost:8000,或者開啟另一個終端視窗使用curl測試我們的應用:
$ curl http://localhost:8000/
Hello, friendly user!
$ curl http://localhost:8888/?name=ljt
Salutations, friendly user!
讓我們把這個例子分成小塊,逐步分析它們:
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web
在程式的最頂部,我們匯入了一些Tornado模組。雖然Tornado還有另外一些有用的模組,但在這個例子中我們必須至少包含這四個模組。
from tornado.options import define, options
define(“port”, default=8000, help=”run on the given port”, type=int)
Tornado包括了一個有用的模組(tornado.options)來從命令列中讀取設定。我們在這裡使用這個模組指定我們的應用監聽HTTP請求的埠。它的工作流程如下:如果一個與define語句中同名的設定在命令列中被給出,那麼它將成為全域性options的一個屬性。如果使用者執行程式時使用了–help選項,程式將打印出所有你定義的選項以及你在define函式的help引數中指定的文字。如果使用者沒有為這個選項指定值,則使用default的值進行代替。Tornado使用type引數進行基本的引數型別驗證,當不合適的型別被給出時丟擲一個異常。因此,我們允許一個整數的port引數作為options.port來訪問程式。如果使用者沒有指定值,則預設為8000。
class IndexHandler(tornado.web.RequestHandler):
def get(self):
greeting = self.get_argument(‘greeting’, ‘Hello’)
self.write(greeting + ‘, friendly user!’)
這是Tornado的請求處理函式類。當處理一個請求時,Tornado將這個類例項化,並呼叫與HTTP請求方法所對應的方法。在這個例子中,我們只定義了一個get方法,也就是說這個處理函式將對HTTP的GET請求作出響應。我們稍後將看到實現不止一個HTTP方法的處理函式。
greeting = self.get_argument(‘greeting’, ‘Hello’)
Tornado的RequestHandler類有一系列有用的內建方法,包括get_argument,我們在這裡從一個查詢字串中取得引數greeting的值。(如果這個引數沒有出現在查詢字串中,Tornado將使用get_argument的第二個引數作為預設值。)
self.write(greeting + ‘, friendly user!’)
RequestHandler的另一個有用的方法是write,它以一個字串作為函式的引數,並將其寫入到HTTP響應中。在這裡,我們使用請求中greeting引數提供的值插入到greeting中,並寫回到響應中。
if name == “main“:
tornado.options.parse_command_line()
app = tornado.web.Application(handlers=[(r”/”, IndexHandler)])
這是真正使得Tornado運轉起來的語句。首先,我們使用Tornado的options模組來解析命令列。然後我們建立了一個Tornado的Application類的例項。傳遞給Application類init方法的最重要的引數是handlers。它告訴Tornado應該用哪個類來響應請求。馬上我們講解更多相關知識。
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
從這裡開始的程式碼將會被反覆使用:一旦Application物件被建立,我們可以將其傳遞給Tornado的HTTPServer物件,然後使用我們在命令列指定的埠進行監聽(通過options物件取出。)最後,在程式準備好接收HTTP請求後,我們建立一個Tornado的IOLoop的例項。
1.2.1.1 引數handlers¶
讓我們再看一眼hello.py示例中的這一行:
app = tornado.web.Application(handlers=[(r”/”, IndexHandler)])
這裡的引數handlers非常重要,值得我們更加深入的研究。它應該是一個元組組成的列表,其中每個元組的第一個元素是一個用於匹配的正則表示式,第二個元素是一個RequestHanlder類。在hello.py中,我們只指定了一個正則表示式-RequestHanlder對,但你可以按你的需要指定任意多個。
1.2.1.2 使用正則表示式指定路徑¶
Tornado在元組中使用正則表示式來匹配HTTP請求的路徑。(這個路徑是URL中主機名後面的部分,不包括查詢字串和碎片。)Tornado把這些正則表示式看作已經包含了行開始和結束錨點(即,字串”/”被看作為”^/$”)。
如果一個正則表示式包含一個捕獲分組(即,正則表示式中的部分被括號括起來),匹配的內容將作為相應HTTP請求的引數傳到RequestHandler物件中。我們將在下個例子中看到它的用法。
1.2.2 字串服務¶
例1-2是一個我們目前為止看到的更復雜的例子,它將介紹更多Tornado的基本概念。
程式碼清單1-2 處理輸入:string_service.py
import textwrap

import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

from tornado.options import define, options
define(“port”, default=8000, help=”run on the given port”, type=int)

class ReverseHandler(tornado.web.RequestHandler):
def get(self, input):
self.write(input[::-1])

class WrapHandler(tornado.web.RequestHandler):
def post(self):
text = self.get_argument(‘text’)
width = self.get_argument(‘width’, 40)
self.write(textwrap.fill(text, int(width)))

if name == “main“:
tornado.options.parse_command_line()
app = tornado.web.Application(
handlers=[
(r”/reverse/(\w+)”, ReverseHandler),
(r”/wrap”, WrapHandler)
]
)
http_server = tornado.httpserver.HTTPServer(app)
http_server.listen(options.port)
tornado.ioloop.IOLoop.instance().start()
如同執行第一個例子,你可以在命令列中執行這個例子使用如下的命令:
pythonstringservice.pyport=8000Web/reverse/stringGETURL curl http://localhost:8000/reverse/stressed
desserts

$ curl http://localhost:8000/reverse/slipup
pupils
其二,到/wrap的POST請求將從引數text中取得指定的文字,並返回按照引數width指定寬度裝飾的文字。下面的請求指定一個沒有寬度的字串,所以它的輸出寬度被指定為程式中的get_argument的預設值40個字元。
$ http://localhost:8000/wrap -d text=Lorem+ipsum+dolor+sit+amet,+consectetuer+adipiscing+elit.
Lorem ipsum dolor sit amet, consectetuer
adipiscing elit.
字串服務示例和上一節示例程式碼中大部分是一樣的。讓我們關注那些新的程式碼。首先,讓我們看看傳遞給Application建構函式的handlers引數的值:
app = tornado.web.Application(handlers=[
(r”/reverse/(\w+)”, ReverseHandler),
(r”/wrap”, WrapHandler)
])
在上面的程式碼中,Application類在”handlers”引數中例項化了兩個RequestHandler類物件。第一個引導Tornado傳遞路徑匹配下面的正則表示式的請求:
/reverse/(\w+)
正則表示式告訴Tornado匹配任何以字串/reverse/開始並緊跟著一個或多個字母的路徑。括號的含義是讓Tornado儲存匹配括號裡面表示式的字串,並將其作為請求方法的一個引數傳遞給RequestHandler類。讓我們檢查ReverseHandler的定義來看看它是如何工作的:
class ReverseHandler(tornado.web.RequestHandler):
def get(self, input):
self.write(input[::-1])
你可以看到這裡的get方法有一個額外的引數input。這個引數將包含匹配處理函式正則表示式第一個括號裡的字串。(如果正則表示式中有一系列額外的括號,匹配的字串將被按照在正則表示式中出現的順序作為額外的引數傳遞進來。)
現在,讓我們看一下WrapHandler的定義:
class WrapHandler(tornado.web.RequestHandler):
def post(self):
text = self.get_argument(‘text’)
width = self.get_argument(‘width’, 40)
self.write(textwrap.fill(text, int(width)))
WrapHandler類處理匹配路徑為/wrap的請求。這個處理函式定義了一個post方法,也就是說它接收HTTP的POST方法的請求。
我們之前使用RequestHandler物件的get_argument方法來捕獲請求查詢字串的的引數。同樣,我們也可以使用相同的方法來獲得POST請求傳遞的引數。(Tornado可以解析URLencoded和multipart結構的POST請求)。一旦我們從POST中獲得了文字和寬度的引數,我們使用Python內建的textwrap模組來以指定的寬度裝飾文字,並將結果字串寫回到HTTP響應中。
1.2.3 關於RequestHandler的更多知識¶
到目前為止,我們已經瞭解了RequestHandler物件的基礎:如何從一個傳入的HTTP請求中獲得資訊(使用get_argument和傳入到get和post的引數)以及寫HTTP響應(使用write方法)。除此之外,還有很多需要學習的,我們將在接下來的章節中進行講解。同時,還有一些關於RequestHandler和Tornado如何使用它的只是需要記住。
1.2.3.1 HTTP方法¶
截止到目前討論的例子,每個RequestHandler類都只定義了一個HTTP方法的行為。但是,在同一個處理函式中定義多個方法是可能的,並且是有用的。把概念相關的功能繫結到同一個類是一個很好的方法。比如,你可能會編寫一個處理函式來處理資料庫中某個特定ID的物件,既使用GET方法,也使用POST方法。想象GET方法來返回這個部件的資訊,而POST方法在資料庫中對這個ID的部件進行改變:

matched with (r”/widget/(\d+)”, WidgetHandler)

class WidgetHandler(tornado.web.RequestHandler):
def get(self, widget_id):
widget = retrieve_from_db(widget_id)
self.write(widget.serialize())

def post(self, widget_id):
    widget = retrieve_from_db(widget_id)
    widget['foo'] = self.get_argument('foo')
    save_to_db(widget)

我們到目前為止只是用了GET和POST方法,但Tornado支援任何合法的HTTP請求(GET、POST、PUT、DELETE、HEAD、OPTIONS)。你可以非常容易地定義上述任一種方法的行為,只需要在RequestHandler類中使用同名的方法。下面是另一個想象的例子,在這個例子中針對特定frob ID的HEAD請求只根據frob是否存在給出資訊,而GET方法返回整個物件:

matched with (r”/frob/(\d+)”, FrobHandler)

class FrobHandler(tornado.web.RequestHandler):
def head(self, frob_id):
frob = retrieve_from_db(frob_id)
if frob is not None:
self.set_status(200)
else:
self.set_status(404)
def get(self, frob_id):
frob = retrieve_from_db(frob_id)
self.write(frob.serialize())
1.2.3.2 HTTP狀態碼¶
從上面的程式碼可以看出,你可以使用RequestHandler類的set_status()方法顯式地設定HTTP狀態碼。然而,你需要記住在某些情況下,Tornado會自動地設定HTTP狀態碼。下面是一個常用情況的綱要:
404 Not Found
Tornado會在HTTP請求的路徑無法匹配任何RequestHandler類相對應的模式時返回404(Not Found)響應碼。
400 Bad Request
如果你呼叫了一個沒有預設值的get_argument函式,並且沒有發現給定名稱的引數,Tornado將自動返回一個400(Bad Request)響應碼。
405 Method Not Allowed
如果傳入的請求使用了RequestHandler中沒有定義的HTTP方法(比如,一個POST請求,但是處理函式中只有定義了get方法),Tornado將返回一個405(Methos Not Allowed)響應碼。
500 Internal Server Error
當程式遇到任何不能讓其退出的錯誤時,Tornado將返回500(Internal Server Error)響應碼。你程式碼中任何沒有捕獲的異常也會導致500響應碼。
200 OK
如果響應成功,並且沒有其他返回碼被設定,Tornado將預設返回一個200(OK)響應碼。
當上述任何一種錯誤發生時,Tornado將預設向客戶端傳送一個包含狀態碼和錯誤資訊的簡短片段。如果你想使用自己的方法代替預設的錯誤響應,你可以重寫write_error方法在你的RequestHandler類中。比如,程式碼清單1-3是hello.py示例添加了常規的錯誤訊息的版本。
程式碼清單1-3 常規錯誤響應:hello-errors.py
import tornado.httpserver
import tornado.ioloop
import tornado.options
import tornado.web

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):
greeting = self.get_argument(‘greeting’, ‘Hello’)
self.write(greeting + ‘, friendly user!’)
def write_error(self, status_code, **kwargs):
self.write(“Gosh darnit, user! You caused a %d error.” % status_code)

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()
當我們嘗試一個POST請求時,會得到下面的響應。一般來說,我們應該得到Tornado預設的錯誤響應,但因為我們覆寫了write_error,我們會得到不一樣的東西:
$ curl -d foo=bar http://localhost:8000/
Gosh darnit, user! You caused a 405 error.
1.2.4 下一步¶
現在你已經明白了最基本的東西,我們渴望你想了解更多。在接下來的章節,我們將向你展示能夠幫助你使用Tornado建立成熟的Web服務和應用的功能和技術。首先是:Tornado的模板系統。
[1] 壓縮包地址已更新到Tornado的最新版本3.1.0。
[2] 書中原文中關於Python3.X版本的相容性問題目前已不存在,因此省略該部分。