Linux网络编程_多线程方式处理客户端数据(实现QQ聊天室)

举报
DS小龙哥 发表于 2022/05/25 23:37:16 2022/05/25
【摘要】 介绍Linux下多线程与网络编程使用,完成QQ群聊系统的实现。

任务1: TCP服务器多线程方式处理客户端数据

​ 可重入函数? 都是私有数据。

​ 不可重入函数? 全局变量 静态变量……

1.​ TCP服务器多线程方式实现数据转发。 类似与QQ群的效果。

(1)​ 需要对数据进行封包: {帧头,ID/姓名,源数据,数据校验和}

(2)​ 服务器需要保存已经连接上来的客户端。(推荐使用链表形式)

连接上一个客户端就将客户端的信息保存到链表节点里。

当前客户端断开连接,就删除链表节点。

(3)​ 支持上线和离线提醒。

#include <string.h>

//内存拷贝

void *memcpy(void *dest, const void *src, size_t n)

{

char *dest1=(char*)dest;

char *src1=(char*)src;

while(n--)

{

*dest1++=*src++;

}

}

(1)客户端

#include <stdio.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

/*--------------------------------*/
//定义socket数据传输的结构体
struct SocketPackageData
{
	unsigned char FrameHead[4];        //帧头固定为0xA1 0xA2 0xA3 0xA4
	char Name[50];            //存放名称
	char SrcDataBuffer[300];  //源数据
	unsigned int CheckSum;    //检验和
};


//数据包的封装与校验
int SetDataPackage(struct SocketPackageData *datapack,char *name,char *srcdata);
int CheckDataPackage(struct SocketPackageData data);
int connect_state=0;  //表示连接成功 0表示失败
struct SocketPackageData TX_PackageData;

void *SocketPthread_func(void*dev)
{
	int tcp_client_fd=*(int*)dev;

	while(1)
	{
		//printf("请输入要发送的数据:");
		scanf("%s",TX_PackageData.SrcDataBuffer);

		SetDataPackage(&TX_PackageData,TX_PackageData.Name,TX_PackageData.SrcDataBuffer);
		
		if(connect_state)
		{
			write(tcp_client_fd,&TX_PackageData,sizeof(struct SocketPackageData));
		}
		else
		{
			return NULL;
		}
	}
}


int main(int argc,char **argv)
{
	pthread_t thread_id;
	int tcp_client_fd;            //客户端套接字描述符
	int Server_Port;               //服务器端口号
	struct sockaddr_in tcp_server; //存放服务器的IP地址信息
	int  rx_len;
	
	if(argc!=4)
	{
		printf("TCP客户端形参格式:./tcp_client <服务器IP地址>  <服务器端口号>  <名称>\n");
		return -1;
	}
	
	Server_Port=atoi(argv[2]); //将字符串的端口号转为整型
	strcpy(TX_PackageData.Name,argv[3]);  //赋值名称
	
	/*1. 创建网络套接字*/
	tcp_client_fd=socket(AF_INET,SOCK_STREAM,0);
	if(tcp_client_fd<0)
	{
		printf("TCP服务器端套接字创建失败!\n");
		return -1;
	}
	
	/*2. 连接到指定的服务器*/
	tcp_server.sin_family=AF_INET; //IPV4协议类型
	tcp_server.sin_port=htons(Server_Port);//端口号赋值,将本地字节序转为网络字节序
	tcp_server.sin_addr.s_addr=inet_addr(argv[1]); //IP地址赋值
	
	if(connect(tcp_client_fd,(const struct sockaddr*)&tcp_server,sizeof(const struct sockaddr))<0)
	{
		 printf("TCP客户端: 连接服务器失败!\n");
		 return -1;
	}
	
	connect_state=1 ;//表示连接成功
	pthread_create(&thread_id,NULL,SocketPthread_func,(void*)&tcp_client_fd);
	
	fd_set readfds; //读事件的文件操作集合
	int select_state,rx_cnt; //接收返回值
	struct SocketPackageData RxTxData; //保存接收和发送的数据
	while(1)
	{
		/*5.1 清空文件操作集合*/
		FD_ZERO(&readfds);
        /*5.2 添加要监控的文件描述符*/
		FD_SET(tcp_client_fd,&readfds);
		/*5.3 监控文件描述符*/
		select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,NULL);
		if(select_state>0)//表示有事件产生
		{
			/*5.4 测试指定的文件描述符是否产生了读事件*/
			if(FD_ISSET(tcp_client_fd,&readfds))
			{
				/*5.5 读取数据*/
				rx_cnt=read(tcp_client_fd,&RxTxData,sizeof(struct SocketPackageData));
				
				if(rx_cnt==sizeof(struct SocketPackageData))
				{
					/*校验数据包是否正确*/
					if(CheckDataPackage(RxTxData)==0)
					{
						printf("%s:",RxTxData.Name);
						printf("%s\n",RxTxData.SrcDataBuffer);
					}
					else
					{
						printf("校验数据包不正确....\n");
					}
				}
				else
				{
					printf("数据大小接收不正确....\n");
				}
				
				if(rx_cnt==0)
				{
					printf("对方已经断开连接!\n");
					connect_state=0 ;//表示断开连接
					break;
				}
			}
		}
		else if(select_state<0) //表示产生了错误
		{
			printf("select函数产生异常!\n");
			break;
		}
	}
	
	/*4. 关闭连接*/
	close(tcp_client_fd);
	return 0;
}



/*
函数功能: 校验数据包是否正确
函数形参:   data :校验的数据包结构
函数返回值: 0表示成功 其他值表示失败
*/
int CheckDataPackage(struct SocketPackageData data)
{
	unsigned int checksum=0;
	int i;
	/*1. 判断帧头是否正确*/
	if(data.FrameHead[0]!=0xA1|| data.FrameHead[1]!=0xA2||
	   data.FrameHead[2]!=0xA3||data.FrameHead[3]!=0xA4)
	   {
		   return -1;
	   }
	/*2. 判断校验和*/
	//for(i=0;i<sizeof(data.SrcDataBuffer)/sizeof(data.SrcDataBuffer[0]);i++)
	for(i=0;i<300;i++)
	{
		checksum+=data.SrcDataBuffer[i];
	}
	if(checksum!=data.CheckSum)
	{
		return -1;
	}
	return 0;
}

/*
函数功能:   封装数据包的数据
函数形参:   *datapack :存放数据包结构体的地址
			char *name :用户名称
			char *srcdata :源数据字符串
函数返回值: 0表示成功 其他值表示失败
*/
int SetDataPackage(struct SocketPackageData *datapack,char *name,char *srcdata)
{
	/*1. 封装帧头*/
	datapack->FrameHead[0]=0xA1;
	datapack->FrameHead[1]=0xA2;
	datapack->FrameHead[2]=0xA3;
	datapack->FrameHead[3]=0xA4;
	
	/*2. 赋值名称*/
	strcpy(datapack->Name,name);
	strcpy(datapack->SrcDataBuffer,srcdata);
	
	/*3. 计算校验和*/
	datapack->CheckSum=0;
	int i;
	for(i=0;i<300;i++)
	{
		datapack->CheckSum+=datapack->SrcDataBuffer[i];
	}
}


(2)服务器

#include <stdio.h>
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>

/*--------------------------------*/
//定义socket数据传输的结构体
struct SocketPackageData
{
	unsigned char FrameHead[4];        //帧头固定为0xA1 0xA2 0xA3 0xA4
	char Name[50];            //存放名称
	char SrcDataBuffer[300];  //源数据
	unsigned int CheckSum;    //检验和
};


//定义服务器端存放客户端信息的结构
struct TCPClientInfo
{
	int clientfd;  //客户端的套接字描述符
	struct TCPClientInfo *next; //保存下一个节点的地址
};

//定义服务器端存放客户端信息的链表相关函数和变量
struct TCPClientInfo *ClientInfoListHead=NULL;  
int GetListNodeCnt(struct TCPClientInfo *head); //获取链表的节点数量
void DeleteListNode(struct TCPClientInfo *head,int clientfd); //删除
void AddListNode(struct TCPClientInfo *head,struct TCPClientInfo src_data); //添加
struct TCPClientInfo *CretorListHead(struct TCPClientInfo *head); //创建

//数据包的封装与校验
int SetDataPackage(struct SocketPackageData *datapack,char *name,char *srcdata);
int CheckDataPackage(struct SocketPackageData data);


/*
函数功能:  TCP客户端处理函数
*/
void *SocketPthread_func(void *arg)
{
	unsigned int  rx_cnt;
	
	struct SocketPackageData RxTxData; //保存接收和发送的数据
	struct TCPClientInfo *p=ClientInfoListHead;
	int tcp_client_fd=*(int*)arg; //客户端套接字描述符
	free(arg);      			 //释放占用的空间
	
	
	//添加新连接的客户端套接字到链表中
	struct TCPClientInfo src_data;
	src_data.clientfd=tcp_client_fd;
	AddListNode(ClientInfoListHead,src_data); //添加
	
	/*5. 数据通信*/
	fd_set readfds; //读事件的文件操作集合
	int select_state; //接收返回值
	int list_cnt=0;
	
	while(1)
	{
		/*5.1 清空文件操作集合*/
		FD_ZERO(&readfds);
        /*5.2 添加要监控的文件描述符*/
		FD_SET(tcp_client_fd,&readfds);
		/*5.3 监控文件描述符*/
		select_state=select(tcp_client_fd+1,&readfds,NULL,NULL,NULL);
		if(select_state>0)//表示有事件产生
		{
			/*5.4 测试指定的文件描述符是否产生了读事件*/
			if(FD_ISSET(tcp_client_fd,&readfds))
			{
				/*5.5 读取数据*/
				rx_cnt=read(tcp_client_fd,&RxTxData,sizeof(struct SocketPackageData));
				//printf("rx=%d,%s,%s,%X,%X,%X,%X\n",rx_cnt,RxTxData.Name,RxTxData.SrcDataBuffer,RxTxData.FrameHead[0],RxTxData.FrameHead[1],RxTxData.FrameHead[2],RxTxData.FrameHead[3]);
				if(rx_cnt==sizeof(struct SocketPackageData))
				{
					/*校验数据包是否正确*/
					if(CheckDataPackage(RxTxData)==0)
					{
						list_cnt=GetListNodeCnt(ClientInfoListHead);//获取链表节点
						printf("在线人数:%d\n",list_cnt);
						
						//轮询按照顺序进行转发
						while(p->next!=NULL)
						{
							p=p->next;
							if(p->clientfd!=tcp_client_fd)
							{
								write(p->clientfd,&RxTxData,sizeof(struct SocketPackageData));
							}
						}
						p=ClientInfoListHead; //将指针重新执行链表头
					}
					else
					{
						printf("数据包校验值不正确!\n");
					}
				}
				else
				{
					printf("数据包大小接收不正确!\n");
				}
				
				if(rx_cnt==0)
				{
					printf("对方已经断开连接!\n");
					break;
				}
			}
		}
		else if(select_state<0) //表示产生了错误
		{
			printf("select函数产生异常!\n");
			break;
		}
	}
	
	//删除已经断开的客户端套接字描述符
	DeleteListNode(ClientInfoListHead,tcp_client_fd); //删除
	/*6. 关闭连接*/
	close(tcp_client_fd);
}


/*
TCP服务器创建
*/
int main(int argc,char **argv)
{
	int tcp_server_fd; //服务器套接字描述符
	int *tcp_client_fd=NULL; //客户端套接字描述符
	struct sockaddr_in tcp_server;
	struct sockaddr_in tcp_client;
	socklen_t tcp_client_addrlen=0;
	int tcp_server_port;  //服务器的端口号
	
	 //判断传入的参数是否合理
	if(argc!=2)
	{
		printf("参数格式:./tcp_server <端口号>\n");
		return -1;
	}		
	tcp_server_port=atoi(argv[1]); //将字符串转为整数
	
	/*1. 创建网络套接字*/
	tcp_server_fd=socket(AF_INET,SOCK_STREAM,0);
	if(tcp_server_fd<0)
	{
		printf("TCP服务器端套接字创建失败!\n");
		return -1;
	}
	
	/*2. 绑定端口号,创建服务器*/
	tcp_server.sin_family=AF_INET; //IPV4协议类型
	tcp_server.sin_port=htons(tcp_server_port);//端口号赋值,将本地字节序转为网络字节序
	tcp_server.sin_addr.s_addr=INADDR_ANY; //将本地IP地址赋值给结构体成员
	
	if(bind(tcp_server_fd,(const struct sockaddr*)&tcp_server,sizeof(struct sockaddr))<0)
	{
		printf("TCP服务器端口绑定失败!\n");
		return -1;
	}
	
	/*3. 设置监听的客户端数量*/
	listen(tcp_server_fd,100);
	
	/*4. 等待客户端连接*/
	pthread_t thread_id;
	
	//创建链表头
	ClientInfoListHead=CretorListHead(ClientInfoListHead);
	while(1)
	{
		tcp_client_addrlen=sizeof(struct sockaddr);
		tcp_client_fd=malloc(sizeof(int)); //申请空间
		*tcp_client_fd=accept(tcp_server_fd,(struct sockaddr *)&tcp_client,&tcp_client_addrlen);
		if(*tcp_client_fd<0)
		{
			printf("TCP服务器:等待客户端连接失败!\n");
		}
		else
		{
			//打印连接的客户端地址信息
		    printf("已经连接的客户端信息: %s:%d\n",inet_ntoa(tcp_client.sin_addr),ntohs(tcp_client.sin_port));
			/*1. 创建线程*/
			if(pthread_create(&thread_id,NULL,SocketPthread_func,(void*)tcp_client_fd)==0)
			{
				/*2. 设置分离属性,让线程结束之后自己释放资源*/
				pthread_detach(thread_id);
			}
		}
	}
	return 0;
}


/*----------------------------------------------------------*/

/*
函数功能: 创建链表头
函数形参: 链表头指针
返回值  : 链表头指针
*/
struct TCPClientInfo *CretorListHead(struct TCPClientInfo *head)
{
	if(head==NULL)
	{
		head=malloc(sizeof(struct TCPClientInfo));
		head->next=NULL;  //下一个地址为空
	}
	return head;
}

/*
函数功能: 在链表结尾添加链表节点
函数形参: 
		head  :链表头指针
		加入的链表节点
*/
void AddListNode(struct TCPClientInfo *head,struct TCPClientInfo src_data)
{
	struct TCPClientInfo *p=head; //保存头地址
	struct TCPClientInfo *NewNode=NULL; //新节点
	/*1. 找到链表结尾的节点*/
	while(p->next!=NULL)
	{
		p=p->next; //移动到下一个节点
	}
	/*2. 申请新节点*/
	NewNode=malloc(sizeof(struct TCPClientInfo));
	if(NewNode==NULL)
	{
		printf("链表节点空间申请失败!\n");
		return;
	}
	//内存数据拷贝
	memcpy((void*)NewNode,(void*)&src_data,sizeof(struct TCPClientInfo));
	NewNode->next=NULL;
	
	/*3. 添加链表节点*/
	p->next=NewNode; 
}


/*
函数功能: 根据套接字描述符删除指定节点
函数形参: 
		head  :链表头指针
		clientfd :要删除的文件描述符
*/
void DeleteListNode(struct TCPClientInfo *head,int clientfd)
{
	/*1. 寻找套接字描述符对应链表节点*/
	struct TCPClientInfo *p=head;
	struct TCPClientInfo *tmp; //保存上一个节点的地址
	while(p->next!=NULL)
	{
		tmp=p;     //保存节点地址4
		p=p->next; //节点5
		if(p->clientfd==clientfd)
		{
			tmp->next=tmp->next->next;
			free(p); //释放p指向的节点
		}
	}
}

/*
函数功能: 获取节点的数量
函数形参: 
		head  :链表头指针
函数返回值: 节点的数量
*/
int GetListNodeCnt(struct TCPClientInfo *head)
{
	struct TCPClientInfo *p=head;
	int cnt=0;
	while(p->next!=NULL)
	{
		p=p->next;
		cnt++;
	}
	return cnt;
}


/*
函数功能: 校验数据包是否正确
函数形参:   data :校验的数据包结构
函数返回值: 0表示成功 其他值表示失败
*/
int CheckDataPackage(struct SocketPackageData data)
{
	unsigned int checksum=0;
	int i;
	/*1. 判断帧头是否正确*/
	if(data.FrameHead[0]!=0xA1|| data.FrameHead[1]!=0xA2||
	   data.FrameHead[2]!=0xA3||data.FrameHead[3]!=0xA4)
	   {
		   printf("帧头校验错误!\n");
		   return -1;
	   }
	/*2. 判断校验和*/
	//for(i=0;i<sizeof(data.SrcDataBuffer)/sizeof(data.SrcDataBuffer[0]);i++)
	for(i=0;i<300;i++)
	{
		checksum+=data.SrcDataBuffer[i];
	}
	if(checksum!=data.CheckSum)
	{
		printf("校验和错误!\n");
		return -1;
	}
	return 0;
}

/*
函数功能:   封装数据包的数据
函数形参:   *datapack :存放数据包结构体的地址
			char *name :用户名称
			char *srcdata :源数据字符串
函数返回值: 0表示成功 其他值表示失败
*/
int SetDataPackage(struct SocketPackageData *datapack,char *name,char *srcdata)
{
	/*1. 封装帧头*/
	datapack->FrameHead[0]=0xA1;
	datapack->FrameHead[1]=0xA2;
	datapack->FrameHead[2]=0xA3;
	datapack->FrameHead[3]=0xA4;
	
	/*2. 赋值名称*/
	strcpy(datapack->Name,name);
	strcpy(datapack->SrcDataBuffer,srcdata);
	
	/*3. 计算校验和*/
	datapack->CheckSum=0;
	int i;
	for(i=0;i<300;i++)
	{
		datapack->CheckSum+=datapack->SrcDataBuffer[i];
	}
}

2.​ 多线程方式实现文件发送。类似于飞秋。群发文件。

实现的功能:

(1)​  主函数创建服务器,循环等待客户端连接

(2)​  当有新的客户端连接之后,就创建新的线程,并且给客户端发送文件

(3)​  发送的文件内容需要进行封包处理,目的: 记录详细的文件信息。

帧头、文件总大小、当前数据大小、校验和、包的当前编号、文件名称。

(4)​  服务器给客户端发送文件数据,客户端收到数据需要进行解包校验

如果数据包接收正确,就需要向服务器发送状态信号,失败之后,服务器会触发重发上次数据包,直到数据包发送完毕。

如果数据包接收失败,也需要向服务器发送状态信息。

如果因为网络问题,客户端没有收到服务器发送数据包数据或者客户端给服务器发送的状态信号,服务器没有接收到。 服务器端5s之后就全部视为失败,就继续重发上次数据包。

如果是因为客户端给服务器发送的状态信号,导致服务器重发数据包,客户端需要判断当前数据包的编号是否与上一次相同,如果相同就不需要写入到文件,需要向服务器发送状态信号。

(5)​  重发5次,客户端还是没有收到,就可以终止传输。

3.​ 编写物联网设备云端的服务器(一对一聊天)

设计要求: 服务器实现(软件模拟)设备与软件之间完成通信。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。