我是一個剛開始接觸網絡服務器的小白,剛在寫一個socket數據接收程序中,發現TCP傳輸數據的時候會產生半包,粘包與分包的問題,網上有一個處理版本,挺不錯的。但是當我解決這個問題的時候,還是覺得應該自己寫一下自己的經驗。
那個博客網站是:http://blog.csdn.net/lfhfut/article/details/1139848
先來說說socket的半包,粘包與分包的問題
首先看兩個概念:
短連接:
連接->傳輸數據->關閉連接
HTTP是無狀態的,瀏覽器和服務器每進行一次HTTP操作,就建立一次連接,但任務結束就中斷連接。
也可以這樣說:短連接是指SOCKET連接後發送後接收完數據後馬上斷開連接。
長連接:
連接->傳輸數據->保持連接 -> 傳輸數據-> 。。。 ->關閉連接。
長連接指建立SOCKET連接後不管是否使用都保持連接,但安全性較差。
之所以出現粘包和半包現象,是因爲TCP當中,只有流的概念,沒有包的概念.
半包
指接受方沒有接受到一個完整的包,只接受了部分,這種情況主要是由於TCP爲提高傳輸效率,將一個包分配的足夠大,導致接受方並不能一次接受完。( 在長連接和短連接中都會出現)。
粘包與分包
指發送方發送的若干包數據到接收方接收時粘成一包,從接收緩衝區看,後一包數據的頭緊接着前一包數據的尾。出現粘包現象的原因是多方面的,它既可能由發送方造成,也可能由接收方造成。發送方引起的粘包是由TCP協議本身造成的,TCP爲提高傳輸效率,發送方往往要收集到足夠多的數據後才發送一包數據。若連續幾次發送的數據都很少,通常TCP會根據優化算法把這些數據合成一包後一次發送出去,這樣接收方就收到了粘包數據。接收方引起的粘包是由於接收方用戶進程不及時接收數據,從而導致粘包現象。這是因爲接收方先把收到的數據放在系統接收緩衝區,用戶進程從該緩衝區取數據,若下一包數據到達時前一包數據尚未被用戶進程取走,則下一包數據放到系統接收緩衝區時就接到前一包數據之後,而用戶進程根據預先設定的緩衝區大小從系統接收緩衝區取數據,這樣就一次取到了多包數據。分包是指在出現粘包的時候我們的接收方要進行分包處理。(在長連接中都會出現)
什麼時候需要考慮半包的情況?
從備註中我們瞭解到Socket內部默認的收發緩衝區大小大概是8K,但是我們在實際中往往需要考慮效率問題,重新配置了這個值,來達到系統的最佳狀態。
一個實際中的例子:用mina作爲服務器端,使用的緩存大小爲10k,這裏使用的是短連接,所有不用考慮粘包的問題。
問題描述:在併發量比較大的情況下,就會出現一次接受並不能完整的獲取所有的數據。
處理方式:
1.通過包頭 包長 包體的協議形式,當服務器端獲取到指定的包長時才說明獲取完整。
2.指定包的結束標識,這樣當我們獲取到指定的標識時,說明包獲取完整。
什麼時候需要考慮粘包的情況?
1.當時短連接的情況下,不用考慮粘包的情況
2.如果發送數據無結構,如文件傳輸,這樣發送方只管發送,接收方只管接收存儲就ok,也不用考慮粘包
3.如果雙方建立連接,需要在連接後一段時間內發送不同結構數據
處理方式:
接收方創建一預處理線程,對接收到的數據包進行預處理,將粘連的包分開
注:粘包情況有兩種,一種是粘在一起的包都是完整的數據包,另一種情況是粘在一起的包有不完整的包
備註:
一個包沒有固定長度,以太網限制在46-1500字節,1500就是以太網的MTU,超過這個量,TCP會爲IP數據報設置偏移量進行分片傳輸,現在一般可允許應用層設置8k(NTFS系)的緩衝區,8k的數據由底層分片,而應用看來只是一次發送。windows的緩衝區經驗值是4k,Socket本身分爲兩種,流(TCP)和數據報(UDP),你的問題針對這兩種不同使用而結論不一樣。甚至還和你是用阻塞、還是非阻塞Socket來編程有關。
1、通信長度,這個是你自己決定的,沒有系統強迫你要發多大的包,實際應該根據需求和網絡狀況來決定。對於TCP,這個長度可以大點,但要知道,Socket內部默認的收發緩衝區大小大概是8K,你可以用SetSockOpt來改變。但對於UDP,就不要太大,一般在1024至10K。注意一點,你無論發多大的包,IP層和鏈路層都會把你的包進行分片發送,一般局域網就是1500左右,廣域網就只有幾十字節。分片後的包將經過不同的路由到達接收方,對於UDP而言,要是其中一個分片丟失,那麼接收方的IP層將把整個發送包丟棄,這就形成丟包。顯然,要是一個UDP發包佷大,它被分片後,鏈路層丟失分片的機率就佷大,你這個UDP包,就佷容易丟失,但是太小又影響效率。最好可以配置這個值,以根據不同的環境來調整到最佳狀態。
send()函數返回了實際發送的長度,在網絡不斷的情況下,它絕不會返回(發送失敗的)錯誤,最多就是返回0。對於TCP你可以字節寫一個循環發送。當send函數返回SOCKET_ERROR時,才標誌着有錯誤。但對於UDP,你不要寫循環發送,否則將給你的接收帶來極大的麻煩。所以UDP需要用SetSockOpt來改變Socket內部Buffer的大小,以能容納你的發包。明確一點,TCP作爲流,發包是不會整包到達的,而是源源不斷的到,那接收方就必須組包。而UDP作爲消息或數據報,它一定是整包到達接收方。
我的TCP協議的數據傳輸,並且數據傳輸最大4096byte,所以應該不會產生半包情況,但粘包的兩種情況應該都會產生
所以我的解決代碼是:
struct NetHead
{
int32_t
nPacketSize; // NETHEAD_SIZE + nDataLen
uint8_t
isZip; //數據是否壓縮 0-未壓縮,1-壓縮
uint8_t
nMainCmd; // eNetTransProtc
uint16_t nSubCmd;// exmaple eClient_GCSVR_CMD
int32_t
nDataLen; //數據長度
};
void *recv_pthread(void *arg)
{
char buffer[MAX_BUFFER];
memset(buffer,0,MAX_BUFFER);
CDataPacket datapacket;
int pheadsize = sizeof(NetHead);
int size = 0;
int packetsize = 0;
int recvsize = 0;
while(1)
{
size = recv(sockfd[((Argmsg *)arg)->sid],(void *)(buffer+recvsize),pheadsize-recvsize,0);
recvsize += size;
if(recvsize < pheadsize)
{
printf("packet head is not complete\n");
continue;
}
printf("-----+++++recv packet head:%d\n",recvsize);
NetHead *pHead = (NetHead *)buffer;
packetsize = pHead->nPacketSize;
printf("packetsize = %d\n",packetsize);
recvsize = 0;
while(1)
{
size = recv(sockfd[((Argmsg *)arg)->sid],(void *)(buffer+recvsize+pheadsize),packetsize-recvsize-pheadsize,0);
recvsize += size;
if(recvsize+pheadsize == packetsize)
{
printf("packet is complete\n");
datapacket.fillPacket(buffer,packetsize);
printf("size:%d,zip:%d,Mcmd:%d,Scmd:%d\n",
pHead->nPacketSize,pHead->isZip,
pHead->nMainCmd,pHead->nSubCmd);
printf("receive massage is :%s time : %d s\n",datapacket.getDataBuffer(),time(NULL)-((Argmsg *)arg)->time);
recvsize = 0;
break;
}
}
}
}
自己也是剛剛接觸這些的小白,只是想在解決問題之後,總結下經驗,並且能夠給和我一樣碰到這類問題的朋友一些幫助,當然我的代碼可能還是存在一些問題,希望有朋友能夠幫我指出問題。希望大家多多給予建議