本文內容全部出自《Python基礎教程》第二版,在此分享自己的學習之路。 lxx___歡迎轉載:http://www.cnblogs.com/Marlowes/p/5538341.htmllxx___ Created on Marlowes 本章將會給讀者展示一些例子,這些例子會使用多種Python ...
本文內容全部出自《Python基礎教程》第二版,在此分享自己的學習之路。
______歡迎轉載:http://www.cnblogs.com/Marlowes/p/5538341.html______
Created on Marlowes
本章將會給讀者展示一些例子,這些例子會使用多種Python的方法編寫一個將網路(比如網際網路)作為重要組成部分的程式。Python是一個很強大的網路編程工具,這麼說有很多原因,首先,Python內有很多針對常見網路協議的庫,在庫頂部可以獲得抽象層,這樣就可以集中精力放在程式的邏輯處理上,而不是停留在網路實現的細節中。其次,雖然現在還沒有現成可用的代碼來處理各種協議格式,但使用Python很容易寫出這樣的代碼,因為Python在處理位元組流的各種模式方面很擅長(在學習處理文本文件的各種方式時應有所體會了)。
使用Python提供瞭如此豐富的網路工具,所以對於Python的網路編程我只進行簡要介紹。可以在本書的其他地方找到一些例子。第十五章包括面向Web的網路編程得相關討論,後面的章節中有很多項目使用了網路模塊來完成工作。如果想要瞭解更多Python中的網路編程,我衷心推薦John Goerzen的Foundations of Python Network Programming,這本書對這個主題討論得很詳細。
在下麵的部分中,我會先大體介紹Python標準庫中可用的一些網路模塊。然後討論SocketsServer類和它的“朋友們”,接著是同時能處理多個連接的各種方法。最後看看Twisted框架——Python中一個豐富、成熟的、用於編寫網路應用程式的框架。
註:如果電腦上安裝了防火牆,那麼它可能會在每次開始運行網路程式時發出警告。它還可能會阻止程式連接到網路。這時應該配置防火牆來允許Python完成工作。如果防火牆有一個交互介面(比如Windows XP的防火牆),那麼只要在請求的時候允許連接就行了。註意,任何連接到網路的軟體都有潛在的風險,即使(尤其)是你自己寫的軟體也不例外。
14.1 少數幾個網路設計模塊
在標準庫中有很多與網路有關的模塊,在其他地方還有更多。除了那些明確處理網路事務的模塊外,還有很多模塊(比如用於在網路傳輸中處理各種形式的數據編碼的模塊)被認為是網路相關的。模塊部分的討論限定於如下的幾個模塊。
14.1.1 socket模塊
在網路編程中的一個基本組件就是套接字(socket)。套接字基本上是兩個端點的程式之間的“信息通道”。程式可能分佈在不同的機器上(通過網路連接),通過套接字相互發送信息。在Python中的大多數的網路編程都隱藏了socket模塊的基本細節,不直接和套接字交互。
套接字包括兩個:伺服器套接字和客戶機套接字。在創建一個伺服器套接字後,讓它等待連接。這樣它就在某個網路地址處(IP地址和一個埠號的組合)監聽。直到有客戶端套接字連接。連接完成後,兩者就可以進行交互了。
處理客戶端套接字通常比處理伺服器套接字更容易,因為伺服器必須準備隨時處理客戶端的連接,同時還要處理多個連接,而客戶機只是簡單地連接,完成事務,斷開連接。在這章後面,我會使用socketserver類族和Twisted框架來處理伺服器端的編程。
一個套接字就是socket模塊中的socket類的一個實例。它的實例化需要3個參數:第1個參數是地址族(預設是socket.AF_INET);第2個參數是流(socket.SOCK_STREAM,預設值)或數據報(sock.SOCK_DGRAM)套接字(還有其他套接字,例如原始套接字SOCK_RAW等);第3個參數是使用的協議(預設是0,使用預設值即可)。對於一個普通的套接字,不需要提供任何參數。
伺服器端套接字使用bind方法後,再調用listen方法去監聽某個特定的地址。客戶端套接字使用connect方法連接到伺服器。在connect方法中使用的地址與伺服器在bind方法中的地址相同(在伺服器端,能實現很多功能,比如使用函數socket.gethostname得到當前主機名)。在這種情況下,一個地址就是一個格式為(host,port)的元組,其中host是主機名(比如www.example.com),port是埠號(一個整數)。listen方法只有一個參數,即伺服器未處理的連接的長度(即允許排隊等待的連接數目,這些連接在禁用之前等待)。
伺服器端套接字開始監聽後,它就可以接受客戶端的連接。這個步驟使用accept方法來完成。這個方法會阻塞(等待)直到客戶端連接,然後該方法就返回一個格式為(client,address)的元組,client是一個客戶端套接字,address是一個前面解釋過的地址。伺服器在處理完與該客戶端的連接後,再次調用accept方法開始等待下一個連接。這個過程通常都是在一個無限迴圈中實現的。
註:這種形式的伺服器端編程稱為阻塞或者是同步網路編程。14.3節會介紹一些非阻塞或者叫非同步網路編程的例子,例子中還使用了線程來同時處理多個客戶機。
套接字有兩個方法:send和recv(用於接收),用於傳輸數據。可以使用字元串參數調用send以發送數據,用一個所需的(最大)位元組數做參數調用recv來接受數據。如果不能確定使用哪個數字比較好,那麼1024是個很好的選擇。
代碼清單14-1和代碼清單14-2展示了一個最簡單的客戶機/伺服器程式。如果在同一臺機器上運行它們(先啟動伺服器),伺服器會列印出發現一個連接的信息。然後客戶機列印出從伺服器端接收到的信息。可以在伺服器運行時同時運行多個客戶機。通過伺服器端所在機器的實際主機名來代替客戶端調用gethostname所得到的主機名,就可以讓兩個運行在不同機器上的程式通過網路連接起來。
註:使用埠號一般是被限制的。在Linux或者UNIX系統中,需要有系統管理員的許可權才能使用1024以下的埠。這些低於1024的埠號被用於標註服務,比如埠80用於Web服務(如果你有的話)。如果用Ctrl+C停止了一個服務,可能要等上一段時間才能使用同一個埠號(否則可能會得到“地址正在使用”的錯誤信息)。
# 代碼清單14-1 一個小型伺服器 import socket s = socket.socket() host = socket.gethostname() port = 1234 s.bind((host, port)) s.listen(5) while True: c, addr = s.accept() print "Got connection from", addr c.send("Thank you for connecting") c.close()
# 代碼清單14-2 一個小型客戶機 import socket s = socket.socket() host = socket.gethostname() port = 1234 s.connect((host, port)) print s.recv(1024)
可以在Python庫參考文檔(http://python.org/doc/lib/module_socket.html)和Gordon McMillan的Socket Programming HOWTO(http://doc.python.org/dev/howto/sockets.html)中找到更多的關於socket模塊的內容。
14.1.2 urllib和urllib2模塊
在能使用的各種網路函數庫中,功能最強大的可能是urllib和urllib2了。通過它們在網路上訪問文件,就好像訪問本地電腦上的文件一樣。通過一個簡單的函數調用,幾乎可以把任何URL所指向的東西用作程式的輸入。想象一下,如果將這兩個模塊與re模塊結合使用的效果:可以下載Web頁面,提取信息,以及自動生成報告等。
這兩個模塊的功能都差不多,但urllib2更好一些。如果只使用簡單的下載,urllib就足夠了。如果需要使用HTTP驗證(HTTP authentication)或cookie,或者要為自己的協議編寫擴展程式的話,那麼urllib2是更好的選擇。
1.打開遠程文件
可以像打開本地文件一樣打開遠程文件,不同之處是只能使用讀模式。使用來自urllib模塊的urlopen,而不是open(或file):
>>> from urllib import urlopen >>> webpage = urlopen("http://www.python.org")
如果電腦連接到了網路,變數webpage現在應該包含一個鏈接到http://www.python.org網頁的類文件對象。
註:如果想要試驗urllib但現在沒有連接網路,可以用以file開頭的URL訪問本地文件,比如file:c:\text\somefile.txt(記得要對"\"進行轉義)。
urlopen返回的類文件對象支持close、read、readline和readlines方法,當然也支持迭代。
假設想要提取在前面打開的Python頁中"About"鏈接的(相對)URL,那麼就可以用正則表達式來實現(有關正則表達式的更多內容請參見第十章的re模塊部分)。
>>> import re >>> text = webpage.read() >>> m = re.search('<a href="([^"]+)" .*?>about</a>', text, re.IGNORECASE) >>> m.group(1) '/about/'
註:如果網頁內容發生了變化,讀者需要自行修改正則表達式。
2.獲取遠程文件
函數urlopen返回一個能從中讀取數據的類文件對象。如果希望urllib為你下載文件併在本地中存儲一個文件的副本,那麼可以使用urlretrieve。urlretrieve返回一個元組(filename, headers)而不是類文件對象,filename是本地文件的名字(由urllib自動創建),headers包含一些遠程文件的信息(我在這裡忽略headers,如果要瞭解更多的相關信息,請在urllib的標準庫文檔中查找urlretrieve)如果想要為下載的副本指定文件名,可以在urlretrieve函數的第2個參數中給出。
urlretrieve('http://www.python.org', 'C:\\python_webpage.html')
這個語句獲取Python的主頁並把它存儲在文件C:\python_webpage.html中。如果沒有指定文件名,文件就會放在臨時的位置,用open函數可以打開它,但如果完成了對它的操作,就可以刪除它以節省硬碟空間。要清理文件時,可以調用urlcleanup函數,但不要提供參數,該函數會負責清理工作。
一些功能
除了通過URL讀取和下載文件,urllib還提供了一些函數操作URL本身(下麵假設你知道URL和CGI的基礎知識),這些函數如下。
☑ quote(string[, safe]):它返回一個字元串,其中所有的特殊字元(這些字元在URL中有特殊含義)都已被對URL友好的字元所代替(就像用%7E代替了~)。如果想在URL中使用一個包含特殊字元的字元串,這個函數就很有用。safe字元串值包含樂不應該採用這種方式編碼的字元。預設是"/"。
☑ quote_plus(string[, safe]):功能和quote差不多,但用+代替空格。
☑ unquote(string):和quote相反。
☑ unquote_plus(string):和quote_plus相反。
☑ urlencode(query[, doseq]):把映射(比如字典)或者包含連個元素的元組的序列——(key, value)這種形式——轉換成URL格式編碼的字元串,這樣的字元串可以在CGI查詢中使用(Python文檔中有更多的信息)。
14.1.3 其他模塊
就像在前面提過的,除了在本章提及的模塊外,Python庫和其他地方還有很多和網路有關的模塊,表14-1列出了Python標準庫中一些和網路相關的模塊。如表格中說明的,本書的其他章節會討論其中一些模塊。
表14-1 標準庫中一些與網路相關的模塊
asynchat asyncore的增強版本(參看第二十四章)
asyncore 非同步套接字處理程式(參看第二十四章)
cgi 基本的CGI支持(請參看第十五章)
Cookie Cookie對象操作,主要用於伺服器
cookielib 客戶端cookie支持
email E-mail消息支持(包括MIME)
ftplib FTP客戶端模塊
gopherlib gopher客戶端模塊
httplib HTTP客戶端模塊
imaplib IMAP4客戶端模塊
mailbox 讀取幾種郵箱的格式
mailcap 通過mailcap文件訪問MIME配置
mhlib 訪問MH郵箱
nntplib NNTP客戶端模塊(參看第二十三章)
poplib POP客戶端模塊
robotparser 支持解析Web伺服器的robot文件
SimpleXMLRPCServer 一個簡單的XML-RPC伺服器(參看第二十七章)
smtpd SMTP伺服器端模塊
smtplib SMTP客戶端模塊
telnetlib Telnet客戶端模塊
urlparse 支持解釋URL
xmlrpclib XML-RPC的客戶端支持(參看第二十七章)
14.2 SocketServer和它的朋友們
正如在前面的socket模塊部分看到的一樣,寫一個簡單的套接字伺服器真的不是很難。如果想實現超出基礎的應用,那麼,最好還是尋求些幫助。SocketServer模塊是標準庫中很多伺服器框架的基礎,這些伺服器框架包括BaseHTTPServer、SimpleHTTPServer、CGIHTTPServer、SimpleXMLRPCServer和DocXMLRPCServer,所有的這些伺服器框架都為基礎伺服器增加了特定的功能。
SocketServer包含了4個基本的類:針對TCP流式套接字的TCPServer;針對UDP數據報套接字的UDPServer;以及針對性不強的UnixStreamServer和UnixDatagramServer。通常可能很少用到後3個。
如果要編寫一個使用SocketServer框架的伺服器,可能會將大部分代碼放在一個請求處理程式(request handler)中。每當伺服器接收到一個請求(來自客戶端的連接)時,就會實例化一個請求處理程式,並且它的各種處理方法(handler method)會在處理請求時被調用。具體調用哪個方法取決於特定的伺服器和使用的處理程式類(handler class)。還可以把它們子類化,使得伺服器調用自定義的處理程式集。BaseRequestHandler類把所有的操作都放到了處理器的一個叫做handler的方法中,這個方法會被伺服器調用。然後這個方法就會訪問屬性self.request中的客戶端套接字。如果使用的是流(如果使用的是TCPServer,這就是可能的),那麼可以使用StreamRequestHandler類,創建了其他兩個新屬性,self.rfile(用於讀取)和self.wfile(用於寫入)。然後就能使用這類文件對象和客戶機進行通信。
SocketServer框架中的其他類實現了對HTTP伺服器的基本支持。其中包括運行CGI腳本,也包括對XML RPC(在第二十七章討論)的支持。
代碼清單14-3展示了代碼清單14-1中的小型伺服器的SocketServer版本。它能和代碼清單14-2中的客戶機協同工作。註意,StreamRequestHandler在連接被處理完後會負責關閉連接。還要註意使用''表示的是伺服器正在其上運行的機器的主機名。
# 代碼清單14-3 一個基於SocketServer的小型伺服器 from SocketServer import TCPServer from SocketServer import StreamRequestHandler class Handler(StreamRequestHandler): def handle(self): addr = self.request.getpeername() print "Got connecting from", addr self.wfile.write("Thank you for connection") server = TCPServer(("", 1234), Handler) server.serve_forever()
在Python庫參考文檔(http://python.org/doc/lib/modules-SocketServer.html)和John Goerzen的The Foundations of Python Network Programming中可以找到關於SocketServer框架的更多信息。
14.3 多個連接
到目前為止討論的伺服器解決方案都是同步的:即一次只能連接一個客戶機並處理它的請求。如果每個請求只是花費很少的時間,比如,一個完整的聊天會話,那麼同時能處理多個連接就很重要。
有3種主要的方法能實現這個目的:分叉(forking)、線程(threading)以及非同步I/O(asynchronous I/O)。通過對SocketServer伺服器(如代碼清單14-4和代碼清單14-5所示)使用混入類(mix-in class),派生進程和線程很容易處理。即使要自己實現它們,這些方法也很容易使用。它們確實有缺點:分叉占據資源,並且如果有太多的客戶端時分叉不能很好分叉(儘管如此,對於合理數量的客戶端,分叉在現代的UNIX或者Linux系統中是很高效的,如果有一個多CPU系統,那效率會更高);線程處理能導致同步問題。我在這裡不會深入討論這些問題的任何細節(我也不會深入討論多線程),但我在接下來的幾節中會向你展示這些技術。
什麼是分叉和線程處理
或許你不知道什麼是分叉和線程處理,這裡做一些說明。分叉是一個UNIX術語:當分叉一個進程(一個運行的程式)時,基本上是複製了它,並且分叉後的兩個進程都從當前的執行點繼續運行,並且每個進程都有自己的記憶體副本(比如變數)。一個進程(原來的那個)成為父進程,另一個(複製的)成為子進程。如果你是一個科幻小說迷,可以把它們想象成並行宇宙(parallel universe)。分叉操作在時間線(timeline)上創建了一個分支,最後得到了兩個獨立存在的進程。幸好進程可以判斷哪個是原進程哪個是子進程(通過查看fork函數的返回值)。因此它們所執行的操作不同(如果相同,那麼還有什麼意義?兩個進程都會做同樣的工作,會使你自己的電腦停頓)。
在使用一個分叉的伺服器中,每一個客戶端連接都要利用分叉創造一個子進程。父進程繼續監聽新的連接,同時子進程處理客戶端。當客戶端的請求結束時,子進程就退出了。因為分叉的進程是並行運行的,客戶端之間不必互相等待。
因為分叉有點耗費資源(每個分叉出來的進程都需要自己的記憶體),這就出現了另一種方法:線程。線程是輕量級的進程或子進程,所有的線程都存在於相同的(真正的)進程中,共用記憶體。資源消耗的下降伴隨著一個缺陷:因為線程共用記憶體,所以必須確保它們的變數不會衝突,或者是在同一時間修改同一內容,這就會造成混亂。這些問題都可以歸結為同步問題。在現代操作系統中(除了Windows,它不支持分叉),分叉實際是很快的,現代的硬體能比以往更好地處理資源消耗。如果不想被同步問題所困擾,分叉是一個很好的選擇。
能避免這類並行問題最好不過了。本章後面會看到基於select函數的其他解決方法。避免線程和分叉的另外一種方法是轉換到Stackless Python(http://stackless.com),一個為了能夠在不同的上下文之間快速、方便切換而設計的Python版本。它支持一個叫做微線程(microthreads)的類線程的並行形式,微線程比真線程的伸縮性要好。比如,Stackless Python微線程已經被用在星戰前夜線上(EVE Online,http://www.eve-online.com)來為成千上萬的使用者服務。
非同步I/O在底層的實現有點難度。基本的機制是select模塊的select函數(14.3.2節會介紹),這是非常難處理的。幸好存在更高的層次處理非同步I/O的框架,這就為處理一個強大可伸縮的機制提供了一個簡單的、抽象的介面。包括在標準庫中的這種類型的基本框架由asyncore和asynchat模塊組成,這會在第二十四章討論。Twisted(在本章的最後討論)是一個非常強大的非同步網路編程框架。
14.3.1 使用SocketServer進行分叉和線程處理
使用SocketServer框架創建分叉或者線程伺服器太簡單了,幾乎不需要解釋。代碼清單14-4和代碼清單14-5展示瞭如何讓代碼清單14-3中的伺服器使用分叉和線程。如是handle方法要花很長時間完成,那麼分叉和線程行為就很有用。註意,Windows不支持分叉。
# 代碼清單14-4 使用了分叉技術的伺服器 from SocketServer import TCPServer, ThreadingMixIn, StreamRequestHandler class Server(ThreadingMixIn, TCPServer): pass class Handler(StreamRequestHandler): def handle(self): address = self.request.getpeername() print "Got connection from", address self.wfile.write("Thank you for connecting") server = Server(("", 1234), Handler) server.serve_forever() # 代碼清單14-5 使用了線程處理的伺服器 from SocketServer import TCPServer, ThreadingMixIn, StreamRequestHandler class Server(ThreadingMixIn, TCPServer): pass class Handler(StreamRequestHandler): def handle(self): address = self.request.getpeername() print "Got connection from", address self.wfile.write("Thank you for connecting") server = Server(("", 1234), Handler) server.serve_forever()
14.3.2 帶有select和poll的非同步I/O
當一個伺服器與一個客戶端通信時,來自客戶端的數據可能不是連續的。如果使用分叉或線程處理,那就不是問題。當一個程式在等待數據,另一個並行的程式可以繼續處理它們自己的客戶端。另外的處理方法是只處理在給定時間內真正要進行通信的客戶端。不需要一直監聽,只要監聽(或讀取)一會兒,然後把它放到其他客戶端的後面。
這是asyncore/asynchat(參見第二十四章)框架和Twisted框架(參加下一節)從採用的方法,這種功能的基礎是select函數,如果poll函數可用,那也可以是它,這兩個函數都來自select模塊。這兩個函數中,poll的伸縮性更好,但它只能在UNIX系統中使用(這就是說,在Windows中不可用)。
select函數需要3個序列作為它的必選參數,此外還有一個可選的以秒為單位的超時時間作為第4個參數。這些序列是文件描述符整數(或者是帶有返回這樣整數的fileno方法的對象)。這些就是我們等待的連接。3個序列用於輸入、輸出以及異常情況(錯誤以及類似的東西)。如果沒有給定超時時間,select會阻塞(也就是處於等待狀態),直到其中的一個文件描述符已經為行動做好了準備;如果給定了超時時間,select最多阻塞給定的超時時間,如果給定的超時時間是0,那麼就給出一個連續的poll(即不阻塞)。select的返回值是3個序列(也就是一個長度為3的元組),每個代表相應參數的一個活動子集。比如,返回的第1個序列是一個輸入文件描述符的序列,其中有一些可以讀取的東西。
序列能包含文件對象(在Windows中行不通)或套接字。代碼清單14-6展示了一個使用select的為很多連接服務的伺服器(註意,伺服器套接字本身被提供給select,這樣select就能在準備接受一個新的連接時發出通知)。伺服器是個簡單的記錄器(logger),它輸出(在本地)來自客戶機的所有數據。可以使用Telnet(或者寫一個簡單的基於套接字的客戶機來為它提供數據)連接它來進行測試。嘗試用多個Telnet去連接來驗證伺服器能同時為多個客戶端服務(伺服器的日誌會記錄其中兩個客戶端的輸入信息)。
# 代碼清單14-6 使用了select的簡單伺服器 import socket import select s = socket.socket() host = socket.gethostname() port = 1234 s.bind((host, port)) s.listen(5) inputs = [s] while True: rs, ws, es = select.select(inputs, [], []) for r in rs: if r is s: c, address = s.accept() print "Got connection from", address inputs.append(c) else: try: data = r.recv(1024) disconnected = not data except socket.error: disconnected = True if disconnected: print r.getpeername(), "disconnected" inputs.remove(r) else: print data
poll方法使用起來比select簡單。在調用poll時,會得到一個poll對象。然後就可以使用poll對象的register方法註冊一個文件描述符(或者是帶有fileno方法的對象)。註冊後可以使用unregister方法移除註冊的對象。註冊了一些對象(比如套接字)以後,就可以調用poll方法(帶有一個可選的超時時間參數)並得到一個(fd, event)格式列表(可能是空的),其中fd是文件描述符,event則告訴你發生了什麼。這是一個位掩碼(bitmask),意思是它是一個整數,這個整數的每個位對應不同的時間。那些不同的事件是select模塊的常量,在表14-2中會進行解釋,為了驗證是否設置了一個給定位(也就是說,一個給定的時間是否發生了),可以使用按位與操作符(&):
if event & select.POLLIN: ...
表14-2 select模塊中的polling事件常量
POLLIN 讀取來自文件描述符的數據
POLLPRI 讀取來自文件描述符的緊急數據
POLLOUT 文件描述符已經準備好數據,寫入時不會發生阻塞
POLLERR 與文件描述符有關的錯誤情況
POLLHUP 掛起,連接丟失
POLLNVAL 無效請求,連接沒有打開
代碼清單14-7中的程式時代碼清單14-6中的伺服器的重寫,現在使用poll來代替select。註意我添加了一個從文件描述符(ints)到套接字對象的映射(fdmap)。
# 代碼清單14-7 使用poll的簡單伺服器 import socket import select s = socket.socket() host = socket.gethostname() port = 1234 s.bind((host, port)) fdmap = {s.fileno(): s} s.listen(5) p = select.poll() p.register(s) while True: events = p.poll() for fd, event in events: if fd == s.fileno(): c, address = s.accept() print "Got connection from", address p.register(c) fdmap[c.fileno()] = c elif event & select.POLLIN: data = fdmap[fd].recv(1024) if not data: # Not data -- disconnected print fdmap[fd].getpeername(), "disconnected" p.unregister(fd) del fdmap[fd] else: print data
更多信息
在Python庫參考文檔(http://python.org/doc/lib/module-select.html)中可以找到更多的關於select和poll的信息。也可以閱讀標準庫中的asyncore和asynchat模塊的源代碼(位於Python安裝程式的asyncore.py和asynchat.py文件中)。
14.4 Twisted
來自於Twisted Matrix實驗室(http://twistedmatrix.com)的Twisted是一個事件驅動的Python網路框架,原來是為網路游戲開發的,現在被所有類型的網路軟體使用。在Twisted中,需要實現事件處理程式,這很像在GUI工具包(參見第十二章)中做的那樣。實際上,Twisted能很好地和幾個常見的GUI工具包(Tk、GTK、Qt以及wxWidgets)協同工作。本節會介紹一些基本概的概念並且展示如何使用Twisted來做一些相對簡單的網路編程。掌握了基本概念以後,就能根據Twisted的文檔去做一些更高級的網路編程。Twisted是一個非常豐富的框架,並且支持Web伺服器、客戶機、SSH2、SMTP、POP3、IMAP4、AIM、ICQ、IRC、MSN、Jabber、NNTP和DNS,等等。
14.4.1 下載並安裝Twisted
安裝Twisted很容易。首先訪問Twisted Matrix的網頁(http://twistedmatrix.com),然後點擊下載鏈接。如果使用的是Windows的話,那麼下載與Python版本對應的Windows安裝程式,如果使用的是其他的系統那就下載源代碼檔案文件(如果使用了包管理器,比如Portage、RPM、APT、Fink或者MacPorts,那就可以直接下載並安裝Twisted)。Windows安裝程式是無須說明的按步進行的安裝嚮導,他可能會花些時間解壓縮和編譯,你要做的就是等待。為了安裝源代碼檔案文檔,首先要解壓縮(使用tar,然後根據下載的是哪種類型的檔案文件來決定使用gunzip還是bunzip2)然後運行Distutils腳本:
python setup.py install
應該可以使用Twisted了。
14.4.2 編寫Twisted伺服器
這章之前編寫的基本套接字伺服器是顯式的。其中的一些有很清楚的事件迴圈,用來查找新的連接和新數據,而基於SocketServer的伺服器有一個隱式的迴圈,在迴圈中伺服器查找連接併為每個連接創建一個處理程式,但處理程式在要讀數據時必須是顯式的。Twisted(以及在第二十四章討論的asyncore/asynchat框架)使用一個事件甚至多個基於事件的方法。要編寫基本的伺服器,就要實現處理比如新客戶端連接、新數據到達以及一個客戶端斷開連接等事件的事件處理程式。具體的類能通過基本類儘力更精煉的事件,比如包裝“數據到達”事件、收集數據直到新的一行,然後觸發“一行數據到達”的事件。
註:有一個類似Twisted特征的內容我在本節沒有處理,那就是延遲和延遲執行,請參考Twisted文檔以瞭解更多信息(比如可以看Twisted文檔中的HOWTO頁中名為"Deferreds are beautiful"的教程)。
事件處理程式在一個協議(protocol)中定義;在一個新的連接到達時,同樣需要一個創建這種協議對象的工廠(factory),但如果只是想要創建一個通用的協議類的示例,那麼就可以使用Twisted自帶的工廠。factory類在twisted.internet.protocol模塊中。當編寫自己的協議時,要使用和超類一樣的模塊中的protocol。得到了一個連接後,事件處理程式connectionMade就會被調用;丟失了一個連接後,connectionLost就會被調用。來自客戶端的數據是通過處理程式dataReceived接收的。當然不能使用事件處理策略來把數據發回到客戶端,如果要實現此功能,可以使用對象self.transort,這個對象有一個write方法,也有一個包含客戶機地址(主機名和埠號)的client屬性。
代碼清單14-8包含代碼清單14-6和代碼清單14-7中伺服器的Twisted版本。希望讀者也會覺得Twisted版本更簡單、更易讀。這裡只涉及一點設置,必須實例化factory,還要設置它的protocol屬性,這樣它在和客戶機通信時就知道使用什麼協議(自定義協議)。然後就開始在給定的埠處使用工廠進行監聽,這個工廠要通過實例化協議對象來準備處理連接。程式使用的是reactor中的listenTCP函數來監聽,最後通過調用同一個模塊中的run函數啟動伺服器。
# 代碼清單14-8 使用Twisted的簡單伺服器 from twisted.internet import reactor from twisted.internet.protocol import Protocol, Factory class SimpleLogger(Protocol): def connectionMade(self): print "Got connection from", self.transport.client def connectionLost(self, reason): print self.transport.client, "disconnected" def dataReceived(self, data): print data factory = Factory() factory.protocol = SimpleLogger reactor.listenTCP(1234, factory) reactor.run()
如果用Telnet連接到此伺服器併進行測試的話,那麼每行可能只輸出一個字元取決於緩衝或類似的東西。當然可以使用sys.stdout.write來代替print。但在很多情況下可能更喜歡每次得到一行,而不是任意的數據。編寫一個處理這種情況的自定義協議很容易,實際上已經有一個現成的類了。twisted.protocols.basic模塊中包含一個有用的預定義協議,是LineReceiver。它實現了dataReceived並且只要收到了一整行就調用事件處理程式lineReceived。
註:如果要在接受數據時做些事情,可以使用由LineReveiver定義的叫做rawDataReceived的事件處理程式,也可以使用lineReceived,但它依賴於dataReceived的實現LineReceiver。
轉換協議只需要很少的工作。代碼清單14-9展示了轉換的結果。如果在運行伺服器時查看輸出結果,會看到換行符被去掉了,換句話說,使用print不再提供兩倍的換行符。
# 代碼清單14-9 一個使用了LineReceiver協議改進的記錄伺服器 from twisted.internet import reactor from twisted.internet.protocol import Factory from twisted.protocol.basic import LineReceiver class SimpleLogger(LineReceiver): def connectionMade(self): print "Got connection from", self.transport.client def connectionLost(self, reason): print self.transport.client, "disconnected" def lineReceived(self, line): print line factory = Factory() factory.protocol = SimpleLogger reactor.listenTCP(1234, factory) reactor.run()
關於Twisted框架的內容有很多我沒有講到。如果想要學習更多知識,應該查看線上的文檔,文檔可以在Twisted的網站(http://twistedmatrix.com)上找到。
14.5 小結
本章介紹了Python中網路編程中的一些方法。究竟選擇什麼方法取決於程式特定的需要和開發者的偏好。選擇了某種方法後,就需要瞭解具體方法的更多內容。本章介紹的內容如下。
☑ 套接字和socket模塊:套接字程式(進程)之間進行通信的信息通道,可能會通過網路來通信。socket模塊給提供了對客戶端和伺服器端套接字的低級訪問功能。伺服器端套接字會在指定的地址監聽客戶端的連接,而客戶機是直接連接的。
☑ urllib和urllib2:這些模塊可以在給出數據源的URL時讓從不同的伺服器讀取和下載數據。urllib模塊是一個簡單一些的實現,而urllib2是可擴展的,而且很強大。兩者都通過urlopen等簡單的函數來工作。
☑ SocketServer框架:這是一個同步的網路伺服器基類。位於標準庫中,使用它可以很容易地編寫伺服器。它甚至用CGI支持簡單的Web服務(HTTP)。如果想同時處理多個連接,可以使用分叉和線程來處理混入類。
☑ select和poll:這兩個函數讓你可以考慮一組連接並且能找出已經準備好讀取或者寫入的連接。
☑ Twisted:這是來自Twisted Matrix實驗室的框架,支持絕大多數的網路協議,它內容豐富並且很複雜,儘管很龐大,有的習慣用語卻不太容易記,但它的基本用法簡單、直觀。Twisted框架是非同步的,因此它在伸縮性和效率方面表現的很好。如果能使用Twisted,它可能很多自定義網路應用程式的最佳選擇。
14.5.1 本章的新函數
本章涉及的新函數如表14-3所示。
表14-3 本章的新函數
urllib.urlopen(url[, data[, proxies]]) 通過URL打開一個類文件對象
urllib.urlretrieve(url[, fname[, hook[, data]]]) 通過URL下載一個文件
urllib.quote(string[, safe]) 引用特定的URL字元
urllib.quote_plus(string[, safe]) 和quote相同,但是將空格引用為+
urllib.unquote(string) 和quote相反
urllib.unquote_plus(string) 和quote_plus相反
urllib.urlencode(query[, doseq]) 在CGI請求中使用的編碼映射
select.select(iseq, oseq, eseq[, timeout]) 找出準備好讀取/寫入的套接字
select.poll() 為polling套接字創建一個poll對象
reactor.listenTCP(port, factory) Twisted函數,監聽連接
reactor.run() Twisted函數,主伺服器迴圈
14.5.2 接下來學什麼
所有關於網路程式設計的內容都介紹完了嗎?不可能,下一章會處理網路世界中的一個非常具體並且更為人們熟悉的實體:Web。