TCP粘包分析與處理


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並不存在粘包的問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM