<style>td.subtitle { background-color: rgba(251, 251, 254, 1) } td.name { font-family: "Consolas" } td.center { text-align: center } td.name span { font-size: 13px; font-style: italic } td.name p { line-height: 2ex } td.example { font-family: "Consolas"; color: rgba(96, 96, 96, 1); font-size: 12px } td.example p { line-height: 2ex } td.example p span { color: rgba(176, 176, 176, 1); font-size: 12px } td.example a.advc { color: rgba(96, 96, 96, 1) } a.advc { border-bottom: 1px solid rgba(128, 128, 128, 1); text-decoration: none } a.subcata { border-bottom: 1px solid rgba(128, 128, 128, 1) } p.subcata { line-height: 30px; font-size: 18px } h2.dscp span { font-style: italic } .codebox { display: table-cell; width: 800px; padding-left: 20px; padding-right: 20px } .codebox span { color: rgba(144, 144, 144, 1) } .codebox i { fontsize: 8px } a.return { color: rgba(187, 187, 187, 1); font-size: 0.8em } code { font-family: "Courier New", Courier, monospace; font-size: 0.9em; line-height: 1.8em; border: 1px solid rgba(176, 176, 176, 1); border-radius: 2px; background: rgba(248, 248, 248, 1); padding: 2px; margin: 0 4px; vertical-align: middle }</style>
回傳目錄
本篇索引
(1)基本原理
(2)socket模塊
(3)select模塊
(4)asyncore模塊
(5)asynchat模塊
(6)socketserver模塊
(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 = https://www.cnblogs.com/initcircuit/p/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 = https://www.cnblogs.com/initcircuit/p/s1.recv(1024)
print("Received: %s" %data.decode('ascii'))
timestr = time.ctime(time.time()) + "\r\n"
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), "%.24s\r\n", 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 = https://www.cnblogs.com/initcircuit/p/client_socket.recv(1024)
print("Received: %s" %data.decode('ascii'))
timestr = time.ctime(time.time()) + "\r\n"
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'\x7f\x00\x00\x01'),在將地址傳遞給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 = https://www.cnblogs.com/initcircuit/p/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 = https://www.cnblogs.com/initcircuit/p/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 = https://www.cnblogs.com/initcircuit/p/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()) +"\r\n"
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'\r\n\r\n'時, 向客戶端發送服務器本地時間,并關閉這個套接字,
# 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 = https://www.cnblogs.com/initcircuit/p/[]
self.got_terminator = False
self.set_terminator(b'\r\n\r\n')
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()) + "\r\n"
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 = https://www.cnblogs.com/initcircuit/p/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 = https://www.cnblogs.com/initcircuit/p/self.request.recv(1024)
if len(recv_data):
print(recv_data)
if b'\r\n' in recv_data:
resp = time.ctime() + "\r\n"
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'\r\n',則處理程式將本地時間發送給客戶端,
(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('World\r\n'.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 = https://www.cnblogs.com/initcircuit/p/self.request.recv(1024)
if len(recv_data):
print(recv_data)
if b'\r\n' in recv_data:
resp = time.ctime() + "\r\n"
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, |
回傳目錄
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/183598.html
標籤:Python
