圖解Python網絡編程


返回目錄

 

本篇索引

(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 = 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()) + "\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 = 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 = 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 = 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 = 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 = []
        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 = 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 = 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 = 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。

 

 

 

 

返回目錄

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM