python中TCP協議中的粘包問題
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'))
伺服器端:
1.在伺服器端先收到命令,開啟子程序,然後計算返回的資料的長度
2.先利用struct模組將資料長度轉成固定4個位元組傳給客戶端
3.再向客戶端傳送真實的資料。