TCP粘包現象
TCP粘包通俗來講,就是發送方發送的多個資料包,到接收方後粘連在一起,導緻資料包不能完整的展現發送的資料。
TCP粘包原因分析
導緻TCP粘包的原因,可能是發送方的原因,也有可能是接受方的原因。
發送方
由于TCP需要盡可能高效和可靠,是以TCP協定預設采用Nagle算法,以合并相連的小資料包,再一次性發送,以達到提升網絡傳輸效率的目的。但是接收方并不知曉發送方合并資料包,而且資料包的合并在TCP協定中是沒有分界線的,是以這就會導緻接收方不能還原其本來的資料包。
接收方
TCP是基于“流”的。網絡傳輸資料的速度可能會快過接收方處理資料的速度,這時候就會導緻,接收方在讀取緩沖區時,緩沖區存在多個資料包。在TCP協定中接收方是一次讀取緩沖區中的所有内容,是以不能反映原本的資料資訊。
解決TCP粘包
分析了産生TCP粘包的原因之後,針對發生的原因,針對性的采取解決方法。
禁用Negle算法
因為TCP協定采用Negle算法,導緻粘包。是以可以禁用Nagle算法。
const char chOpt = 1;
int nErr = setsockopt(m_socket, IPPROTO_TCP, TCP_NODELAY, &chOpt, sizeof(char));
if(nErr == -1)
{
TRACE( "setsockopt() error\n", WSAGetLastError());
return ;
}
這種方法雖然能一定程度上解決TCP粘包,但是并不能完全解決問題。因為接收方也是可能造成粘包的原因,這種方法隻是發送方有效。而且禁用Nagle算法,一定程度上使TCP傳輸效率降低了。是以,這并不是一種理想的方法。
PUSH标志
PUSH是TCP報頭中的一個标志位,發送方在發送資料的時候可以設定這個标志位。該标志通知接收方将接收到的資料全部送出給接收程序。這裡所說的資料包括與此PUSH包一起傳輸的資料以及之前就為該程序傳輸過來的資料。
當Server端收到這些資料後,它需要立刻将這些資料送出給應用層程序,而不再等待是否還有額外的資料到達。
設定PUSH标志也不能完全解決TCP粘包,隻是降低了接收方粘包的可能性。實際上現在的TCP協定棧基本上都可以自行處理這個問題,而不是交給應用層處理。是以設定PUSH标志,也不是一種理想的方法。
自定協定
自定協定,将資料包分為了封包和解包兩個過程。在發送方發送資料時,對發送的資料進行封包操作。在接收方接收到資料時對接收的資料包需要進行解包操作。
自定協定時,封包就是為發送的資料增加標頭,標頭包含資料的大小的資訊,資料就跟随在標頭之後。當然標頭也可以有其他的資訊,比如一些做校驗的資訊。這裡主要讨論TCP粘包的問題,是以不考慮其他的。
發送方封包
PACKAGE_HEAD pPackageHead; //PACKAGE_HEAD 標頭結構體
char PackageHead[1024];
int headLen = sizeof(PACKAGE_HEAD);
int packgeContextLen = strlen(packageContext); //packageContext 發送的資料
pPackageHead->nDataLen = packgeContextLen; //包的大小
char *packge = (char*)malloc(headLen + packgeContextLen); //包的記憶體配置設定
memset(packge, 0, headLen + packgeContextLen);
char *packgeCpy = (char*)memcpy(packge, (char*)&pPackageHead, headLen);//拷貝標頭
packgeCpy += headLen;
packge = (char*)memcpy(packgeCpy, (char*)&packageContext, packgeContextLen);//拷貝包内容
int ret = 0;
ret = send(m_hSocket, packge, headLen + packgeContextLen, 0); //發送包
if (ret == SOCKET_ERROR || ret == 0)
{
return ret;
}
接收方解包
char PackageHead[1024];
char PackageContext[1024*20];
int len;
PACKAGE_HEAD *pPackageHead; //PACKAGE_HEAD 標頭結構體
while( m_bClose == false )
{
memset(PackageHead, 0, sizeof(PACKAGE_HEAD));
len = ReceiveSize(m_TcpSock, (char*)PackageHead, sizeof(PACKAGE_HEAD)); //接收標頭
if( len == SOCKET_ERROR )
{
break;
}
if(len == 0)
{
break;
}
pPackageHead = (PACKAGE_HEAD *)PackageHead;
memset(PackageContext,0,sizeof(PackageContext));
if(pPackageHead->nDataLen>0) //根據標頭中的資料長度,接收資料
{
len = ReceiveSize(m_TcpSock, (char*)PackageContext,pPackageHead->nDataLen);
}
}
接收指定長度的資料函數
//接收指定長度的資料
int ReceiveSize(SOCKET m_hSocket, char* strData, int gLen)
{
if(strData == NULL)
return ERR_BADPARAM;
char *p = strData;
int len = gLen;
int ret = 0;
int returnlen = 0;
while( len > 0)
{
ret = recv( m_hSocket, p+(iLen-len), iLen-returnlen, 0);
if (ret == SOCKET_ERROR || ret == 0)
{
return ret;
}
len -= ret;
returnlen += ret;
}
return returnlen;
}
這樣就可以達到解決TCP粘包的問題。在實際使用中標頭還帶有更多的資訊,而且包尾可能還會帶上分隔符,在redis、FTP中就是這樣處理的。
UDP不存在粘包
由于UDP不是面向‘流’的,而且UDP是具有消息邊界的。也就是說UDP的發送的每一個資料包都是獨立的。是以UDP并不存在粘包的問題。