天天看點

7.粘包的解決方案

1. 流式套接字(SOCK_STREAM)

       流式套接字類型用于套接字之間進行流式I/O操作.所謂流就是指在一對互相相連的套接字的一端所寫入的位元組流被另一端連續接入,接入方所收到的位元組流沒有邊界或分隔符,也沒有所謂的記錄長度,塊大小或資料分組概念.隻要有資料可讀,則資料将被傳回給接收方緩存.

       流式套接字的另一個特點就是資料嚴格按寫入時的順序被接收方所讀取.

2. 資料報套接字(SOCK_DGRAM)

      資料包套接字用于無連接配接通信.即通信前雙方不需要建立任何連接配接,隻需要建立一個非連接配接的資料報套接字.

      無連接配接通信時傳送資料不需要嚴格按照發送時的順序被接收方所接收.不需要嚴格可靠的傳輸,允許解釋部分資料,當資料丢失時,不試圖進行重傳資料.

      對資料的差錯的發現和糾正隻能通過應用層來進行保證.

3. 粘包問題解決

   (1)、定長包

   (2)、包尾加\r\n(ftp)

   (3)、標頭加上包體長度

   (4)、更複雜的應用層協定

           對于(2),缺點是如果消息本身含有\r\n字元,則也分不清消息的邊界。

           對于(1),即我們需要發送和接收定長包。

          TCP協定是面向流的,read和write調用的傳回值往往小于參數指定的位元組數。對于read調用,如果接收緩沖區中有20位元組,請求讀100個位元組,就會傳回20。對于write調用,如果請求寫100個位元組,而發送緩沖區中隻有20個位元組的空閑位置,那麼write會阻塞,直到把100個位元組全部交給發送緩沖區才傳回,但如果socket檔案描述符有O_NONBLOCK标志,則write不阻塞,直接傳回20。為避免這些情況幹擾主程式的邏輯,確定讀寫我們所請求的位元組數,

ssize_t readn(int fd,void *buf,size_t count)
{
	size_t nleft = count ; // 未讀取的資料
	ssize_t nread;// 已讀取的資料
	char *bufp= (char*)buf;
	while(nleft > 0)
	{
		if( (nread = read(fd,bufp,nleft)) < 0)
		{
			if( errno == EINTR)
				 nread = 0;//  繼續讀取資料
			else
				return -1;
		}
		else if( nread == 0) // 對方關閉或已經讀到eof
			break;
		bufp +=nread;
		nleft -= nread;
	
	}
	return count-nleft;
}

ssize_t writen(int fd,const void *buf,size_t count)
{
	size_t nleft=count;  // 未讀取的
	ssize_t nwritten;    // 已讀取的
 	char *bufp = (char*)buf;
 	
 	while(nleft > 0)
 	{
 		if((nwritten = write(fd,bufp,nleft)) < 0)
 		{
 			if( errno == EINTR)
 				continue;
 			else
 				return -1;
 		}
 		else if( nwritten == 0)
 			continue;
 		bufp  += nwritten;
 		nleft -= nwritten;
 	}
 	return count;
}
           

 說明: 以上兩個函數的主要作用就是對定長資料進行讀寫操作.

=======================================================

完整的用戶端伺服器程式如下:

// echoser.c 
/*
    處理粘包問題
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>

#define ERR_EXIT(m) \
		do{ \
			perror(m); \
			exit(EXIT_FAILURE); \
			}while(0)
			

ssize_t readn(int fd,void *buf,size_t count)
{
	size_t nleft = count ; // 未讀取的資料
	ssize_t nread;// 已讀取的資料
	char *bufp= (char*)buf;
	while(nleft > 0)
	{
		if( (nread = read(fd,bufp,nleft)) < 0)
		{
			if( errno == EINTR)
				 nread = 0;//  繼續讀取資料
			else
				return -1;
		}
		else if( nread == 0) // 對方關閉或已經讀到eof
			break;
		bufp +=nread;
		nleft -= nread;
	
	}
	return count-nleft;
}

ssize_t writen(int fd,const void *buf,size_t count)
{
	size_t nleft=count;  // 未讀取的
	ssize_t nwritten;    // 已讀取的
 	char *bufp = (char*)buf;
 	
 	while(nleft > 0)
 	{
 		if((nwritten = write(fd,bufp,nleft)) < 0)
 		{
 			if( errno == EINTR)
 				continue;
 			else
 				return -1;
 		}
 		else if( nwritten == 0)
 			continue;
 		bufp  += nwritten;
 		nleft -= nwritten;
 	}
 	return count;
}


void do_service(int conn)  
{  
    char recvbuf[1024];  
    while(1)  
    {  
        memset(recvbuf,0,sizeof(recvbuf));  
        int ret=readn(conn,recvbuf,sizeof(recvbuf));  
        if(ret == 0)  
        {  
            printf("client close\n");  
            break;
        }  
        else if(ret == -1)  
            ERR_EXIT("read err");  
        writen(conn,recvbuf,ret);  
    }  
} 


void handler(int sig)
{
	// wait(NULL); // 隻能等待第一個退出的子程序
	while(waitpid(-1,NULL,WNOHANG) > 0)
		;
}


int main()
{
	//signal(SIGCHLD,SIG_IGN);  // 忽略SIGCHLD信号
	signal(SIGCHLD,handler);
	int listenfd; 
	if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP) ) < 0)
			// listenfd = socket(AF_INET,SOCK_STREAM,0)
			ERR_EXIT("socket error");
	
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);// INADDR_ANY,這個宏表示本地的任意IP位址
	//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	//inet_aton("127.0.0.1",&servaddr.sin_addr);
	

	int on = 1;
	if( setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
		ERR_EXIT("setsockopt err");

	if( bind( listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
		ERR_EXIT("bind err");
	if( listen(listenfd,SOMAXCONN) < 0)  //  INADDR_ANY,這個宏表示本地的任意IP位址
			ERR_EXIT("lesten err");

	struct sockaddr_in peeraddr; // 傳出參數
	socklen_t peerlen = sizeof(peeraddr);// 傳入傳出參數,必須有初始值
	int conn; // 已經連接配接套接字(變為主動套接字,可以主動connect)
	
	pid_t pid;
	while(1)
	{
		
		if( (conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)// 3次握手完成
			ERR_EXIT("accept err");
		//通過peeraddr列印連接配接上來的用戶端ip和端口号
			printf("recv connect ip=%s ,port = %d \n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
		
		pid = fork();
		if( pid == -1)
			ERR_EXIT("fork err");
		if(pid == 0) /// 子程序
		{
			close(listenfd);
			do_service(conn);
			exit(EXIT_SUCCESS);
		}
		
		else /// 父程序
			close(conn);
	}
	
}
           

用戶端程式

/// echolic.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>

#define ERR_EXIT(m) \
		do{ \
			perror(m); \
			exit(EXIT_FAILURE); \
		}while(0)


ssize_t readn(int fd,void *buf,size_t count)
{
	size_t nleft = count ; // 未讀取的資料
	ssize_t nread;// 已讀取的資料
	char *bufp= (char*)buf;
	while(nleft > 0)
	{
		if( (nread = read(fd,bufp,nleft)) < 0)
		{
			if( errno == EINTR)
				 nread = 0;//  繼續讀取資料
			else
				return -1;
		}
		else if( nread == 0) // 對方關閉或已經讀到eof
			break;
		bufp +=nread;
		nleft -= nread;
	
	}
	return count-nleft;
}

ssize_t writen(int fd,const void *buf,size_t count)
{
	size_t nleft=count;  // 未讀取的
	ssize_t nwritten;    // 已讀取的
 	char *bufp = (char*)buf;
 	
 	while(nleft > 0)
 	{
 		if((nwritten = write(fd,bufp,nleft)) < 0)
 		{
 			if( errno == EINTR)
 				continue;
 			else
 				return -1;
 		}
 		else if( nwritten == 0)
 			continue;
 		bufp  += nwritten;
 		nleft -= nwritten;
 	}
 	return count;
}



int main()
{
	int sock;
	if( (sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
			ERR_EXIT("sock err");
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));

	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); 
	
	if( connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
			ERR_EXIT("connect error");
			

	char sendbuf[1024] = {0};  
    char recvbuf[1024] = {0};  
    while( fgets(sendbuf,sizeof(sendbuf),stdin) != NULL)  
    {  
        writen(sock,sendbuf,sizeof(sendbuf));  
        readn(sock,recvbuf,sizeof(recvbuf));  
        fputs(recvbuf,stdout);  
        memset(recvbuf,0,sizeof(recvbuf));    
        memset(sendbuf,0,sizeof(sendbuf));  
    }  
    close(sock);   
	return 0;
}
           

  說明: 調試以上兩個程式發現,這個這個有個小問題,就是不管每次發送多少個字元,發送過去的都是1024個位元組.

是以比較浪費網絡流量.另一個問題就是C/S兩端的定義的資料一樣大小,才能通信,如果一個1024,一個1023,就不能通信.

            一個比較簡單的解決方案就是定義一個包結構,指定發送資料的長度:

struct packet {
    int len;
    char buf[1024];
};
           

完整伺服器程式:

// echoser.c 
/*
    處理粘包問題
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>
#include <signal.h>

#define ERR_EXIT(m) \
		do{ \
			perror(m); \
			exit(EXIT_FAILURE); \
			}while(0)

struct packet
{
	int len;
	char buf[1024];
};			

ssize_t readn(int fd,void *buf,size_t count)
{
	size_t nleft = count ; // 未讀取的資料
	ssize_t nread;// 已讀取的資料
	char *bufp= (char*)buf;
	while(nleft > 0)
	{
		if( (nread = read(fd,bufp,nleft)) < 0)
		{
			if( errno == EINTR)
				 nread = 0;//  繼續讀取資料
			else
				return -1;
		}
		else if( nread == 0) // 對方關閉或已經讀到eof
			break;
		bufp +=nread;
		nleft -= nread;
	
	}
	return count-nleft;
}

ssize_t writen(int fd,const void *buf,size_t count)
{
	size_t nleft=count;  // 未讀取的
	ssize_t nwritten;    // 已讀取的
 	char *bufp = (char*)buf;
 	
 	while(nleft > 0)
 	{
 		if((nwritten = write(fd,bufp,nleft)) < 0)
 		{
 			if( errno == EINTR)
 				continue;
 			else
 				return -1;
 		}
 		else if( nwritten == 0)
 			continue;
 		bufp  += nwritten;
 		nleft -= nwritten;
 	}
 	return count;
}

/*
void do_service(int conn)  
{  
    char recvbuf[1024];  
    while(1)  
    {  
        memset(recvbuf,0,sizeof(recvbuf));  
        int ret=readn(conn,recvbuf,sizeof(recvbuf));  
        if(ret == 0)  
        {  
            printf("client close\n");  
            break;
        }  
        else if(ret == -1)  
            ERR_EXIT("read err");  
        writen(conn,recvbuf,ret);  
    }  
} 
*/

void do_service(int conn)
{
	struct packet recvbuf;
	int n;
	while(1)
	{
		memset(&recvbuf,0,sizeof(recvbuf));
		int ret = readn(conn,&recvbuf.len,4);// 先接收資料長度
		if(ret == -1)
			ERR_EXIT("read error");
		else if(ret <4) // 用戶端關閉
		{
			printf("client close\n");
			break;
		}
		
		n=ntohl(recvbuf.len);// 網絡位元組序轉換成主機位元組序
		ret = readn(conn,recvbuf.buf,n);// 再接收(讀取)資料,讀取的資料長度已确定
		if(ret == 1)
			ERR_EXIT("read err");
		if( ret < n)//  不足n個位元組序,關閉,也就時隻能讀取定長資料
		{
			printf("client close \n");
			break;
		}
		fputs(recvbuf.buf,stdout);
		writen(conn,&recvbuf,4+n); ///回射回去的資料長度同樣是n+4 
	}
}

void handler(int sig)
{
	// wait(NULL); // 隻能等待第一個退出的子程序
	while(waitpid(-1,NULL,WNOHANG) > 0)
		;
}


int main()
{
	//signal(SIGCHLD,SIG_IGN);  // 忽略SIGCHLD信号
	signal(SIGCHLD,handler);
	int listenfd; 
	if( (listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP) ) < 0)
			// listenfd = socket(AF_INET,SOCK_STREAM,0)
			ERR_EXIT("socket error");
	
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));
	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);// INADDR_ANY,這個宏表示本地的任意IP位址
	//servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
	//inet_aton("127.0.0.1",&servaddr.sin_addr);
	

	int on = 1;
	if( setsockopt(listenfd,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(on)) < 0)
		ERR_EXIT("setsockopt err");

	if( bind( listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
		ERR_EXIT("bind err");
	if( listen(listenfd,SOMAXCONN) < 0)  //  INADDR_ANY,這個宏表示本地的任意IP位址
			ERR_EXIT("lesten err");

	struct sockaddr_in peeraddr; // 傳出參數
	socklen_t peerlen = sizeof(peeraddr);// 傳入傳出參數,必須有初始值
	int conn; // 已經連接配接套接字(變為主動套接字,可以主動connect)
	
	pid_t pid;
	while(1)
	{
		
		if( (conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen)) < 0)// 3次握手完成
			ERR_EXIT("accept err");
		//通過peeraddr列印連接配接上來的用戶端ip和端口号
			printf("recv connect ip=%s ,port = %d \n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
		
		pid = fork();
		if( pid == -1)
			ERR_EXIT("fork err");
		if(pid == 0) /// 子程序
		{
			close(listenfd);
			do_service(conn);
			exit(EXIT_SUCCESS);
		}
		
		else /// 父程序
			close(conn);
	}
	
}


           

用戶端程式:

/// echolic.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string.h>

#define ERR_EXIT(m) \
		do{ \
			perror(m); \
			exit(EXIT_FAILURE); \
		}while(0)

struct packet
{
	int len;
	char buf[1024];
};


ssize_t readn(int fd,void *buf,size_t count)
{
	size_t nleft = count ; // 未讀取的資料
	ssize_t nread;// 已讀取的資料
	char *bufp= (char*)buf;
	while(nleft > 0)
	{
		if( (nread = read(fd,bufp,nleft)) < 0)
		{
			if( errno == EINTR)
				 nread = 0;//  繼續讀取資料
			else
				return -1;
		}
		else if( nread == 0) // 對方關閉或已經讀到eof
			break;
		bufp +=nread;
		nleft -= nread;
	
	}
	return count-nleft;
}

ssize_t writen(int fd,const void *buf,size_t count)
{
	size_t nleft=count;  // 未讀取的
	ssize_t nwritten;    // 已讀取的
 	char *bufp = (char*)buf;
 	
 	while(nleft > 0)
 	{
 		if((nwritten = write(fd,bufp,nleft)) < 0)
 		{
 			if( errno == EINTR)
 				continue;
 			else
 				return -1;
 		}
 		else if( nwritten == 0)
 			continue;
 		bufp  += nwritten;
 		nleft -= nwritten;
 	}
 	return count;
}



int main()
{
	int sock;
	if( (sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP)) < 0)
			ERR_EXIT("sock err");
	struct sockaddr_in servaddr;
	memset(&servaddr,0,sizeof(servaddr));

	servaddr.sin_family = AF_INET;
	servaddr.sin_port = htons(5188);
	servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); 
	
	if( connect(sock,(struct sockaddr*)&servaddr,sizeof(servaddr)) < 0)
			ERR_EXIT("connect error");
			
	
	struct packet sendbuf;
	struct packet recvbuf;
	memset(&sendbuf,0,sizeof(sendbuf));
	memset(&recvbuf,0,sizeof(recvbuf));
	
	int n;
    while( fgets(sendbuf.buf,sizeof(sendbuf.buf),stdin) != NULL)  
    {  
    	n = strlen(sendbuf.buf);
    	sendbuf.len = htonl(n); // 轉換成網絡位元組序
    	writen(sock,&sendbuf,4+n);
    	
    	
    	
    	//接收部分程式和伺服器的接收程式是一樣的
    	int ret = readn(sock,&recvbuf.len,4);// 先接收資料長度
		if(ret == -1)
			ERR_EXIT("read error");
		else if(ret <4) // 用戶端關閉
		{
			printf("client close\n");
			break;
		}
		
		n=ntohl(recvbuf.len);// 網絡位元組序轉換成主機位元組序
		ret = readn(sock,recvbuf.buf,n);// 再接收(讀取)資料,讀取的資料長度已确定
		if(ret == 1)
			ERR_EXIT("read err");
		if( ret < n)//  不足n個位元組序,關閉,也就時隻能讀取定長資料
		{
			printf("client close \n");
			break;
		}
		fputs(recvbuf.buf,stdout);
    	
      	memset(&sendbuf,0,sizeof(sendbuf));
		memset(&recvbuf,0,sizeof(recvbuf));
    }  
    close(sock);   
	return 0;
}
           

繼續閱讀