您好,登錄后才能下訂單哦!
TCP協議中的粘包問題
1.粘包現象
基于TCP實現一個簡易遠程cmd功能
#服務端 import socket import subprocess sever = socket.socket() sever.bind(('127.0.0.1', 33521)) sever.listen() while True: client, address = sever.accept() while True: try: cmd = client.recv(1024).decode('utf-8') p1 = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr= subprocess.PIPE) data = p1.stdout.read() err_data = p1.stderr.read() client.send(data) client.send(err_data) except ConnectionResetError: print('connect broken') client.close() break sever.close() #客戶端 import socket client = socket.socket() client.connect(('127.0.0.1', 33521)) while True: cmd = input('請輸入指令(Q\q退出)>>:').strip().lower() if cmd == 'q': break client.send(cmd.encode('utf-8')) data = client.recv(1024) print(data.decode('gbk')) client.close()
上述是基于TCP協議的遠程cmd簡單功能,在運行時會發生粘包。
2、什么是粘包?
只有TCP會發生粘包現象,UDP協議永遠不會發生粘包;
TCP:(transport control protocol,傳輸控制協議)流式協議。在socket中TCP協議是按照字節數進行數據的收發,數據的發送方發出的數據往往接收方不知道數據到底長度是多長,而TCP協議由于本身為了提高傳輸的效率,發送方往往需要收集到足夠的數據才會進行發送。使用了優化方法(Nagle算法),將多次間隔較小且數據量小的數據,合并成一個大的數據塊,然后進行封包。這樣,接收端,就難于分辨出來了,必須提供科學的拆包機制。 即面向流的通信是無消息保護邊界的。
UDP:(user datagram protocol,用戶數據報協議)數據報協議。在socket中udp協議收發數據是以數據報為單位,服務端和客戶端收發數據是以一個單位,所以不會使用塊的合并優化算法,, 由于UDP支持的是一對多的模式,所以接收端的skbuff(套接字緩沖區)采用了鏈式結構來記錄每一個到達的UDP包,在每個UDP包中就有了消息頭(消息來源地址,端口等信息),這樣,對于接收端來說,就容易進行區分處理了。 即面向消息的通信是有消息保護邊界的。
TCP協議不會丟失數據,UDP協議會丟失數據。
udp的recvfrom是阻塞的,一個recvfrom(x)必須對唯一一個sendinto(y),收完了x個字節的數據就算完成,若是y>x數據就丟失,這意味著udp根本不會粘包,但是會丟數據,不可靠。
tcp的協議數據不會丟,沒有收完包,下次接收,會繼續上次繼續接收,己端總是在收到ack時才會清除緩沖區內容。數據是可靠的,但是會粘包。
3、什么情況下會發生粘包?
1.由于TCP協議的優化算法,當單個數據包較小的時候,會等到緩沖區滿才會發生數據包前后數據疊加在一起的情況。然后取的時候就分不清了到底是哪段數據,這是第一種粘包。
2.當發送的單個數據包較大超過緩沖區時,收數據方一次就只能取一部分的數據,下次再收數據方再收數據將會延續上次為接收數據。這是第二種粘包。
粘包的本質問題就是接收方不知道發送數據方一次到底發送了多少數據,解決問題的方向也是從控制數據長度著手,也就是如何設置緩沖區的問題
4、如何解決粘包問題?
解決問題思路:上述已經明確粘包的產生是因為接收數據時不知道數據的具體長度。所以我們應該先發送一段數據表明我們發送的數據長度,那么就不會產生數據沒有發送或者沒有收取完全的情況。
1.struct 模塊(結構體)
struct模塊的功能可以將python中的數據類型轉換成C語言中的結構體(bytes類型)
import struct s = 123456789 res = struct.pack('i', s) print(res) res2 = struct.unpack('i', res) print(res2) print(res2[0])
2.粘包的解決方案基本版
既然我們拿到了一個可以固定長度的辦法,那么應用struct模塊,可以固定長度了。
為字節流加上自定義固定長度報頭,報頭中包含字節流長度,然后一次send到對端,對端在接收時,先從緩存中取出定長的報頭,然后再取真實數據
#服務器端 import socket import subprocess import struct sever = socket.socket() sever.bind(('127.0.0.1', 33520)) sever.listen() while True: client, address = sever.accept() while True: try: cmd = client.recv(1024).decode('utf-8') #利用子進程模塊啟動程序 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) #管道輸出的信息有正確和錯誤的 data = p.stdout.read() err_data = p.stderr.read() #先將數據的長度發送給客戶端 length = len(data)+len(err_data) #利用struct模塊將數據的長度信息轉化成固定的字節 len_data = struct.pack('i', length) #以下將信息傳輸給客戶端 #1.數據的長度 client.send(len_data) #2.正確的數據 client.send(data) #2.錯誤管道的數據 client.send(err_data) except Exception as e: client.close() print('連接中斷。。。。') break #客戶端 import socket import struct client = socket.socket() client.connect(('127.0.0.1', 33520)) while True: cmd = input('請輸入指令>>:').strip().encode('utf-8') client.send(cmd) #1.先接收傳過來數據的長度是多少,我們通過struct模塊固定了字節長度為4 length = client.recv(4) #將struct的字節再轉回去整型數字 len_data = struct.unpack('i', length) print(len_data) len_data = len_data[0] print('數據長度為%s:' % len_data) all_data = b'' recv_size = 0 #2.接收真實的數據 #循環接收直到接收到數據的長度等于數據的真實長度(總長度) while recv_size < len_data: data = client.recv(1024) recv_size += len(data) all_data += data print('接收長度%s' % recv_size) print(all_data.decode('gbk'))
#總結:
服務器端:
客戶端(兩次接收):
很顯然,如果僅僅只是這樣肯定無法滿足在實際生產中一些需求。那么該怎么修改?
我們可以把報頭做成字典,字典里包含將要發送的真實數據的詳細信息,然后json序列化,然后用struck將序列化后的數據長度打包成4個字節(4個字節足夠用了)
我們可以將自定義的報頭設置成這種這種格式。
發送時:
1先發報頭長度
2再編碼報頭內容然后發送
3最后發真實內容
接收時:
1先收報頭長度,用struct取出來
2根據取出的長度收取報頭內容,然后解碼,反序列化
3從反序列化的結果中取出待取數據的詳細信息,然后去取真實的數據內容
#服務器端 import socket import subprocess import datetime import json import struct sever = socket.socket() sever.bind(('127.0.0.1', 33520)) sever.listen() while True: client, address = sever.accept() while True: try: cmd = client.recv(1024).decode('utf-8') #啟動子進程 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) #得到子進程運行的數據 data = p.stdout.read() #子進程運行正確的輸出管道數據,數據讀出來后是字節 err_data = p.stderr.read() #子進程運行錯誤的輸出管道數據 #計算數據的總長度 length = len(data) + len(err_data) print('數據總長度:%s' % length) #先需要發送報頭信息,以下為創建報頭信息(至第一次發送) #需要添加時間信息 time_info = datetime.datetime.now() #設置一個字典將一些額外的信息和長度信息放進去然后json序列化,報頭字典 masthead = {} #將時間數據放入報頭字典中 masthead['time'] = str(time_info) #時間格式不能被json序列化,所以將其轉化為字符串形式 masthead['length'] = length #將報頭字典json序列化 json_masthead = json.dumps(masthead) #得到json格式的報頭 # 將json格式的報頭編碼成字節形式 masthead_data = json_masthead.encode('utf-8') #利用struct將報頭編碼的字節的長度轉成固定的字節(4個字節) masthead_length = struct.pack('i', len(masthead_data)) #1.發送報頭的長度(第一次發送) client.send(masthead_length) #2.發送報頭信息(第二次發送) client.send(masthead_data) #3.發送真實數據(第三次發送) client.send(data) client.send(err_data) except ConnectionResetError: print('客戶端斷開連接。。。') client.close() break #客戶端 import socket import struct import json client = socket.socket() client.connect(('127.0.0.1', 33520)) while True: cmd = input('請輸入cmd指令(Q\q退出)>>:').strip() if cmd == 'q': break #發送CMD指令至服務器 client.send(cmd.encode('utf-8')) #1.第一次接收,接收報頭信息的長度,由于struct模塊固定長度為4字節,括號內直接填4 len_masthead = client.recv(4) #利用struct反解報頭長度,由于是元組形式,取值得到整型數字masthead_length masthead_length = struct.unpack('i', len_masthead)[0] #2.第二次接收,接收報頭信息,接收長度為報頭長度masthead_length 被編碼成字節形式的json格式的字典, # 解字符編碼得到json格式的字典masthead_data masthead_data = client.recv(masthead_length).decode('utf-8') #得到報頭字典masthead masthead = json.loads(masthead_data) print('執行時間%s' % masthead['time']) #通過報頭字典得到數據長度 data_length = masthead['length'] #3.第三次接收,接收真實數據,真實數據長度為data_length # data = client.recv(data_length) #有可能真實數據長度太大會撐爆內存。 #所以循環讀取數據 all_data = b'' length = 0 #循環直到長度大于等于數據長度 while length < data_length: data = client.recv(1024) length += len(data) all_data += data print('數據的總長度:%s' % data_length) #我的電腦是Windows系統,所以用gbk解碼系統發出的信息 print(all_data.decode('gbk'))
總結:
1.TCP協議中,會產生粘包現象。粘包現象產生本質就是讀取數據長度未知。
2.解決粘包現象本質就是處理讀取數據長度。
3.報頭的作用就是解決數據傳輸過程中數據長度怎么計算傳達和傳輸其他額外信息的。
以上所述是小編給大家介紹的python中TCP協議中的粘包問題詳解整合,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復大家的。在此也非常感謝大家對億速云網站的支持!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。