Python中的網絡編程
. 本節中使用的主要模塊是socket模塊,在模塊中可以找到socket()函數,該函數用於創建套接字對象。套接字也有自己的方法集,這些方法可以實現基於套接字的網絡通信。
socket()模塊函數
. 創建套接字要使用socket.socket()函數,其一般語法如下:
socket(socket_family,socket_type,protocol = 0)
. 其中socket_family是AF_UNIX或AF_INET,socket_type是SOCK_STREAM或SOCK_DGRAM,protocol通常省略,默認爲0。因此創建TCP或UDP套接字的形式如下:
tcpSock = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
udpSock = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
. 在有了套接字對象後,就可以使用套接字對象的方法來做進一步交互。對Python來說,可以使用“from socket import * ”將socket屬性引入命名空間中,這樣能夠大大縮短代碼:
tcpSock = socket.socket(AF_INET,SOCK_STREAM)
套接字對象(內置)方法
. 以下的表列舉了一些常見的套接字對象的方法和屬性:
名稱 | 描述 |
---|---|
服務器套接字方法 | |
s.bind() | 將地址(主機號、端口號對)綁定到套接字上 |
s.listen() | 設置並啓動TCP監聽器 |
s.accept() | 被動接受TCP客戶端連接,一直等待知道連接到達(阻塞),每個連接進來的客戶端,都會通過accept函數返回一個不同的客戶端的socket對象和屬於客戶端的套接字 |
客戶端套接字方法 | |
s.connect() | 主動發起TCP服務器連接 |
s.connect_ex() | 上個方法的升級版,會以錯誤碼的形式返回問題,而不是拋出異常 |
普通的套接字方法 | |
s.recv() | 接受TCP消息 |
s.recv_into() | 接受TCP消息到指定的緩衝區 |
s.send() | 發送TCP消息 |
s.sendall() | 完整的發送TCP消息 |
s.recvfrom() | 接受UDP消息 |
s.recvfrom_into() | 接受UDP消息到指定的緩衝區 |
s.sendto() | 發送UDP消息 |
s.getpeername() | 連接到套接字(TCP)的遠程地址 |
s.getsockname() | 當前套接字的地址 |
s.getsockopt() | 返回給定套接字選項的值 |
s.setsockopt() | 設置給定套接字選項的值 |
s.shutdown() | 關閉連接 |
s.close() | 關閉套接字 |
s.detach() | 在未關閉文件描述符的情況下關閉套接字,返回文件描述符 |
s.ioctl() | 控制套接字的模式(僅支持 Windows) |
面向阻塞的套接字方法 | |
s.setblocking() | 設置套接字的阻塞或非阻塞模式 |
s.settimeout() | 設置阻塞套接字操作的超時時間 |
s.gettimeout() | 獲取阻塞套接字操作的超時時間 |
面向文件的套接字方法 | |
s.fileno() | 套接字的文件描述符 |
s.makefile() | 創建與套接字關聯的文件對象 |
數據屬性 | |
s.family | 套接字家族 |
s.type | 套接字類型 |
s.proto | 套接字協議 |
創建TCP服務器
. 首先看看創建TCP服務器的一般僞代碼:
ss = socket() #創建服務器套接字
ss.bind() #套接字與地址綁定
ss.listen() #監聽連接
inf_loop: #服務器無限連接
cs = ss.accept() #接受客戶端連接
comm_loop: #通信循環
cs.recv()/cs.send() #對話(接受/發送)
cs.close() #關閉客戶端套接字
ss.close() #關閉服務器套接字(可選)
. 所有的套接字都通過socket()函數來創建對象,因爲服務器需要佔用一個端口等待客戶端的請求,所以需要綁定一個本地地址,特別的TCP服務器必須監聽(傳入的)連接,一旦這個過程完了之後,服務器就可以開始它的無限循環。
調用accept()函數之後,就開啓了一個簡單的(單線程)服務器,它會等待客戶端的連接。默認情況下,accept()是阻塞的,這意味着執行將被暫停,直到一個連接到達(套接字也確實支持非阻塞模式)。
一旦服務器接收了一個連接,就會返回(利用accept())一個獨立的客戶端套接字,用來與即將到來的消息進行交換。也就是說,當一個傳入的請求到達時,服務器會創建一個新的通信端口來直接與客戶端進行通信,再次空出主要的端口(原始服務器套接字),以使其能夠接受新的客戶端連接;而通過使用這個新的套接字,客戶端和服務器之間就可以開始參與發送和接受的對話中,直到連接終止(當一方關閉連接或者向對方發送一個空字符串時,通常就會關閉連接)。
接下來創建一個TCP服務器,它接受來自客戶端的消息,並返回加了時間戳的相同內容的消息:
from socket import *
from time import ctime
Host = ''
Port = 21567
Bufsize = 1024
Addr = (Host,Port)
tcpSerSock = socket(AF_INET,SOCK_STREAM)
tcpSerSock.bind(Addr)
tcpSerSock.listen(5)
while True:
print('waiting for connection...')
tcpCliSock,addr = tcpSerSock.accept()
print('...connected from:',addr)
while True:
data = tcpCliSock.recv(Bufsize)
if not data:
break
senddata = '[%s] %s.' % (bytes(ctime(),'utf-8'),data)
tcpCliSock.send(senddata.encode())
tcpCliSock.close()
tcpSerSock.close()
創建TCP客戶端
. 創建客戶端相對要簡單一些,首先還是看看僞碼:
cs = socket() # 創建客戶端套接字
cs.connect() # 嘗試連接服務器
comm_loop: # 通信循環
cs.send()/cs.recv() # 對話(發送 / 接收)
cs.close() # 關閉客戶端套接字
. 一旦客戶端擁有了一個套接字,它就可以利用套接字的connect()方法直接創建一個到服務器的連接,當連接建立之後就可以參與到與服務器的對話中,最後,如果客戶端完成了它的事務,它就可以關閉套接字,終止此次連接。
以下是TCP客戶端的代碼示例:
from socket import *
Host = 'localhost'
Port = 21567
Bufsize = 1024
Addr = (Host,Port)
tcpCliSock = socket(AF_INET,SOCK_STREAM)
tcpCliSock.connect(Addr)
while True:
data = input('> ')
if not data:
break
tcpCliSock.send(data.encode())
data = tcpCliSock.recv(Bufsize)
if not data:
break
print(data.decode('utf-8'))
tcpCliSock.close()
. 客戶端也有一個無限循環,但是不會意味着它會永遠運行下去,客戶端循環在以下兩種情況將會跳出:用戶沒有輸入;或者服務器終止且對recv()方法的調用失敗。
執行TCP服務器和客戶端
. 首先要運行的應該是服務器,因爲如果先運行客戶端,那麼將無法進行任何連接。服務器可以視爲一個被動夥伴,因爲必須首先建立自己,然後被動地等待連接。另一方面,客戶端是一個主動的合作伙伴,因爲它主動發起一個連接。
創建UDP服務器
. UDP服務器不需要TCP服務器那麼多設置,因爲它不是面向連接的,除了等待傳入的連接外,幾乎不需要做任何其他工作。它的僞碼如下:
ss = socket() # 創建服務器套接字
ss.bind() # 綁定服務器套接字
inf_loop: # 服務器無限循環
cs = ss.recvfrom()/ss.sendto() # 關閉(接收 / 發送)
ss.close() # 關閉服務器套接字(可選)
. 從上面的僞代碼中可以看出,除了普通的創建套接字並將其綁定到本地地址(主機名/端口號對)外,並沒有額外的工作。UDP 和 TCP 服務器之間的另一個顯著差異是,因爲數據報套接字是無連接的,所以就沒有爲了成功通信而使客戶端連接到一個獨立的套接字“轉換“的操作,以下是UDP加時間戳的服務器代碼示例:
from socket import *
from time import ctime
Host = ''
Port = 21567
Bufsize = 1024
Addr = (Host,Port)
udpSerSock = socket(AF_INET,SOCK_DGRAM)
udpSerSock.bind(Addr)
while True:
print('waiting for message...')
data,addr = udpSerSock.recvfrom(Bufsize)
senddata = '[%s] %s.' % (bytes(ctime(),'utf-8'),data)
udpSerSock.sendto(senddata.encode(),addr)
print('...received from and returned to :',addr)
udpSerSock.close()
創建UDP客戶端
. UDP的客戶端代碼是最短的,它的僞代碼如下:
cs = socket() # 創建客戶端套接字
comm_loop: # 通信循環
cs.sendto()/cs.recvfrom() # 對話(發送 / 接收)
cs.close() # 關閉客戶端套接字
. 一旦創建了套接字對象,就進入對話循環中與服務器進行信息交互,最後當通信結束的時候會關閉套接字。以下是UDP的時間戳客戶端代碼示例:
from socket import *
Host = 'localhost'
Port = 21567
Bufsize = 1024
Addr = (Host,Port)
udpCliSock = socket(AF_INET,SOCK_DGRAM)
while True:
data = input('> ')
if not data:
break
udpCliSock.sendto(data.encode(),Addr)
data,Addr = udpCliSock.recvfrom(Bufsize)
if not data:
break
print(data)
udpCliSock.close()
socket模塊屬性
. 除了現在熟悉的 socket.socket()函數之外,socket 模塊還提供了更多用於網絡應用開發的屬性,下標列出了常用的屬性:
屬性名 | 描述 |
---|---|
數據屬性 | |
AF_UNIX、AF_INET、AF_INET6 、AF_NETLINK 、AF_TIPC | Python 中支持的套接字地址家族 |
SO_STREAM、SO_DGRAM | 套接字類型(TCP=流,UDP=數據報) |
has_ipv6 | 指示是否支持 IPv6 的布爾標記 |
異常 | |
error | 套接字相關錯誤 |
herror | 主機和地址相關錯誤 |
gaierror | 地址相關錯誤 |
timeout | 超時時間 |
函數 | |
socket() | 以給定的地址家族、套接字類型和協議類型(可選)創建一個套接字對象 |
socketpair() | 以給定的地址家族、套接字類型和協議類型(可選)創建一對套接字對象,函數用於創建一對無名的、相互連接的套接字。 如果函數成功,則返回0,創建好的套接字分別是sv[0]和sv[1];否則返回-1,錯誤碼保存於errno中 |
create_connection() | 常規函數,它接收一個地址(主機名,端口號)對,返回套接字對象 |
fromfd() | 以一個打開的文件描述符創建一個套接字對象 |
ssl() | 通過套接字啓動一個安全套接字層連接;不執行證書驗證 |
getaddrinfo() | 獲取一個五元組序列形式的地址信息 |
getnameinfo() | 給定一個套接字地址,返回(主機名,端口號)二元組 |
getfqdn() | 返回完整的域名 |
gethostname() | 返回當前主機名 |
gethostbyname() | 將一個主機名映射到它的 IP 地址 |
gethostbyname_ex() | gethostbyname()的擴展版本,它返回主機名、別名主機集合和 IP 地址列表 |
gethostbyaddr() | 將一個 IP 地址映射到 DNS 信息;返回與 gethostbyname_ex()相同的 3 元組 |
getprotobyname() | 將一個協議名(如‘tcp’)映射到一個數字 |
getservbyname()/getservbyport() | 將一個服務名映射到一個端口號,或者反過來;對於任何一個函數來說,協議名都是可選的 |
ntohl()/ntohs() | 將來自網絡的整數轉換爲主機字節順序 |
htonl()/htons() | 將來自主機的整數轉換爲網絡字節順序 |
inet_aton()/inet_ntoa() | 將 IP 地址八進制字符串轉換成 32 位的包格式,或者反過來(僅用於 IPv4 地址) |
inet_pton()/inet_ntop() | 將IP 地址字符串轉換成打包的二進制格式,或者反過來(同時適用於 IPv4 和IPv6 地址) |
getdefaulttimeout()/setdefaulttimeout() | 以秒(浮點數)爲單位返回默認套接字超時時間;以秒(浮點數)爲單位設置默認套接字超時時間 |