粘包问题
一.什么是粘包?
粘包指的是數(shù)據(jù)與數(shù)據(jù)之間沒有明確的分界線,導(dǎo)致不能正確讀取數(shù)據(jù)!
TCP、UDP協(xié)議傳輸數(shù)據(jù)時的具體流程,TCP協(xié)議也稱之為流式協(xié)議,UDP稱之為數(shù)據(jù)報協(xié)議
?
發(fā)送方:
當(dāng)應(yīng)用程序調(diào)用send函數(shù)時,應(yīng)用程序會將數(shù)據(jù)從應(yīng)用程序拷貝到操作系統(tǒng)的緩存區(qū),再由操作系統(tǒng)從緩存區(qū)讀取數(shù)據(jù)并發(fā)送出去.
接收方:
對方計算機(jī)操作系統(tǒng)先收到數(shù)據(jù),先將數(shù)據(jù)存放到操作系統(tǒng)的緩沖區(qū)中,當(dāng)應(yīng)用程序調(diào)用recv時,實際上是從操作系統(tǒng)緩沖區(qū)將數(shù)據(jù)拷貝到應(yīng)用程序的過程.
上述過程對于TCP和UDP都是相同的,不同之處在于:
UDP:
UDP在收發(fā)數(shù)據(jù)時是基于數(shù)據(jù)包的,即一個包一個包的發(fā)送,包與包之間有著明確的分界,到達(dá)操作系統(tǒng)緩存區(qū)后也是一個一個獨立的數(shù)據(jù)包,接收方從操作系統(tǒng)緩沖區(qū)中將數(shù)據(jù)包拷貝到應(yīng)用程序.
這種方式存在的問題:
1.發(fā)送方發(fā)送的數(shù)據(jù)長度每個操作系統(tǒng)會有不同的限制,數(shù)據(jù)超過限制則無法發(fā)送
2.接收方接受數(shù)據(jù)時如果應(yīng)用程序的提供的緩存容量小于數(shù)據(jù)包的長度將造成數(shù)據(jù)丟失,而緩存區(qū)大小不可能無限放大.
TCP:
當(dāng)我們需要傳輸較大的數(shù)據(jù),或需要保證數(shù)據(jù)的完整性時 ,最簡單的方式就是使用TCP協(xié)議
與UDP不同TCP會增加一套校驗的規(guī)則來保證數(shù)據(jù)的完整性,會將超過TCP最大長度的數(shù)據(jù)拆分成多個TCP包,并在傳輸數(shù)據(jù)時為每一個TCP數(shù)據(jù)包指定一個順序號,接收方在收到TCP數(shù)據(jù)包后按照順序?qū)?shù)據(jù)包進(jìn)行重組,重組后的數(shù)據(jù)全都是二進(jìn)制數(shù)據(jù),且每次收到的二進(jìn)制數(shù)據(jù)之間沒有明顯的分界.
1.當(dāng)單個數(shù)據(jù)包較小時接收方可能一次性讀取多個包的數(shù)據(jù)
2.當(dāng)整體數(shù)據(jù)較大時接收方可能一次僅讀取一個包的一部分內(nèi)容
3.TCP為了提高效率,會將數(shù)據(jù)較小且發(fā)送間隔較短的數(shù)據(jù)合并發(fā)送,該機(jī)制也會導(dǎo)致發(fā)送方將兩個數(shù)據(jù)包粘在一起發(fā)送.
二.粘包的解決方案
服務(wù)器:
import socket,subprocess
server = socket.socket()
server.bind(('192.168.12.207',4396))
server.listen()
while True:
client,addr = server.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()
print('數(shù)據(jù)長度:%s'%(len(data) + len(err_data)))
length = len(data) + len(err_data)
len_str = str(length).encode('utf-8')
#先發(fā)送長度數(shù)據(jù)再發(fā)送真實數(shù)據(jù),長度數(shù)據(jù)可能和真實數(shù)據(jù)粘在一起,而接收方不知道長度數(shù)據(jù)的字節(jié)數(shù),導(dǎo)致粘包
client.send(len_str)
client.send(data)
client.send(err_data)
except ConnectionResetError:
print('連接異常')
client.close()
break
客戶端:
import socket
client = socket.socket()
client.connect(('192.168.12.207',4396))
while True:
cmd = input('>>:').strip()
client.send(cmd.encode('utf-8'))
length = client.recv(1024)
len_data =int(length.decode('utf-8'))
print('數(shù)據(jù)長度為%s'%len_data)
data_all= b''
data_size = 0
while data_size < len_data:
data = client.recv(len_data)
data_size += len(data)
data_all += data
print('接受長度為%s'%data_size)
print(data_all.decode('GBK'))
為了解決長度數(shù)據(jù)和真實數(shù)據(jù)的粘包問題,我們要用到:struct結(jié)構(gòu)體 可以將python中的數(shù)據(jù)類型轉(zhuǎn)換成C中的結(jié)構(gòu)體(轉(zhuǎn)換成bytes)
import struct
num = 100
#該函數(shù) 將一個python中的數(shù)據(jù)類型轉(zhuǎn)換成bytes 第一個參數(shù)通常是i 其能轉(zhuǎn)換的數(shù)據(jù)范圍是C語言中的int范圍
#如果int不夠,那就使用q 表示的是long long型
res = struct.pack('i',num)
print(res)
print(len(res))
#該函數(shù)將bytes類型轉(zhuǎn)換
res2 = struct.unpack('i',res)
print(res2) #得到一個元組(100,)
print(res2[0]) #得到整型 100
修正版本:
服務(wù)器:
import socket,subprocess,struct
server = socket.socket()
server.bind(('192.168.12.207',4396))
server.listen()
while True:
client,addr = server.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()
print('數(shù)據(jù)長度:%s'%(len(data) + len(err_data)))
length = len(data) + len(err_data)
len_data = struct.pack('i',length)
client.send(len_data)
client.send(data)
client.send(err_data)
except ConnectionResetError:
print('連接異常')
client.close()
break
客戶端:
import socket,struct
client = socket.socket()
client.connect(('192.168.12.207',4396))
while True:
cmd = input('>>:').strip()
client.send(cmd.encode('utf-8'))
#接受一個長度為4的固定字節(jié)
length = client.recv(4)
len_data = struct.unpack('i',length)[0]
print('數(shù)據(jù)長度為%s'%len_data)
data_all= b''
data_size = 0
while data_size < len_data:
data = client.recv(1024)
data_size += len(data)
data_all += data
print('接受長度為%s'%data_size)
print(data_all.decode('GBK'))
2.自定義報頭解決粘包
上述方案已經(jīng)完美的解決了粘包的問題,但是擴(kuò)展性不高,例如我們要實現(xiàn)文件上傳下載,不光要傳輸文件的數(shù)據(jù),還需要傳輸文件名字,md5值等等,如何實現(xiàn)?
發(fā)送端:
1.先將所有的額外信息打包到一個頭中
2.然后先發(fā)送頭部數(shù)據(jù)
3.最后發(fā)送真實數(shù)據(jù)
接收端:
1.接受固定長度的頭部數(shù)據(jù)長度
2.根據(jù)長度數(shù)據(jù)獲取頭部數(shù)據(jù)
3,根據(jù)頭部數(shù)據(jù)獲取真實數(shù)據(jù)
服務(wù)器:
import socket,subprocess,struct,datetime,json
server = socket.socket()
server.bind(('192.168.12.207',4396))
server.listen()
while True:
client,addr = server.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()
print('數(shù)據(jù)長度:%s'%(len(data) + len(err_data)))
length = len(data) + len(err_data)
#先發(fā)送額外數(shù)據(jù),將要發(fā)送的真實數(shù)據(jù)長度先存到字典中
t = {}
t['time'] = str(datetime.datetime.now())
t['size'] = length
#將字典轉(zhuǎn)換成json格式
t_json = json.dumps(t)
#將字典轉(zhuǎn)換成字節(jié)
t_data = t_json.encode('utf-8')
t_length = struct.pack('i',len(t_data))
#1.先發(fā)送額外數(shù)據(jù)長度
client.send(t_length)
#2.發(fā)送額外信息
client.send(t_data)
#3.發(fā)送真實數(shù)據(jù)
client.send(data)
client.send(err_data)
except ConnectionResetError:
print('連接異常')
client.close()
break
客戶端:
import socket,struct,json
client = socket.socket()
client.connect(('192.168.12.207',4396))
while True:
cmd = input('>>:').strip()
if not cmd:
print('命令不能為空!')
continue
client.send(cmd.encode('utf-8'))
#1.先接受額外信息長度
length = client.recv(4)
len_data = struct.unpack('i',length)[0]
#2.接受額外信息
t_data = client.recv(len_data)
print(t_data.decode('utf-8'))
json_dic = json.loads(t_data.decode('utf-8'))
print('執(zhí)行時間為%s'%json_dic['time'])
data_size = json_dic['size']
#3,接受真實信息
data_all= b''
rcv_size = 0
while rcv_size < data_size:
data = client.recv(1024)
rcv_size += len(data)
data_all += data
print('接受長度為%s'%rcv_size)
print(data_all.decode('GBK'))
?
轉(zhuǎn)載于:https://www.cnblogs.com/lizeqian1994/p/10191875.html
與50位技術(shù)專家面對面20年技術(shù)見證,附贈技術(shù)全景圖總結(jié)
- 上一篇: [From 10.1~10.5] 对象和
- 下一篇: PE 学习之路 —— 区块表