python/socket編程之粘包


python/socket編程之粘包

粘包

  只有TCP有粘包現象,UDP永遠不會粘包。

  首先需要掌握一個socket收發消息的原理

發送端可以是1k,1k的發送數據而接受端的應用程序可以2k,2k的提取數據,當然也有可能是3k或者多k提取數據,也就是說,應用程序是不可見的,因此TCP協議是面來那個流的協議,這也是容易出現粘包的原因而UDP是面向笑死的協議,每個UDP段都是一條消息,應用程序必須以消息為單位提取數據,不能一次提取任一字節的數據,這一點和TCP是很同的。怎樣定義消息呢?認為對方一次性write/send的數據為一個消息,需要命的是當對方send一條信息的時候,無論鼎城怎么樣分段分片,TCP協議層會把構成整條消息的數據段排序完成后才呈現在內核緩沖區。

例如基於TCP的套接字客戶端往服務器端上傳文件,發送時文件內容是按照一段一段的字節流發送的,在接收方看來更笨不知道文件的字節流從何初開始,在何處結束。

所謂粘包問題主要還是因為接收方不知道消息之間的界限,不知道一次性提取多少字節的數據所造成的

發送方引起的粘包是由TCP協議本身造成的,TCP為提高傳輸效率,發送方往往要收集到足夠多數據后才發上一個TCP段。如連續幾次下需要send的數據都很少,通常TCP會根據優化算法把 這些數據合成一個TCP段后 一次發送出去,這樣接收方就收到了粘包數據

TCP(transport control protocol,傳輸控制協議)是面向連接的,面向流的,提供高可靠性服務。收發兩端(客戶端和服務器端)都要有一一成對的socket,因此,發送端為了將多個發往接收端的包,更有效的發到對方,使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合並成一個大的數據塊,然后進行封包。這樣,接收端,就難於分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
UDP(user datagram protocol,用戶數據報協議)是無連接的,面向消息的,提供高效率服務。不會使用塊的合並優化算法,, 由於UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對於接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
tcp是基於數據流的,於是收發的消息不能為空,這就需要在客戶端和服務端都添加空消息的處理機制,防止程序卡住,而udp是基於數據報的,即便是你輸入的是空內容(直接回車),那也不是空消息,udp協議會幫你封裝上消息頭,實驗略
udp的recvfrom是阻塞的,一個recvfrom(x)必須對一個一個sendinto(y),收完了x個字節的數據就算完成,若是y>x數據就丟失,這意味着udp根本不會粘包,但是會丟數據,不可靠

tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。

  

兩種情況下會發生粘包:

1.發送端需要等本機的緩沖區滿了以后才發送出去,造成粘包(發送數據時間間隔很端,數據很小,會合在一個起,產生粘包)

2.接收端不及時接收緩沖區的包,造成多個包接受(客戶端發送一段數據,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的數據 ,就產生粘包)

粘包實例:

服務端
import socket
import subprocess
din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ip_port=('127.0.0.1',8080)
din.bind(ip_port)
din.listen(5)
conn,deer=din.accept()
data1=conn.recv(1024)
data2=conn.recv(1024)
print(data1)
print(data2)

 

客戶端
import socket
import subprocess
din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ip_port=('127.0.0.1',8080)
din.connect(ip_port)
din.send('helloworld'.encode('utf-8'))
din.send('sb'.encode('utf-8'))

 low比的解決粘包的方法:

在客戶端發送下邊添加一個時間睡眠,就可以避免粘包現象。在服務端接收的時候也要進行時間睡眠,才能有效的避免粘包情況

#客戶端
import socket
import time
import subprocess
din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ip_port=('127.0.0.1',8080)
din.connect(ip_port)
din.send('helloworld'.encode('utf-8'))
time.sleep(3)
din.send('sb'.encode('utf-8'))
#服務端
import socket
import time
import subprocess
din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
ip_port=('127.0.0.1',8080)
din.bind(ip_port)
din.listen(5)
conn,deer=din.accept()
data1=conn.recv(1024)
time.sleep(4)
data2=conn.recv(1024)
print(data1)
print(data2)

大神解決粘包的方法:

  為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然后依次send到對端,對端在接受時,先從緩存中取出定長的報頭,然后再取真是數據。

  使用struct模塊

  該模塊可以把一個類型,如數字,轉成固定長度的bytes字節

struct模塊 

  該模塊可以把一個類型,如數字,轉成固定長度的bytes

  >>> res=struct.pack('i',1111111111111)  #打包成固定長度的bytes

  >>> struct.unpack(“I”,res)             #解包

采用自定義報頭的形式:

1 #服務端 
2 import socket #導入模塊 
3 import struct #導入模塊 
4 import subprocess #導入模塊 
5 din=socket.socket(socket.AF_INET,socket.SOCK_STREAM) #基於網路家族以數據流的形式傳輸 
6 ip=('127.0.0.1',8080) #標識服務端唯一的地址 
7 din.bind(ip) #綁定地址 
8 din.listen(5) #設置鏈接緩沖池最大數量 
9 while True: 
10 conn,addr=din.accept() #等待鏈接進入 
11 print('------>',addr) 
12 while True: 
13 try: #開始捕捉異常,在windows中使用 
14 cmd=conn.recv(1024) #把接收的內容賦值給變量cmd 
15 res=subprocess.Popen(cmd.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE) #設置一個管道,以shell的方式寫入,判斷如果是正確的命令就放到標准輸出的管道中,否則就放到錯誤輸出的管道中 
16 dui=res.stdout.read() #讀取標准輸出管道中的內容賦值給dui 
17 cuo=res.stderr.read() #讀取錯誤輸出管道中的內容賦值給cuo 
18 data_bat=len(dui)+len(cuo) #把dui和cuo的內容長度相加賦值給data_bat 
19 conn.send(struct.pack('i',data_bat)) #通過模塊模仿報頭形式發送給客戶端 
20 conn.send(dui) #把dui的發送給客戶端 
21 conn.send(cuo) #把cuo的發送給客戶端 
22 except Exception: #結束捕捉異常
23 break #跳出 24 conn.close() #斷開鏈接 25 din.close() #關閉基於網絡家族傳輸的鏈接
 1 #客戶端
 2 import socket
 3 import struct
 4 din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
 5 ip_port=('127.0.0.1',8080)
 6 din.connect(ip_port)
 7 while True:
 8     cmd=input('請輸入命令:').strip()
 9     if not cmd:continue
10     din.send(bytes(cmd,encoding='utf-8'))
11     baotou=din.recv(4)
12     data_size=struct.unpack('i',baotou)[0]
13     rec_size=0
14     rec_data=b''
15     while rec_size < data_size:
16         data=din.recv(1024)
17         rec_size+=len(data)
18         rec_data+=data
19 
20     print(rec_data.decode('gbk'))
21 
22 din.close()

牛逼報頭的形式:

 1 #服務端
 2 import struct   #導入模塊
 3 import socket   #導入模塊
 4 import json     #導入模塊
 5 import subprocess   #導入模塊
 6 din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)   #基於網絡家族以流的方式創建一個鏈接路
 7 ip=('127.0.0.1',8080)  #設定服務端唯一的地址
 8 din.bind(ip)  #綁定地址
 9 din.listen(5)  #激活啟動,設置鏈接池最大緩數量
10 while True:
11     conn,addr=din.accept()   #等待接受數據鏈接
12     print('------->>>')
13     while True:
14         try:   #開始捕捉異常,在windows中使用
15             dr=conn.recv(1024)   #接收內容並賦值給dr
16             res=subprocess.Popen(dr.decode('utf-8'),shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE)   #設置一個管道,把對的內容一個對的管道中,把錯誤的放進一個錯誤的管道中。
17             dui=res.stdout.read()  #讀取標准輸出的內容並賦值給dui
18             cuo=res.stderr.read()  #讀取錯誤輸出的內容並賦值給cuo
19             data_lik=len(dui)+len(cuo)  #計算標准輸出和錯誤輸出的總長度並賦值給data_lik
20             head_dic={'data_lik':data_lik}  #自己頂一個字典 並賦值給head_dic
21             head_json=json.dumps(head_dic)  #把自己定義的字典進行一個序列化
22             head_bytes=head_json.encode('utf-8')  #把序列化后的內容進行轉碼 ,因為網絡傳輸都是通過字節進行傳輸
23             head_len=len(head_bytes)  #把轉換成字節碼的長度拿到 並賦值給head_len
24             #發送報頭長度
25             conn.send(struct.pack('i',head_len))  #發送自己定義的報頭長度
26             # 發送報頭
27             conn.send(head_bytes)       #發送自己定義的報頭
28             #發送真是數據  
29             conn.send(dui)              #發送真實的數據
30             conn.send(cuo)              #發送真實的數據
31         except Exception:               #結束捕捉異常
32             break                       #跳出
33     conn.close()                        #斷開鏈接
34 din.close()                             #關閉連接 
 1 #客戶端
 2 import struct    #導入模塊
 3 import socket    #導入模塊
 4 import json      #導入模塊
 5 din=socket.socket(socket.AF_INET,socket.SOCK_STREAM)   #基於網絡家族和流的方式傳輸 創建連接
 6 ip=('127.0.0.1',8080)     #設置唯一的地址
 7 din.connect(ip)           #綁定地址
 8 while True:  
 9     run=input('請輸入命令:')      #用戶交互界面
10     if not run:continue           #判斷如果run為空就跳出
11     din.send(bytes(run,encoding='utf-8'))   #通過字節形式發送用戶輸入的的內容
12     data=din.recv(4)          #接收內容長度為4,並賦值給data
13     head_len=struct.unpack('i',data)[0]  #解報頭並索引出值賦值給head_len
14     head_bytes=din.recv(head_len)      #接收長度為head_len變量中的值,賦值給head_bytes
15     head_json=head_bytes.decode('utf-8')   #把剛剛接收的內容進行轉換,轉換成utf-8
16     head_dic=json.loads(head_json)       #在把轉換成utf-8的內容 反序列化成字典的形式
17     data_lik=head_dic['data_lik']        #在把字典的查找的值賦值給data_lik
18     recv_size=0                         #設定變量並賦值為0
19     recv_data=b''                      #設定變量並創建一個空字節的
20     while recv_size < data_lik:       #如果變量小於字典查找出來的值時,循環為真。
21         data=din.recv(1024)          #接收內容賦值給data
22         recv_size+=len(data)        #把得到的內容長度加到變量recv_size中,這樣就能實現如果沒有取完內容就一直取得效果
23         recv_data+=data           #把拿到的值加到新創建的字節中
24         print(recv_data.decode('gbk'))   #打印以GBK轉碼的內容
25 din.close()   #關閉連接

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM