socket是介於應用層和網絡各個協議族通訊之間的抽象層。socket將底層複雜的網絡協議和與目標設備通訊的操做封裝爲一系列接口。實現應用層脫離網咯協議層,使用戶直接面向socket編程。socket的類型有流式的socket、數據報的socket和原始的socket。流式的socket是基於TCP協議的,它被普遍應用於大型的、須要安全性保障數據的傳輸。數據報的socket是基於UDP協議的,它是一個不可靠的、無鏈接協議,常常被應用於不須要TCP的排序的、以速度換取安全和準確性的,以及不須要流量控制功能的應用程序,好比傳輸語音和影像報文等。程序員
發送端每次向接收端發送的數據都會存儲在接收端的緩衝區,若是接收端對緩衝區的數據的讀取不恰當就會致使黏包現象,所謂黏包現象的意思是說,緩衝區的數據沒有被一次性讀完,致使本次數據的讀取缺失和下一次讀取時會附上上次未讀取完的數據。對於TCP協議而言,接收端對接收的數據量是不可見的,不知道一條信息有多少字節。當接收端所能容納的數據量很大時,就能一次性讀取緩衝區所有的數據,反之就產生黏包現象。那如何避免黏包現象呢?咱們知道IP協議的數據報由報頭和數據兩部分組成,報頭封裝了消息發送端的一系列信息;而UDP協議也像IP協議相似封裝了消息頭,有了消息頭等信息,接收端就能採起一系列措施來避免黏包現象。因此TCP協議要想避免黏包現象,用戶程序員能夠模仿IP協議和UDP協議,爲數據封裝一個消息頭。不妨,咱們作一個簡單的、僅僅包含數據大小的消息頭,而後在接收端獲取消息頭,經過消息頭所包含的數據大小再向內存中讀取數據,這樣便解決了黏包現象。shell
(1)配置socket
(2)綁定服務端自己設備IP和端口號
(3)設置連接數
(4)創建鏈接
(5)接收消息
(6)具體業務邏輯
(7)發送消息
(8)關閉全部鏈接編程
在創建鏈接和收發消息時須要使用死循環,在收發消息時須要使用異常處理機制來保證當客戶端非法斷開鏈接時服務端不受影響,任然能繼續運做。服務端使用accept函數創建鏈接,它返回一個元組,元組的第一個元素爲客戶端的鏈接,第二個元素爲客戶端的地址。基於TCP協議的socket,接收消息使用的是recv函數,其參數爲一次性接收數據的大小;發送消息使用的是send函數,其參數爲所要發送的以字節形式的數據。緩存
from socket import * address_family = AF_INET # 協議族 socket_type = SOCK_STREAM # socket類型 request_queue_size = 5 # 連接數 buffer_size = 1024 # 一次接收消息的容量 ip_And_port = ("127.0.0.1", 8080) # IP和端口號 tcp_server = socket(address_family,socket_type) # 配置socket tcp_server.bind(ip_And_port) # 綁定IP和端口號 tcp_server.listen(request_queue_size) # 設置連接數 while True: #連接循環 print("開始接收新的客戶端連接") conn, addr = tcp_server.accept() # 創建鏈接 print("鏈接conn爲:", conn) print("客戶端地址:", addr) while True: #信息循環 try: data = conn.recv(buffer_size) # 接受數據 print("客戶端發來的是:",data.decode("utf-8")) string = "回你一句,省得尷尬" conn.send(string.encode()) # 發送數據 except Exception: break #關閉流 conn.close() tcp_server.close()
(1)配置socket
(2)創建與目標的IP和端口號的鏈接
(3)發送消息
(4)具體業務邏輯
(5)接收消息
(6)關閉全部鏈接安全
客戶端,相對於服務端而言,它把綁定目標設備IP和端口號與創建鏈接合爲一個操做。使用connect函數鏈接到服務端,參數爲服務端的設備IP和端口號。通常而言,客戶端會根據本身的需求主動向服務端發起鏈接,可是爲了安全起見和服務器性能的考慮,現實中都是服務器先斷開鏈接。服務器
from socket import * address_family = AF_INET # 協議族 socket_type = SOCK_STREAM # socket類型 buffer_size = 1024 # 一次接收消息的容量 ip_And_port = ("127.0.0.1", 8080) # IP和端口號 tcp_client=socket(address_family,socket_type) # 實例化socket tcp_client.connect(ip_And_port) # 創建鏈接 while True: # 客戶端運行 msg=input('>>: ').strip() if not msg:continue #用戶輸入不爲空時繼續 tcp_client.send(msg.encode('utf-8')) #發送消息 print('客戶端已經發送消息') data=tcp_client.recv(buffer_size) #接收消息 print('收到服務端發來的消息:',data.decode('utf-8')) tcp_client.close() # 關閉鏈接
在上面簡單粗糙的代碼中存在着不少問題。咱們先看看服務端的問題,服務端在接收消息時若是消息爲空(客戶端直接按了回車鍵),換句話說,服務端的緩存中沒有任何東西,而此時客戶端任然在等待服務端的響應,這是萬萬不該該的。因此在客戶端和服務端中都應要有對這些低級錯誤進行過慮的功能。在這兩個程序中其實還有一個最重要的bug沒有解決,那就是黏包問題。經過前面的講解咱們知道須要在發送端的消息中封裝一個消息頭而且提供數據的大小,當發送端發送數據時,一條數據可分爲兩步發送,第一次發送的是數據的大小,第二次發送的纔是數據;在接收端中,接收數據時可先接收發送端發送的第一個數據,咱們知道接收端的緩衝區的數據是黏在一塊的,爲了保證接收端接收的第一個數據必定是數據的大小,客戶端和服務端應該共同約定第一個數據值大小的位數。咱們可使用struct模塊下的pack函數和unpack函數將數據的大小以某種形式進行轉化(通常都轉化爲int), pack函數是對數據的封裝,而unpack函數是對數據的解封。下面將經過一個遠程命令的程序對以上存在的較多問題進行簡單的修改。網絡
from socket import * import subprocess import struct address_family = AF_INET socket_type = SOCK_STREAM request_queue_size = 5 buffer_size = 1024 ip_And_port = ("127.0.0.1", 8080) tcp_server=socket(address_family,socket_type) tcp_server.bind(ip_And_port) tcp_server.listen(request_queue_size) while True: conn,addr=tcp_server.accept() while True: try: cmd=conn.recv(buffer_size) if not cmd:break print('收到客戶端的命令',cmd) #執行命令,獲得命令的運行結果cmd_res res=subprocess.Popen(cmd.decode('utf-8'),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) err=res.stderr.read() if err: cmd_res=err else: cmd_res=res.stdout.read() if not cmd_res: cmd_res='執行成功'.encode('gbk') length=len(cmd_res) #將length以int的形式封裝在struct中 data_length=struct.pack('i',length) conn.send(data_length) conn.send(cmd_res) print("信息發送完畢") except Exception as e: print(e) break from socket import * import subprocess import struct address_family = AF_INET socket_type = SOCK_STREAM request_queue_size = 5 buffer_size = 1024 ip_And_port = ("127.0.0.1", 8080) tcp_server=socket(address_family,socket_type) tcp_server.bind(ip_And_port) tcp_server.listen(request_queue_size) while True: conn,addr=tcp_server.accept() while True: try: cmd=conn.recv(buffer_size) if not cmd:break print('收到客戶端的命令',cmd) #執行命令,獲得命令的運行結果cmd_res res=subprocess.Popen(cmd.decode('utf-8'),shell=True, stderr=subprocess.PIPE, stdout=subprocess.PIPE, stdin=subprocess.PIPE) err=res.stderr.read() if err: cmd_res=err else: cmd_res=res.stdout.read() if not cmd_res: cmd_res='執行成功'.encode('gbk') length=len(cmd_res) #將length以int的形式封裝在struct中 data_length=struct.pack('i',length) conn.send(data_length) conn.send(cmd_res) print("信息發送完畢") except Exception as e: print(e) break
from socket import * import struct from functools import partial address_family = AF_INET socket_type = SOCK_STREAM # socket類型 buffer_size = 1024 # 一次接收消息的容量 ip_And_port = ("127.0.0.1", 8080) # IP和端口號 tcp_client=socket(address_family,socket_type) tcp_client.connect(ip_And_port) while True: cmd=input('>>: ').strip() if not cmd:continue if cmd == 'quit':break tcp_client.send(cmd.encode('utf-8')) length_data=tcp_client.recv(4) length=struct.unpack('i',length_data)[0] #取出緩衝區接收的全部數據,並將其存放在迭代器中 myiter=iter(partial(tcp_client.recv, buffer_size), b'') for i in myiter: print(i.decode("gbk")) tcp_client.close()
(1)配置socket
(2)綁定IP和端口號
(3)接收數據
(4)發送數據包(須要指明目標IP和端口號)
(5)關閉全部流多線程
與TCP協議不一樣的是,UDP協議不會發生黏包現象。雖然不會發生黏包現象,但它是無鏈接的、不安全的協議。好比當客戶端發送消息給服務端時,客戶端並不知道它所發送的消息是否達到服務端。並且它和TCP協議有點類似的地方在於接收端在接收數據時也是不知道數據的大小。併發
from socket import * address_family = AF_INET socket_type = SOCK_DGRAM ip_And_port = ("127.0.0.1", 8080) buffer_size=1024 udp_server=socket(address_family, socket_type) udp_server.bind(ip_And_port) while True: data, addr = udp_server.recvfrom(buffer_size) string = "Welcome to hear" udp_server.sendto(string.encode("gbk"), addr) print("信息發送成功")
(1)配置socket
(2)發送數據包(須要指明目標IP和端口號)
(3)接收數據
(4)關閉全部流socket
不須要創建鏈接,只是須要在使用sendto函數時以元組的形式指明目標設備便可。
from socket import * address_family = AF_INET socket_type = SOCK_DGRAM ip_And_port = ("127.0.0.1", 8080) buffer_size=1024 udp_client=socket(address_family,socket_type) while True: msg=input('>>: ').strip() udp_client.sendto(msg.encode('utf-8'), ip_And_port) data, addr = udp_client.recvfrom(buffer_size) print(data.decode('gbk'))
基於TCP的socket只能實現一對一服務。好比當一個客戶端與服務端在進行通訊時,另外一個客戶端此時只能處於等待狀態,直到服務端結束當前通訊開始下一輪通訊。若是想實現socket的併發編程,咱們可使用socketserver模塊。該模塊的實現原理是基於socket和線程的組合。若是對socketserver的實現原理感興趣,能夠參考socketserver模塊的源碼。socketserver模塊分爲Server類和Request類。Server類通常多用於處理鏈接,Request類多用於處理通訊。如下分別是官方文檔Server類和Request類某些具體類的繼承結構圖。
由以上的繼承結構圖可知,在使用併發編程定義一個新的類時須要繼承socketserver模塊下的BaseRequestHandler類。而繼承該類須要的事是覆蓋原有的handle函數,在該函數中實現數據的收發。其實你會你會發現咱們的代碼沒有多大變化,咱們僅僅只是將代碼放進自定義類的handle函數,然後使用自定義的類做爲參數傳進socketserver模塊下的ThreadingTCPServer類進行實例化而已。
import socketserver class MyServer(socketserver.BaseRequestHandler): """ 對於TCP協議來講,self.request是客戶端的請求連接 對於UDP協議來講,self.request是接收的消息 """ def handle(self): print('conn is: ',self.request) print('addr is: ',self.client_address) while True: try: #收消息 data=self.request.recv(1024) if not data:break print('收到客戶端的消息是',data,self.client_address) #發消息 self.request.sendall(data.upper()) except Exception as e: print(e) break #測試 if __name__ == '__main__': s=socketserver.ThreadingTCPServer(('127.0.0.1',8080),MyServer) #多線程 s.serve_forever() #運行
from socket import * ip_port=('127.0.0.1',8080) back_log=5 buffer_size=1024 tcp_client=socket(AF_INET,SOCK_STREAM) tcp_client.connect(ip_port) while True: msg=input('>>: ').strip() if not msg:continue if msg == 'quit':break tcp_client.send(msg.encode('utf-8')) data=tcp_client.recv(buffer_size) print('收到服務端發來的消息:',data.decode('utf-8')) tcp_client.close()