SOCKET网络编程三:TCP粘包问题处理

TCP是字节流协议,原始数据之间是没有边界的。

发送端为了将多个发往接收端的包,更加高效的的发给接收端,于是采用了优化算法(Nagle算法),将多次间隔较小、数据量较小的数据,合并成一个数据量大的数据块,然后进行封包。

所谓粘包问题本质还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

粘包问题解决方案

0、发送和接收都定义固定大小。

1、发送方接收方都协商定义数据结构,每次发送后,接收方先解消息头确定,消息内容长度。

2、消息包之间定义明确结束标志,例如\n。

方案1实现:

客户端

#include<unistd.h>//read/write

#include <sys/types.h>
#include <sys/socket.h>


#include <netinet/in.h>
#include <arpa/inet.h>


#include <stdlib.h>
#include <stdio.h>
#include <errno.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 有符号整数
//size_t 无符号整数	
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)//被信号中断
			{
				continue;
			}
			else
				return -1;
		}
		else if(nread == 0)//对等方关闭
		{
			count = count - nleft;//已经读取的字节数
			break;
		}
		else
		{
			bufp += nread;
		    nleft -= nread;
		}
	}
	return count;
}

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;
		}
		else
		{
			bufp += nwritten;
		    nleft -= nwritten;
		}
	}
	return count;
}



int main(void)
{
	int sock;//被动套接字
	if(	(sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//创建套接字小于0表示失败
		ERR_EXIT("socket");

	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");


	struct packet sendbuf;
	struct packet recvbuf;

	memset(&recvbuf,0,sizeof(recvbuf));
	memset(&sendbuf,0,sizeof(sendbuf));

	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");
		}
		else if(ret < 4)
		{
			printf("srv_close\n");
			break;
		} 	
		else
		{
			n = ntohl(recvbuf.len);//转换主机字节序
			ret = readn(sock,&recvbuf.buf,n);
			if(ret == -1)
			{
				ERR_EXIT("read");
			}
			else if(ret < n)
			{
				printf("srv_close\n");
				break;
			} 	
			else
			{
				fputs(recvbuf.buf,stdout);//打印
							
				memset(&recvbuf,0,sizeof(recvbuf));
				memset(&sendbuf,0,sizeof(sendbuf));
			}
		}
	}

	close(sock);

	return 0;
}

服务端: 

#include<unistd.h>//read/write

#include <sys/types.h>
#include <sys/socket.h>


#include <netinet/in.h>
#include <arpa/inet.h>


#include <stdlib.h>
#include <stdio.h>
#include <errno.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 有符号整数
//size_t 无符号整数
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)//被信号中断
			{
				continue;
			}
			else
				return -1;
		}
		else if(nread == 0)//对等方关闭
		{
			count = count - nleft;//已经读取的字节数
			break;
		}
		else
		{
			bufp += nread;
		    nleft -= nread;
		}
	}
	return count;
}
 
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;
		}
		else
		{
			bufp += nwritten;
		    nleft -= nwritten;
		}
	}
	return count;
}

void echo_srv(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");
		}
		else if(ret < 4)
		{
			printf("client_close\n");
			break;
		} 	
		else
		{
			n = ntohl(recvbuf.len);
			ret = readn(conn,&recvbuf.buf,n);
			if(ret == -1)
			{
				ERR_EXIT("read");
			}
			else if(ret < n)
			{
				printf("client_close\n");
				break;
			} 	
			else
			{
				fputs(recvbuf.buf,stdout);//打印
				writen(conn,&recvbuf,4+n);//回射-这里!!
			}
		}
	}
}

int main(void)
{
	/*signal(SIGCHLD,SIG_IGN);*/
	int listenfd;//被动套接字
	if(	(listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//创建套接字小于0表示失败
/*	if(	(listenfd = socket(PF_INET,SOCK_STREAM,0))<0);*///让内核自己选定协议
		ERR_EXIT("socket");

	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);//表示本机的任意地址
	/*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");

	if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
		ERR_EXIT("bind");

	if(listen(listenfd,SOMAXCONN)<0)//监听后变为被动套接字
		ERR_EXIT("listen");
	
	struct sockaddr_in peeraddr;
	socklen_t peerlen = sizeof(peeraddr);
	int conn;//主动套接字

//父子进程可以共享文件描述符
	while(1)
	{
		if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
			ERR_EXIT("accept");

		printf("ip=%s,port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
		

		int pid = fork();
		if(pid == -1)
			ERR_EXIT("frok");
		if(pid == 0)
		{//子进程不需要处理监听套接字
			close(listenfd);
			echo_srv(conn);
			exit(EXIT_SUCCESS);//如果通信结束(客户端关闭)直接结束进程,否则子进程也会去accept
		}
		else
		{//父进程不需要处理连接套接字
			close(conn);
		}
	}

	return 0;
}


方案2实现:

客户端:

#include<unistd.h>//read/write

#include <sys/types.h>
#include <sys/socket.h>


#include <netinet/in.h>
#include <arpa/inet.h>


#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

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


//ssize_t 有符号整数
//size_t 无符号整数	
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)//被信号中断
			{
				continue;
			}
			else
				return -1;
		}
		else if(nread == 0)//对等方关闭
		{
			count = count - nleft;//已经读取的字节数
			break;
		}
		else
		{
			bufp += nread;
		    nleft -= nread;
		}
	}
	return count;
}

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;
		}
		else
		{
			bufp += nwritten;
		    nleft -= nwritten;
		}
	}
	return count;
}

//从套接口接收数据,但并不把数据从缓冲区清除
ssize_t recv_peek(int sockfd,void *buf,int len)
{
	while(1)
	{
		int ret = recv(sockfd,buf,len,MSG_PEEK);
		if((ret == -1) && (errno == EINTR))
			continue;

		printf("recv_peek :ret = %d,errno = %d\n",ret,errno);
		return ret;
	}
}


//偷窥方案:
ssize_t readline(int sockfd,void *buf,size_t maxlen)//一行最大的字节数
{//只要遇到/n就返回
	int ret;
	int nread;
	char *bufp = (char *)buf;
	int nleft = maxlen;
	while(1)
	{
		ret = recv_peek(sockfd,bufp,nleft);
		if(ret<0)
		{
			return ret;
		}
		else if(ret == 0)//表示对方关闭了套接口
		{
			return ret;
		}

		nread = ret;

		int i;
		for(i =0; i<nread;i++)
		{
			if(bufp[i] == '\n')//如果找到了结束符就将数据读取出来
			{
				ret = readn(sockfd,bufp,i+1);//下标i,总共有i+1个字符
				if(ret != i+1)
					exit(EXIT_FAILURE);
				return ret;
			}
		}
		//如果没有找到结束符,就读出来先缓存起来
		if(nread > nleft)//ret = recv_peek(sockfd,bufp,nleft);不可能
			exit(EXIT_FAILURE);

		nleft -= nread;//剩余的字节数

		ret = readn(sockfd,bufp,nread);
		if(ret != nread)//偷窥到的数据是可以全部读取出来的
		{
			exit(EXIT_FAILURE);
		}
		bufp += nread;
	}
	return -1;
}


int main(void)
{
	int sock;//被动套接字
	if(	(sock = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//创建套接字小于0表示失败
		ERR_EXIT("socket");

	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");


	char sendbuf[1024] = {0};
	char recvbuf[1024] = {0};

	while(fgets(sendbuf,sizeof(sendbuf),stdin)!=NULL)
	{
		writen(sock,sendbuf,strlen(sendbuf));//发送

		int ret = readline(sock,recvbuf,sizeof(recvbuf));
		if(ret == -1)
		{
			ERR_EXIT("readline");
		}
		else if(ret == 0)
		{
			printf("client_close\n");
			break;
		} 	
		
		fputs(recvbuf,stdout);//打印
							
		memset(recvbuf,0,sizeof(recvbuf));
		memset(sendbuf,0,sizeof(sendbuf));

	}
	close(sock);

	return 0;
}

服务端:

#include<unistd.h>//read/write

#include <sys/types.h>
#include <sys/socket.h>


#include <netinet/in.h>
#include <arpa/inet.h>


#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>

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

//ssize_t 有符号整数
//size_t 无符号整数
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)//被信号中断
			{
				continue;
			}
			else
				return -1;
		}
		else if(nread == 0)//对等方关闭
		{
			count = count - nleft;//已经读取的字节数
			break;
		}
		else
		{
			bufp += nread;
		    nleft -= nread;
		}
	}
	return count;
}
 
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;
		}
		else
		{
			bufp += nwritten;
		    nleft -= nwritten;
		}
	}
	return count;
}


//从套接口接收数据,但并不把数据从缓冲区清除
ssize_t recv_peek(int sockfd,void *buf,size_t len)
{
	while(1)
	{
		int ret = recv(sockfd,buf,len,MSG_PEEK);
		if((ret == -1) && (errno == EINTR))
			continue;
		printf("recv_peek :ret = %d,errno = %d\n",ret,errno);
		return ret;
	}
}


//偷窥方案:
ssize_t readline(int sockfd,void *buf,size_t maxline)//一行最大的字节数
{//只要遇到/n就返回
	int ret;
	int nread;
	char *bufp = (char *)buf;
	int nleft = maxline;
	while(1)
	{
		ret = recv_peek(sockfd,bufp,nleft);
		if(ret<0)
			return ret;
		else if(ret == 0)//表示对方关闭了套接口
			return ret;

		nread = ret;

		int i;
		for(i =0; i<nread;i++)
		{
			if(bufp[i] == '\n')//如果找到了结束符就将数据读取出来
			{
				ret = readn(sockfd,bufp,i+1);//下标i,总共有i+1个字符
				if(ret != i+1)
					exit(EXIT_FAILURE);
				return ret;
			}
		}
		//如果没有找到结束符,就读出来先缓存起来
		if(nread > nleft)//ret = recv_peek(sockfd,bufp,nleft);不可能
			exit(EXIT_FAILURE);

		nleft -= nread;//剩余的字节数

		ret = readn(sockfd,bufp,nread);
		if(ret != nread)//偷窥到的数据是可以全部读取出来的
		{
			exit(EXIT_FAILURE);
		}
		bufp += nread;
	}
	return -1;
}

void echo_srv(int conn)
{
	char recvbuf[1024];
	while(1)
	{
		memset(recvbuf,0,sizeof(recvbuf));
		int ret = readline(conn,recvbuf,1024);//按行接收

		if(ret == -1)
		{
			ERR_EXIT("readline");
		}
		if(ret == 0)
		{
			printf("client_close\n");
			break;
		} 	
	
		fputs(recvbuf,stdout);//打印
		writen(conn,recvbuf,strlen(recvbuf));//回射-这里!!
		
	}
}

int main(void)
{
	/*signal(SIGCHLD,SIG_IGN);*/
	int listenfd;//被动套接字
	if(	(listenfd = socket(PF_INET,SOCK_STREAM,IPPROTO_TCP))<0)//创建套接字小于0表示失败
/*	if(	(listenfd = socket(PF_INET,SOCK_STREAM,0))<0);*///让内核自己选定协议
		ERR_EXIT("socket");

	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);//表示本机的任意地址
	/*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");

	if(bind(listenfd,(struct sockaddr*)&servaddr,sizeof(servaddr))<0)
		ERR_EXIT("bind");

	if(listen(listenfd,SOMAXCONN)<0)//监听后变为被动套接字
		ERR_EXIT("listen");
	
	struct sockaddr_in peeraddr;
	socklen_t peerlen = sizeof(peeraddr);
	int conn;//主动套接字

//父子进程可以共享文件描述符
	while(1)
	{
		if((conn = accept(listenfd,(struct sockaddr*)&peeraddr,&peerlen))<0)
			ERR_EXIT("accept");

		printf("ip=%s,port=%d\n",inet_ntoa(peeraddr.sin_addr),ntohs(peeraddr.sin_port));
		

		int pid = fork();
		if(pid == -1)
			ERR_EXIT("frok");
		if(pid == 0)
		{//子进程不需要处理监听套接字
			close(listenfd);
			echo_srv(conn);
			exit(EXIT_SUCCESS);//如果通信结束(客户端关闭)直接结束进程,否则子进程也会去accept
		}
		else
		{//父进程不需要处理连接套接字
			close(conn);
		}
	}

	return 0;
}


 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值