主頁 > 後端開發 > 圖解Python網路編程

圖解Python網路編程

2020-10-21 10:00:22 後端開發

<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]]]]) 給定關于主機的hostport資訊,回傳值為包含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]]) 創建與套接字關聯的檔案物件,modebufsize的含義與內置 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個引數rlistwlistxlist都是串列,每個串列包含一系列檔案描述符或類似檔案描述符的物件(當某個物件具有 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]) 注冊新的檔案描述符fdfd為一個檔案描述符、 或一個類似檔案描述符的物件(當某個物件具有fileno() 方法時,它就是類似檔案描述符的物件, 比如套接字),eventmask可取值見下表,可以“按位或”, 如果忽略eventmask,則僅檢查 POLLIN, POLLPRI, POLLOUT 事件,
p.unregister(fd) 從輪詢物件中洗掉檔案描述符fd,如果沒有注冊,則引發 KeyError 例外,
p.poll([timeout]) 對所有已注冊的檔案描述符進行輪詢,timeout位可選的超時毫秒數(浮點數), 回傳一個元組串列,串列中每個元組的形式為:(fd, event),其中 fd 是檔案描述符串列、 event 是指示事件的位掩碼(含義見下表), 例如,要檢查 POLLIN 事件,只需使用event & POLLIN測驗值即可, 如果回傳空串列,則表示到達超時值且沒有發生任何事件,

 

eventmaskevent支持的事件:

常量描述常量描述
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) 將套接字系結到addressaddress是一個 (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_STREAMsocket.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 模塊提供了幾個UDPServerTCPServer的派生類, 用以實作并發功能,這些派生類分別是:
  ● 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()

 

對于ForkingUDPServerForkingTCPServer,額外有以下控制屬性:

屬性說明
max_children 子行程的最大數量
timeout 收集僵尸行程的操作時間間隔
active_children 跟蹤正在運行多少個活動行程

 

對于TheadingUDPServerTheadingTCPServer,額外有以下控制屬性:

屬性說明
daemon_threads 若設為True,則這些執行緒都變成后臺執行緒,會隨主執行緒退出而退出, 默認為 False,

 

 

 

 

回傳目錄

 

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/183598.html

標籤:Python

上一篇:python bytearray/bytes/string區別(47)

下一篇:用python一行代碼實作1—100之和,你會嗎

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more