第十四章 網絡編程
本章首先概述Python標準庫中的一些網絡模塊。然后討論SocketServer和相關的類,并介紹同時處理多個連接的各種方法。最后,簡單地說一說Twisted,這是一個使用Python編寫網絡程序的框架,功能豐富而成熟。
幾個網絡模塊
模塊socket
網絡編程中的一個基本組件是套接字(socket)。
套接字基本上是一個信息通道,兩端各有一個程序。
套接字分為兩類:服務器套接字和客戶端套接字。
為傳輸數據,套接字提供了兩個方法:send和recv(表示receive)。
要發送數據,可調用方法send并提供一個字符串;
要接收數據,可調用recv并指定最多接收多少個字節的數據。
最簡單的服務器
服務器套接字先調用方法bind,再調用方法listen來監聽特定的地址。
然后,客戶端套接字就可連接到服務器了,辦法是調用方法connect并提供調用方法bind時指定的地址(在服務器端,可使用函數socket.gethostname獲取當前機器的主機名)。這里的地址是一個格式為(host, port)的元組,其中host是主機名(如www.example.com),而port是端口號(一個整數)。
方法listen接受一個參數——待辦任務清單的長度(即最多可有多少個連接在隊列中等待接納,到達這個數量后將開始拒絕連接)
服務器套接字開始監聽后,就可接受客戶端連接了,這是使用方法accept來完成的。這個方法將阻斷(等待)到客戶端連接到來為止,然后返回一個格式為(client, address)的元組,其中client是一個客戶端套接字,而address是前面解釋過的地址。服務器能以其認為合適的方式處理客戶端連接,然后再次調用accept以接著等待新連接到來。這通常是在一個無限循環中完成的。
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()
最簡單的客戶端
import socket
s = socket.socket()host = socket.gethostname()
port = 1234s.connect((host,port))
print(s.recv(1024))
模塊urllib和urllib2
1,打開遠程文件
from urllib.request import urlopen
import rewebpage = urlopen('https://beyondyanyu.blog.csdn.net')#變量webpage將包含一個類似于文件的對象,這個對象與該網站相關聯
text = webpage.read()
m = re.search(b'<a href="([^"]+)".*?>about</a>',text,re.IGNORECASE)
m.group(1)
2,獲取遠程文件
函數urlopen
返回一個類似于文件的對象,可從中讀取數據。
可使用urlretrieve
,下載文件,并將其副本存儲在一個本地文件中。
這個函數不返回一個類似于文件的對象,而返回一個格式為(filename, headers)的元組,其中filename是本地文件的名稱(由urllib自動創建),而headers包含一些有關遠程文件的信息。
調用函數urlcleanup
且不提供任何參數,清空所有臨時文件。
獲取CSDN的主頁,并將其存儲到文件C:\webpage.html中。
urlretrieve('https://beyondyanyu.blog.csdn.net', 'C:\\python_webpage.html')
一些實用的函數
函數名稱 | 描述 |
---|---|
quote(string[, safe]) | 返回一個字符串,其中所有的特殊字符(在URL中有特殊意義的字符)都已替換為對URL友好的版本(如將~替換為%7E)參數safe是一個字符串(默認為’/’),包含不應像這樣對其進行編碼的字符。 |
quote_plus(string[, safe]) | 類似于quote,但也將空格替換為加號。 |
unquote(string) | 與quote相反。 |
unquote_plus(string) | 與quote_plus相反。 |
urlencode(query[, doseq]) | 將映射(如字典)或由包含兩個元素的元組(形如(key,value))組成的序列轉換為“使用URL編碼的”字符串。 |
其他模塊
模塊 | 描述 |
---|---|
asynchat | 包含補充asyncore的功能 |
asyncore | 異步套接字處理程序 |
cgi | 基本的CGI支持 |
Cookie | Cookie對象操作,主要用于服務器 |
cookielib | 客戶端Cookie支持 |
電子郵件(包括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客戶端支持 |
SocketServer及相關的類
模塊SocketServer是標準庫提供的服務器框架的基石,這個框架包括BaseHTTPServer、SimpleHTTPServer、CGIHTTPServer、SimpleXMLRPCServer和DocXMLRPCServer等服務器,它們在基本服務器的基礎上添加了各種功能。
SocketServer包含4個基本的服務器:TCPServer
(支持TCP套接字流)、UDPServer(支持UDP數據報套接字)以及更難懂的UnixStreamServer和UnixDatagramServer。后面3個你可能不會用到。
使用模塊SocketServer編寫服務器時,大部分代碼都位于請求處理器中。
基本請求處理程序類BaseRequestHandler將所有操作都放在一個方法中——服務器調用的方法handle。這個方法可通過屬性self.request來訪問客戶端套接字。
如果處理的是流(使用TCPServer時很可能如此),可使用StreamRequestHandler類,它包含另外兩個屬性:self.rfile(用于讀取)和self.wfile(用于寫入)。
基于SocketServer的極簡服務器
from socketserver import TCPServer,StreamRequestHandlerclass Handler(StreamRequestHandler):def handle(self):addr = self.request.getpeername()print('Got connection from',addr)self.wfile.write('Thank you for connecting')server = TCPServer(('',1234),Handler)
server.serve_forever()
多個連接
處理多個連接的主要方式有三種:分叉(forking)、線程化和異步I/O。
分叉占用的資源較多,且在客戶端很多時可伸縮性不佳。
進程:運行著的程序
分叉:對進程(運行的程序)進行分叉時,基本上是復制它,而這樣得到的兩個進程都將從當前位置開始繼續往下執行,且每個進程都有自己的內存副本(變量等)。原來的進程為父進程,復制的進程為子進程。查看函數fork的返回值可以區別父子進程。
在分叉服務器中,對于每個客戶端連接,都將通過分叉創建一個子進程。父進程繼續監聽新連接,而子進程負責處理客戶端請求。客戶端請求結束后,子進程直接退出。由于分叉出來的進程并行地運行,因此客戶端無需等待。
鑒于分叉占用的資源較多(每個分叉出來的進程都必須有自己的內存),還有另一種解決方案:線程化。
線程是輕量級進程(子進程),都位于同一個進程中并共享內存。
這減少了占用的資源,但也帶來了一個缺點:由于線程共享內存,你必須確保它們不會彼此干擾或同時修改同一項數據,否則將引起混亂。這些問題都屬于同步問題。
種避免線程和分叉的辦法是使用Stackless Python。它是一個能夠快速而輕松地在不同上下文之間切換的Python版本。它支持一種類似于線程的并行方式,名為微線程,其可伸縮性比真正的線程高得多。
使用SocketServer實現分叉和線程化
僅當方法handle需要很長時間才能執行完畢時,分叉和線程化才能提供幫助。請注意,Windows不支持分叉。
分叉服務器
from socketserver import TCPSercer,ForkingMixIn,StreamRequestHandler
class Server(ForkingMixIn,TCPSercer):pass
class Handler(StreamRequestHandler):def handle(self):addr = self.request.getpeername()print('Got connection from',addr)self.wfile.write('Thank you for connecting')server = Server(('',1234),Handler)
server.serve_forever()
線程化服務器
from socketserver import TCPServer, ThreadingMixIn, StreamRequestHandler
class Server(ThreadingMixIn, TCPServer): pass
class Handler(StreamRequestHandler):def handle(self):addr = self.request.getpeername()print('Got connection from', addr)self.wfile.write('Thank you for connecting')server = Server(('', 1234), Handler)
server.serve_forever()
使用select和poll實現異步I/O
當服務器與客戶端通信時,來自客戶端的數據可能時斷時續。如果使用了分叉和線程化,這就不是問題:因為一個進程(線程)等待數據時,其他進程(線程)可繼續處理其客戶端。
然而,另一種做法是只處理當前正在通信的客戶端。你甚至無需不斷監聽,只需監聽后將客戶端加入隊列即可。這就是框架asyncore/asynchat和Twisted采取的方法。
這種功能的基石是函數select或poll)。這兩個函數都位于模塊select中,其中poll的可伸縮性更高,但只有UNIX系統支持它(Windows不支持)。
使用select的簡單服務器
函數select接受三個必不可少的參數和一個可選參數,其中前三個參數為序列,而第四個參數為超時時間(單位為秒)。這三個序列分別表示需要輸入和輸出以及發生異常(錯誤等)的連接。
如果沒有指定超時時間,select將阻斷(即等待)到有文件描述符準備就緒;
如果指定了超時時間,select將最多阻斷指定的秒數;
如果超時時間為零,select將不斷輪詢(即不阻斷)。
select返回三個序列(即一個長度為3的元組),其中每個序列都包含相應參數中處于活動狀態的文件描述符。
import socket,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,addr = s.accept()print('Got connection from',addr)inputs.append(c)else:try:data = r.recv(1024)disconnected = not dataexcept socket.error:disconnected = Trueif disconnected:print(r.getpeername(),'disconnected')inputs.remove(r)else:print(data)
select模塊中的輪詢事件常量
事件名 | 描述 |
---|---|
POLLIN | 文件描述符中有需要讀取的數據 |
POLLPRI | 文件描述符中有需要讀取的緊急數據 |
POLLOUT | 文件描述符為寫入數據做好了準備 |
POLLERR | 文件描述符出現了錯誤狀態 |
POLLHUP | 掛起。連接已斷開。 |
POLLNVAL | 無效請求。連接未打開 |
使用poll的簡單服務器
方法poll使用起來比select容易。調用poll時,將返回一個輪詢對象。
使用方法register向這個對象注冊文件描述符(或包含方法fileno的對象)。
注冊后可使用方法unregister將它們刪除。注冊對象(如套接字)后,可調用其方法poll(它接受一個可選的超時時間參數)。
這將返回一個包含(fd, event)元組的列表(可能為空),其中fd為文件描述符,而event是發生的事件。event是一個位掩碼,這意味著它是一個整數,其各個位對應于不同的事件。
import socket,selects = 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 in fdmap:c, addr = s.accept()print('Got connection from', addr)p.register(c)fdmap[c.fileno()] = celif event & select.POLLIN:data = fdmap[fd].recv(1024)if not data: # 沒有數據 --連接已關閉print(fdmap[fd].getpeername(), 'disconnected')p.unregister(fd)del fdmap[fd]else:print(data)
Twisted
Twisted是由Twisted Matrix Laboratories(http://twistedmatrix.com)開發的,這是一個事件驅動的Python網絡框架。
使用Twisted創建的簡單服務器
事件處理程序是在協議中定義的。
你還需要一個工廠,它能夠在新連接到來時創建這樣的協議對象。
如果你只想創建自定義協議類的實例,可使用Twisted自帶的工廠——模塊twisted.internet.protocol中 的Factory類。
編寫自定義協議時,將模塊twisted.internet.protocol中的Protocol作為超類。
有新連接到來時,將調用事件處理程序connectionMade;
連接中斷時,將調用connectionLost。
來自客戶端的數據是通過處理程序dataReceived接收的。
from twisted.internet import reactor
from twisted.internet.protocol import Protocol, Factoryclass 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 = SimpleLoggerreactor.listenTCP(1234, factory)
reactor.run()
使用協議LineReceiver改進后的日志服務器
如果使用telnet連接到這個服務器以便測試它,每行輸出可能只有一個字符,是否如此取決于緩沖等因素。
為此,可編寫一個自定義協議。模塊twisted.protocols.basic包含幾個預定義的協議,其中一個就是LineReceiver。
它實現了dataReceived,并在每收到一整行后調用事件處理程序lineReceived。
from twisted.internet import reactor
from twisted.internet.protocol import Factory
from twisted.protocols.basic import LineReceiverclass 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()
小結
概念 | 描述 |
---|---|
套接字和模塊socket | 套接字是讓程序(進程)能夠通信的信息通道,這種通信可能需要通過網絡進行。模塊socket讓你能夠在較低的層面訪問客戶端套接字和服務器套接字。服務器套接字在指定的地址處監聽客戶端連接,而客戶端套接字直接連接到服務器。 |
urllib和urllib2 | 這些模塊讓你能夠從各種服務器讀取和下載數據,為此你只需提供指向數據源的URL即可。模塊urllib是一種比較簡單的實現,而urllib2功能強大、可擴展性極強。這兩個模塊都通過諸如urlopen等函數來完成工作。 |
框架SocketServer | 這個框架位于標準庫中,包含一系列同步服務器基類,讓你能夠輕松地編寫服務器。它還支持使用CGI的簡單Web(HTTP)服務器。如果要同時處理多個連接,必須使用支持分叉或線程化的混合類。 |
select和poll | 這兩個函數讓你能夠在一組連接中找出為讀取和寫入準備就緒的連接。這意味著你能夠以循環的方式依次為多個連接提供服務,從而營造出同時處理多個連接的假象。另外,相比于線程化或分叉,雖然使用這兩個函數編寫的代碼要復雜些,但解決方案的可伸縮性和效率要高得多。 |
Twisted | 這是Twisted Matrix Laboratories開發的一個框架,功能豐富而復雜,支持大多數主要的網絡協議。雖然這個框架很大且其中使用的一些成例看起來宛如天書,但其基本用法簡單而直觀。框架Twisted也是異步的,因此效率和可伸縮性都非常高。對很多自定義網絡應用程序來說,使用Twisted來開發很可能是最佳的選擇。 |
本章介紹的新函數
函數 | 描述 |
---|---|
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() | 創建一個輪詢對象,用于輪詢套接字 |
reactor.listenTCP(port, factory) | 監聽連接的Twisted函數 |
reactor.run() | 啟動主服務器循環的Twisted函數 |