07tcp粘包的原因以及处理的方法,网络三大编程函数的使用

1流协议与粘包

在这里插入图片描述
说明
tcp 字节流 无边界
udp 消息、数据报 有边界
对等方,一次读操作,不能保证完全把消息读完。
对方接受数据包的个数是不确定的。

产生粘包问题的原因
	1、SQ_SNDBUF 套接字本身有缓冲区 (发送缓冲区、接受缓冲区)
	2、tcp传送的端 mss大小限制
	3、链路层也有MTU大小限制,如果数据包大于>MTU要在IP层进行分片,导致消息分割。
	4、tcp的流量控制和拥塞控制,也可能导致粘包
	5、tcp延迟发送机制 等等
结论:tcp/ip协议,在传输层没有处理粘包问题。

粘包解决方案   
	本质上是要在应用层维护消息与消息的边界
	定长包
	包尾加\r\n(ftp)
	包头加上包体长度
	更复杂的应用层协议

例1:包头加上包体长度编程实践

包头加上包体长度
	发报文时,前四个字节长度(转成网络字节序)+包体
	收报文时,先读前四个字节,求出长度;根据长度读数据。

7client_stick1.c

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


#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>

/*

	包头+包体编程
*/

/*

	*******readn、writen、readline属于同一个系列,称为网络编程三大函数*******
	
1.1 read与write原型

  ssize_t  read(int fd, void* buf, size_t count);
  ssize_t 是有符号整数
  返回值如下:
	a)成功返回读取的字节数,这里可能等于 count 或者小于 count
	 	(当 count > 文件 size 的时候,返回实际读到的字节数);	
	b)刚开始读就遇到EOF 则返回 0;
	c)读取失败返回 -1, 并设置相应的 errno

	ssize_t write(int fd, const void *buf, size_t count); 
	返回值如下:
	
	a)成功返回写入的字节数,这里同上;
	b)写入失败返回 -1,并设置相应的 errno;
	c)当返回值为0 时,表示什么也没有写进去,这种情况在socket编程中出现可能是
	因为连接已关闭,在写磁盘文件的时候一般不会出现。
	


 1.2 为什么要封装一个readn 函数和 writen 函数,现有的read 函数和 write 含有有什么缺陷? 

    这个是因为在调用read(或 write)函数的时候,读(写)一次的返回值可能不是我们想到读
		的字节数(即read函数中的 count 参数),这经常在读取管道,或者网络数去时出现。


 1.3 readn 函数 和 writen 函数

	1.3.1 readn保证在没有遇到EOF的情况下,一定可以读取n个字节。它的返回值有三种:  
		a) >0,表示成功读取的字节数,如果小于n,说明中间遇到了EOF;
		b)==0 表示一开始读取就遇到EOF;
		c) -1 表示错误(这里的errno绝对不是EINTR)

	1.3.2 writen函数保证一定写满n个字节,返回值:
		a)n 表示写入成功n个字节
		b)-1 写入失败(这里也没有EINTR错误)
*/

/*
	tcp粘包处理:包头+包体长度
	

*/


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

struct packet
{
	int len;		//长度在前,首地址与结构体的首地址是一致的
	char buf[1024];	//数据在后
} packet;

/*
	使用说明:
		//1一次全部读走 //2次读完数据 //出错分析 //对方已关闭
	   
	思想:
		tcpip是流协议,不能保证1次读操作,能全部把报文读走,所以要循环
		读指定长度的数据。
	按照count大小读数据,若读取的长度ssize_t<count 说明读到了一个结束符,
	对方已关闭
	
	函数功能:
		从一个文件描述符中读取count个字符到buf中
	参数:
		@buf:接受数据内存首地址
		@count:接受数据长度
	返回值:
		@ssize_t:返回读的长度 若ssize_t<count 读失败失败
*/
ssize_t readn(int fd, void *buf, size_t count)
{
	size_t nleft = count;	//剩下需要读取的数据个数
	ssize_t nread;			//成功读取的字节数
	char * bufp = (char*)buf;//将参数接过来
	while (nleft > 0) 
	{
		//如果errno被设置为EINTR为被信号中断,如果是被信号中断继续,
		//不是信号中断则退出。
		 if ((nread = read(fd, bufp, nleft)) < 0) 
		 {
		 	//异常情况处理
		 	if (errno == EINTR) //读数据过程中被信号中断了
		 		continue;	//再次启动read
		 		//nread = 0;//等价于continue
		 	return -1;	
		 }else if (nread == 0)	//到达文件末尾EOF,数据读完(读文件、读管道、socket末尾、对端关闭)
		 	break;
		 bufp += nread;	//将字符串指针向后移动已经成功读取个数的大小。
		 nleft -=nread;	//需要读取的个数=需要读取的个数-已经成功读取的个数
	}
	return (count - nleft);//返回已经读取的数据个数
}

/*
	思想:tcpip是流协议,不能1次把指定长度数据,全部写完 
	按照count大小写数据
	若读取的长度ssize_t<count 说明读到了一个结束符,对方已关闭。
	
	
	函数功能:
		向文件描述符中写入count个字符
	函数参数:
		@buf:待写数据首地址
		@count:待写长度
	返回值:
		@ssize_t:返回写的长度 -1失败
*/
ssize_t writen(int fd, 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 ((nwritten < 0) && (errno == EINTR)) //读数据过程中被信号中断了
		 		continue;			//再次启动write
		 		//nwritten = 0;		//等价continue
		 	else
		 		return -1;
		 }
		 
		 bufp += nwritten;	//移动缓冲区指针
		 nleft -=nwritten;	//记录剩下未读取的数据
	}
	return count;//返回已经读取的数据个数
}


void test()
{
	int sockfd = 0;
	const char *serverip = "192.168.66.128";
	
	//创建socket
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		ERR_EXIT("socket()");
	//定义socket结构体 man 7 ip
	
	struct sockaddr_in srvsddr;
	srvsddr.sin_family = AF_INET;
	srvsddr.sin_port = htons(8001);//转化为网络字节序
	srvsddr.sin_addr.s_addr = inet_addr(serverip);
	//进程-》内核
	if( connect(sockfd, (struct sockaddr *)&srvsddr,sizeof(srvsddr)) < 0)
		ERR_EXIT("connect()");
	size_t n;
	struct packet sendbuf;
	struct packet recvbuf;
	memset(&sendbuf, 0, sizeof(sendbuf));
	memset(&recvbuf, 0, sizeof(recvbuf));
	
	while( fgets(sendbuf.buf, sizeof(sendbuf.buf), stdin) != NULL )
	{
		//获取字符串的大小
		n = strlen(sendbuf.buf);
		//将n转换为网络字节序
		sendbuf.len = htonl(n);
		//将获取的包发送
		writen(sockfd, &sendbuf, 4 + n);
		
		//第一次 先读取数据包的长度
		ssize_t ret = readn(sockfd, &recvbuf.len, 4);
		if (ret == -1) 
		{
			ERR_EXIT("readn");
		}else if (ret < 4)
		{
			printf("client close\n");
			break;
		}
		//转换为本地字节存储
		n = ntohl(recvbuf.len);
		//第二次 根据数据长度读取所有数据
		ret = readn(sockfd, recvbuf.buf, n);
		if (ret == -1) 
		{
			ERR_EXIT("readn");
		}else if (ret < n)
		{
			//因为告诉了数据的长度,所以ret等于n才是正确的
			printf("client close\n");//对于网络来说,意味着对端关闭了
			break;
		}
		
		fputs(recvbuf.buf, stdout);
		memset(&sendbuf, 0, sizeof(sendbuf));
		memset(&recvbuf, 0, sizeof(recvbuf));
	}
	//异常处理
	close(sockfd);
	return ;
}

int main()
{
	test();
	return 0;
}

8server_stick1.c

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <errno.h>
#include <unistd.h>
#include <signal.h>

/*

	包头+包体编程 
*/


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

struct packet
{
	int len;		//长度在前,首地址与结构体的首地址是一致的
	char buf[1024];	//数据在后
};


/*
	使用说明:
		//1一次全部读走 //2次读完数据 //出错分析 //对方已关闭
	   
	思想:
		tcpip是流协议,不能保证1次读操作,能全部把报文读走,所以要循环
		读指定长度的数据。
	按照count大小读数据,若读取的长度ssize_t<count 说明读到了一个结束符,
	对方已关闭
	
	函数功能:
		从一个文件描述符中读取count个字符到buf中
	参数:
		@buf:接受数据内存首地址
		@count:接受数据长度
	返回值:
		@ssize_t:返回读的长度 若ssize_t<count 读失败失败
*/
ssize_t readn(int fd, void *buf, size_t count)
{
	size_t nleft = count;	//剩下需要读取的数据个数
	ssize_t nread;			//成功读取的字节数
	char * bufp = (char*)buf;//将参数接过来
	while (nleft > 0) 
	{
		//如果errno被设置为EINTR为被信号中断,如果是被信号中断继续,
		//不是信号中断则退出。
		 if ((nread = read(fd, bufp, nleft)) < 0) 
		 {
		 	//异常情况处理
		 	if (errno == EINTR) //读数据过程中被信号中断了
		 		continue;	//再次启动read
		 		//nread = 0;//等价于continue
		 	return -1;	
		 }else if (nread == 0)	//到达文件末尾EOF,数据读完(读文件、读管道、socket末尾、对端关闭)
		 	break;
		 bufp += nread;	//将字符串指针向后移动已经成功读取个数的大小。
		 nleft -=nread;	//需要读取的个数=需要读取的个数-已经成功读取的个数
	}
	return (count - nleft);//返回已经读取的数据个数
}

/*
	思想:tcpip是流协议,不能1次把指定长度数据,全部写完 
	按照count大小写数据
	若读取的长度ssize_t<count 说明读到了一个结束符,对方已关闭。
	
	
	函数功能:
		向文件描述符中写入count个字符
	函数参数:
		@buf:待写数据首地址
		@count:待写长度
	返回值:
		@ssize_t:返回写的长度 -1失败
*/
ssize_t writen(int fd, 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 ((nwritten < 0) && (errno == EINTR)) //读数据过程中被信号中断了
		 		continue;			//再次启动write
		 		//nwritten = 0;		//等价continue
		 	else
		 		return -1;
		 }
		 
		 bufp += nwritten;	//移动缓冲区指针
		 nleft -=nwritten;	//记录剩下未读取的数据
	}
	return count;//返回已经读取的数据个数
}

void do_service(int conn)
{
	struct packet recvbuf;
	int n;
	while (1) 
	{
		memset(&recvbuf, 0, sizeof(recvbuf));
		//读取包头 4个字节
		int ret = readn(conn, &recvbuf.len, 4);
		if (ret == -1) 
			ERR_EXIT("read()");
		//如果读取的个数小于4,则客服端已经关闭
		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");
		else if (ret < n)
		{
			printf("client close\n");
			break;
		}
		//将数据打印输出
		fputs(recvbuf.buf, stdout);
		//将接收到的数据再直接发出去
		writen(conn, &recvbuf, 4 + n);//注意写数据的时候,多加4个字节	 
	}	
}

void test()
{
	int sockfd = 0;
	int conn = 0;
	const char *serverip = "192.168.66.128";
	
	//创建socket
	//创建socket
	if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
		ERR_EXIT("socket()");
	
	//定义socket结构体 man 7 ip
	struct sockaddr_in srvsddr;
	srvsddr.sin_family = AF_INET;
	srvsddr.sin_port = htons(8001);//转化为网络字节序
	//第一种
	#if 0
	srvsddr.sin_addr.s_addr = inet_addr(serverip);
	#endif
	//第二种
	#if 0
	//srvsddr.sin_addr.s_addr = htonl(INADDR_ANY);//INADDR_ANY 就是0.0.0.0 不存在网络字节序
	//srvaddr.sin_addr.s_addr = inet_addr(INADDR_ANY); //绑定本机的任意一个地址
	#endif
	//第三种
	//建议使用这种
	#if 1
	int ret;
	ret = inet_pton(AF_INET, serverip, &srvsddr.sin_addr);
	if (ret == 0)
	{
		ERR_EXIT("inet_pton()");
	}	
	#endif
	
	//设置端口复用
	//使用SO_REUSEADDR选项可以使得不必等待TIME_WAIT状态消失就可以重启服务器
	int optval = 1;
    if( setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) < 0)
		ERR_EXIT("setsockopt()");
		
	if(bind(sockfd, (struct sockaddr *)&srvsddr,sizeof(srvsddr)) <0 )
		ERR_EXIT("bind()");
		
	if(listen(sockfd, SOMAXCONN) < 0)
		ERR_EXIT("listen()");
	
	struct sockaddr_in peeraddr;
	socklen_t peerlen = sizeof(peeraddr);//值-结果参数
	pid_t pid;
	
	while (1) 
	{
		if ((conn = accept(sockfd, (struct sockaddr *)&peeraddr, &peerlen)) < 0) 
			ERR_EXIT("accept()");
			
		printf("客户端的ip:%s port:%d\n", inet_ntoa(peeraddr.sin_addr), ntohs(peeraddr.sin_port));
				
		pid = fork(); 
		if (pid == -1) 
		{
			ERR_EXIT("fork()");
		}
		if (pid == 0) 
		{
			//子进程不需要监听socket
			close(sockfd);
			do_service(conn);
			exit(EXIT_SUCCESS);
		}else 
		{
			close(conn);//父进程不需要连接socket
		} 
	}
	return ;
}

int main()
{
	test();
	return 0;
}

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【优质项目推荐】 1、项目代码均经过严格本地测试,运行OK,确保功能稳定后才上传平台。可放心下载并立即投入使用,若遇到任何使用问题,随时欢迎私信反馈与沟通,博主会第一时间回复。 2、项目适用于计算机相关专业(如计科、信息安全、数据科学、人工智能、通信、物联网、自动化、电子信息等)的在校学生、专业教师,或企业员工,小白入门等都适用。 3、该项目不仅具有很高的学习借鉴价值,对于初学者来说,也是入门进阶的绝佳选择;当然也可以直接用于 毕设、课设、期末大作业或项目初期立项演示等。 3、开放创新:如果您有一定基础,且热爱探索钻研,可以在此代码基础上二次开发,进行修改、扩展,创造出属于自己的独特应用。 欢迎下载使用优质资源!欢迎借鉴使用,并欢迎学习交流,共同探索编程的无穷魅力! 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip 基于业务逻辑生成特征变量python实现源码+数据集+超详细注释.zip
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值