一、遠程執行命令程式解決粘包問題
1、tcp協議的流式問題
應用程式所看到的資料是一個整體,或說是一個流(stream),一條訊息有多少位元組對應用程式是不可見的,因此TCP協議是面向流的協議,這也是容易出現粘包問題的原因,
2、粘包現象
只有TCP有粘包現象,UDP永遠不會粘包

這里快取的作用:C端 S端都會把自己的資料存到快取,快取的打開順序如上圖所示的水桶放的東西一樣一層一層打開,所以發和收資料都是在跟自己的快取打交道,可以發空,但不能收空,
所謂粘包問題主要還是因為接收方不知道訊息之間的界限,不知道一次性提取多少位元組的資料所造成的
而UDP是面向訊息的協議,每個UDP段都是一條訊息,應用程式必須以訊息為單位提取資料,不能一次提取任意位元組的資料
- 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時才會清除緩沖區內容,資料是可靠的,但是會粘包,
4、兩種情況下會發生粘包,
(1)發送端需要等緩沖區滿才發送出去,造成粘包(發送資料時間間隔很短,資料了很小,會合到一起,產生粘包)
(2)接收方不及時接識訓沖區的包,造成多個包接收(客戶端發送了一段資料,服務端只收了一小部分,服務端下次再收的時候還是從緩沖區拿上次遺留的資料,產生粘包)
5、拆包的發生情況
當發送端緩沖區的長度大于網卡的MTU時,tcp會將這次發送的資料拆成幾個資料包發送出去,
6、補充問題一:為何tcp是可靠傳輸,udp是不可靠傳輸
tcp在資料傳輸時,發送端先把資料發送到自己的快取中,然后協議控制將快取中的資料發往對端,對端回傳一個ack=1,發送端則清理快取中的資料,對端回傳ack=0,則重新發送資料,所以tcp是可靠的
而udp發送資料,對端是不會回傳確認資訊的,因此不可靠
7、補充問題二:send(位元組流)和recv(1024)及sendall
recv里指定的1024意思是從快取里一次拿出1024個位元組的資料
send的位元組流是先放入己端快取,然后由協議控制將快取內容發往對端,如果待發送的位元組流大小大于快取剩余空間,那么資料丟失,用sendall就會回圈呼叫send,資料不會丟失
8、正確解決粘包問題:
(方法:自定義應用層協議)
為位元組流加上自定義固定長度報頭,報頭中包含位元組流長度,然后一次send到對端,對端在接收時,先從快取中取出定長的報頭,然后再取真實資料
固定報頭長度要用到struct模塊
import struct #該模塊可以把一個型別,如數字,轉成固定長度的bytes struct.pack('i',int型別) #也可以還原bytes struct.unpack('i',int型別)

import json,struct #假設通過客戶端上傳1T:1073741824000的檔案a.txt #為避免粘包,必須自定制報頭 header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T資料,檔案路徑和md5值 #為了該報頭能傳送,需要序列化并且轉為bytes head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并轉成bytes,用于傳輸 #為了讓客戶端知道報頭的長度,用struck將報頭長度這個數字轉成固定長度:4個位元組 head_len_bytes=struct.pack('i',len(head_bytes)) #這4個位元組里只包含了一個數字,該數字是報頭的長度 #客戶端開始發送 conn.send(head_len_bytes) #先發報頭的長度,4個bytes conn.send(head_bytes) #再發報頭的位元組格式 conn.sendall(檔案內容) #然后發真實內容的位元組格式 #服務端開始接收 head_len_bytes=s.recv(4) #先收報頭4個bytes,得到報頭長度的位元組格式 x=struct.unpack('i',head_len_bytes)[0] #提取報頭的長度 head_bytes=s.recv(x) #按照報頭長度x,收取報頭的bytes格式 header=json.loads(json.dumps(header)) #提取報頭 #最后根據報頭的內容提取真實的資料,比如 real_data_len=s.recv(header['file_size']) s.recv(real_data_len)
#server import subprocess import struct from socket import * server = socket(AF_INET, SOCK_STREAM) # print(server) server.bind(('127.0.0.1', 8082)) server.listen(5) while True: conn, client_addr = server.accept() print(conn) print(client_addr) while True: try: cmd = conn.recv(1024) obj = subprocess.Popen(cmd.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) stdout = obj.stdout.read() stderr = obj.stdout.read() total_size = len(stdout) + len(stderr) # 先發送資料的長度 conn.send(struct.pack('i',total_size)) # 發送真正的資料 conn.send(stdout) conn.send(stderr) except Exception: break conn.close() server.close() #client import struct from socket import * client = socket(AF_INET, SOCK_STREAM) # print(client) client.connect(('127.0.0.1', 8082)) while True: cmd = input(">>: ").strip() if len(cmd) == 0: continue client.send(cmd.encode('utf-8')) # 先收資料的長度 n = 0 header = b'' while n < 4: data = client.recv(1) header += data n += len(data) total_size = struct.unpack('i', header)[0] # 收真正的資料 recv_size = 0 res = b'' while recv_size < total_size: data = client.recv(1024) res += data recv_size += len(data) print(res.decode('gbk')) client.close()解決粘包問題
二、定制復雜的報頭
我們可以把報頭做成字典,字典里包含將要發送的真實資料的詳細資訊,然后json序列化,然后用struck將序列化后的資料長度打包成4個位元組(4個自己足夠用了)
發送時:
先發報頭長度
再編碼報頭內容然后發送
最后發真實內容
接收時:
先手報頭長度,用struct取出來
根據取出的長度收取報頭內容,然后解碼,反序列化
從反序列化的結果中取出待取資料的詳細資訊,然后去取真實的資料內容
#server import subprocess import os import struct from socket import * server = socket(AF_INET, SOCK_STREAM) # print(server) server.bind(('127.0.0.1', 8082)) server.listen(5) while True: conn, client_addr = server.accept() print(conn) print(client_addr) while True: try: msg = conn.recv(1024).decode('utf-8') cmd,file_path=msg.split() if cmd == "get": # 先發送報頭 total_size=os.path.getsize(file_path) conn.send(struct.pack('q',total_size)) # 再發送檔案 with open(r'%s' %file_path,mode='rb') as f: for line in f: conn.send(line) except Exception: break conn.close() server.close() #client import struct from socket import * client = socket(AF_INET, SOCK_STREAM) # print(client) client.connect(('127.0.0.1', 8082)) while True: cmd = input(">>: ").strip() # get 檔案路徑 if len(cmd) == 0: continue client.send(cmd.encode('utf-8')) # 先收資料的長度 n = 0 header = b'' while n < 8: data = client.recv(1) header += data n += len(data) total_size = struct.unpack('q', header)[0] print(total_size) # 收真正的資料 recv_size = 0 with open('aaa.jpg', mode='wb') as f: while recv_size < total_size: data = client.recv(1024) f.write(data) recv_size += len(data) client.close()#定制復雜的報頭版本1
#server import subprocess import os import struct import json from socket import * server = socket(AF_INET, SOCK_STREAM) # print(server) server.bind(('127.0.0.1', 8082)) server.listen(5) while True: conn, client_addr = server.accept() print(conn) print(client_addr) while True: try: msg = conn.recv(1024).decode('utf-8') cmd,file_path=msg.split() if cmd == "get": # 一、制作報頭 header_dic={ "total_size":os.path.getsize(file_path), "filename":os.path.basename(file_path), "md5":"1231231231232132131232311" } header_json=json.dumps(header_dic) header_json_bytes=header_json.encode('utf-8') # 二、發送資料 # 1、先發送報頭的長度 header_size=len(header_json_bytes) conn.send(struct.pack('i',header_size)) # 2、再發送報頭 conn.send(header_json_bytes) # 3、最后發送真實的資料 with open(r'%s' %file_path,mode='rb') as f: for line in f: conn.send(line) except Exception: break conn.close() server.close() #client import struct import json from socket import * client = socket(AF_INET, SOCK_STREAM) # print(client) client.connect(('127.0.0.1', 8082)) while True: cmd = input(">>: ").strip() # get 檔案路徑 if len(cmd) == 0: continue client.send(cmd.encode('utf-8')) # 1、先接收報頭的長度 res=client.recv(4) header_size=struct.unpack('i',res)[0] # 2、再接收報頭 header_json_bytes=client.recv(header_size) header_json=header_json_bytes.decode('utf-8') header_dic=json.loads(header_json) print(header_dic) # 3、最后接收真實的資料 total_size=header_dic['total_size'] filename=header_dic['filename'] recv_size = 0 with open(r"D:\python全堆疊15期\day32\代碼\03 定制復雜的報頭\版本2\download\%s" %filename, mode='wb') as f: while recv_size < total_size: data = client.recv(1024) f.write(data) recv_size += len(data) client.close()#定制復雜的報頭版本2
-----32-----
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/53200.html
標籤:Python
上一篇:Python+Pytest+Allure+Git+Jenkins介面自動化框架
下一篇:確認過眼神,看清 HTTP 協議
