詳解python中TCP協(xié)議中的粘包問(wèn)題
TCP協(xié)議中的粘包問(wèn)題
1.粘包現(xiàn)象
基于TCP實(shí)現(xiàn)一個(gè)簡(jiǎn)易遠(yuǎn)程cmd功能
#服務(wù)端 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ǐng)輸入指令(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協(xié)議的遠(yuǎn)程cmd簡(jiǎn)單功能,在運(yùn)行時(shí)會(huì)發(fā)生粘包。
2、什么是粘包?
只有TCP會(huì)發(fā)生粘包現(xiàn)象,UDP協(xié)議永遠(yuǎn)不會(huì)發(fā)生粘包;
TCP:(transport control protocol,傳輸控制協(xié)議)流式協(xié)議。在socket中TCP協(xié)議是按照字節(jié)數(shù)進(jìn)行數(shù)據(jù)的收發(fā),數(shù)據(jù)的發(fā)送方發(fā)出的數(shù)據(jù)往往接收方不知道數(shù)據(jù)到底長(zhǎng)度是多長(zhǎng),而TCP協(xié)議由于本身為了提高傳輸?shù)男?,發(fā)送方往往需要收集到足夠的數(shù)據(jù)才會(huì)進(jìn)行發(fā)送。使用了優(yōu)化方法(Nagle算法),將多次間隔較小且數(shù)據(jù)量小的數(shù)據(jù),合并成一個(gè)大的數(shù)據(jù)塊,然后進(jìn)行封包。這樣,接收端,就難于分辨出來(lái)了,必須提供科學(xué)的拆包機(jī)制。 即面向流的通信是無(wú)消息保護(hù)邊界的。
UDP:(user datagram protocol,用戶數(shù)據(jù)報(bào)協(xié)議)數(shù)據(jù)報(bào)協(xié)議。在socket中udp協(xié)議收發(fā)數(shù)據(jù)是以數(shù)據(jù)報(bào)為單位,服務(wù)端和客戶端收發(fā)數(shù)據(jù)是以一個(gè)單位,所以不會(huì)使用塊的合并優(yōu)化算法,, 由于UDP支持的是一對(duì)多的模式,所以接收端的skbuff(套接字緩沖區(qū))采用了鏈?zhǔn)浇Y(jié)構(gòu)來(lái)記錄每一個(gè)到達(dá)的UDP包,在每個(gè)UDP包中就有了消息頭(消息來(lái)源地址,端口等信息),這樣,對(duì)于接收端來(lái)說(shuō),就容易進(jìn)行區(qū)分處理了。 即面向消息的通信是有消息保護(hù)邊界的。
TCP協(xié)議不會(huì)丟失數(shù)據(jù),UDP協(xié)議會(huì)丟失數(shù)據(jù)。
udp的recvfrom是阻塞的,一個(gè)recvfrom(x)必須對(duì)唯一一個(gè)sendinto(y),收完了x個(gè)字節(jié)的數(shù)據(jù)就算完成,若是y>x數(shù)據(jù)就丟失,這意味著udp根本不會(huì)粘包,但是會(huì)丟數(shù)據(jù),不可靠。
tcp的協(xié)議數(shù)據(jù)不會(huì)丟,沒(méi)有收完包,下次接收,會(huì)繼續(xù)上次繼續(xù)接收,己端總是在收到ack時(shí)才會(huì)清除緩沖區(qū)內(nèi)容。數(shù)據(jù)是可靠的,但是會(huì)粘包。
3、什么情況下會(huì)發(fā)生粘包?
1.由于TCP協(xié)議的優(yōu)化算法,當(dāng)單個(gè)數(shù)據(jù)包較小的時(shí)候,會(huì)等到緩沖區(qū)滿才會(huì)發(fā)生數(shù)據(jù)包前后數(shù)據(jù)疊加在一起的情況。然后取的時(shí)候就分不清了到底是哪段數(shù)據(jù),這是第一種粘包。
2.當(dāng)發(fā)送的單個(gè)數(shù)據(jù)包較大超過(guò)緩沖區(qū)時(shí),收數(shù)據(jù)方一次就只能取一部分的數(shù)據(jù),下次再收數(shù)據(jù)方再收數(shù)據(jù)將會(huì)延續(xù)上次為接收數(shù)據(jù)。這是第二種粘包。
粘包的本質(zhì)問(wèn)題就是接收方不知道發(fā)送數(shù)據(jù)方一次到底發(fā)送了多少數(shù)據(jù),解決問(wèn)題的方向也是從控制數(shù)據(jù)長(zhǎng)度著手,也就是如何設(shè)置緩沖區(qū)的問(wèn)題
4、如何解決粘包問(wèn)題?
解決問(wèn)題思路:上述已經(jīng)明確粘包的產(chǎn)生是因?yàn)榻邮諗?shù)據(jù)時(shí)不知道數(shù)據(jù)的具體長(zhǎng)度。所以我們應(yīng)該先發(fā)送一段數(shù)據(jù)表明我們發(fā)送的數(shù)據(jù)長(zhǎng)度,那么就不會(huì)產(chǎn)生數(shù)據(jù)沒(méi)有發(fā)送或者沒(méi)有收取完全的情況。
1.struct 模塊(結(jié)構(gòu)體)
struct模塊的功能可以將python中的數(shù)據(jù)類型轉(zhuǎn)換成C語(yǔ)言中的結(jié)構(gòu)體(bytes類型)
import struct s = 123456789 res = struct.pack('i', s) print(res) res2 = struct.unpack('i', res) print(res2) print(res2[0])
2.粘包的解決方案基本版
既然我們拿到了一個(gè)可以固定長(zhǎng)度的辦法,那么應(yīng)用struct模塊,可以固定長(zhǎng)度了。
為字節(jié)流加上自定義固定長(zhǎng)度報(bào)頭,報(bào)頭中包含字節(jié)流長(zhǎng)度,然后一次send到對(duì)端,對(duì)端在接收時(shí),先從緩存中取出定長(zhǎng)的報(bào)頭,然后再取真實(shí)數(shù)據(jù)
#服務(wù)器端 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') #利用子進(jìn)程模塊啟動(dòng)程序 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) #管道輸出的信息有正確和錯(cuò)誤的 data = p.stdout.read() err_data = p.stderr.read() #先將數(shù)據(jù)的長(zhǎng)度發(fā)送給客戶端 length = len(data)+len(err_data) #利用struct模塊將數(shù)據(jù)的長(zhǎng)度信息轉(zhuǎn)化成固定的字節(jié) len_data = struct.pack('i', length) #以下將信息傳輸給客戶端 #1.數(shù)據(jù)的長(zhǎng)度 client.send(len_data) #2.正確的數(shù)據(jù) client.send(data) #2.錯(cuò)誤管道的數(shù)據(jù) 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('請(qǐng)輸入指令>>:').strip().encode('utf-8') client.send(cmd) #1.先接收傳過(guò)來(lái)數(shù)據(jù)的長(zhǎng)度是多少,我們通過(guò)struct模塊固定了字節(jié)長(zhǎng)度為4 length = client.recv(4) #將struct的字節(jié)再轉(zhuǎn)回去整型數(shù)字 len_data = struct.unpack('i', length) print(len_data) len_data = len_data[0] print('數(shù)據(jù)長(zhǎng)度為%s:' % len_data) all_data = b'' recv_size = 0 #2.接收真實(shí)的數(shù)據(jù) #循環(huán)接收直到接收到數(shù)據(jù)的長(zhǎng)度等于數(shù)據(jù)的真實(shí)長(zhǎng)度(總長(zhǎng)度) while recv_size < len_data: data = client.recv(1024) recv_size += len(data) all_data += data print('接收長(zhǎng)度%s' % recv_size) print(all_data.decode('gbk'))
#總結(jié):
服務(wù)器端:
- 1.在服務(wù)器端先收到命令,打開(kāi)子進(jìn)程,然后計(jì)算返回的數(shù)據(jù)的長(zhǎng)度
- 2.先利用struct模塊將數(shù)據(jù)長(zhǎng)度轉(zhuǎn)成固定4個(gè)字節(jié)傳給客戶端
- 3.再向客戶端發(fā)送真實(shí)的數(shù)據(jù)。
客戶端(兩次接收):
- 1.第一次只接受4個(gè)字節(jié),因?yàn)殚L(zhǎng)度數(shù)據(jù)就是4個(gè)字節(jié)。這樣防止了數(shù)據(jù)粘包。解碼得到長(zhǎng)度數(shù)據(jù)
- 2.第二次循環(huán)接收真實(shí)數(shù)據(jù),拼接真實(shí)數(shù)據(jù)完成解碼讀取數(shù)據(jù)。
很顯然,如果僅僅只是這樣肯定無(wú)法滿足在實(shí)際生產(chǎn)中一些需求。那么該怎么修改?
我們可以把報(bào)頭做成字典,字典里包含將要發(fā)送的真實(shí)數(shù)據(jù)的詳細(xì)信息,然后json序列化,然后用struck將序列化后的數(shù)據(jù)長(zhǎng)度打包成4個(gè)字節(jié)(4個(gè)字節(jié)足夠用了)
我們可以將自定義的報(bào)頭設(shè)置成這種這種格式。
發(fā)送時(shí):
1先發(fā)報(bào)頭長(zhǎng)度
2再編碼報(bào)頭內(nèi)容然后發(fā)送
3最后發(fā)真實(shí)內(nèi)容
接收時(shí):
1先收?qǐng)?bào)頭長(zhǎng)度,用struct取出來(lái)
2根據(jù)取出的長(zhǎng)度收取報(bào)頭內(nèi)容,然后解碼,反序列化
3從反序列化的結(jié)果中取出待取數(shù)據(jù)的詳細(xì)信息,然后去取真實(shí)的數(shù)據(jù)內(nèi)容
#服務(wù)器端 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') #啟動(dòng)子進(jìn)程 p = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) #得到子進(jìn)程運(yùn)行的數(shù)據(jù) data = p.stdout.read() #子進(jìn)程運(yùn)行正確的輸出管道數(shù)據(jù),數(shù)據(jù)讀出來(lái)后是字節(jié) err_data = p.stderr.read() #子進(jìn)程運(yùn)行錯(cuò)誤的輸出管道數(shù)據(jù) #計(jì)算數(shù)據(jù)的總長(zhǎng)度 length = len(data) + len(err_data) print('數(shù)據(jù)總長(zhǎng)度:%s' % length) #先需要發(fā)送報(bào)頭信息,以下為創(chuàng)建報(bào)頭信息(至第一次發(fā)送) #需要添加時(shí)間信息 time_info = datetime.datetime.now() #設(shè)置一個(gè)字典將一些額外的信息和長(zhǎng)度信息放進(jìn)去然后json序列化,報(bào)頭字典 masthead = {} #將時(shí)間數(shù)據(jù)放入報(bào)頭字典中 masthead['time'] = str(time_info) #時(shí)間格式不能被json序列化,所以將其轉(zhuǎn)化為字符串形式 masthead['length'] = length #將報(bào)頭字典json序列化 json_masthead = json.dumps(masthead) #得到j(luò)son格式的報(bào)頭 # 將json格式的報(bào)頭編碼成字節(jié)形式 masthead_data = json_masthead.encode('utf-8') #利用struct將報(bào)頭編碼的字節(jié)的長(zhǎng)度轉(zhuǎn)成固定的字節(jié)(4個(gè)字節(jié)) masthead_length = struct.pack('i', len(masthead_data)) #1.發(fā)送報(bào)頭的長(zhǎng)度(第一次發(fā)送) client.send(masthead_length) #2.發(fā)送報(bào)頭信息(第二次發(fā)送) client.send(masthead_data) #3.發(fā)送真實(shí)數(shù)據(jù)(第三次發(fā)送) client.send(data) client.send(err_data) except ConnectionResetError: print('客戶端斷開(kāi)連接。。。') client.close() break #客戶端 import socket import struct import json client = socket.socket() client.connect(('127.0.0.1', 33520)) while True: cmd = input('請(qǐng)輸入cmd指令(Q\q退出)>>:').strip() if cmd == 'q': break #發(fā)送CMD指令至服務(wù)器 client.send(cmd.encode('utf-8')) #1.第一次接收,接收?qǐng)?bào)頭信息的長(zhǎng)度,由于struct模塊固定長(zhǎng)度為4字節(jié),括號(hào)內(nèi)直接填4 len_masthead = client.recv(4) #利用struct反解報(bào)頭長(zhǎng)度,由于是元組形式,取值得到整型數(shù)字masthead_length masthead_length = struct.unpack('i', len_masthead)[0] #2.第二次接收,接收?qǐng)?bào)頭信息,接收長(zhǎng)度為報(bào)頭長(zhǎng)度masthead_length 被編碼成字節(jié)形式的json格式的字典, # 解字符編碼得到j(luò)son格式的字典masthead_data masthead_data = client.recv(masthead_length).decode('utf-8') #得到報(bào)頭字典masthead masthead = json.loads(masthead_data) print('執(zhí)行時(shí)間%s' % masthead['time']) #通過(guò)報(bào)頭字典得到數(shù)據(jù)長(zhǎng)度 data_length = masthead['length'] #3.第三次接收,接收真實(shí)數(shù)據(jù),真實(shí)數(shù)據(jù)長(zhǎng)度為data_length # data = client.recv(data_length) #有可能真實(shí)數(shù)據(jù)長(zhǎng)度太大會(huì)撐爆內(nèi)存。 #所以循環(huán)讀取數(shù)據(jù) all_data = b'' length = 0 #循環(huán)直到長(zhǎng)度大于等于數(shù)據(jù)長(zhǎng)度 while length < data_length: data = client.recv(1024) length += len(data) all_data += data print('數(shù)據(jù)的總長(zhǎng)度:%s' % data_length) #我的電腦是Windows系統(tǒng),所以用gbk解碼系統(tǒng)發(fā)出的信息 print(all_data.decode('gbk'))
總結(jié):
1.TCP協(xié)議中,會(huì)產(chǎn)生粘包現(xiàn)象。粘包現(xiàn)象產(chǎn)生本質(zhì)就是讀取數(shù)據(jù)長(zhǎng)度未知。
2.解決粘包現(xiàn)象本質(zhì)就是處理讀取數(shù)據(jù)長(zhǎng)度。
3.報(bào)頭的作用就是解決數(shù)據(jù)傳輸過(guò)程中數(shù)據(jù)長(zhǎng)度怎么計(jì)算傳達(dá)和傳輸其他額外信息的。
以上所述是小編給大家介紹的python中TCP協(xié)議中的粘包問(wèn)題詳解整合,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
相關(guān)文章
python第三方庫(kù)visdom的使用入門(mén)教程
Visdom:一個(gè)靈活的可視化工具,可用來(lái)對(duì)于 實(shí)時(shí),富數(shù)據(jù)的 創(chuàng)建,組織和共享,本文主要介紹了python第三方庫(kù)visdom的使用入門(mén)教程,分享給大家,感興趣的可以了解一下2021-05-05理解Python數(shù)據(jù)離散化手寫(xiě)if-elif語(yǔ)句與pandas中cut()方法實(shí)現(xiàn)
這篇文章主要介紹了通過(guò)手寫(xiě)if-elif語(yǔ)句與pandas中cut()方法實(shí)現(xiàn)示例理解Python數(shù)據(jù)離散化詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05Go/Python/Erlang編程語(yǔ)言對(duì)比分析及示例代碼
這篇文章主要介紹了Go/Python/Erlang編程語(yǔ)言對(duì)比分析及示例代碼,本文重點(diǎn)是給大家介紹go語(yǔ)言,從語(yǔ)言對(duì)比分析的角度切入介紹,需要的朋友可以參考下2018-04-04pyqt5 實(shí)現(xiàn)多窗口跳轉(zhuǎn)的方法
今天小編就為大家分享一篇pyqt5 實(shí)現(xiàn)多窗口跳轉(zhuǎn)的方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-06-06Python List cmp()知識(shí)點(diǎn)總結(jié)
在本篇內(nèi)容里小編給大家整理了關(guān)于Python List cmp()用法相關(guān)知識(shí)點(diǎn),有需要的朋友們跟著學(xué)習(xí)下。2019-02-02