最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

一文告訴你什么是 TCP 數(shù)據(jù)粘包,該如何解決!

2023-09-12 15:22 作者:補(bǔ)給站Linux內(nèi)核  | 我要投稿

粘包問題概述

描述背景

采用TCP協(xié)議進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)傳送的軟件設(shè)計(jì)中,普遍存在粘包問題。這主要是由于現(xiàn)代操作系統(tǒng)的網(wǎng)絡(luò)傳輸機(jī)制所產(chǎn)生的。

我們知道,網(wǎng)絡(luò)通信采用的套接字(socket)技術(shù),其實(shí)現(xiàn)實(shí)際是由系統(tǒng)內(nèi)核提供一片連續(xù)緩存(流緩沖)來實(shí)現(xiàn)應(yīng)用層程序與網(wǎng)卡接口之間的中轉(zhuǎn)功能。

多個數(shù)據(jù)包被連續(xù)存儲于連續(xù)的緩存中,在對數(shù)據(jù)包進(jìn)行讀取時由于無法確定發(fā)送方的發(fā)送邊界,而采用某一估測值大小來進(jìn)行數(shù)據(jù)讀出,若雙方的size不一致時就會使數(shù)據(jù)包的邊界發(fā)生錯位,導(dǎo)致讀出錯誤的數(shù)據(jù)分包,進(jìn)而曲解原始數(shù)據(jù)含義。

粘包的概念

粘包問題的本質(zhì)就是數(shù)據(jù)讀取邊界錯誤所致,通過下圖可以形象地理解其現(xiàn)象。

如圖1所示,當(dāng)前的socket緩存中已經(jīng)有6個數(shù)據(jù)分組到達(dá),其大小如圖中數(shù)字。而應(yīng)用程序在對數(shù)據(jù)進(jìn)行收取時(如圖2),采用了300字節(jié)的要求去讀取,則會誤將pkg1和pkg2一起收走當(dāng)做一個包來處理。

而實(shí)際上,很可能pkg1是一個文本文件的內(nèi)容,而pkg2則可能是一個音頻內(nèi)容,這風(fēng)馬牛不相及的兩個數(shù)據(jù)包卻被揉進(jìn)一個包進(jìn)行處理,顯然有失妥當(dāng)。嚴(yán)重時可能因?yàn)閬G了pkg2而導(dǎo)致軟件陷入異常分支產(chǎn)生烏龍事件。

因此,粘包問題必須引起所有軟件設(shè)計(jì)者(項(xiàng)目經(jīng)理)的高度重視!

那么,或許會有讀者發(fā)問,為何不讓接收程序按照100字節(jié)來讀取呢?我想如果你了解一些TCP編程的話就不會有這樣的問題。

網(wǎng)絡(luò)通信程序中,數(shù)據(jù)包通常是不能確定大小的,尤其在軟件設(shè)計(jì)階段無法真的做到確定為一個固定值。比如聊天軟件客戶端若采用TCP傳輸一個用戶名和密碼到服務(wù)端進(jìn)行驗(yàn)證登陸,我想這個數(shù)據(jù)包不過是幾十字節(jié),至多幾百字節(jié)即可發(fā)送完畢,而有時候要傳輸一個很大的視頻文件,即使分包發(fā)送也應(yīng)該一個包在幾千字節(jié)吧。(據(jù)說,某國電信平臺的MW中見到過一次發(fā)送1.5萬字節(jié)的電話數(shù)據(jù))。

這種情況下,發(fā)送數(shù)據(jù)的分包大小無法固定,接收端也就無法固定。所以一般采用一個較為合理的預(yù)估值進(jìn)行輪詢接收。(網(wǎng)卡的MTU都是1500字節(jié),因此這個預(yù)估值一般為MTU的1~3倍)。

相信讀者對粘包問題應(yīng)該有了初步認(rèn)識了。

粘包回避設(shè)計(jì)

一:定長發(fā)送

在進(jìn)行數(shù)據(jù)發(fā)送時采用固定長度的設(shè)計(jì)。也就是無論發(fā)送多大數(shù)據(jù)都分包為固定長度(為便于描述,此處定長為記為LEN),即發(fā)送端在發(fā)送數(shù)據(jù)時都以LEN為長度進(jìn)行分包。

這樣接收方都以固定的LEN進(jìn)行接收,如此一來發(fā)送和接收就能一一對應(yīng)了。分包的時候不一定能完整的恰好分成多個完整的LEN的包,最后一個包一般都會小于LEN,這時候最后一個包可以在不足的部分填充空白字節(jié)。

當(dāng)然,這種方法會有缺陷:

1. 最后一個包的不足長度被填充為空白部分,也即無效字節(jié)序。那么接收方可能難以辨別這無效的部分,它本身就是為了補(bǔ)位的,并無實(shí)際含義。這就為接收端處理其含義帶來了麻煩。當(dāng)然也有解決辦法,可以通過增添標(biāo)志位的方法來彌補(bǔ),即在每一個數(shù)據(jù)包的最前面增加一個定長的報(bào)頭,然后將該數(shù)據(jù)包的末尾標(biāo)記一并發(fā)送。接收方根據(jù)這個標(biāo)記確認(rèn)無效字節(jié)序列,從而實(shí)現(xiàn)數(shù)據(jù)的完整接收。

2. 在發(fā)送包長度隨機(jī)分布的情況下,會造成帶寬浪費(fèi)。比如發(fā)送長度可能為 1,100,1000,4000字節(jié)等等,則都需要按照定長最大值即4000來發(fā)送,數(shù)據(jù)包小于4000字節(jié)的其他包也會被填充至4000,造成網(wǎng)絡(luò)負(fù)載的無效浪費(fèi)。

綜上,此方案適在發(fā)送數(shù)據(jù)包長度較為穩(wěn)定(趨于某一固定值)的情況下有較好的效果。

二:尾部標(biāo)記序列

在每個要發(fā)送的數(shù)據(jù)包的尾部設(shè)置一個特殊的字節(jié)序列,此序列帶有特殊含義,跟字符串的結(jié)束符標(biāo)識”\0”一樣的含義,用來標(biāo)示這個數(shù)據(jù)包的末尾,接收方可對接收的數(shù)據(jù)進(jìn)行分析,通過尾部序列確認(rèn)數(shù)據(jù)包的邊界。

這種方法的缺陷較為明顯:

1. 接收方需要對數(shù)據(jù)進(jìn)行分析,甄別尾部序列。

2. 尾部序列的確定本身是一個問題。什么樣的序列可以向”\0”一樣來做一個結(jié)束符呢?這個序列必須是不具備通常任何人類或者程序可識別的帶含義的數(shù)據(jù)序列,就像“\0”是一個無效字符串內(nèi)容,因而可以作為字符串的結(jié)束標(biāo)記。那普通的網(wǎng)絡(luò)通信中,這個序列是什么呢?我想一時間很難找到恰當(dāng)?shù)拇鸢浮?/p>

三:頭部標(biāo)記分步接收

這個方法是作者有限學(xué)識里最好的辦法了。它既不損失效率,還完美解決了任何大小的數(shù)據(jù)包的邊界問題。

這個方法的實(shí)現(xiàn)是這樣的:

1. 定義一個用戶報(bào)頭,在報(bào)頭中注明每次發(fā)送的數(shù)據(jù)包大小。

2. 接收方每次接收時先以報(bào)頭的size進(jìn)行數(shù)據(jù)讀取,這必然只能讀到一個報(bào)頭的數(shù)據(jù),從報(bào)頭中得到該數(shù)據(jù)包的數(shù)據(jù)大小。

3. 再按照此大小進(jìn)行再次讀取,就能讀到數(shù)據(jù)的內(nèi)容了。

這樣一來,每個數(shù)據(jù)包發(fā)送時都封裝一個報(bào)頭,然后接收方分兩次接收一個包,第一次接收報(bào)頭,根據(jù)報(bào)頭大小第二次才接收數(shù)據(jù)內(nèi)容。

(此處的data[0]的本質(zhì)是一個指針,指向數(shù)據(jù)的正文部分,也可以是一篇連續(xù)數(shù)據(jù)區(qū)的起始位置。因此可以設(shè)計(jì)成data[user_size],這樣的話。)

下面通過一個圖來展現(xiàn)設(shè)計(jì)思想。

由圖看出,數(shù)據(jù)發(fā)送多了封裝報(bào)頭的動作;接收方將每個包的接收拆分成了兩次。

這方案看似精妙,實(shí)則也有缺陷:

1. 報(bào)頭雖小,但每個包都需要多封裝sizeof(_data_head)的數(shù)據(jù),積累效應(yīng)也不可完全忽略。

2. 接收方的接收動作分成了兩次,也就是進(jìn)行數(shù)據(jù)讀取的操作被增加了一倍,而數(shù)據(jù)讀取操作的recv或者read都是系統(tǒng)調(diào)用,這對內(nèi)核而言的開銷是一個不能完全忽略的影響,對程序而言性能影響可忽略(系統(tǒng)調(diào)用的速度非??欤?/p>

優(yōu)點(diǎn):避免了程序設(shè)計(jì)的復(fù)雜性,其有效性便于驗(yàn)證,對軟件設(shè)計(jì)的穩(wěn)定性要求來說更容易達(dá)標(biāo)。


【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【749907784】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實(shí)戰(zhàn)項(xiàng)目及代碼)? ? ? ?



補(bǔ)充

何時需要考慮粘包問題?

1、如果利用tcp每次發(fā)送數(shù)據(jù),就與對方建立連接,然后雙方發(fā)送完一段數(shù)據(jù)后,就關(guān)閉連接,這樣就不會出現(xiàn)粘包問題(因?yàn)橹挥幸环N包結(jié)構(gòu),類似于http協(xié)議)。

關(guān)閉連接主要要雙方都發(fā)送close連接(參考tcp關(guān)閉協(xié)議)。如:A需要發(fā)送一段字符串給B,那么A與B建立連接,然后發(fā)送雙方都默認(rèn)好的協(xié)議字符如"hello give me sth abour yourself",然后B收到報(bào)文后,就將緩沖區(qū)數(shù)據(jù)接收,然后關(guān)閉連接,這樣粘包問題不用考慮到,因?yàn)榇蠹叶贾朗前l(fā)送一段字符。

2、如果發(fā)送數(shù)據(jù)無結(jié)構(gòu),如文件傳輸,這樣發(fā)送方只管發(fā)送,接收方只管接收存儲就ok,也不用考慮粘包。

3、如果雙方建立連接,需要在連接后一段時間內(nèi)發(fā)送不同結(jié)構(gòu)數(shù)據(jù),如連接后,有好幾種結(jié)構(gòu):

1)"hello give me sth abour yourself"

2)"Don't give me sth abour yourself"

那這樣的話,如果發(fā)送方連續(xù)發(fā)送這個兩個包出去,接收方一次接收可能會是"hello give me sth abour yourselfDon't give me sth abour yourself" 。

這樣接收方就傻了,到底是要干嘛?不知道,因?yàn)閰f(xié)議沒有規(guī)定這么詭異的字符串,所以要處理把它分包,怎么分也需要雙方組織一個比較好的包結(jié)構(gòu),所以一般可能會在頭加一個數(shù)據(jù)長度之類的包,以確保接收。

粘包出現(xiàn)的原因:在流傳輸中出現(xiàn),UDP不會出現(xiàn)粘包,因?yàn)樗邢⑦吔纭?/strong>

1 發(fā)送端需要等緩沖區(qū)滿才發(fā)送出去,造成粘包 ;

2 接收方不及時接收緩沖區(qū)的包,造成多個包接收;

解決辦法:

為了避免粘包現(xiàn)象,可采取以下幾種措施。

一是對于發(fā)送方引起的粘包現(xiàn)象,用戶可通過編程設(shè)置來避免,TCP提供了強(qiáng)制數(shù)據(jù)立即傳送的操作指令push,TCP軟件收到該操作指令后,就立即將本段數(shù)據(jù)發(fā)送出去,而不必等待發(fā)送緩沖區(qū)滿;

二是對于接收方引起的粘包,則可通過優(yōu)化程序設(shè)計(jì)、精簡接收進(jìn)程工作量、提高接收進(jìn)程優(yōu)先級等措施,使其及時接收數(shù)據(jù),從而盡量避免出現(xiàn)粘包現(xiàn)象;

三是由接收方控制,將一包數(shù)據(jù)按結(jié)構(gòu)字段,人為控制分多次接收,然后合并,通過這種手段來避免粘包。

以上提到的三種措施,都有其不足之處。

第一種編程設(shè)置方法雖然可以避免發(fā)送方引起的粘包,但它關(guān)閉了優(yōu)化算法,降低了網(wǎng)絡(luò)發(fā)送效率,影響應(yīng)用程序的性能,一般不建議使用。

第二種方法只能減少出現(xiàn)粘包的可能性,但并不能完全避免粘包,當(dāng)發(fā)送頻率較高時,或由于網(wǎng)絡(luò)突發(fā)可能使某個時間段數(shù)據(jù)包到達(dá)接收方較快,接收方還是有可能來不及接收,從而導(dǎo)致粘包。

第三種方法雖然避免了粘包,但應(yīng)用程序的效率較低,對實(shí)時應(yīng)用的場合不適合。

為什么基于TCP的通訊程序需要進(jìn)行封包和拆包

TCP是個"流"協(xié)議,所謂流,就是沒有界限的一串?dāng)?shù)據(jù).大家可以想想河里的流水,是連成一片的,其間是沒有分界線的.但一般通訊程序開發(fā)是需要定義一個個相互獨(dú)立的數(shù)據(jù)包的,比如用于登陸的數(shù)據(jù)包,用于注銷的數(shù)據(jù)包.由于TCP"流"的特性以及網(wǎng)絡(luò)狀況,在進(jìn)行數(shù)據(jù)傳輸時會出現(xiàn)以下幾種情況.

假設(shè)我們連續(xù)調(diào)用兩次send分別發(fā)送兩段數(shù)據(jù)data1和data2,在接收端有以下幾種接收情況(當(dāng)然不止這幾種情況,這里只列出了有代表性的情況).?

A. 先接收到data1,然后接收到data2。

B. 先接收到data1的部分?jǐn)?shù)據(jù),然后接收到data1余下的部分以及data2的全部。

C. 先接收到了data1的全部數(shù)據(jù)和data2的部分?jǐn)?shù)據(jù),然后接收到了data2的余下的數(shù)據(jù)。

D. 一次性接收到了data1和data2的全部數(shù)據(jù)。

對于A這種情況正是我們需要的,不再做討論。對于B,C,D的情況就是大家經(jīng)常說的"粘包",就需要我們把接收到的數(shù)據(jù)進(jìn)行拆包,拆成一個個獨(dú)立的數(shù)據(jù)包。為了拆包就必須在發(fā)送端進(jìn)行封包。

另:對于UDP來說就不存在拆包的問題,因?yàn)閁DP是個"數(shù)據(jù)包"協(xié)議,也就是兩段數(shù)據(jù)間是有界限的,在接收端要么接收不到數(shù)據(jù),要么就是接收一個完整的一段數(shù)據(jù),不會少接收也不會多接收。

為什么會出現(xiàn)B.C.D的情況

"粘包"可發(fā)生在發(fā)送端也可發(fā)生在接收端。

1. 由Nagle算法造成的發(fā)送端的粘包:

Nagle算法是一種改善網(wǎng)絡(luò)傳輸效率的算法。簡單的說,當(dāng)我們提交一段數(shù)據(jù)給TCP發(fā)送時,TCP并不立刻發(fā)送此段數(shù)據(jù),而是等待一小段時間,看看在等待期間是否還有要發(fā)送的數(shù)據(jù),若有則會一次把這兩段數(shù)據(jù)發(fā)送出去。

這是對Nagle算法一個簡單的解釋,詳細(xì)的請看相關(guān)書籍。像C和D的情況就有可能是Nagle算法造成的.

2. 接收端接收不及時造成的接收端粘包:

TCP會把接收到的數(shù)據(jù)存在自己的緩沖區(qū)中,然后通知應(yīng)用層取數(shù)據(jù)。當(dāng)應(yīng)用層由于某些原因不能及時的把TCP的數(shù)據(jù)取出來,就會造成TCP緩沖區(qū)中存放了幾段數(shù)據(jù)。

怎樣封包和拆包

最初遇到"粘包"的問題時,我是通過在兩次send之間調(diào)用 sleep 來休眠一小段時間來解決。

這個解決方法的缺點(diǎn)是顯而易見的,使傳輸效率大大降低,而且也并不可靠。后來就是通過應(yīng)答的方式來解決,盡管在大多數(shù)時候是可行的,但是不能解決象B的那種情況,而且采用應(yīng)答方式增加了通訊量,加重了網(wǎng)絡(luò)負(fù)荷。再后來就是對數(shù)據(jù)包進(jìn)行封包和拆包的操作。

封包就是給一段數(shù)據(jù)加上包頭,這樣一來數(shù)據(jù)包就分為包頭和包體兩部分內(nèi)容了(以后講過濾非法包時封包會加入"包尾"內(nèi)容)。

包頭其實(shí)上是個大小固定的結(jié)構(gòu)體,其中有個結(jié)構(gòu)體成員變量表示包體的長度,這是個很重要的變量,其他的結(jié)構(gòu)體成員可根據(jù)需要自己定義。根據(jù)包頭長度固定以及包頭中含有包體長度的變量就能正確的拆分出一個完整的數(shù)據(jù)包。

對于拆包,目前我最常用的是以下兩種方式。

1. 動態(tài)緩沖區(qū)暫存方式。之所以說緩沖區(qū)是動態(tài)的,是因?yàn)楫?dāng)需要緩沖的數(shù)據(jù)長度超出緩沖區(qū)的長度時會增大緩沖區(qū)長度。大概過程描述如下:

A, 為每一個連接動態(tài)分配一個緩沖區(qū),同時把此緩沖區(qū)和SOCKET關(guān)聯(lián),常用的是通過結(jié)構(gòu)體關(guān)聯(lián).B, 當(dāng)接收到數(shù)據(jù)時首先把此段數(shù)據(jù)存放在緩沖區(qū)中.?C, 判斷緩存區(qū)中的數(shù)據(jù)長度是否夠一個包頭的長度,如不夠,則不進(jìn)行拆包操作.D, 根據(jù)包頭數(shù)據(jù)解析出里面代表包體長度的變量.?E, 判斷緩存區(qū)中除包頭外的數(shù)據(jù)長度是否夠一個包體的長度,如不夠,則不進(jìn)行拆包操作.?F ,取出整個數(shù)據(jù)包。這里的"取"的意思是不光從緩沖區(qū)中拷貝出數(shù)據(jù)包,而且要把此數(shù)據(jù)包從緩存區(qū)中刪除掉.刪除的辦法就是把此包后面的數(shù)據(jù)移動到緩沖區(qū)的起始地址。

這種方法有兩個缺點(diǎn):1. 為每個連接動態(tài)分配一個緩沖區(qū)增大了內(nèi)存的使用。2.有三個地方需要拷貝數(shù)據(jù),一個地方是把數(shù)據(jù)存放在緩沖區(qū),一個地方是把完整的數(shù)據(jù)包從緩沖區(qū)取出來,一個地方是把數(shù)據(jù)包從緩沖區(qū)中刪除。

前面提到過這種方法的缺點(diǎn)。下面給出一個改進(jìn)辦法, 即采用環(huán)形緩沖.但是這種改進(jìn)方法還是不能解決第一個缺點(diǎn)以及第一個數(shù)據(jù)拷貝,只能解決第三個地方的數(shù)據(jù)拷貝(這個地方是拷貝數(shù)據(jù)最多的地方). 第2種拆包方式會解決這兩個問題.

環(huán)形緩沖實(shí)現(xiàn)方案是定義兩個指針,分別指向有效數(shù)據(jù)的頭和尾。在存放數(shù)據(jù)和刪除數(shù)據(jù)時只是進(jìn)行頭尾指針的移動。

2. 利用底層的緩沖區(qū)來進(jìn)行拆包

由于TCP也維護(hù)了一個緩沖區(qū),所以我們完全可以利用TCP的緩沖區(qū)來緩存我們的數(shù)據(jù),這樣一來就不需要為每一個連接分配一個緩沖區(qū)了。另一方面我們知道recv或者wsarecv都有一個參數(shù),用來表示我們要接收多長長度的數(shù)據(jù)。利用這兩個條件我們就可以對第一種方法進(jìn)行優(yōu)化。

對于阻塞SOCKET來說,我們可以利用一個循環(huán)來接收包頭長度的數(shù)據(jù),然后解析出代表包體長度的那個變量,再用一個循環(huán)來接收包體長度的數(shù)據(jù)。相關(guān)代碼如下:

m_TcpSock是一個封裝了SOCKET的類的變量,其中的ReceiveSize用于接收一定長度的數(shù)據(jù),直到接收了一定長度的數(shù)據(jù)或者網(wǎng)絡(luò)出錯才返回。

對于非阻塞的SOCKET,比如完成端口,我們可以提交接收包頭長度的數(shù)據(jù)的請求。當(dāng) GetQueuedCompletionStatus 返回時,我們判斷接收的數(shù)據(jù)長度是否等于包頭長度,若等于,則提交接收包體長度的數(shù)據(jù)的請求;若不等于,則提交接收剩余數(shù)據(jù)的請求。當(dāng)接收包體時,采用類似的方法。


原文作者:一起學(xué)嵌入式




一文告訴你什么是 TCP 數(shù)據(jù)粘包,該如何解決!的評論 (共 條)

分享到微博請遵守國家法律
浦县| 武威市| 聂拉木县| 集安市| 天气| 怀安县| 金沙县| 富阳市| 杭锦后旗| 江津市| 辽宁省| 江门市| 德阳市| 清苑县| 泗洪县| 阳春市| 中方县| 宣威市| 哈巴河县| 饶阳县| 阿拉善盟| 宜州市| 遵义市| 万源市| 珲春市| 噶尔县| 仁布县| 平南县| 宁德市| 庆城县| 旅游| 迁西县| 东辽县| 滦平县| 南皮县| 弥渡县| 门源| 天津市| 怀远县| 呼伦贝尔市| 苏尼特左旗|