1. 程式人生 > >TCP 粘包解決

TCP 粘包解決

TCP 粘包:
什麼是粘包現象 :
  TCP粘包是指傳送方傳送的若干包資料到接收方接收時粘成一包,從接收緩衝區看,後一包資料的頭緊接著前一包資料的尾

為什麼出現粘包現象 :
(1) 傳送方原因   我們知道,TCP預設會使用Nagle演算法。而Nagle演算法主要做兩件事:
         1)只有上一個分組得到確認,才會傳送下一個分組;
         2)收集多個小分組,在一個確認到來時一起傳送。所以,正是Nagle演算法造成了傳送方有可能造成粘包現象。                               由於TCP協議本身的機制(面向連線的可靠地協議-三次握手機制)客戶端與伺服器會維持一個連線(Channel),資料在連線不斷開的情況下,可以持續不斷地將多個數據包發往伺服器

,但是如果傳送的網路資料包太小,那麼他本身會啟用Nagle演算法(可配置是否啟用)對較小的資料包進行合併(基於此,TCP的網路延遲要UDP的高些)然後再發送(超時或者包大小足夠)。那麼這樣的話,伺服器在接收到訊息(資料流)的時候就無法區分哪些資料包是客戶端自己分開發送的,這樣產生了粘包;伺服器在接收到資料庫後,放到緩衝區中,如果訊息沒有被及時從快取區取走,下次在取資料的時候可能就會出現一次取出多個數據包的情況,造成粘包現象(確切來講,對於基於TCP協議的應用,不應用包來描述,而應用流來描述),個人認為伺服器接收端產生的粘包應該與linux核心處理socket的方式select輪詢機制的線性掃描頻度無關。
(2) 接收方原因
    TCP接收到分組時,並不會立刻送至應用層處理,或者說,應用層並不一定會立即處理;實際上,TCP將收到的分組儲存至接收快取裡,然後應用程式主動從快取裡讀收到的分組。這樣一來,如果TCP接收分組的速度大於應用程式讀分組的速度多個包就會被存至快取,應用程式讀時,就會讀到多個首尾相接粘到一起的包

什麼時候需要處理粘包現象 :
(1) 如果傳送方傳送的多個分組本來就是同一個資料的不同部分,比如一個很大的檔案被分成多個分組傳送,這時,當然不需要處理粘包的現象;
(2) 但如果多個分組本毫不相干,甚至是並列的關係,我們就一定要處理粘包問題了。比如,我當時要接收的每個分組都是一個有固定格式的商品資訊

,如果不處理粘包問題,每個讀進來的分組我只會處理最前邊的那個商品,後邊的就會被丟棄。這顯然不是我要的結果。

如何處理粘包現象 :
(1) 傳送方
對於傳送方造成的粘包現象,我們可以通過關閉Nagle演算法來解決,使用TCP_NODELAY選項來關閉Nagle演算法。
(2) 接收方
遺憾的是TCP並沒有處理接收方粘包現象的機制,我們只能在應用層進行處理
(3) 應用層處理
應用層的處理簡單易行!並且不僅可以解決接收方造成的粘包問題,還能解決傳送方造成的粘包問題。
解決方法就是迴圈處理:應用程式在處理從快取讀來的分組時,讀完一條資料時,就應該迴圈讀下一條資料,直到所有的資料都被處理;但是如何判斷每條資料的長度呢?                                                                                                                                             解決方法:
       1、關閉TCP套接字的NAGL的演算法(效率低下)
       2、每傳送一次,等待對方收到才傳送下一條。(效率低下)            
       3、回答機制:
       4、在資料之前新增特殊訊號長度
             [ 5 ] abcefg
             [13] 1224323432423

   解決TCP粘包的伺服器的程式碼實現 : 

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
//伺服器
int main()
{
//1建立套接字
	int sock=socket(AF_INET,SOCK_STREAM,0);
	if(sock<0)
	{
		perror("socket fail");
		return -1;
	}
	socklen_t slen=sizeof(socklen_t);
	if(setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&slen,sizeof(slen))<0)
	{
		perror("setsockopt fail");
		return -1;
	}
//2繫結  /*先填充 再繫結bind*/
	struct sockaddr_in myaddr;
	bzero(&myaddr,sizeof(myaddr));
	myaddr.sin_family		=AF_INET;
	myaddr.sin_port			=htons(7979);
	myaddr.sin_addr.s_addr		=INADDR_ANY;
	if(bind(sock,(struct sockaddr*)&myaddr,sizeof(myaddr))<0)
	{
		perror("bind fail");
		return -1;
	}
//3監聽
	if(listen(sock,1)<0)
	{
		perror("listen fail");
		return -1;
	}
	int newsock=accept(sock,NULL,NULL);
	//sleep(5);//在10秒的睡眠時,客戶分三次傳送1 2 3字串。
	char buf[100]="";
	int ilen=0;
	short num=0;
	int total=0;
	char* p=NULL;
	while(1)
	{
		total=0;
		p=(char*)&num;
		bzero(buf,sizeof(buf));
		//讀取長度               [5] abcefg 相當於讀取的括號中的 5
		while(total<2)//1 2
		{
			ilen=recv(newsock,p+total,1,0);	
			total+=ilen;
		}
		//讀取字元內容           [5] abcefg 相當於讀取 abcefg 的資料
		total=0;
		while(total<num)
		{
			ilen=recv(newsock,buf+total,1,0);
			total+=ilen;
		}
		printf("收到%d位元組 內容:%s\n",num,buf);
	}
	close(sock);
	return 0;
}

   伺服器讀取內容的效果 , 完整的將客戶端傳送過來的資料進行了讀取 :

   客戶端傳送資料的程式碼 : 

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
//伺服器
int main()
{
//1建立
	int sock=socket(AF_INET,SOCK_STREAM,0);
	if(sock<0)
	{
		perror("fail\n");
		return -1;
	}
//2繫結
//3傳送連線
	struct sockaddr_in myaddr;
        bzero(&myaddr,sizeof(myaddr));
        myaddr.sin_family               =AF_INET;
        myaddr.sin_port                 =htons(7979);
        myaddr.sin_addr.s_addr          =inet_addr("127.0.0.1");
	
	if(connect(sock,(struct sockaddr*)&myaddr,sizeof(myaddr))<0)
	{
		perror("連線失敗");
		return -1;
	}
//4傳送資訊
	short ilen=5;                //每次傳送資料前先發送資料長度 如:  [5] abcefg
	send(sock,"12345",5,0);
	
	ilen=17;
	send(sock,&ilen,2,0);
	send(sock,"abcdefghijklmnopq",17,0);

	ilen=12;
	send(sock,&ilen,2,0);
	send(sock,"How are you!",12,0);

	sleep(5);
//3關閉
	close(sock);
}

   我們再來看看現象 : 

結果將資料完好的讀取了出來 , 是因為一個位元組傳送 , 一個位元組讀取的所以沒有出現粘包現象 :

 

我們再來看看粘包的現象  ,  這次傳送過來的是結構體資料 , 而伺服器需要讀取結構體的資料 .

結果 出現粘包現象 :

  怎麼解決上面出現的粘包現象 , 只需要將客戶端傳送的資料 sleep(2) , 再把伺服器的 sleep(5)註釋掉 , 解決了 , 每次等傳送一個結構體就等待 , 直到傳送完畢再發送第二個結構體 , 就沒有出現粘包現象 .

結果 資料完好的讀取了出來 :