本篇主要對Python下網絡編程中用到的socket模塊進行初步總結。首先從網絡基礎理論出發,介紹了TCP協議和UDP協議;而後總結了socket中的經常使用函數;最後經過實際代碼展現基本函數的應用。html
要想理解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間的鏈接。數據結構
Socket(中文譯爲套接字)是操做系統內核中的一個數據結構,它幾乎是全部網絡通訊的基礎。網絡通訊,歸根到底仍是進程間的通訊(不一樣計算機上的進程間通訊, 又稱爲網絡通訊, IP協議進行的主要是端到端通訊)。在網絡中,每個節點(計算機或路由)都有一個網絡地址,也就是IP地址。兩個進程通訊時,首先要肯定各自所在的網絡節點的網絡地址。可是,網絡地址只能肯定進程所在的計算機,而一臺計算機上極可能同時運行着多個進程,因此僅憑網絡地址還不能肯定究竟是和網絡中的哪個進程進行通訊,所以套接字中還須要包括其餘的信息,也就是端口號(PORT)。在一臺計算機中,一個端口號一次只能分配給一個進程,也就是說,在一臺計算機中,端口號和進程之間是一 一對應的關係。socket
socket使用(IP地址,協議,端口號)來標識一個進程。因此,使用端口號和網絡地址的組合能夠惟一的肯定整個網絡中的一個網絡進程。端口號的範圍從0~65535,一類是由互聯網指派名字和號碼公司ICANN負責分配給一些經常使用的應用程序固定使用的「周知的端口」,其值通常爲0~1023, 用戶自定義端口號通常大於等於1024。函數
網絡上的兩個程序經過一個雙向的通訊鏈接實現數據的交換,這個鏈接的一端稱爲一個socket。每個socket都用一個半相關描述{協議、本地地址、本地端口}來表示;一個完整的套接字則用一個相關描述{協議、本地地址、本地端口、遠程地址、遠程端口}來表示。socket也有一個相似於打開文件的函數調用,該函數返回一個整型的socket描述符,隨後的鏈接創建、數據傳輸等操做都是經過socket描述符來實現的。以TCP協議中socket創建鏈接的「三次握手」爲例,其過程以下:post
如圖所示,當客戶端經過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返回,至此三次握手完畢,鏈接創建。spa
兩個socket經過「網絡」交互數據進行數據交互時只負責兩件事:創建鏈接,傳遞數據;同時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通訊的一個不一樣之處。
本篇初步總結了Python網絡編程中socket模塊的使用,事實上只是大體總結了基本函數的用法。其中諸多內容借鑑了一下兩篇博客,感謝兩位博主。
參考資料:
https://www.cnblogs.com/maociping/p/5112019.html
https://www.cnblogs.com/zhangyux/p/6109284.html