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;
}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值