TCP為了保證可靠傳輸,盡量減少額外開銷(每次發(fā)包都要驗證)麻车,因此采用了流式傳輸缀皱,面向流的傳輸,相對于面向消息的傳輸动猬,可以減少發(fā)送包的數(shù)量啤斗,從而減少了額外開銷。但是赁咙,對于數(shù)據(jù)傳輸頻繁的程序來講钮莲,使用TCP可能會容易粘包。當然彼水,對接收端的程序來講崔拥,如果機器負荷很重,也會在接收緩沖里粘包凤覆。這樣链瓦,就需要接收端額外拆包,增加了工作量盯桦。因此慈俯,這個特別適合的是數(shù)據(jù)要求可靠傳輸,但是不需要太頻繁傳輸?shù)膱龊希▋纱尾僮鏖g隔100ms拥峦,具體是由TCP等待發(fā)送間隔決定的贴膘,取決于內(nèi)核中的socket的寫法)
(2)UDP,由于面向的是消息傳輸事镣,它把所有接收到的消息都掛接到緩沖區(qū)的接受隊列中步鉴,因此揪胃,它對于數(shù)據(jù)的提取分離就更加方便璃哟,但是氛琢,它沒有粘包機制,因此随闪,當發(fā)送數(shù)據(jù)量較小的時候阳似,就會發(fā)生數(shù)據(jù)包有效載荷較小的情況,也會增加多次發(fā)送的系統(tǒng)發(fā)送開銷(系統(tǒng)調(diào)用铐伴,寫硬件等)和接收開銷撮奏。因此,應該最好設置一個比較合適的數(shù)據(jù)包的包長当宴,來進行UDP數(shù)據(jù)的發(fā)送畜吊。(UDP最大載荷為1472,因此最好能每次傳輸接近這個數(shù)的數(shù)據(jù)量户矢,這特別適合于視頻玲献,音頻等大塊數(shù)據(jù)的發(fā)送,同時梯浪,通過減少握手來保證流媒體的實時性)
====================================================================
粘包問題分析與對策
TCP粘包是指發(fā)送方發(fā)送的若干包數(shù)據(jù)到接收方接收時粘成一包捌年,從接收緩沖區(qū)看,后一包數(shù)據(jù)的頭緊接著前一包數(shù)據(jù)的尾挂洛。
出現(xiàn)粘包現(xiàn)象的原因是多方面的礼预,它既可能由發(fā)送方造成,也可能由接收方造成虏劲。
什么時候需要考慮粘包問題
1如果利用tcp每次發(fā)送數(shù)據(jù)托酸,就與對方建立連接,然后雙方發(fā)送完一段數(shù)據(jù)后柒巫,就關(guān)閉連接励堡,這樣就不會出現(xiàn)粘包問題(因為只有一種包結(jié)構(gòu),類似于http協(xié)議)。
關(guān)閉連接主要是要雙方都發(fā)送close連接(參考tcp關(guān)閉協(xié)議)吻育。如:A需要發(fā)送一段字符串給B念秧,那么A與B建立連接,然后發(fā)送雙方都默認好的協(xié)議字符如"hello give me sth abour yourself"布疼,然后B收到報文后摊趾,就將緩沖區(qū)數(shù)據(jù)接收,然后關(guān)閉連接游两,這樣粘包問題不用考慮到砾层,因為大家都知道是發(fā)送一段字符。
2如果發(fā)送數(shù)據(jù)無結(jié)構(gòu)贱案,如文件傳輸肛炮,這樣發(fā)送方只管發(fā)送,接收方只管接收存儲就ok,也不用考慮粘包3如果雙方建立連接侨糟,需要在連接后一段時間內(nèi)發(fā)送不同結(jié)構(gòu)數(shù)據(jù)碍扔,如連接后,有好幾種結(jié)構(gòu):
1)"hellogive me sth abour yourself"
2)"Don'tgive me sth abour yourself"
那這樣的話秕重,如果發(fā)送方連續(xù)發(fā)送這個兩個包出去不同,接收方一次接收可能會是"hellogive me sth abour yourselfDon't give me sth abour yourself"這樣接收方就傻了,到底是要干嘛溶耘?不知道二拐,因為協(xié)議沒有規(guī)定這么詭異的字符串,所以要處理把它分包凳兵,怎么分也需要雙方組織一個比較好的包結(jié)構(gòu)百新,所以一般可能會在頭加一個數(shù)據(jù)長度之類的包,以確保接收庐扫。
TCP無保護消息邊界的解決
針對這個問題饭望,一般有3種解決方案:
(1)發(fā)送固定長度的消息
(2)把消息的尺寸與消息一塊發(fā)送
(3)使用特殊標記來區(qū)分消息間隔
為什么基于TCP的通訊程序需要進行封包和拆包
TCP是個"流"協(xié)議,所謂流聚蝶,就是沒有界限的一串數(shù)據(jù)杰妓,大家可以想想河里的流水,是連成一片的碘勉,其間是沒有分界線的巷挥。但一般通訊程序開發(fā)是需要定義一個個相互獨立的數(shù)據(jù)包的,比如用于登陸的數(shù)據(jù)包验靡,用于注銷的數(shù)據(jù)包倍宾。由于TCP"流"的特性以及網(wǎng)絡狀況,在進行數(shù)據(jù)傳輸時會出現(xiàn)以下幾種情況胜嗓。
假設我們連續(xù)調(diào)用兩次send分別發(fā)送兩段數(shù)據(jù)data1和data2,在接收端有以下幾種接收情況(當然不止這幾種情況,這里只列出了有代表性的情況).
A.先接收到data1,然后接收到data2.
B.先接收到data1的部分數(shù)據(jù),然后接收到data1余下的部分以及data2的全部.
C.先接收到了data1的全部數(shù)據(jù)和data2的部分數(shù)據(jù),然后接收到了data2的余下的數(shù)據(jù).
D.一次性接收到了data1和data2的全部數(shù)據(jù).
對于A這種情況正是我們需要的,不再做討論.對于B,C,D的情況就是大家經(jīng)常說的"粘包",就需要我們把接收到的數(shù)據(jù)進行拆包高职,拆成一個個獨立的數(shù)據(jù)包,為了拆包就必須在發(fā)送端進行封包辞州。
另:對于UDP來說就不存在拆包的問題,因為UDP是個"數(shù)據(jù)包"協(xié)議,也就是兩段數(shù)據(jù)間是有界限的怔锌,在接收端要么接收不到數(shù)據(jù)要么就是接收一個完整的一段數(shù)據(jù),不會少接收也不會多接收变过。
為什么會出現(xiàn)B.C.D的情況
1.由Nagle算法造成的發(fā)送端的粘包:Nagle算法是一種改善網(wǎng)絡傳輸效率的算法.簡單的說,當我們提交一段數(shù)據(jù)給TCP發(fā)送時,TCP并不立刻發(fā)送此段數(shù)據(jù),而是等待一小段時間,看看在等待期間是否還有要發(fā)送的數(shù)據(jù),若有則會一次把這兩段數(shù)據(jù)發(fā)送出去.這是對Nagle算法一個簡單的解釋,詳細的請看相關(guān)書籍. C和D的情況就有可能是Nagle算法造成的.
2.接收端接收不及時造成的接收端粘包:TCP會把接收到的數(shù)據(jù)存在自己的緩沖區(qū)中,然后通知應用層取數(shù)據(jù).當應用層由于某些原因不能及時的把TCP的數(shù)據(jù)取出來,就會造成TCP緩沖區(qū)中存放了幾段數(shù)據(jù).
怎樣封包和拆包
最初遇到"粘包"的問題時,我是通過在兩次send之間調(diào)用sleep來休眠一小段時間來解決埃元。這個解決方法的缺點是顯而易見的,使傳輸效率大大降低媚狰,而且也并不可靠岛杀。后來就是通過應答的方式來解決,盡管在大多數(shù)時候是可行的崭孤,但是不能解決B的那種情況类嗤,而且采用應答方式增加了通訊量,加重了網(wǎng)絡負荷. 再后來就是對數(shù)據(jù)包進行封包和拆包的操作糊肠。
封包
封包就是給一段數(shù)據(jù)加上包頭,這樣一來數(shù)據(jù)包就分為包頭和包體兩部分內(nèi)容了(以后講過濾非法包時封包會加入"包尾"內(nèi)容)。包頭其實上是個大小固定的結(jié)構(gòu)體遗锣,其中有個結(jié)構(gòu)體成員變量表示包體的長度货裹,這是個很重要的變量,其他的結(jié)構(gòu)體成員可根據(jù)需要自己定義黄伊。根據(jù)包頭長度固定以及包頭中含有包體長度的變量就能正確的拆分出一個完整的數(shù)據(jù)包泪酱。
拆包
對于拆包目前我最常用的是以下兩種方式:
(1)動態(tài)緩沖區(qū)暫存方式派殷。之所以說緩沖區(qū)是動態(tài)的是因為當需要緩沖的數(shù)據(jù)長度超出緩沖區(qū)的長度時會增大緩沖區(qū)長度还最。
大概過程描述如下:
A,為每一個連接動態(tài)分配一個緩沖區(qū),同時把此緩沖區(qū)和SOCKET關(guān)聯(lián),常用的是通過結(jié)構(gòu)體關(guān)聯(lián).
B,當接收到數(shù)據(jù)時首先把此段數(shù)據(jù)存放在緩沖區(qū)中.
C,判斷緩存區(qū)中的數(shù)據(jù)長度是否夠一個包頭的長度,如不夠,則不進行拆包操作.
D,根據(jù)包頭數(shù)據(jù)解析出里面代表包體長度的變量.
E,判斷緩存區(qū)中除包頭外的數(shù)據(jù)長度是否夠一個包體的長度,如不夠,則不進行拆包操作.
F,取出整個數(shù)據(jù)包.這里的"取"的意思是不光從緩沖區(qū)中拷貝出數(shù)據(jù)包,而且要把此數(shù)據(jù)包從緩存區(qū)中刪除掉.刪除的辦法就是把此包后面的數(shù)據(jù)移動到緩沖區(qū)的起始地址.
這種方法有兩個缺點.
1) 為每個連接動態(tài)分配一個緩沖區(qū)增大了內(nèi)存的使用.
2) 有三個地方需要拷貝數(shù)據(jù),一個地方是把數(shù)據(jù)存放在緩沖區(qū),一個地方是把完整的數(shù)據(jù)包從緩沖區(qū)取出來,一個地方是把數(shù)據(jù)包從緩沖區(qū)中刪除.第二種拆包的方法會解決和完善這些缺點.
前面提到過這種方法的缺點.下面給出一個改進辦法, 即采用環(huán)形緩沖.但是這種改進方法還是不能解決第一個缺點以及第一個數(shù)據(jù)拷貝,只能解決第三個地方的數(shù)據(jù)拷貝(這個地方是拷貝數(shù)據(jù)最多的地方).第2種拆包方式會解決這兩個問題.
環(huán)形緩沖實現(xiàn)方案是定義兩個指針,分別指向有效數(shù)據(jù)的頭和尾.在存放數(shù)據(jù)和刪除數(shù)據(jù)時只是進行頭尾指針的移動.
(2)利用底層的緩沖區(qū)來進行拆包
由于TCP也維護了一個緩沖區(qū),所以我們完全可以利用TCP的緩沖區(qū)來緩存我們的數(shù)據(jù),這樣一來就不需要為每一個連接分配一個緩沖區(qū)了毡惜。另一方面我們知道recv或者wsarecv都有一個參數(shù),用來表示我們要接收多長長度的數(shù)據(jù)拓轻。利用這兩個條件我們就可以對第一種方法進行優(yōu)化。
對于阻塞SOCKET來說经伙,我們可以利用一個循環(huán)來接收包頭長度的數(shù)據(jù)扶叉,然后解析出代表包體長度的那個變量,再用一個循環(huán)來接收包體長度的數(shù)據(jù)帕膜。