在Python網絡編程系列,我們主要學習以下內容:
5. 常見的Python異步編程框架
6. 協程在Python網絡編程中的使用
本文介紹Python下的基本套接字編程,主要基於 socket 模塊,包括簡單的TCP/UDP套接字編程。通過調用 socket 模塊的 socket() 函數創建一個新的套接字對象,在創建套接字時需要指定新 socket 對象使用的地址族和套接字類型,下文將分別予以介紹。
地址族通常作為 socket() 函數的第一個參數, AF_UNIX 只用於類UNIX平台,如果前者沒有被定義,那么該協議對應的地址族都不可用。
socket.AF_UNIX 屬於該類型的地址就是一個字符串。AF_UNIX對應的數值是:1。
socket.AF_INET IPv4地址族,(host, port) 形式的二元組,host是一個表示網絡主機的字符串,port為套接字的端口號。AF_INET對應的數值是:2。
socket.AF_INET6 (host, port, flowinfo, scopeid) 形式的四元組。AF_INET6對應的數值是:23。
套接字類型用於 socket() 函數的第二個參數,但是只有 SOCK_STREAM (TCP)和 SOCK_DGRAM (UDP)是比較常見的。
socket.SOCK_STREAM 面向流(TCP連接)的套接字,對應的數值:1。
socket.SOCK_DGRAM 面向數據報(UDP連接)的套接字,對應的數值:2。
socket.SOCK_RAW 對應的數值:3。
socket.SOCK_RDM 對應的數值:4。
socket.SOCK_SEQPACKET 對應的數值:5。
Socket 對象的只讀屬性
Python的socket對象具有以下屬性:
socket.family 創建套接字時傳入的地址族參數,訪問該屬性獲得的是數值。
socket.type 創建套接字時傳入的套接字類型,訪問該屬性獲得的是數值。
socket.proto
創建套接字
socket.socket([family[, type[, proto]]])
socket() 函數創建一個新的套接字對象;
參數:
family——套接字對象使用的地址族,可選值:AF_INET——IPv4地址族,AF_INET6——IPv6地址族,AF_UNIX——針對類UNIX系統的套接字;
type——套接字對象的類型,可選值:SOCK_STREAM——TCP連接套接字,SOCK_DGRAM——UDP數據報套接字;
proto——協議數通常是0,一般可以忽略該參數。
返回:一個新的套接字對象。
*注意:
關於地址族的問題,目前最流行的仍然是IPv4地址族,所以本文基本針對IPv4地址族展開。在Python的套接字編程中,IPv4地址是 (hostname, port) 形式的二元組,其中hostname是一個字符串,內容是網絡主機的DNS域名或點分IP地址(如:'8.8.8.8');port 是一個整數,代表遠端目標 socket 監聽的端口。
查看套接字的文件描述符(fd)
socket.fileno()
返回調用該方法的 socket 對象的 fd,fd 可以用於 select() 等機制。在Linux中一切皆文件,套接字也不例外,每個套接字都有自己的文件描述符,調用 fileno() 可以查看對應 socket 對象的描述符。
Windows 下返回的這個值不能用於類似 os.fdopen() 這樣直接操作 fd 的函數,但是在 UNIX/Linux 系統上沒有這個限制。
綁定套接字
socket.bind(address)
bind() 將套接字綁定到一個地址上,前提是該socket對象尚未被綁定到某個地址;
參數:符合創建該套接字時聲明的地址族格式的地址;對於AF_INET而言,如果(host, port)中的host是 "" 即空字符串,則說明允許來自一切主機的連接。
返回值:試圖綁定一個已經綁定的套接字將拋出 socket.error 。正常調用時返回值為空。
套接字監聽連接
socket.listen(backlog)
listen() 只由服務端 socket 調用,監聽連接到該套接字上的連接。
參數 backlog 指定該套接字可以容納的最大連接數,至少是0;
listen() 返回值為空。
阻塞等待連接
socket.accept()
accept() 等待並接受一個連接,能夠調用該方法的套接字必須(1). 已經綁定到一個特定的地址,並且(2). 監聽連接。如果沒有客戶端連接,則 accept() 函數阻塞直到有客戶端發起連接。
返回值: (conn, addr) 形式的二元組,其中:
conn :一個新的套接字對象,這個套接字對象已經連接,可以用來收發消息。
addr :連接到本套接字上來的套接字對象的網絡地址,在IPv4地址族下,這是一個(host, port) 形式的二元組。
*注意:
accept() 方法的特點在於當前服務端內核中如果沒有已經完成三次握手的套接字(已建立連接隊列為空),則 accept() 函數會阻塞;否則accept()函數會返回一個新的socket對象,這個套接字和服務端先前監聽的套接字不同,前者稱為監聽套接字,而后者稱為連接套接字。
在面向對象的Python中,監聽套接字就是調用該方法的 socket 對象,返回的 socket 對象是連接套接字,連接套接字已經經過三次握手建立起了連接,C/S 可以通過該套接字進行通信。
在多進程套接字編程中,往往在 server 的 accept() 成功返回時創建一個子進程,子進程和父進程的進程影像遵循“copy-on-write”規則,所以開始時是共享監聽套接字和連接套接字的。一般會讓父進程關閉其進程內的連接套接字,而子進程關閉其進程內的監聽套接字,這樣保證 server 端只有一個父進程處於監聽狀態,同時不斷孵化子進程處理到來的客戶端請求。
client socket 發起連接
socket.connect(address)
主動調用該方法的 socket 是客戶端,連接到一個遠程的 socket 對象。該函數會阻塞直到服務端接受或者拒絕客戶端的連接請求;
參數 address 是符合該套接字地址族格式的地址,對於IPv4地址族而言,;
connect() 返回值為空。
從套接字中讀取數據
socket.recv(bufsize[, flags])
recv() 從套接字中接收bufsize字節的數據,返回這些數據的字符形式。對於已經連接的套接字,會一直阻塞直到數據到來或套接字斷開連接。
參數:
bufsize -- 最大接收的數據長度,通常應該設為2的指數次;
flags -- 默認為0,和 UNIX recv(2) 中的參數 flags 的含義相同,
返回值:接收到的字符串數據;如果套接字斷開連接,則返回空字符串;
socket.recv_into(buffer[, nbytes[, flags]])
recv_into() 從 socket 中讀取 nbytes 字節的數據寫到緩存 buffer 中,而不是創建一個新的字符串;
參數:
buffer —— 接收讀取到的數據的緩存,
nbytes ——打算讀取的字節數,如果為0或者沒有指定,則會讀取 buffer 能容納的上限個字節;
flags同 recv()
返回:實際讀取的字節數。
向套接字發送數據
socket.send(string[, flags])
send() 將 string 中的數據發送到套接字中,返回實際寫入的字節數。如果實際寫入的字節數少於len(string),那么需要重新發送剩下的字節。如果套接字的緩存中沒有多余的空間,該函數會一直阻塞直到空間充裕;
參數 string——要發送的字節序列;flags——同 recv()中的flags參數;
返回實際發送的字節數。
socket.sendall(string[, flags])
發送 string 中的全部字節,該函數一直阻塞直到完全發送。發送的套接字應該已經連接到另一個遠程套接字,該方法一直發送數據直到全部發送完成,或者發送出現錯誤;
參數:同 send() 方法
如果發送成功,sendall() 返回None;否則拋出異常,不同於 send() 方法,sendall() 無法得知實際發送了多少字節。
關閉套接字——close()
socket.close()
作用:關閉套接字,套接字關閉后,所有針對已關閉套接字的操作都會失敗。套接字被GC的時候,會自動關閉。關閉套接字會釋放連接占用的資源,但是並不一定立刻關閉連接,如果想要及時關閉連接,應該在 close() 前調用 shutdown() 。
參數:不需要參數;
返回值:返回值為空。
關閉套接字——shutdown()
socket.shutdown(how)
作用:關閉連接的一端或兩端一起關閉;shutdown() 和 close() 的最大區別在於 shutdown() 可以選擇部分關閉套接字,而 close() 默認關閉整個套接字的兩端。考慮這樣的情形,一方申請關閉套接字,實際上只是聲明自己不會再往套接字中寫數據,而套接字中此時可能還沒有接收完的來自對方的數據,如果調用 close() ,則不僅己方以后不能再寫數據,也不能再讀數據了。如果希望部分關閉套接字,比如關閉己方的寫,但保留己方的讀,這樣可以保證數據完全傳輸。
參數 how -- 以何種方式關閉連接,可選值: socket.SHUT_RD 此后不能再讀(receive); socket.SHUT_WR 此后不能再寫(send); socket.SHUT_RDWR 此后不能讀寫。根據平台的不同,關閉連接的一端可能導致另一端也同樣關閉。
利用上面介紹的 socket 對象的方法,可以完成簡單的 TCP 套接字編程。
TCP套接字編程
圖1 TCP套接字編程中的服務端(左)與客戶端(右)
圖 1 顯示了TCP套接字編程的一般流程,server 只有綁定和監聽后,才能阻塞在 accept() 調用上等待客戶端的連接,客戶端調用 connect() 后,將會與服務端通過三次握手建立TCP連接。下面的簡單示例,顯示了一個單進程的 TCP 回顯server和client.
例1.1 單進程TCP回顯server
# -*- coding: utf-8 -*-
# single_proc_tcp_server.py
import socket listen_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) listen_sock.bind(("", 4424)) listen_sock.listen(10) # terminate with Ctrl-C try: while True: conn_sock, address = listen_sock.accept() # accept() 返回新的套接字和對端的IPv4二元組地址 print("New connection from: ", address) while True: data = conn_sock.recv(1024) if not data:
break
conn_sock.sendall(data) conn_sock.close() # 關閉連接套接字 print("Connection closed from", address) finally: listen_sock.close() # 關閉監聽套接字
服務端的運行結果:
('New connection from: ', ('127.0.0.1', 64268)) ('Connection closed from', ('127.0.0.1', 64268)) ('New connection from: ', ('127.0.0.1', 64270)) ('Connection closed from', ('127.0.0.1', 64270)) ('New connection from: ', ('127.0.0.1', 64272)) ('Connection closed from', ('127.0.0.1', 64272))
例1.2 TCP回顯client
# -*- coding: utf-8 -*- # single_proc_tcp_client.py import socket import time sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('127.0.0.1', 4424)) print("Connected to server.") data = """This is a few lines of data sent from client. """ for line in data.splitlines(): sock.sendall(line) print("Sent: ", line) resp = sock.recv(1024)
print("Received: ", resp) time.sleep(1)
print("Close connection to server.") sock.close()
客戶端的運行結果:
Connected to server. ('Sent: ', 'This is a ') ('Received: ', 'This is a ') ('Sent: ', 'few lines of data ') ('Received: ', 'few lines of data ') ('Sent: ', 'sent from client.') ('Received: ', 'sent from client.') Close connection to server.
例1.1 中的服務端是沒有並發功能的,客戶端中使用 sleep() 模擬比較耗時的客戶端連接。同時運行多次客戶端腳本,可以發現任意時刻服務端只能響應一個客戶端的連接,其他的客戶端將被阻塞。所以這種單進程的 server 是最簡單的示例,其並不具備實際的使用意義。
UDP套接字編程
下圖顯示了UDP套接字編程的一般流程,
圖2 UDP套接字編程的一般流程
與面向連接流的 TCP 協議不同,UDP 是面向無連接數據報的傳輸層協議,無連接的最大特點就是客戶端和服務端之間不需要為通信專門建立連接,而是像把信交給郵遞員那樣把數據包發給UDP套接字,至於數據能否如期到達,UDP套接字是不保證的。體現在代碼層面,服務端只需要綁定到具體的網絡和端口即可,而客戶端不需要在數據傳輸前通過 connect() 建立專門的連接。
socket 對象與UDP套接字編程相關的方法包括:
從套接字中讀取數據
socket.recvfrom(bufsize[, flags])
作用:從套接字中接收bufsize字節的數據,同時返回發送數據的套接字的地址;
參數:參數同 recv()
返回:(string, address)形式的二元組,string是接收到的數據,address則是發送這個數據的套接字的地址,具體的地址形式則取決於套接字所屬的地址族。
socket.recvfrom_into(buffer[, nbytes[, flags]])
recvfrom_into() 從套接字中讀取 nbytes 字節的數據寫到緩存 buffer 中,而不是創建一個新的字符串;
參數:buffer —— 接收讀取到的數據的緩存,nbytes ——打算讀取的字節數,如果為0或者沒有指定,則會讀取 buffer 能容納的上限個字節;flags同 recv() 和 recvfrom() 等函數,默認為0;
返回: (nbytes, address) 形式的二元組,nbytes是實際寫入的字節數,address是發送方套接字的地址。
向套接字發送數據
socket.sendto(string, address)
socket.sendto(string, flags, address)
sendto() 將 string 中的字節發送給 address 處的套接字,發送方不能連接到其他的套接字,也不能調用過 socket.bind() 方法,對於UDP套接字很有用,因為可以一次給很多的地址發送數據;
參數:string——要發送的字節序列;address——目標套接字地址;flags——同以上的其他方法中的flags參數;
返回:返回成功發送的字節數。
*注意:
通常send()、sendall()、recv()用於 TCP socket,因為 TCP 連接建立后,兩個已經連接的套接字之間通信不需要再額外指定對方的地址,而且接收來自對方的數據時也不需要額外獲知對方的地址,服務端在accept() 返回時就知道客戶端的地址,而客戶端建立連接時就知道服務端的地址。
sendto()、recvfrom() 則多用於 UDP 套接字,因為UDP socket不是面向連接的,因此每次發送消息都要指定這次要發送給誰,同時接收到的數據可能來自多個遠端socket,可能需要在接收數據時獲得發送方的地址,此時這兩個方法就能派上用場。
這種划分不是絕對的,需要根據具體需求,例如下面例2.2中的 client,由於確信只有一個通信的套接字即 server 端,因此接收數據時不關心對方的地址,就可以使用 recv()。
例2.1 單進程 UDP 回顯 server
#-*-encoding:utf-8-*- # single_proc_udp_server.py import socket sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.bind(("", 4424)) try:
while True:
data, address = sock.recvfrom(1024) print("Datagram from:", address) sock.sendto(data, address) finally: sock.close()
服務端運行結果:
('Datagram from:', ('127.0.0.1', 62848)) ('Datagram from:', ('127.0.0.1', 62848)) ('Datagram from:', ('127.0.0.1', 62848))
例2.2 單進程 UDP 回顯 client
#-*-encoding:utf-8-*- # single_proc_udp_client.py import socket import time sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) data = """This is a few lines of data sent from client. """ for line in data.splitlines(): sock.sendto(line, ('localhost', 4424)) print("Sent:", line) resp = sock.recv(1024) print("Received: ", resp) time.sleep(3) sock.close()
客戶端運行結果:
('Sent:', 'This is a ') ('Received: ', 'This is a ') ('Sent:', 'few lines of data ') ('Received: ', 'few lines of data ') ('Sent:', 'sent from client.') ('Received: ', 'sent from client.')
UDP 套接字編程相對TCP套接字編程要簡單,UDP 靈活快捷的背后犧牲了安全和可靠等諸多特性,常用在對數據安全性要求不高的多媒體傳輸中。
socket對象的其他方法
獲取對端 socket 地址
socket.getpeername()
getpeername() 獲取連接對方套接字的地址。s 必須已經連接:(1)或者是已經正確調用過 s.connetct(),(2)或者是由監聽套接字的 accept() 函數生成的連接套接字;
返回值:遠程連接套接字的地址,具體的形式取決於套接字的地址族。
獲取套接字自身的地址
socket.getsockname()
getsockname() 返回某個套接字自己的地址,IPv4地址族下是一個 (ip, port) 形式的二元組。
返回當前套接字的地址,具體的形式取決於套接字的地址族。
設置套接字是否阻塞
socket.setblocking(flag)
作用:設置一個套接字的阻塞狀態。
參數:
flags -- 為0時將套接字設為非阻塞,為1時將套接字設為阻塞。
默認情況下,初始創建的套接字都是阻塞的。非阻塞狀態下,如果 recv() 函數沒有獲取到任何數據或者 send() 函數沒有立刻發出數據,都會拋出socket.error異常;而在阻塞模式下,這些調用會阻塞直到能夠繼續為止。 s.setblocking(0) 等價於 s.settimeout(0.0) ,而 s.setblocking(1) 等價於 s.settimeout(None) 。
返回:成功返回None。
獲取當前套接字的超時設置
socket.gettimeout()
gettimeout() 返回當前套接字對象的超時秒數;
返回值:返回當前套接字對象的 float 型超時秒數;如果當前套接字對象沒有超時行為,那么返回 None 。
套接字可能所處的狀態——阻塞,非阻塞或者超時。
設置套接字的超時
socket.settimeout(value)
settimeout() 為阻塞套接字設置超時時限(秒),
參數:
value——非負浮點數或None,如果指定了非負浮點數,當阻塞套接字等待達到指定的超時時限時,拋出timeout異常;如果value為None,等於關閉了套接字的超時限制,等於將套接字設為阻塞模式,此時套接字在阻塞調用上將無限等待直到事件發生。
返回:成功調用則返回None。
套接字的阻塞方法
套接字的 accept()、connect()、send()、recv()等方法都有可能導致套接字阻塞,即程序暫時無法繼續獲得CPU。可能的原因是沒有客戶端的連接、還沒有和服務端建立起連接、緩沖池已滿暫時無法寫入等,Python為套接字定義了三種狀態:未阻塞、阻塞和超時,下文將會專門探討Python中套接字的超時機制。
套接字的超時(timeout)行為
標准的 C 套接字通常只有阻塞和非阻塞兩種狀態,Python 套接字的超時是指通過設置套接字的 timeout 選項,當套接字調用了一個可能引起阻塞的方法,並在該方法上等待的時長超過了預先設置的閥值時,套接字不再繼續等待,而是拋出異常 socket.error。超時可以用來控制套接字等待事件的上限,避免socket對象被長時間阻塞在某個調用上。
socket 模塊中和 timeout 相關的函數包括:getdefaulttimeout() 和 setdefaulttimeout()。socket 對象和timeout相關的方法包括:gettimeout() 和 settimeout()。當一個 socket 對象的timeout值為None時,說明這個socket對象是一個“阻塞型”的套接字,也就是一旦在某個方法上阻塞,套接字會一直等待直到事件發生或條件滿足。
獲取套接字的選項
socket.getsockopt(level, optname[, buflen])
作用:返回給定套接字的某些參數;
參數:
level:
SOL_SOCKET——跟套接字自身相關的選項;
SOL_IP——跟IP協議相關的選項;
SOL_TCP——跟TCP協議相關的選項;
SOL_UDP——跟UDP協議相關的選項;
optname:
socket模塊規定的一些以SO_開頭的屬性;
buflen:可以為空,表示接受返回參數的緩存空間大小;如果沒有設置,說明假設選項是整形的;
返回:返回套接字選項的整型值;如果選項並不是整形的,而是一些結構體,那么傳入的buflen限制了接受選項的字節串的長度,接收到字節串后,可以使用struct模塊提供的工具來解包(unpacking)。
設置套接字的選項
socket.setsockopt(level, optname, value)
作用:類似於getsockopt(),但作用是為套接字設置某些選項的值;
從套接字創建 file 對象
socket.makefile([mode[, bufsize]])
作用:創建一個Python文件對象,該對象能夠從創建它的套接字里讀取或寫入到創建它的套接字中。創建的文件對象與套接字可以獨立地關閉。套接字必須處在非阻塞狀態,不能設定超時;只有當f和s都關閉時,才會真正關閉創建f的套接字。
參數:參數mode和bufsize和內置函數file()的參數相同;
返回:返回一個和當前套接字相關的Python文件對象
socket.ioctl(control, option)
socket.connect_ex(address)