黏包
最近一直再看python的網(wǎng)絡(luò)編程,黏包問題是TCP協(xié)議所獨(dú)有的一種問題,自己平時也有些理解方面的不清晰,所以我的第一篇筆記就從它開始吧。
首先是為什么會出現(xiàn)黏包現(xiàn)象?
只有TCP協(xié)議中才會出現(xiàn)黏包現(xiàn)象,因?yàn)門CP協(xié)議是面向流的協(xié)議,在發(fā)送的數(shù)據(jù)傳輸過程中還有緩存機(jī)制來避免數(shù)據(jù)丟失,因此在連續(xù)發(fā)送小數(shù)據(jù)時或者接收數(shù)據(jù)的大小大于規(guī)定數(shù)值的時候會出現(xiàn)黏包現(xiàn)象。而黏包最本質(zhì)的原因就是接收方不知道接收的數(shù)據(jù)包的大小。
服務(wù)端的代碼
import socket
read = socket.socket()
read.bind(('127.0.0.1',8090))
read.listen()
conn,addr=read.accept()
conn.send(b'ipconfig')
ret=conn.recv(1024).decode('gbk')
print(ret)
conn.close()
read.close()
服務(wù)器端代碼
import socket
import subprocess
sk=socket.socket()
sk.connect(('127.0.0.1',8090))
cmd=sk.recv(1024).decode('gbk')
ret=subprocess.Popen(cmd,shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
sk.send(ret.stdout.read())
sk.send(ret.stderr.read())
上面代碼用pycharm執(zhí)行之后,和命令行比較之后就明白其實(shí)接受的數(shù)據(jù)并不全,但是得益于TCP協(xié)議的特性,未接受完的數(shù)據(jù)會在下一次接收時接收到,并不會丟失,但是也因此帶來了問題,和下次需要接收的數(shù)據(jù)連到了一起。
- 當(dāng)然上面只是黏包的一種情況
黏包的解決方法
有問題自然就會有解決方法,既然黏包的問題本質(zhì)是我們不知道要接收的數(shù)據(jù)包的大小,那么解決問題自然就是首先把我們要發(fā)送的數(shù)據(jù)包大小發(fā)送給接收端就好。這個方法可以解決黏包問題,當(dāng)我們要發(fā)送較大的數(shù)據(jù)時,一次性不可能把所有的數(shù)據(jù)都接收過來,所以就要先設(shè)置一個配置項(xiàng),每次接受固定長度,而一旦我們知道了需要接受的數(shù)據(jù)總量,那么依據(jù)TCP協(xié)議的特性,我們只要設(shè)置好接收的filesize,保證最后接受到的數(shù)據(jù)大小符合發(fā)送的數(shù)據(jù),那么就不會造成數(shù)據(jù)的丟失。
- 當(dāng)然這個方法也有一定的壞處,因?yàn)閳箢^的發(fā)送與確認(rèn)也要浪費(fèi)一次交互的機(jī)會,這就讓我們的程序效率降低了一些
解決方法的升級版
得益于struct模塊,我們可以把任意長度的報頭,轉(zhuǎn)換成四個字節(jié)大小的信息,這樣我們在發(fā)送報頭數(shù)據(jù)的時候就不要事先發(fā)送報頭的長度,因?yàn)榻邮辗街澜?jīng)過struct模塊的轉(zhuǎn)換,報頭的長度變成了固定的四個字節(jié)。
而struct模塊也暫時只用得到pack與unpack的方法。這樣就省去了一次交互的機(jī)會,讓我們的程序效率變得更高。
實(shí)例代碼
下面就是一個應(yīng)用了升級版的解決方法的傳輸文件的示例
服務(wù)器端
#實(shí)現(xiàn)一個大文件的上傳或下載
#配置文件 ip地址 端口號
import json
import socket
import struct
cbc=socket.socket()
cbc.bind(('127.0.0.1',9000))
cbc.listen()
buffer=2048#此處的配置文件盡量不要太大
conn,addr=cbc.accept()
#接收報頭長度
head_len=conn.recv(4)
head_len=struct.unpack('i',head_len)[0]#此處unpack之后是元組格式的信息
json_head=conn.recv(head_len).decode('utf-8')
head=json.loads(json_head)
filesize=head['filesize']
with open(head['filename'],'wb') as f:#這邊就直接接收到當(dāng)前路徑下
while filesize:
if filesize>=buffer:
content=conn.recv(buffer)
f.write(content)
filesize-=buffer
else:
content=conn.recv(filesize)
f.write(content)
break
conn.close()
cbc.close()
客戶端代碼
import os
import json
import socket
import struct
cbc=socket.socket()
cbc.connect(('127.0.0.1',9000))
buffer=2048#定制配置文件
#發(fā)送文件
#定制報頭
head={'filepath':r'發(fā)送文件的路徑',
'filename':r'文件的名稱,注意加上后綴',
'filesize':None
}
file_path=os.path.join(head['filepath'],head['filename'])
filesize=os.path.getsize(os.path.join(head['filepath'],head['filename']))
head['filesize']=filesize
json_head=json.dumps(head)#字典轉(zhuǎn)成字符串
bytes_head=json_head.encode('utf-8')#字符串轉(zhuǎn)成二進(jìn)制
#計算head的長度bytes
head_len=len(bytes_head)
pack_len=struct.pack('i',head_len)#struct打包成四個字節(jié)
#發(fā)送包頭
cbc.send(pack_len)#先發(fā)報頭長度,固定的四個字節(jié)
cbc.send(bytes_head)#再發(fā)bytes類型的報頭
with open(file_path,'rb')as f:
while filesize:
if filesize>=buffer:
content=f.read(buffer)#每次讀取的文件大小
cbc.send(content)
filesize-=buffer
else:
content=f.read(filesize)
cbc.send(content)
break
cbc.close()
這樣就解決了黏包問題
一些廢話
俗話說,萬事開頭難,學(xué)習(xí)了python一段時間后,我才遲遲的開始動筆寫博客,一方面也對自己的學(xué)習(xí)過程有個交代,另一方面也讓自己的知識更加鞏固一些。
而今天的學(xué)習(xí)我本以為是沒有問題的,但是在寫博客的時候我還是遇到了許多問題,說明每天的學(xué)習(xí)需要鞏固,而博客也恰好提供給我這樣一個平臺,讓我找到自己的問題所在。
路遙知馬力,日久見人心,我也希望我自己的博客能夠堅(jiān)持下去,發(fā)現(xiàn)我自己對于技術(shù)的本心。