圖解Python網路編程
- 2020 年 3 月 5 日
- 筆記
本篇索引
(1)基本原理
(2)socket模組
(3)select模組
(4)asyncore模組
(5)asynchat模組
(1)基本原理
本篇指的網路編程,僅僅是指如何在兩台或多台電腦之間,通過網路收發數據包;而不涉及具體的應用層功能(如Web伺服器、 郵件收發、網路爬蟲等等),那些屬於應用編程的範疇,需要了解的可參看下一篇 Internet 應用編程。
關於使用Python進行網路通訊編程,簡單的例子網路上一搜一大把,但基本都是僅僅幾行最簡單的套接字程式碼, 用來做個小實驗可以,但並不能實用。因為大多數Python的書和文檔著重點在於講Python語法, 並不會太細地把網路編程的底層原理給你講清楚,比如:同步/非同步的關係、執行緒並發監聽的實現架構等等。 如果你要了解那些知識,需要去看《Unix網路編程》、《TCP/IP詳解-卷1》之類的書。
本篇試圖在講Python網路編程的基礎上,把涉及到的原理稍帶整理一起描述一下。 一方面希望能幫到想進一步掌握Python網路編程的初學者、另一方面也方便我自己快速查閱用。
● IP地址、埠
每台電腦(伺服器)都有一個固定的IP地址,而一台伺服器上可能運行若干個不同的程式, 每個程式提供一種服務(比如:郵件服務程式、Web服務程式等等),每個不同的服務程式會佔用一個埠號(也有佔有多個埠的,比較少見), 埠(port)是一個16位數字,範圍從065535。其中0~1023為保留埠,保留給特定的網路協議使用 (比如:HTTP固定使用80埠、HTTPS固定使用443埠)。一般你自己的服務程式可任意使用10000以上的埠。 它們的示意關係如下圖所示:
由於要訪問一個服務程式需要知道“一個IP地址和一個埠號”,因此兩者加一起合稱一個“地址(address)”。 在Python中,一個地址(address)一般用一個元組來表示,形如:address = (ipaddr, port)。
● 套接字
服務程式與客戶端程式進行通行,需要通過一個叫做 socket(套接字)的媒介。socket 的本意是“插口”, 在網路通訊中一般把它翻譯成“套接字”。套接字的作用,就相當於在伺服器程式和客戶端程式之間建立了一根虛擬的專線, 伺服器程式和客戶端程式可以分別通過自己這端的套接字,向對方寫入和讀出數據 (在Python中,套接字一般為一個 socket 類型的實例),如此即可實現伺服器和客戶端的數據通訊。 在伺服器程式中,同一個埠可生成若干個套接字,每個套接字跟一個特定的客戶端進行通訊。 在客戶端,如果與一個服務程式通訊,一般只需生成一個套接字即可。 如下圖所示:
● 編碼問題
由於網路是以ascii文本格式傳輸數據的,而在Python3中,所有字元串都是Unicode編碼的。 因此,將字元串通過網路發送時必須轉碼。而從網路收到數據時,也必須進行解碼以轉換成Python的字元串。
發送時,可使用字元串的encode()
方法進行轉碼,也可直接使用內置的bytes類型。 接收時,可使用字元串的decode()
方法進行解碼。
# 轉碼示例 s.send('Hello world!'.encode('ascii')) # 方法一:使用encode()轉碼 s.send(b'Hello world!') # 方法二:直接發送bytes類型(位元組序列) # 解碼示例 recv_data = s.recv(1024) recv_str = recv_data.decode('ascii') # 使用decode()解碼
(2)socket模組
socket模組提供了最原始的仿UNIX的網路編程方式,因為它非常底層,所以很適合用來說明網路編程的概念, 但在實際工作中基本上不太會直接用socket模組去編寫網路程式。實際工作中, 一般都會使用Python庫中提供的更加方便的模組或類(比如SocketServer等)來編寫網路程式。
● 基本的UDP編程模型
UDP的編程模型比較簡單,雖然伺服器 socket 和客戶端 socket 也是一對一通訊,但是一般發完數據就放手, 伺服器程式不需要花心思去管理多個客戶端的連接。大體流程示意可參看下圖:
在伺服器程式端,先生成一個套接字,然後通過bind()
方法綁定到本地地址和特定埠,之後就可以通過recvfrom()
方法監聽客戶端數據了。 recvfrom()
方法為阻塞運行,即:如果客戶端沒有新的數據進來,伺服器程式會僵在這裡, 只有等到客戶端有新的數據進來,這個方法才會返回,然後繼續運行後面的語句。 上圖是一個基本示意,各個方法的詳細解釋可參看後文的表格。
以下為一個UDP伺服器程式的示例:
# UdpServer.py # 功能:接收客戶端數據,將客戶端發過來的字元串加個頭“echo:”再回發過去) import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.bind(("", 10000)) # 伺服器程式綁定本地10000埠,空字元串表示本地IP地址 while True: data, address = s.recvfrom(256) print("Received a connection from %s" % str(address)) s.sendto(b"echo:" + data, address)
以下為UDP客戶端測試程式:
# UdpClient.py import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # AF_INET指IPv4,SOCK_DGRAM指UDP,後面會有詳釋 s.sendto(b"Hello", ("127.0.0.1", 10000)) # 伺服器地址和埠(客戶端一般會由作業系統隨機分配發送埠) resp, addr = s.recvfrom(256) print(resp) s.sendto(b"World", ("127.0.0.1", 10000)) resp, addr = s.recvfrom(256) print(resp) s.close()
需要注意的是,在網路編程中,伺服器程式和客戶端程式是需要一定配合的, 需要避免進入雙方都在等對方數據的卡住狀態,如下圖所示:
● 基本的TCP編程模型
使用UDP通訊的伺服器程式一般不太需要太複雜的編程技術。而如果使用TCP通訊, 不使用“並發”或“非同步”或“select()”編程技術基本是沒法實用的。在實用中,一般只要使用這三種技術中的一種就可以了。 簡單來說:“並發”是指多進程或多執行緒編程;“非同步”是指在作業系統中先註冊某種事件,當這個事件發生時, 由作業系統回調你事先註冊的函數;“select()”方法後面會專門解釋。
這裡為說明概念,先演示最原始的單進程、單執行緒、什麼技術都不用的原始TCP通訊模型,如下圖所示:
以下為一個TCP伺服器程式示例:
# TcpServer.py # 功能:接收客戶端的TCP連接,列印客戶端發送過來的字元串,並將伺服器本地時間發給客戶端 from socket import * import time s = socket(AF_INET, SOCK_STREAM) # AF_INET指IPv4,SOCK_STREAM至TCP,後面會有詳釋 s.bind(('', 10001)) # 伺服器程式綁定本地10000埠 s.listen(5) while True: s1, addr = s.accept() print("Got a connection from %s" %str(addr)) data = s1.recv(1024) print("Received: %s" %data.decode('ascii')) timestr = time.ctime(time.time()) + "rn" s1.send(timestr.encode('ascii')) s1.close()
以下為TCP客戶端測試程式:
# TcpClient.py from socket import * s = socket(AF_INET, SOCK_STREAM) s.connect(('127.0.0.0.1', 10001)) s.send(b'Hello') tm = s.recv(1024) s.close() print("The time is %s" % tm.decode('ascii'))
TCP的編程需要伺服器程式管理若干個 socket,所以編程模型與上面的UDP略有不同, 多了一個listen()
和accept()
步驟。listen()
等會兒再講, 先講accept()
。
在示常式序中我們可以看到s1, addr = s.accept()
的用法。其中,s 是原始的用於監聽埠10001的套接字實例, accept()
方法會阻塞運行。當有客戶端發起connect()
連接時,accept()
方法會接受這個連接, 並返回一個元組:分別是新套接字實例 s1 、客戶端地址 addr。s1 用於與這個客戶端通訊,s 仍然用於監聽埠10001, 看有沒有新的客戶端連入。
之後運行的recv()
方法,也是阻塞運行的。當這個客戶端沒有發送新的數據過來時, 伺服器主流程就會僵在這裡,無法繼續往下運行。如果有新的客戶端請求連接時,只能在作業系統中排隊等待。 前面的listen()
方法就是用來定義作業系統中這個等待隊列的長度的, 其入參即可指定作業系統中在這個監聽套接字 s 上允許排隊等待的最大客戶端數量。 以前,在不使用前面提到的並發等3個編程技巧時,一般這個值需要為1024或者更多, 而如果使用了並發等編程技巧,一般這個值只需要5就足夠了。
當 s1 與客戶端通訊完畢,需要調用close()
方法關閉這個套接字。 在套接字關閉後,程式主流程再次回到上面的s1, addr = s.accept()
語句,繼續監聽新的連接。 若此時已經有客戶端在作業系統中排隊等待,則會立即從作業系統中取出一個等待的客戶端,然後建立新的套接字實例。 若無等待的客戶端,則本語句會阻塞,直到下一次有客戶端connect()
進來時,再返回。
很顯然,這種同時只能處理一個客戶端連接的伺服器程式是沒法用的, 如果前一個客戶端與伺服器通訊的時間比較長,那新的客戶端連接請求只能在作業系統中排隊等待, 而無法立即與伺服器建立通訊,後面我們將看到,如何用並發等編程技術解決這個問題。
以下為一個通訊時間較常的TCP客戶端測試程式:
# TcpClient.py from socket import * import time s = socket(AF_INET, SOCK_STREAM) s.connect(('127.0.0.0.1', 10001)) time.sleep(5) # 與伺服器建立連接後,不放手,先等5秒鐘再發送數據 s.send(b'Hello') tm = s.recv(1024) s.close() print("The time is %s" % tm.decode('ascii'))
你可以開2個終端運行這個通訊時間較常的客戶端程式,看看伺服器是怎樣反應的。
另外,可以比較一下以前用純C語言寫TCP伺服器程式,作為參考:
// TcpServer.c #include <netinet/in.h> #include <string.h> #include <time.h> int main(int argc, char **argv) { int listenfd, connfd; char buff[4096]; time_t ticks; struct sckaddr_in servaddr; listenfd = Socket(AF_INET, SOCK_STREAM, 0); memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(13); Bind(linstenfd, (SA *)&servaddr, sizeof(servaddr)); Listen(listenfd, 1024); for(;;) { connfd = Accept(listenfd, (SA *) NULL, NULL); ticks = time(NULL); snprintf(buff, sizeof(buff), "%.24srn", ctime(&ticks)); Write(connfd, buff, strlen(buff)); Close(connfd); } }
● 採用並發技術的TCP編程模型
並發是指採用子進程或多執行緒方式進行編程。並發編程的核心思想是,當與客戶端的連接建立後, 在主執行緒(或父進程)內不要有使用recv()
等可能造成阻塞的行為, 這些有可能導致阻塞的行為都通訊都交給其他後台執行緒(或子進程)去做, 主執行緒(或父進程)永遠只阻塞在accept()
上,負責監聽新的連接並立即處理。
以下以執行緒並發為例,示意並發的TCP的編程模型:
以下為一個執行緒並發的TCP伺服器程式:
# TcpServerThreading.py from socket import * import time, threading s = socket(AF_INET, SOCK_STREAM) s.bind(('', 10001)) s.listen(5) thread_list = [] def client_commu(client_socket): data = client_socket.recv(1024) print("Received: %s" %data.decode('ascii')) timestr = time.ctime(time.time()) + "rn" client_socket.send(timestr.encode('ascii')) client_socket.close() while True: ns, addr = s.accept() print("Got a connection from %s" %str(addr)) t = threading.Thread(target=client_commu, args=(ns,)) t.daemon = True # 將新執行緒處理成後台執行緒,主執行緒結束時將不等待後台執行緒 thread_list.append(t) t.start()
程式碼比較簡單,很容易看懂。核心思想就如前述:每來一個新的客戶端連接,就開一個新執行緒負責與這個客戶端通訊, 而主執行緒永遠只阻塞在accept()
上監聽新連接。
● socket模組的函數
以下為 socket 模組中可用的函數、方法、屬性的詳細解釋,大部分都同 UNIX 中的同名用法。
(1)模組函數
函數或變數 | 說明 |
---|---|
模組變數 | |
has_ipv6 |
布爾值,支援IPv6時為 True。 |
連接相關 | |
socket(family, type [,proto]) | 新建套接字,返回一個 SocketType 類型的實例。family 為IP層協議,常用:AF_INET(IPv4)、AF_INET6(IPv6)。type 為套接字類型,常用:SOCK_DGRM(UDP)、SOCK_STREAM(TCP)。proto 為協議編號,通常省略(默認為0) |
socketpair([family [,type [,proto]]]) | 僅適用於創建family 為 AF_UNIX 的“UNIX域”套接字。該概述主要用於設置 os.fork() 創建的進程之間的通訊。例如:父進程調用 socketpair() 創建一對套接字,然後父進程和子進程 就可以使用這些套接字相互通訊了。 |
fromfd(fd, family, type [,proto]) | 通過整數文件描述符創建套接字對象,文件描述符必須引用之前創建的套接字。 該方法返回一個 SocketType 實例。 |
create_connection(address [,timeout]) | 建立與address 的TCP連接,並返回已連接的套接字對象。 address 為:(host, port) 形式的元組,timeout 指定一個可選的超時期。 |
查看主機資訊 | |
gethostname() | 返回本機的主機名。 |
getfqdn([name]) | 若忽略入參name ,則返回本機主機名。其他詳查文檔。 |
gethostbyname(hostname) | 將主機名hostname (如:’www.python.org’)轉換為 IP 地址。不支援 IPV6。 這個函數會自動去查詢Internet上的地址。 |
gethostbyname_ex(hostname) | 將主機名hostname (如:’www.python.org’)轉換為 IP 地址。 但返回元組:(hostname, aliaslist, ipaddrlist),其中hosthame是主機名, aliaslist是同一個地址的可選主機名列表,ipaddrlist是同一個主機上同一個介面的IPv4地址列表。 |
gethostbyaddr(ip_address) | 返回資訊與上面 gethostbyname_ex() 相同,但入參為IP地址。 |
getaddrinfo(host, port [,family [,socktype [,proto [,flags]]]]) | 給定關於主機的host 和port 資訊,返回值為包含5個元素的元組: (family, socktype, proto, cannonname, sockaddr),可視為 gethostbyname() 函數的增強版。 |
getnameinfo(address, flags) | 給定套接字地址address (為(ipaddr, port) 形式的元組),將其轉換為 flag 指定的地址資訊,主要用於獲取與地址有關的詳細資訊。詳可查看文檔。 |
查詢協議資訊 | |
getprotobyname(protocolname) | 將協議名稱(如:’icmp’)轉換為協議編號(如:IPROTO_ICMP的值), 以便傳給 socket() 函數的第3個參數。 |
getservbyname(servicename [,protocolname]) | 將 Internet 服務名稱和協議名稱轉換為該服務的埠號。 protocolname 可以為:’tcp’或’udp’。例如:getservbyname(‘ftp’,’tcp’) |
getservbyport(port [,protocolname]) | 與上面相反,通過埠號查詢服務名稱。如果沒有任何服務用於指定埠, 則引發 socket.error 錯誤。 |
超時資訊 | |
getdefaulttimeout() | 返回默認的套接字超時秒數(浮點數),None表示不設置任何超時期。 |
setdefaulttimeout(timeout) | 為新建的套接字對象設置默認超時期,入參為超時秒數(浮點數),若為 None 表示沒有超時(默認值) |
轉碼相關 | |
htonl(x) | 將主機的32位整數x 轉為網路位元組順序(大尾)。 |
htons(x) | 將主機的32位整數x 轉為網路位元組順序(小尾)。 |
ntohl(x) | 將來自網路的32位整數(大尾)x 轉換為主機位元組順序。 |
ntohs(x) | 將來自網路的32位整數(小尾)x 轉換為主機位元組順序。 |
inet_aton(ip_string) | 將字元串形式的IPv4地址(如:’127.0.0.1’)轉換成32位二進位分組格式,用作地址的原始編碼。 返回值是由4個二進位字元組成的字元串(如:b’x7fx00x00x01’)。在將地址傳遞給C程式時比較有用。 |
inet_ntoa(packed_ip) | 與上面 inet_aton() 功能相反。常用於從C程式傳來的地址數據解包。 |
inet_pton(family, ip_string) | 功能與上面 inet_aton() 類似,但支援IPv6,family 可指定地址族。 |
inet_ntop(family, packed_ip) | 與 inet_pton() 功能相反,用於解包地址。 |
(2)套接字屬性和方法
屬性和方法 | 說明 |
---|---|
屬性 | |
s.family |
套接字地址族(如:AF_INET)。 |
s.type |
套接字類型(如:SOCK_STREAM)。 |
s.proto |
套接字協議編號。 |
連接相關方法 | |
s.bind(address) | 通常為伺服器用。將套接字綁定到特定地址和埠。address 為元組形式的: (hostname, port),注意 hostname 必須要加引號,空字元串、’localhost’都表示本機IP地址。 |
s.listen(backlog) | 通常為伺服器用。指定作業系統能在本埠上最大可以等待的還未被accept()處理的連接數量。 |
s.accept() | 通常為伺服器用。接受連接並返回 (conn, address),其中conn 是新的套接字對象, 可以用這個新的套接字和某個連入的特定客戶端通訊。 address 是另一端的套接字地址埠資訊,為(hostname, port)元組。 |
s.connect(address) | 通常為客戶端用。連接到遠端address 指定的地址和埠(為 (hostname, port) 元組形式)。 如果有錯誤則引發 socket.error。 |
s.connect_ex() | 與上類似,但是成功時返回0,失敗時返回 errno 的值。 |
s.close() | 關閉套接字。伺服器客戶端都可使用。 |
s.shutdown(how) | 關閉1個或2個連接。若how 為 s.SHUT_RD,則不允許接收; 若為 s.SHUT_WR,則不允許發送;若為 s.SHUT_RDWR,則接收和發送都不允許。 |
UDP 數據讀寫 | |
s.recvfrom(bufsize [,flags]) | UDP專用。返回 (data, address) 對,address 為 (hostname, port) 元組形式。 bufsize 指定要接收的最大位元組數。flags 通常可以忽略(默認為0), 詳可查看文檔。 |
s.recvfrom_info(buffer [,nbytes [,flags]]) | 與 recvfrom() 類似,但接收的數據存儲在入參對象buffer 中, nbytes 指定要接收的最大位元組數,如忽略則最大為buffer 大小。 flags 同上。 |
s.sendto(string [,flags] ,address) | UDP專用。將string 發送到address 指定的地址和埠 (為 (hostname, port) 元組形式)。返回發送的位元組數。flags 同上。 |
TCP 數據讀寫 | |
s.recv(bufsize [,flags]) | 接收套接字數據,數據以字元串形式返回。bufsize 指定要接收的位元組數。 flags 通常可以忽略(默認為0),詳可查看文檔。 |
s.recv_into(buffer [,nbytes [,flags]]) | 與 recv() 類似,但將數據寫入支援緩衝區介面的對象buffer 中, nbytes 指定要接收的最大位元組數,如忽略則最大為buffer 大小。 flags 含義同上。 |
s.send(string [,flags]) | 將string 中的數據發送到套接字,flags 含義同上。 返回發送的位元組數量(可能小於string 中的位元組數),如有錯誤則拋出異常。 |
s.sendall(string [,flags]) | 將string 中的數據發送到套接字,但在返回之前會嘗試發送所有數據。 成功則返回 None,失敗則拋出異常。flags 含義同上。 |
套接字參數相關方法 | |
s.getsockname() | 返回套接字自己的地址埠,通常為一個元組:(ipaddr, port)。 |
s.getpeername() | 返回遠端套接字的地址埠,通常為一個元組:(ipaddr, port),並非所有系統都支援該函數。 |
s.gettimeout() | 返回當前套接字的超時秒數(浮點數),如果沒有設置超時期,則返回None。 |
s.getsockopt(level, optname [,buflen]) | 返回套接字選項的值。level 定義選項的級別, optname 為特定的選項。 buflen 表示接收選項的最大長度,通常可忽略。 |
s.settimeout(timeout) | 設置套接字操作的超時秒數(浮點數),設None表示沒有超時。如果發生超時, 則引發 socket.timeout 異常。 |
s.setblocking(flag) | 若flag 設為0,則套接字為非阻塞模式。在非阻塞模式下, s.recv() 和 s.send() 調用將立即返回,若 s.recv() 沒有發現任何數據、或者 s.send() 無法立即發送數據,那麼將引發 socket.error 異常。 |
s.setsockopt(level, optname, value) | 設置給定套接字選項的值。參數含義同 s.getsockopt() |
文件相關 | |
s.fileno() | 返回套接字的文件描述符。 |
s.makefile([mode [,bufsize]]) | 創建與套接字關聯的文件對象。mode 和bufsize 的含義與內置 open() 函數相同,文件對象使用套機子文件描述符的複製版本。 |
s.ioctl() | 受限訪問 Windows 上的 WSAIoctol 介面。詳可查閱文檔。 |
● socket模組的異常
socket模組定義了以下異常:
異常 | 說明 |
---|---|
error | 繼承自OSError,表示與套接字或地址有關的錯誤。它返回一個 (errno, mesg) 元組(錯誤編號、錯誤消息) 以及底層調用返回的錯誤。 |
herror | 繼承自OSError,表示與地址有關的錯誤。它返回一個 (errno, mesg) 元組(錯誤編號、錯誤消息)。 |
timeout | 繼承自OSError,套接字操作超時時出現的異常。異常值是字元串 ‘timeout’。 |
gaierror | 繼承自OSError,表示 getaddrinfo()和 getnameinfo() 函數中與地址有關的錯誤。 它返回一個 (errno, mesg) 元組(錯誤編號、錯誤消息)。 |
errno 為socket模組中定義的以下常量之一:
常量 | 描述 | 常量 | 描述 |
---|---|---|---|
EAI_ADDRFAMILY | 不支援地址族 | EAI_NODATA | 沒有與節點名稱相關的地址 |
EAI_AGAIN | 名稱解析暫時失效 | EAI_NONAME | 未提供節點名稱或服務名稱 |
EAI_BADFLAGS | 標誌無效 | EAI_PROTOCOL | 不支援該協議 |
EAI_BADHINTS | 提示不當 | EAI_SERVICE | 套接字類型不支援該服務名稱 |
EAI_FAIL | 不可恢復的名稱解析失敗 | EAI_SOCKTYPE | 不支援該套接字類型 |
EAI_FAMILY | 主機不支援的地址組 | EAI_SYSTEM | 系統錯誤 |
EAI_MEMORY | 記憶體分配失敗 |
(3)select模組
select模組可使用select()
和poll()
系統調用。 select()
通常用來實現輪詢,可以在不使用執行緒或子進程的情況下, 實現與多個客戶端進行通訊。它的用法直接模仿原始UNIX中的select()
系統調用。 在 Linux 中,它可以用於文件、套接字、管道;在 Windows 中,它只能用於套接字。 poll()
函數可以直接利用Linux底層的poll()系統調用
, Windows不支援poll()
函數。
● select()
使用select()
實現同時與多個客戶端通訊的核心編程思想是:select()
函數可以阻塞在多個套接字上,只要這些套接字中有一個收到數據或收到連接, select()
就會返回,並且在返回值中包含這個收到數據的套接字。 然後用戶自己的伺服器程式可以根據返回值自行判斷,是哪個客戶端對應的套接字收到了數據, 若返回的套接字是最原始的監聽套接字,則說明有新客戶端的連接請求。
select()
函數的語法如下:
select(rlist, wlist, xlist [,timeout])
查詢一組文件描述符的輸入、輸出和異常狀態。前3個參數rlist
、wlist
、 xlist
都是列表,每個列表包含一系列文件描述符或類似文件描述符的對象(當某個對象具有 fileno() 方法時,它就是類似文件描述符的對象,比如:套接字)。 rlist
為輸入文件描述符的列表、wlist
為輸出文件描述符的列表、 xlist
為異常文件描述符的列表,這3個列表都可以是空列表。
一般情況下,本函數為阻塞運行,即當入參的上述3個列表中若沒有事件發生,則本函數將阻塞掛起。 timeout
參數為指定的超時秒數(浮點數),若忽略則為阻塞運行, 若為0則函數僅將執行一次輪詢並立即返回。
當有事件在入參的3個列表中發生時,本函數即返回。返回值是一個列表元組:(rs, ws, xs), rs 是入參rlist
的子集,為rlist
中發生期待事件的文件描述符列表; 比如:若入參rlist
為一系列套接字,若有一個或多個套接字收到數據, 那麼select()
將返回,並且在 rs 中包含這些收到數據的套接字。
同樣的:ws 是入參wlist
的子集,只要wlist
中的任何一個或多個文件描述符允許寫入,那麼select()
將立即在 ws 中返回這個子集。 因此,往入參wlist
中放入元素時必須十分小心。 最後,xs 是入參xlist
的子集。
如果超時時沒有對象準備就緒,那麼將返回3個空列表。如果發生錯誤,那麼將觸發 select.error 異常。
以下為一個使用 select() 實現的伺服器例子,功能為在伺服器螢幕列印從客戶端收到的任何數據,直到客戶端關閉連接為止:
# select_server.py import socket, select s = socket.socket() s.bind(('', 10001)) 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 data except socket.error: disconnected = True if disconnected: print(r.getpeername(), 'disconnected') inputs.remove(r) else: print(data)
上面程式中,入參 inputs 的初始值只包含一個監聽套接字s,當收到客戶端的連接請求時, select()
函數會返回,並且在 rs 中包含這個套接字。 然後s.accept()
會新生成一個套接字 c,伺服器程式會將其放入 inputs 列表。 以後若是收到這個客戶端的數據,則select()
返回時的 rs 中會包含這個新套接字 c, 若是收到其他客戶端的連接請求時,則select()
返回時的 rs 中會包含原始套接字 s。 之後的程式靠判斷 rs 中究竟是哪個套接字,來決定後續的行為。
最後,若客戶端調用close()
關閉連接(本質上是發送一個長度為0的數據:b”), 則伺服器收到這個0長度數據後,在螢幕列印關閉連接的客戶端地址,並將這個與之對應的套接字移出 input 隊列。
● poll()
poll()函數可創建利用poll()系統調用
的“輪詢對象”,Windows不支援 poll() 函數。
poll()返回的輪詢對象支援以下方法:
方法 | 說明 |
---|---|
p.register(fd [,eventmask]) | 註冊新的文件描述符fd ,fd 為一個文件描述符、 或一個類似文件描述符的對象(當某個對象具有fileno() 方法時,它就是類似文件描述符的對象, 比如套接字)。eventmask 可取值見下表,可以“按位或”。 如果忽略eventmask ,則僅檢查 POLLIN, POLLPRI, POLLOUT 事件。 |
p.unregister(fd) | 從輪詢對象中刪除文件描述符fd ,如果沒有註冊,則引發 KeyError 異常。 |
p.poll([timeout]) | 對所有已註冊的文件描述符進行輪詢。timeout 位可選的超時毫秒數(浮點數)。 返回一個元組列表,列表中每個元組的形式為:(fd, event),其中 fd 是文件描述符列表、 event 是指示事件的位掩碼(含義見下表)。 例如,要檢查 POLLIN 事件,只需使用event & POLLIN 測試值即可。 如果返回空列表,則表示到達超時值且沒有發生任何事件。 |
eventmask
和event
支援的事件:
常量 | 描述 | 常量 | 描述 |
---|---|---|---|
POLLIN | 可用於讀取的數據 | POLLERR | 錯誤情況 |
POLLPRI | 可用於讀取的緊急數據 | POLLHUP | 保持狀態 |
POLLOUT | 準備寫入 | POLLNVAL | 無效請求 |
以下為一個使用 poll() 實現的伺服器例子:
import socket, select s = socket.socket() s.bind(('', 8009)) s.listen(5) fdmap = {} p = select.poll() p.register(s) while True: events = p.poll() # 阻塞運行,若無新的事件本函數會掛起 for fd, event in events: if fd == s.fileno(): c, addr = s.accept() print('Got connection from', addr) p.register(c) fdmap[c.fileno()] = c elif 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)
總體來說,poll()
的使用比select()
略為簡單。上面程式中,首先通過 p.register(s)
註冊要監聽的套接字,然後調用events = p.poll()
等待連接或數據,當p.poll()
返回時,即遍歷其返回值。若fd為監聽套接字 s 的文件描述符,則通過調用s.accept()
新建一個與此客戶端通訊的套接字, 然後其通過p.register(c)
註冊進監聽事件,再將這個套接字放入字典 fdmap 以備以後可直接通過 fd 拿出套接字。
之後,每當收到新的數據,若非監聽套接字 s 收到數據,則說明是與客戶端通訊的某個套接字 c 收到了數據,則通過data = fdmap[fd].recv(1024)
把數據收進來。若收到數據長度為0, 說明是用戶端關閉套接字,則在本處理程式中,使用p.unregister(fd)
解除對這個套接字的監聽。最後在 fdmap 字典中刪除這個套接字的索引。
(4)asyncore模組
asyncore模組用來編寫“非同步”網路程式(內部核心原理是使用select()
系統調用), 它可以用於希望提供並發性但又無法使用多執行緒(或子進程)的環境。
回憶一下非同步的核心思想:當發生某事件時(比如收到客戶端數據、或收到新的客戶端連接請求等等), 由作業系統來回調運行你先前為這個事件定義好的函數或方法。這些事先定義好的函數或方法只會由作業系統來調用, 而不會影響你自己程式的主流程。
不過由於asyncore模組過於底層,一般工作中不太會直接使用asyncore模組編寫網路程式, 而會用其他更高級的模組(如:asynchat等),這裡僅僅用asyncore模組來說明非同步網路編程的基本方法。
asyncore模組主要提供了一個 dispatcher 類,其所有功能都幾乎都由 dispatcher 類提供, dispatcher 類內部封裝了一個普通套接字對象,其初始化語法如下:
dispatcher([sock])
上面的 dispatch() 函數定義事件驅動型非阻塞套接字對象(比較拗口哈)。sock
是現有的套接字對象。 如果忽略該參數,則後面需使用 create_socket() 方法創建套接字。一般我們在編程中通過繼承 dispatcher 類並重定義它的一些方法,來實現自己需要的功能。
● dispatcher 對象支援以下方法
方法或函數 | 說明 |
---|---|
可重定義的基類方法 | |
d.handle_accept() | 收到新連接時系統會自動調用該方法。 |
d.handle_connect() | 作為客戶端進行連接。 |
d.handle_close() | 套接字關閉時系統會自動調用該方法。 |
d.handle_error() | 發生未捕獲的異常時系統會自動調用該方法。 |
d.handle_expt() | 收到套接字外帶數據時系統會自動調用該方法。 |
d.handle_read() | 從套接字收到新數據時,系統會自動調用該方法。 |
d.handle_write() | 當 d.writable() 方法返回True時,系統會自動調用該方法。 |
d.readable() | 內部的select()方法使用該函數查看對象是否準備讀取數據,如果是則返回 True。 接下來系統會自動調用 d.handle_read() 來讀取數據。 |
d.writable() | select()方法使用該函數查看對象是否想寫入數據,如果是則返回 True。 |
底層方法(直接操作其內部的套接字) | |
d.create_socket(family, type) | 新建套接字,參數含義與底層 socket() 相同。 |
d.bind(address) | 將套接字綁定到address ,address 是一個 (host, port) 元組。 |
d.listen([backlog]) | 監聽傳入連接,參數含義與底層 listen() 相同。 |
d.accept() | 接受連接,返回元組 (client, addr),其中client 是新建的套接字對象, addr 是客戶端的地址/埠元組。 |
d.close() | 關閉套接字 |
d.connect(address) | 建立連接,address 是一個 (host, port) 元組。 |
d.recv(size) | 最大接收size 個位元組,返回空字元串表示客戶端已關閉了通道。 |
d.send(data) | 發送數據data (字元串) |
asyncore 模組的函數 | |
loop([timeout [,use_poll [,map[,count]]]]) | 無限輪詢事件。使用 select() 函數進行輪詢,如果use_poll 參數為True, 則使用 poll() 進行輪詢。timeout 表示超時秒數,默認為30秒。 map 是一個字典,包含所有要監視的通道。count 指定返回之前要執行的輪詢操作次數(默認為None,即一直輪詢,直到所有通道關閉) |
● asyncore的使用示例
下例展示了一個asyncore的伺服器程式,它的功能是:當收到客戶端發送過來的任何數據時, 在伺服器螢幕上顯示這個收到的數據,並將伺服器本地時間發送給客戶端。 由客戶端決定何時關閉連接。
# asyncore_server.py import asyncore import socket import time # 該類僅處理“接受連接”事件 class asyncore_server(asyncore.dispatcher): def __init__(self, port): asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.bind(('', port)) self.listen(5) def handle_accept(self): client, addr = self.accept() return asyncore_tcp_handler(client) # 該類為每個具體客戶端生成一個實例,並處理伺服器和這個客戶端的通訊 class asyncore_tcp_handler(asyncore.dispatcher): def __init__(self, sock = None): asyncore.dispatcher.__init__(self, sock) self.writable_flag = False def handle_read(self): recv_data = self.recv(4096) if len(recv_data) > 0: print(recv_data) self.writable_flag = True def writable(self): return self.writable_flag def handle_write(self): timestr = time.ctime(time.time()) + "rn" bytes_sent = self.send(timestr.encode('ascii')) self.writable_flag = False def handle_close(self): print('The client is closed.') self.close() a = asyncore_server(10001) # 創建監聽伺服器 asyncore.loop() # 無限輪詢
程式要點分析如下:
(1)本程式定義了一個 asycore_server 類和一個 asyncore_tcp_handler 類, 都繼承自asyncore.dispatcher
。前者(asycore_server類)用於監聽所有的新連接事件, 後者(asyncore_tcp_handler類)用於處理與某個已建立連接的具體服務端通訊。
(2)程式的最下面兩行:先建立一個 asycore_server 的實例,然後進入無限循環, 監聽 10001 埠的所有新連接事件。
(3)當有新的客戶端連入時,系統會自動回調此監聽實例的 handle_accept()
方法, 在這個方法中,我們通過調用底層的accept()
方法,得到一個新的套接字 client, 並用這個新套接字生成一個 ascycore_tcp_handler 實例,負責與這個客戶端一對一通訊。
(4)當已建立連接的客戶端向伺服器發送數據時,系統會自動調用 asyncore_tcp_handler 實例的 handle_read()
方法。在這個方法中,我們通過調用底層的recv()
方法, 得到客戶端法來的數據,並將其 print 到伺服器螢幕上,然後將我們自定義的實例屬性 writable_flag
設為 True。
(5)由於我們已經重寫了實例的writable()
方法,當我們在上面將實例屬性 writable_flag
設為 True時,這個writable()
方法也會返回 True。 由於系統在後台不停地在監視writable()
方法的返回值,當發現這個方法返回值為 True時,系統即自動調用本實例的 handle_write()
方法。
(6)在handle_write()
方法中,我們通過調用底層方法send()
, 將本地時間發送給客戶端。發送完後別忘了將writable_flag
屬性設回 False, 否則系統會不停地調用handle_write()
方法。
(7)當客戶端提出關閉連接時(即客戶端調用:close()
方法), 系統回會自動調用本實例的handle_close()
方法,我們可以在此方法中調用底層的 close()
方法,關閉服務端與此客戶端的連接的連接,然後本實例就會自動銷毀。
以下是一個客戶端的例子,用來測試這個伺服器:
from socket import * import time s = socket(AF_INET, SOCK_STREAM) s.connect(('127.0.0.1', 10001)) # 第一次發送數據並接收 s.send('Hello'.encode('ascii')) tm = s.recv(1024) print("The time is %s" % tm.decode('ascii')) # 等待1秒鐘 time.sleep(1) # 第二次發送數據並接收 s.send('World'.encode('ascii')) tm = s.recv(1024) print("The time is %s" % tm.decode('ascii')) # 關閉連接 s.close()
(5)asynchat模組
asynchat模組將asyncore的底層I/O功能進行了封裝,提供了更高級的編程介面, 非常適用於基於簡單請求/響應機制的網路協議(如 HTTP)。
asynchat模組提供了一個名為async_chat
的基類,用戶需要繼承這個基類, 並自定義兩個必要的方法:incoming_data()
和found_terminator()
。 當網路收到數據時,系統會自動調用incoming_data()
方法。
對於發送數據,async_chat
在內部實現了一個 FIFO 隊列, 用戶可以通過調用push()
方法將要發送的數據壓入隊列,然後就不用管了, 系統會自動在網路可發送時,將 FIFO 隊列中的數據發送出去。
可使用以下函數,定義async_chat
的實例,sock
是與客戶端一對一通訊的套接字對象。
async_chat([sock])
async_chat
的實例除了繼承了asyncore.dispatcher
基類提供的方法之外, 還具有以下自己的方法:
方法 | 說明 |
---|---|
a.collect_incoming_data(data) | 通道收到數據時系統會自動調用該方法。data 是本實例套接字通道收到的數據, 用戶必須自己實現該方法,在該方法中用戶通常需要將收到的數據保存起來已供後續處理。 |
a.set_terminator(term) | 設置本實例套接字通道的終止符,term 可以是字元串、整數或者 None。 如果term 是字元串,則在輸入流出現該字元串時,系統會自動調用 a.found_terminator() 方法。如果term 是整數,則它指定一次收的位元組數, 當通道收到指定的位元組數後,系統自動調用方法。 如果term 是 None,則持續收集數據。 |
a.get_terminator() | 返回本實例套接字通道的終止符。 |
a.found_terminator() | 當本實例的套接字通道收到由本實例的set_terminator() 方法設置終止符時, 系統會自動調用該方法。該方法必須由用戶實現。 通常,它會處理此前由collect_incoming_data() 方法保存的數據。 |
a.push(data) | 將數據壓入 FIFO 隊列,data 是要發送的位元組序列。 |
a.discard_buffers() | 丟棄 FIFO 隊列中保存的所有數據。 |
a.close_when_done() | 將 None 壓入 FIFO 隊列,表示傳出數據流已到達文件尾。 當系統從 FIFO 中讀到 None 時將關閉本套接字通道。 |
a.push_with_producer(producer) | 將一生產者對象producer 加入到生產者 FIFO 隊列。 producer 可以是任何具有方法more() 的對象。 重複調用本方法可以將多個生產者對象推入生產者 FIFO 隊列。 |
simple_producer(data [,buffer_size]) | 這是 asynchat 模組為a.push_with_producer() 單獨定義的類, 可以用來創建簡單的生產者對象,從位元組序列data 生成數據塊, buffer_size 指定數據塊大小(默認512)。 |
asynchat 模組總是和 asyncore 模組一起使用。一般使用asyncore.dispatch
實例來監聽埠, 然後由 asynchat 模組的async_chat
的子類實例來處理與每個客戶端的連接。下面是一個簡單的實例, 伺服器在螢幕列印任何從客戶端收到的數據,當發現終止符b'rnrn'
時, 向客戶端發送伺服器本地時間,並關閉這個套接字。
# asynchat_server.py import asynchat, asyncore, socket import time class asyncore_http(asyncore.dispatcher): def __init__(self, port): asyncore.dispatcher.__init__(self) self.create_socket(socket.AF_INET, socket.SOCK_STREAM) self.bind(('', port)) self.listen(5) def handle_accept(self): client, addr = self.accept() return asynchat_tcp_handler(client) class asynchat_tcp_handler(asynchat.async_chat): def __init__(self, conn=None): asynchat.async_chat.__init__(self, conn) self.data = [] self.got_terminator = False self.set_terminator(b'rnrn') def collect_incoming_data(self, data): if not self.got_terminator: self.data.append(data) print(data) def found_terminator(self): self.got_terminator = True timestr = time.ctime(time.time()) + "rn" self.push(timestr.encode('ascii')) self.close_when_done() a = asyncore_http(10001) asyncore.loop()
以上例子對比前面的純使用 asyncore 模組的例子,在寫與客戶端通訊的程式時,要簡潔很多。
(6)socketserver模組
socketserver模組包括很多TCP、UDP、UNIX域 套接字伺服器實現的類,用它們來編寫伺服器程式非常方便。 要使用該模組,用戶必須繼承並實現2個類:一個是 Handler 類(事件處理程式)、一個是 Server 類(伺服器程式)。 這兩個類需要配合使用。
● Handler 類(事件處理程式)
用戶需要自定義一個 Handler 類(繼承自基類BaseRequestHandler
), 其中需自定義實現以下方法:
方法 | 說明 |
---|---|
h.setup() | 對本實例進行一些初始化工作,默認情況下,它不執行任何操作。 如果用戶希望在處理網路連接前,先作一些配置工作(如建立 SSL 連接), 那麼可以改寫該方法。 |
h.handle() | 當 Server 類監聽到新的客戶端連接請求或收到來自已連接的客戶端的數據, 系統將自動回調這個函數。在這個函數中,用戶可以自定處理客戶端連接或數據。 |
h.finish() | 完成h.handle() 方法後,系統會自動回調此本方法作一些清理工作。 默認情況下,它不執行任何操作。如果執行h.setup() 或h.handle() 時發生異常, 則不會調用本方法。 |
BaseRequestHandler 實例的一些可用屬性:
屬性 | 說明 |
---|---|
h.request |
對於 TCP 連接,是本實例內置的套接字對象。 對於 UDP 連接,是包好收到數據的位元組字元串。 |
h.client_address |
為客戶端的(地址, 埠)元組。 |
h.server |
本實例對應的 Server 實例。 |
h.rfile |
可以像操作文件對象一樣,從h.rfile 讀取客戶端數據 (用例如:data = h.rfile.readline())。 |
h.wfile |
可以像操作文件對象一樣,向h.wfile 寫入數據, 這些數據會被傳送到已建立連接的客戶端 (用例如:h.wfile.write(‘Hello’.encode(‘ascii’)) )。 |
BaseRequestHandler
還有兩個派生類,用於簡化操作。 如果用戶僅使用 TCP 進行通訊,那麼自定義的 Handler 類可繼承自StreamRequestHandler
類。 如果用戶僅使用 UDP 進行通訊,那麼自定義的 Handler 類可繼承自DatagramRequestHandler
類。 在這兩種情況下,用戶僅需實現h.handle()
方法就可以了。
● Server 類(伺服器程式)
定義完上面的 Handler 類後,用戶還需要定義一個 Server 類。 socketserver 模組提供了5個可供用戶繼承的類,分別是:
● BaseServer(address, handler)
;
● UDPServer(address, handler)
:繼承自 BaseServer;
● TCPServer(address, handler)
:繼承自 BaseServer;
● UnixDatagramServer(address, handler)
:繼承自 UDPServer,UNIX域專用;
● UnixStreamServer(address, handler)
:繼承自 TCPServer,UNIX域專用;
其中入參address
為 (ipaddr, port) 元組, handler
為用戶為此 Server 實例配對的自定義 Handler 類(注意是“類”,不是實例)。 用戶可根據自己的連接類型,自行選擇繼承相應的 Server 類實現服務程式。
Server 實例具有以下共有方法和屬性:
方法或屬性 | 說明 |
---|---|
s.fileno() | 返回本實例對應的套接字的文件描述符,使得本實例可供select() 直接使用。 |
s.serve_forever() | 進入無限循環,處理本實例對應埠的所有請求。 |
s.shutdown() | 停止s.serve_forever() 無限循環。 |
s.server_address |
本實例監聽的(地址, 埠)元組。 |
s.socket |
本實例對應的套接字對象。 |
s.RequestHandlerClass |
本實例對應的 Handler 類(事件處理)。 |
Server 還可以定義以下“類變數”來配置一些基本參數;以下的“類方法”一般不必動,但也可以改寫:
類變數或類方法 | 說明 |
---|---|
Server.socket_type |
伺服器使用的套接字類型,如socket.SOCK_STREAM 或 socket.SOCK_DGRAM 等。 |
Server.address_family |
伺服器套接字使用的地址族,如:socket.AF_INET 等。 |
Server.request_queue_size |
傳遞給套接字的listen() 方法的隊列值大小,默認值為 5。 |
Server.timeout |
伺服器等待新請求的超時秒數,超時期結束後,伺服器會自動回調本類的 Server.handle_timeout() 類方法。 |
Server.allow_reuse_address |
布爾標誌,指示套接字是否允許重用地址。在程式終止後,一般其他程式若要使用本埠, 需要等幾分鐘時間。但若此標誌為 True,則其他程式可在本程式結束後立即使用本埠。 默認為 False。 |
Server.bind() | 對伺服器執行bind() 操作。 |
Server.activate() | 對伺服器執行listen() 操作。 |
Server.handle_timeout() | 伺服器發生超時時會自動回調本方法。 |
Server.handle_error(request, client_address) | 此方法處理操作過程中發生的未處理異常,若要獲取關於上一個異常的資訊, 可使用 traceback 模組的sys.exc_info() 或其他函數。 |
Server.verify_request(request, client_address) | 在進一步處理之前,如果需要驗證連接,則可以重新定義本方法。 本方法可以實現防火牆功能或執行某寫驗證。 |
● socketserver 使用示例
以下為一個單進程、單執行緒的 socketserver 伺服器程式示例:
# my_socketserver.py from socketserver import TCPServer, StreamRequestHandler import time class MyTCPHandler(StreamRequestHandler): def handle(self): print('Got connection from: ', self.client_address) while True: recv_data = self.request.recv(1024) if len(recv_data): print(recv_data) if b'rn' in recv_data: resp = time.ctime() + "rn" self.request.send(resp.encode('ascii')) else: print(self.client_address, ' Disconnected') break; class MyTCPServer(TCPServer): allow_reuse_address = True serv = MyTCPServer(('', 10001), MyTCPHandler) serv.serve_forever()
在上面的示常式序中,用戶定義了兩個繼承類:MyTcpHandler 用於處理客戶端連接和客戶端數據, MyTcpServer 用於定義伺服器類。
(1)在主程式中,先初始化一個 serv 實例,並為其綁定伺服器地址/埠和 Handler 類。之後, 即調用 serv 實例的 serve_forever()
方法,進入無限循環監聽埠。 此時會在 serv 實例內部自動生成一個 MyTCPHandler 的實例,用以監聽伺服器埠並處理數據。
(2)當客戶端發起連接時,系統會自動回調內部 MyTCPHandler 的實例的handle()
方法。 在此方法中,示常式序使用while True:
和self.request.recv()
結構, 接收從客戶端發來的數據。
(3)若客戶端發來普通數據,則在伺服器在螢幕上列印這個發來的數據。 若客戶端發來的數據中含有換行符 b’rn’,則處理程式將本地時間發送給客戶端。
(4)若客戶端關閉連接(即發送長度為0的數據),則處理程式通過break
語句退出 while True:
循環,並結束handle()
方法,此時服務端也會在內部關閉連接, 並銷毀這個內部的 MyTCPHandler 實例。再生成一個新的 MyTCPHandler 實例來監聽和處理下一次客戶端的連接。
(5)需要理解的是:對於這種單進程單執行緒的伺服器程式,當前一個客戶端與伺服器程式還處於連接狀態時, 下一個客戶端是無法連入這個伺服器程式的,只能在作業系統層面等待(listen()
函數的入參 即是用來指示:這個埠在作業系統層面可以等待的客戶端的隊列的長度)。 只有當前一個客戶端關閉連接後,伺服器程式才能從作業系統的等待隊列中,取出下一個客戶端進行處理。
以下是客戶端測試程式的例子:
# client.py from socket import * import time s = socket(AF_INET, SOCK_STREAM) s.connect(('127.0.0.1', 10001)) s.send('Hello'.encode('ascii')) time.sleep(1) s.send('Worldrn'.encode('ascii')) tm = s.recv(1024) print("The time is %s" % tm.decode('ascii')) s.close()
● socketserver的並發處理
在前面的例子中,伺服器程式不能同時處理多個客戶端的連接,只能等一個客戶端關閉連接後, 再處理下一個客戶端的數據。socketserver 模組提供了非常方便的並發擴展功能, 只要將上面的程式稍作修改,就能變成“子進程”或“多執行緒”併發模式,同時處理若干個客戶端的連接。
簡單來講,socketserver 模組提供了幾個UDPServer
和TCPServer
的派生類, 用以實現並發功能,這些派生類分別是:
● ForkingUDPServer(address, handler)
:UDPServer 的子進程並發版(Windows不支援);
● ForkingTCPServer(address, handler)
:TCPServer 的子進程並發版(Windows不支援);
● TheadingUDPServer(address, handler)
:UDPServer 的多執行緒並發版;
● TheadingTCPServer(address, handler)
:TCPServer 的多執行緒並發版;;
在實際使用中,只要從以上幾個類繼承實現自己的 Server 類就可以了。對,就是這麼簡單! 比如,對於上面的伺服器示常式序,只要將程式中的TCPServer
改成TheadingTCPServer
, 就變成了多進程並發伺服器程式,程式會為每個客戶端連接創建一個獨立的執行緒,可同時與多個客戶端進行通訊。
修改後的多執行緒版伺服器程式如下:
# my_socketserver.py from socketserver import ThreadingTCPServer, StreamRequestHandler import time class MyTCPHandler(StreamRequestHandler): def handle(self): print('Got connection from: ', self.client_address) while True: recv_data = self.request.recv(1024) if len(recv_data): print(recv_data) if b'rn' in recv_data: resp = time.ctime() + "rn" self.request.send(resp.encode('ascii')) else: print(self.client_address, ' Disconnected') break; class MyTCPServer(ThreadingTCPServer): allow_reuse_address = True serv = MyTCPServer(('', 10001), MyTCPHandler) serv.serve_forever()
對於ForkingUDPServer
和ForkingTCPServer
,額外有以下控制屬性:
屬性 | 說明 |
---|---|
max_children |
子進程的最大數量 |
timeout |
收集殭屍進程的操作時間間隔 |
active_children |
跟蹤正在運行多少個活動進程 |
對於TheadingUDPServer
和TheadingTCPServer
,額外有以下控制屬性:
屬性 | 說明 |
---|---|
daemon_threads |
若設為True,則這些執行緒都變成後台執行緒,會隨主執行緒退出而退出。 默認為 False。 |