Python網絡編程之socket應用
1 引言
2 網絡基礎
3 socket介紹
4 socket基本使用
5 總結
1 引言
本篇主要對Python下網絡編程中用到的socket模塊進行初步總結。首先從網絡基礎理論出發,介紹了TCP協議和UDP協議;然後總結了socket中的常用函數;最後通過實際代碼展示基本函數的應用。
2 網絡基礎
要想理解socket,首先得熟悉一下TCP/IP協議族。TCP/IP(Transmission Control Protocol/Internet Protocol)即傳輸控制協議/網間協議,定義了主機如何連入因特網及數據如何在它們之間傳輸的標準,從字面意思來看TCP/IP是TCP和IP協議的合稱,但實際上TCP/IP協議是指因特網整個TCP/IP協議族。下面對TCP/IP協議族中與socket緊密相關的的TCP協議和UDP協議進行介紹。
TCP是流協議,而UDP是數據報協議。換句話說,TCP在客戶機和服務器之間建立持續的開放連接,在該連接的生命期內,字節可以通過該連接寫出(並且保證順序正確)。然而,通過 TCP 寫出的字節沒有內置的結構,所以需要高層協議在被傳輸的字節流內部分隔數據記錄和字段。
UDP是數據報協議,不需要在客戶機和服務器之間建立連接,它只是在地址之間傳輸報文。UDP的一個很好特性在於它的包是自分隔的(self-delimiting),也就是一個數據報都準確地指出它的開始和結束位置。然而,UDP的一個可能的缺點在於,它不保證包將會按順序到達,甚至根本就不保證。不過,UDP有一個很大的有點就是效率高。
TCP與UDP直接的對比就好似手機通信和郵寄信件通信。TCP猶如手機通信機制,當呼叫者通過手機撥打接受者手機,只有接受者按下接聽鍵,兩方才算建立起了連接,且只要沒人掛斷,連接就一直是存活的,也只有在連接存活情況下兩者才能通話。UDP就如有郵局系統,只有有人來寄信,郵局就回幫他寄,但是不會去管收件人是否存在、也不管收件人什麽時候能收到信,如果寄件人陸續寄出多封信,收件人收的的信先後順序是混亂的,如果有的信沒有送達,那這封信就此消失在歷史的塵埃中。
通常,人們用socket來建立計算機網絡中多個主機(或進程)TCP/IP間的連接。
3 socket介紹
Socket(中文譯為套接字)是操作系統內核中的一個數據結構,它幾乎是所有網絡通信的基礎。網絡通信,歸根到底還是進程間的通信(不同計算機上的進程間通信, 又稱為網絡通信, IP協議進行的主要是端到端通信)。在網絡中,每一個節點(計算機或路由)都有一個網絡地址,也就是IP地址。兩個進程通信時,首先要確定各自所在的網絡節點的網絡地址。但是,網絡地址只能確定進程所在的計算機,而一臺計算機上很可能同時運行著多個進程,所以僅憑網絡地址還不能確定到底是和網絡中的哪一個進程進行通信,因此套接字中還需要包括其他的信息,也就是端口號(PORT)。在一臺計算機中,一個端口號一次只能分配給一個進程,也就是說,在一臺計算機中,端口號和進程之間是一 一對應的關系。
socket使用(IP地址,協議,端口號)來標識一個進程。所以,使用端口號和網絡地址的組合可以唯一的確定整個網絡中的一個網絡進程。端口號的範圍從0~65535,一類是由互聯網指派名字和號碼公司ICANN負責分配給一些常用的應用程序固定使用的“周知的端口”,其值一般為0~1023, 用戶自定義端口號一般大於等於1024。
網絡上的兩個程序通過一個雙向的通信連接實現數據的交換,這個連接的一端稱為一個socket。每一個socket都用一個半相關描述{協議、本地地址、本地端口}來表示;一個完整的套接字則用一個相關描述{協議、本地地址、本地端口、遠程地址、遠程端口}來表示。socket也有一個類似於打開文件的函數調用,該函數返回一個整型的socket描述符,隨後的連接建立、數據傳輸等操作都是通過socket描述符來實現的。以TCP協議中socket建立連接的“三次握手”為例,其過程如下:
如圖所示,當客戶端通過socket調用connect時,觸發了連接請求,向服務器發送了SYN J包,這時connect進入阻塞狀態;服務器監聽到連接請求,即收到SYN J包,調用socket的accept函數接收請求向客戶端發送SYN K ,ACK J+1,這時accept進入阻塞狀態;客戶端收到服務器的SYN K ,ACK J+1之後,這時connect返回,並對SYN K進行確認;服務器收到ACK K+1時,accept返回,至此三次握手完畢,連接建立。
兩個socket通過“網絡”交互數據進行數據交互時只負責兩件事:建立連接,傳遞數據;同時socket在收發數據時遵循的原則:有發就有收,收發必相等!
4 socket基本使用
1)socket函數
功能:使用給定的地址族、套接字類型、協議編號(默認為0)來創建套接字。
格式:socket.socket([family[, type[, proto]]])
參數:
family : AF_INET (默認ipv4),AF_INET6(ipv6) , AF_UNIX(Unix系統進程間通信).
type : SOCK_STREAM (TCP), SOCK_DGRAM(UDP) .
protocol : 一般為0或者默認
備註:如果socket創建失敗會拋出一個socket.error異常
2)服務器端函數
a)bind函數
格式:s.bind(address)
功能:將地址address綁定到套接字, 地址以元組(host,port)的形式表示。
參數:
address為元組(host,port)
host: ip地址, 為一個字符串
post: 自定義主機號, 為整型
b)listen函數
格式:s.listen(backlog)
功能:使服務器的這個端口和IP處於監聽狀態,等待網絡中某一客戶機的連接請求。如果客戶端有連接請求,端口就會接受這個連接。
參數:backlog : 操作系統可以掛起的最大連接數量。該值至少為1,大部分應用程序設為5就可以了
c)accept函數
格式:s.accept()
功能:接受遠程計算機的連接請求,建立起與客戶機之間的通信連接。服務器處於監聽狀態時,如果某時刻獲得客戶機的連接請求,此時並不是立即處理這個請求,而是將這個請求放在等待隊列中,當系統空閑時再處理客戶機的連接請求。
返回值:返回一個數組(conn,address),其中conn是新的套接字對象,可以用來接收和發送數據。address是連接客戶端的地址
3)客戶端函數
a)connect函數
格式:s.connect(address)
功能:用來請求連接遠程服務器
參數:address為遠程服務器地址, 格式為元組(hostname,port),如果連接出錯,返回socket.error錯誤
b)connect_ex函數
格式:s.connect_ex(address)
備註:connect()函數的擴展版本,出錯時返回出錯碼,而不是拋出異常
4)通用函數
a)recv函數
格式:s.recv(bufsize[,flag])
功能:接收遠端主機傳來的數據
參數:
bufsize : 指定要接收的數據大小
flag : 提供有關消息的其他信息,通常可以忽略
返回值:返回值為數據以字符串形式</code>
b)send函數
格式:s.send(string[,flag])
功能:發送數據給指定的遠端主機
參數:
string : 要發送的字符串數據
flag : 提供有關消息的其他信息,通常可以忽略
返回值:返回值是要發送的字節數量,該數量可能小於string的字節大小。
c)sendall函數
格式:s.sendall(string[,flag])
功能:內部調用了send函數,完整發送TCP數據。將string中的數據發送到連接的套接字,但在返回之前會嘗試發送所有數據。
參數:同send函數
返回值 : 成功返回None,失敗則拋出異常。
d)close函數
格式:s.close()
功能:關閉套接字
e)recvfrom函數
格式:s.recvfrom(bufsize[.flag])
功能:與recv()類似,區別是返回值不同
返回值:返回一個數組(data,address),其中data是包含接收數據的字符串,address是發送數據的套接字地址。
f)sendto函數
格式:s.sendto(string[,flag],address)
功能:將數據發送到套接字
參數:
string : 要發送的字符串數據
flag : 提供有關消息的其他信息,通常可以忽略
address是形式為(ipaddr,port)的元組,指定遠程地址
返回值:返回值是要發送的字節數量
備註:該函數主要用於UDP協議。
g)settimeout函數
格式:s.settimeout(timeout)
功能:設置套接字操作的超時期
參數:timeout是一個浮點數,單位是秒。值為None表示沒有超時期。一般,超時期應該在剛創建套接字時設置,因為它們可能用於連接的操作(如 client 連接最多等待5s )
h)getpeername函數
格式:s.getpeername()
功能:獲取連接套接字的遠程地址
返回值:返回值通常是元組(ipaddr,port)。
i)getsockname函數
格式:s.getsockname()
功能:獲取套接字自己的地址
返回值:通常是一個元組(ipaddr,port)
socket中常用的函數就上面這些了。先用上面這些函數嘗試TCP協議下的socket通信。
服務器端代碼如下:
import socket sk = socket.socket() sk.bind((‘127.0.0.1‘ ,8088)) sk.listen(5) print(‘正在等待客戶端連接……‘) conn , addr = sk.accept() print(‘客戶端已連接到服務器……‘) mes_from_client = conn.recv(1024).decode(‘utf-8‘) print(mes_from_client) mes_to_server = ‘你好,客戶端,已收到你的信息!‘.encode(‘utf-8‘)#發送的數據必須是byte類型 conn.send(mes_to_server) conn.close() sk.close()
客戶端代碼:
import socket sk = socket.socket() sk.connect((‘127.0.0.1‘,8088)) mes_to_server = ‘你好,服務器!‘.encode(‘utf-8‘)#發送的數據必須是byte類型 sk.send(mes_to_server) mes_from_server = sk.recv(1024).decode(‘utf-8‘) print(mes_from_server) sk.close()
註意:上述兩代碼塊必須放在兩不同的py文件中,且必須先運行服務器代碼,然後在開啟客戶端。開啟服務器後,首先輸出“正在等待客戶端連接……”,然後進程會阻塞在accept函數中,下面的代碼不會被執行,知道有客戶端連接過來。開啟客戶端後,服務器端會先收到客戶端發來的信息,然後客戶端也會受到服務器發來的信息。
上面的例子中,服務器和客戶端都是收發了一條信息後socket關閉,如果要保持連接進行長時間通信呢?那麽,我們可以把收發函數放入一個“while True”循環中:
服務器端代碼:
import socket BUF_SIZE = 1024 #設置緩沖區大小 server_addr = (‘127.0.0.1‘, 8089) #IP和端口構成表示地址 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #生成一個新的socket對象 server.bind(server_addr) #綁定地址 print("socket與地址綁定完成……") server.listen(5) #監聽, 最大監聽數為5 print("socket監聽開始……") client, client_addr = server.accept() #接收TCP連接, 並返回新的套接字和地址, 阻塞函數 print("報告:有客戶端請求連接,正在連接……") print(‘客戶端地址為:{}‘.format( client_addr)) while True : mes_from_client = client.recv(BUF_SIZE) #從客戶端接收數據 mes = mes_from_client.decode(‘utf-8‘) print(‘客戶端說:{}‘.format(mes)) mes = input(‘回復客戶端的信息>‘) mes_to_client = mes.encode(‘utf-8‘) client.sendall(mes_to_client) #發送數據到客戶端 server.close()
客戶端代碼:
import socket BUF_SIZE = 1024 #設置緩沖區的大小 server_addr = (‘127.0.0.1‘, 8089) #IP和端口構成的服務器地址 client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #返回新的socket對象 client.connect(server_addr) #連接服務器 while True: mes = input("發送給服務器的信息> ") mes_to_server = mes.encode(‘utf-8‘) client.sendall(mes_to_server) #發送數據到服務器 mes_from_server = client.recv(BUF_SIZE) #從服務器端接收數據 mes = mes_from_server.decode(‘utf-8‘) print(mes) client.close()
運行上述代碼後,客戶端和服務器可以長時間維持通信。不過,使用socket時一定要註意,有發才有收,收發必相等,否則,就回出現異常。如果需要求換其他客戶端與當前服務器進行通信,必須先斷開當前客戶端的連接。
再來嘗試UDP協議下socket通信:
服務器端代碼:
import socket sk = socket.socket(type=socket.SOCK_DGRAM) sk.bind((‘127.0.0.1‘ , 8090)) print(‘等待客戶端發來消息……‘) msg , addr = sk.recvfrom(1024) # 此處會阻塞 print(msg.decode(‘utf-8‘)) mes_to_server = ‘你好,客戶端,已收到你的信息!‘.encode(‘utf-8‘)#發送的數據必須是byte類型 sk.sendto(mes_to_server,addr) sk.close()
客戶端代碼:
import socket sk = socket.socket(type=socket.SOCK_DGRAM) ip_port = (‘127.0.0.1‘ , 8090) mes_to_server = ‘你好,服務器!‘.encode(‘utf-8‘)#發送的數據必須是byte類型 sk.sendto(mes_to_server , ip_port) ret , addr = sk.recvfrom(1024) print(ret.decode(‘utf-8‘)) sk.close()
如果需要不停收發消息,代碼更改如下:
服務器端代碼:
import socket sk = socket.socket(type=socket.SOCK_DGRAM) sk.bind((‘127.0.0.1‘ , 8090)) print(‘等待客戶端發來消息……‘) while True: msg , addr = sk.recvfrom(1024) # 此處會阻塞 print(‘收到{}發來的信息,內容是:{}‘.format(addr , msg.decode(‘utf-8‘))) mes_to_server = input(‘>>>‘).encode(‘utf-8‘)#發送的數據必須是byte類型 sk.sendto(mes_to_server,addr) sk.close()
客戶端代碼:
import socket sk = socket.socket(type=socket.SOCK_DGRAM) ip_port = (‘127.0.0.1‘ , 8090) while True: mes_to_server = input(‘>>>‘).encode(‘utf-8‘)#發送的數據必須是byte類型 sk.sendto(mes_to_server , ip_port) ret , addr = sk.recvfrom(1024) print(ret.decode(‘utf-8‘)) sk.close()
使用socket進行UDP協議下通信時,可以多個客戶端與服務器通信,也就是說,上面客戶端代碼你可以另開一個進程與服務器通信,且不需要關閉當前客戶端,這是TCP協議與UDP協議下socket通信的一個不同之處。
5 總結
本篇初步總結了Python網絡編程中socket模塊的使用,事實上只是大致總結了基本函數的用法。其中諸多內容借鑒了一下兩篇博客,感謝兩位博主。
參考資料:
https://www.cnblogs.com/maociping/p/5112019.html
https://www.cnblogs.com/zhangyux/p/6109284.html
Python網絡編程之socket應用