python--socket粘包
socket粘包
1 什麽是粘包
須知:只有TCP有粘包現象,UDP永遠不會粘包,首先需要掌握一個socket收發消息的原理,
所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的。
發送端可以是一K一K地發送數據,而接收端的應用程序可以兩K兩K地提走數據,當然也有可能一次提走3K或6K數據,或者一次只提走幾個字節的數據,也就是說,應用程序所看到的數據是一個整體,或說是一個流(stream),一條消息有多少字節對應用程序是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因。
TCP協議層會把構成整條消息的數據段排序完成後才呈現在內核緩沖區。
例如基於tcp的套接字客戶端往服務端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看了,根本不知道該文件的字節流從何處開始,在何處結束。
此外,發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多的數據後才發送一個TCP段。若連續幾次需要send的數據都很少,通常TCP會根據優化算法把這些數據合成一個TCP段後一次發送出去,這樣接收方就收到了粘包數據。
1.TCP(傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務,收發兩端(客戶端和服務端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然後進行封包。這樣,接收端,就難於分辨出來,必須提供科學的拆包機制,即面向流的通信是無消息保護邊界的。
2.tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,放置程序卡主。
Tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容,數據是可靠的,但是會粘包。
兩種情況下會發生粘包:
發送端需要等緩沖區滿才發送出去,造成粘包(發送數據時間間隔很短,數據量很小,會合到一起,產生粘包)
#服務端 import socket,subprocess s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.bind(("127.0.0.1",8000)) s.listen(5) conn,addr=s.accept() data1=conn.recv(1024) data2=conn.recv(1024) print("第一個包",data1) print("第二個包",data2) conn.close() s.close() 執行結果 第一個包 b‘helloworldSB‘ 第二個包 b‘‘ #客戶端 import socket,subprocess s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("127.0.0.1",8000)) s.send("helloworld".encode("utf-8")) s.send("SB".encode("utf-8")) s.close()
解決粘包問題1:low..low方法 在客戶端加個時間延遲,暫且可以解決問題。
#服務端 import socket,subprocess s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.bind(("127.0.0.1",8000)) s.listen(5) conn,addr=s.accept() data1=conn.recv(1024) data2=conn.recv(1024) print("第一個包",data1) print("第二個包",data2) conn.close() s.close() 執行結果 第一個包 b‘helloworld‘ 第二個包 b‘SB‘ #客戶端 import socket,subprocess,time s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("127.0.0.1",8000)) s.send("helloworld".encode("utf-8")) time.sleep(3) s.send("SB".encode("utf-8")) s.close()
接收方不及時接收緩沖區的包,造成多個包接收(客戶端發送了一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據,產生粘包)
#服務端 import socket,subprocess,time s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.bind(("127.0.0.1",8000)) s.listen(5) conn,addr=s.accept() data1=conn.recv(1) #第一次收了個"h" # time.sleep(5) data2=conn.recv(1024) #第二次收了"elloworld" print("第一個包",data1) print("第二個包",data2) conn.close() s.close() #客戶端 import socket,subprocess,time s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("127.0.0.1",8000)) s.send("helloworld".encode("utf-8")) time.sleep(3) s.send("SB".encode("utf-8")) s.close()
解決粘包問題2:比方法1要減少一個low
問題的根源在於,接收端不知道發送端將要傳送的字節流的長度,所以解決粘包的方法就是圍繞,如何讓發送端在發送數據前,把自己將要發送的字節流總大小讓接收端知曉,然後接收端來一個死循環接收完所有數據
low版本的解決方法
模擬以太網協議封裝報頭:
報頭 特點:固定長度
包含對將要發送數據的描述信息
#服務端 import socket,subprocess,struct s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.bind(("127.0.0.1",8000)) s.listen(5) while True: conn,addr=s.accept() while True: try: msg=conn.recv(1024) res=subprocess.Popen(msg.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out_res=res.stdout.read() err_res=res.stderr.read() data_size=(len(out_res)+len(err_res)) #發送報頭 conn.send(struct.pack("i",data_size)) #發送真實數據部分 conn.send(out_res) conn.send(err_res) except Exception: break conn.close() s.close() #客戶端 #粘包 自己封裝報頭 import socket,struct s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("127.0.0.1",8000)) while True: msg=input("請輸入命令:").strip() if not msg:continue s.send(bytes(msg,encoding="utf-8")) #收報頭 baotou=s.recv(4) data_size=struct.unpack("i",baotou)[0] #收收據 recv_size=0 recv_data=b"" while recv_size <data_size: data=s.recv(1024) recv_size+=len(data) recv_data+=data print(recv_data.decode("gbk")) s.close()
為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然後一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然後再取真實數據
3.struct模塊
該模塊可以把一個類型,如數字,轉成固定長度的bytes
>>> res=struct.pack(‘i‘,1111111111111) #打包成固定長度的bytes
>>> struct.unpack(“I”,res) #解包
2 大神解決粘包的方法
我們可以把報頭做成字典,字典裏包含將要發送的真實數據的詳細信息,然後json序列化,然後用struck將序列化後的數據長度打包成4個字節(4個自己足夠用了)
發送時:
先發報頭長度
再編碼報頭內容然後發送
最後發真實內容
接收時:
先手報頭長度,用struct取出來
根據取出的長度收取報頭內容,然後解碼,反序列化
從反序列化的結果中取出待取數據的詳細信息,然後去取真實的數據內容
#服務端 import socket,subprocess,struct,json s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.bind(("127.0.0.1",8000)) s.listen(5) while True: conn,addr=s.accept() while True: try: msg=conn.recv(1024) res=subprocess.Popen(msg.decode("utf-8"), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) out_res=res.stdout.read() err_res=res.stderr.read() data_size=len(out_res)+len(err_res) head_dic={"data_size":data_size} head_json=json.dumps(head_dic) head_bytes=head_json.encode("utf-8")#報頭 #part1:先發報頭的長度 head_len=len(head_bytes) conn.send(struct.pack("i",head_len)) #part2:再發送報頭 conn.send(head_bytes) #part3:最後發送數據真實部分 conn.send(err_res) conn.send(out_res) except Exception: break conn.close() s.close() #客戶端 import socket,struct,json s=socket.socket(socket.AF_INET,socket.SOCK_STREAM) s.connect(("127.0.0.1",8000)) while True: msg=input("請輸入命令:").strip() if not msg:continue s.send(bytes(msg,encoding="utf-8")) #part1:先收報頭的長度 head_struct=s.recv(4) head_len=struct.unpack("i",head_struct)[0] #part2:再收報頭 head_bytes=s.recv(head_len) head_json=head_bytes.decode("utf-8") head_dic=json.loads(head_json) data_size=head_dic["data_size"] #part3:收數據 recv_size=0 recv_data=b"" while recv_size < data_size: data=s.recv(1024) recv_size+=len(data) recv_data+=data print(recv_data.decode("gbk")) s.close()
python--socket粘包