Linux下tcp協議socket的recv函數返回時機分析(粘包)

以前老在網上找別人說recv什麼時候返回,要麼說的很籠統,要麼完全覺得不靠譜,最近還是自己做個試驗分析一下吧:

測試1. 
每次發送大小:1024
每次接收大小:32
結果:pack1
每send發送一個包,包中數據大小1024,帶PUSH標誌
每次接收滿32後recv函數返回。


測試2.
每次發送大小:1024
每次接收大小:2048
結果:pack2
每send發送一個包,包中數據大小1024,帶PUSH標誌
每次接收滿1024後recv函數返回。


測試3. 
每次發送大小:20480
每次接收大小:10240
結果:pack3
每send發送兩個包,包中數據大小爲16384(TCP分片大小,建立連接時商定)與4096,僅最後一個包帶PUSH標誌
第一次接收滿10240後recv函數返回
第二次接收6144後recv函數返回(6144+10240=16384)
第三次接收4096後recv函數返回
Recv >>> [1]10240:10240
Recv >>> [2]10240:6144
Recv >>> [3]10240:4096
Recv >>> [4]10240:10240
Recv >>> [5]10240:6144
Recv >>> [6]10240:4096




測試4. 
每次發送大小:20480
每次接收大小:20480
結果:pack4
每send發送兩個包,包中數據大小爲16384(TCP分片大小,建立連接時商定)與4096,僅最後一個包帶PUSH標誌
第一次接收16384後recv函數返回
第二次接收4096後recv函數返回
Recv >>> [1]20480:16384
Recv >>> [2]20480:4096
Recv >>> [3]20480:16384
Recv >>> [4]20480:4096


測試5.
每次發送大小:400000
每次接收大小:10240
結果:pack5
發送時除最後一個包不夠意外,數據都是按照16384的包大小發送,發送過程中經常性接收窗口滿。
接收過程初期按照16384包大小接收,後期無規律:
Recv >>> [1]10240:10240
Recv >>> [2]10240:6144 //A//滿16384
Recv >>> [3]10240:10240
Recv >>> [4]10240:6144 //B//滿16384,從開始到此滿32768(滑動窗口總大小)
Recv >>> [5]10240:10240
Recv >>> [6]10240:10240
Recv >>> [7]10240:10240
Recv >>> [8]10240:2048 //C//從開始到此處滿65536(兩個滑動窗口總大小)
Recv >>> [9]10240:10240
Recv >>> [10]10240:6144 //D//滿16384
Recv >>> [11]10240:10240
Recv >>> [12]10240:6144 //E//滿16384
Recv >>> [13]10240:10240 //F//從此往後均能收滿整個buffer
Recv >>> [14]10240:10240
。。。。。。都是10240:10240
Recv >>> [40]10240:10240
Recv >>> [41]10240:8192 //E//
Recv >>> [42]10240:6784


測試6.
每次發送大小:400000
每次接收大小:10000
結果:pack6
發送時除最後一個包不夠以外,數據都是按照16384的包大小發送,發送過程中經常性接收窗口滿。
接收過程初期按照16384包大小接收,後期無規律:
Recv >>> [1]10000:10000
Recv >>> [2]10000:6384
Recv >>> [3]10000:10000
Recv >>> [4]10000:6384
Recv >>> [5]10000:10000
Recv >>> [6]10000:10000
Recv >>> [7]10000:10000
Recv >>> [8]10000:2768
Recv >>> [9]10000:10000
Recv >>> [10]10000:6384
Recv >>> [11]10000:10000
Recv >>> [12]10000:6384
Recv >>> [13]10000:10000
。。。。。。都是10000:10000
Recv >>> [41]10000:10000
Recv >>> [42]10000:4912
Recv >>> [43]10000:6784


測試7.
每次發送大小:32,發10000次
每次接收大小:102400
結果:pack7
發送過程中大部分包都以32大小發送,但也有部分包被合在了一起發送,發送的每個包都帶PUSH標誌
接收過程中大部分包都以32大小接收,但也有接收大於32的時候,並且與發送大於32的包並無任何關係。
Recv >>> [1]102400:32
Recv >>> [2]102400:32
Recv >>> [3]102400:32
。。。。。。都是102400:32
Recv >>> [92]102400:32
Recv >>> [93]102400:49184
Recv >>> [94]102400:32
。。。。。。都是102400:32
Recv >>> [151]102400:32
Recv >>> [152]102400:32
Recv >>> [153]102400:22720
Recv >>> [154]102400:32
。。。。。。都是102400:32
Recv >>> [268]102400:32
Recv >>> [271]102400:47840
Recv >>> [272]102400:32
。。。。。。都是102400:32
Recv >>> [397]102400:32
Recv >>> [398]102400:32
Recv >>> [399]102400:59776
Recv >>> [400]102400:32
。。。。。。都是102400:32
Recv >>> [523]102400:32
Recv >>> [526]102400:54208
Recv >>> [527]102400:32
。。。。。。都是102400:32
Recv >>> [630]102400:32
Recv >>> [631]102400:32
Recv >>> [632]102400:65568
。。。。。。都是102400:32
Recv >>> [652]102400:32
Recv >>> [653]102400:32


分析:用戶從接收緩存區讀取內容與kernel向接收緩存區填充內容這兩個過程是互斥的,
recv只從接收緩存區中獲取一次內容,並且也只關心自己獲取時接收緩存區有多少內容:
a、如果當時緩存區中沒有數據,則recv進入阻塞狀態,等待kernel向緩存區被填入數據後重新激活。
b、如果當時緩存區中的數據比用戶接收使用的buffer大,則填滿buffer後recv函數返回。
c、如果當時緩存區中的數據比用戶接收使用的buffer小,則取走緩存區中的所有數據後recv函數返回。
kernel向接收緩存區填充則是收到數據包後到自己的時間片並且緩存區不在被讀,則將拿到的包內容全部放入緩存區中。



這樣就可以解釋測試5中的現象了:
a、kernel第一二次都只收到了一個16384大小的包,所以接收的A處與B處都收到了整個數據包
b、慢慢的包陸陸續續的到來kernel第三次收到了兩個16384大小的包,填滿了整個緩存區,所以接收的C處,
在用戶的時間片內用戶收走了緩存區的全部數據。
c、接下來數據蜂擁而至,導致緩存區一直處於滿的狀態,所以後來的接收都能收滿用戶buffer。
d、接收的E處接收完整個緩存區,隨後接收收尾的包
爲了驗證以上推斷,進行了測試6。


測試7驗證了快速發送時的發送接收情況,證明了send發送與recv接收過程完全沒關係,
即使發送的包帶有PUSH標記,也只能保證每一個包被單獨放入接收緩存區中,無法使recv接收每收到一個包返回。
整個接收過程遵循上面的規則,何時返回,返回多少數據均要看當時緩存區內的數據多少。
像測試7中大部分包每個包都返回只是因爲接收比較快導致kernel每次只來得及向接收緩存區中放一個包而已。


結論:
a、不要期待利用TCP協議一方send一次,另一方recv一次的邏輯,這種邏輯是不可靠,也沒有理論依據的
b、在本機進行消息邏輯控制,或者說信令級傳輸,建議還是使用UDP
c、若必須要使用TCP消息傳輸時一定要加同步頭,例如一個固定的值,用來分割數據包或者驗證是否包亂序或越界

還有一個別人分析的地址,挺不錯的:

http://www.vckbase.com/index.php/wv/10


抓去的數據包的下載地址:

http://www.xlpan.com/home/9343823/057665c9-503e-4c44-8216-f1aef91e9819


後面想到什麼了再補上!

發佈了20 篇原創文章 · 獲贊 21 · 訪問量 34萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章