2022/8/12——UDP实例:基于UDP的网络聊天室

1.需求

1.如果有用户登录,其他用户可以收到这个人的登录信息

2.如果有人发送信息,其他用户可以收到这个人的群聊信息

’3.如果有人下线,其他用户可以收到这个人的下线信息

4.服务器可以发送系统信息

2.流程图

 

 

 3.服务器的搭建

1)宏函数、链表结构体及全局变量

//打印错误信息的宏函数
#define ERR_MSG(msg)  do{\
	fprintf(stderr, " __%d__ ", __LINE__);\
	perror(msg);\
}while(0)

//链表所用的结构体
typedef struct Node
{
	union{
		struct sockaddr_in add;
		int len;
	};
	char name[30];
	struct Node *next;
}Linklist;

//将套接字变量和链表头设置成全局变量便于在子线程使用
int sfd; 
Linklist* L; 

2)功能函数

2.1申请节点

Linklist * node_buy(struct sockaddr_in e,char name[])
{
	Linklist *p = (Linklist *)malloc(sizeof(Linklist));
	if(NULL == p)
	{
		printf("节点申请失败\n");
		return NULL;
	}
	p->add = e;
	strcpy(p->name,name);
	p->next = NULL;
	return p;
}

2.2判空

int list_empty(Linklist *L)
{
	//1表示空  0表示非空
	return NULL == L->next ? 1:0;
}

2.3尾插

int list_intsrt_tail(Linklist *L,struct sockaddr_in e,char name[])
{
	if(NULL==L)
	{
		printf("所给的链表不合法\n");
		return -1;
	}
	Linklist *p = node_buy(e,name);
	Linklist *q = L;
	while(q->next != NULL)
	{
		q = q->next;
	}
	q->next = p;
	L->len++;
	return 0;
}

2.4头删

int list_delete_head(Linklist *L)
{
	//判断逻辑
	if(NULL==L || list_empty(L))
	{
		printf("删除失败\n");
		return -1;
	}
	//头删
	Linklist *p = L->next;   //标记
	L->next = p->next;       //孤立
	free(p);                 //踢开
	p=NULL;
	//表的变化
	L->len--;
	printf("头删成功\n");
	return 0;
}

2.5销毁表

void list_delete_all(Linklist *L)
{
	if(NULL==L)
	{
		return;
	}
	//不断调用头删,将节点进行删除
	while(L->next != NULL)
	{
		list_delete_head(L);
	}
	//将头节点释放
	free(L);
	L=NULL;
	return;
}

3)子线程用于发送系统信息

void* callBack(void* arg)
{
	char str[128] = "";
	char buf[140] = "";
	while(1)
	{
		Linklist *q = L;    //遍历指针
		//从终端读取要发送的信息
		bzero(str, sizeof(str));
		fgets(str, sizeof(str), stdin);
		buf[strlen(str)-1] = 0;

		//转换格式
		bzero(buf, sizeof(buf));
		int res = sprintf(buf,"**system**%s",str);
		if(res < 0)
		{
			ERR_MSG("sprintf");
			return NULL;
		}

		while(q->next!=NULL)
		{
			q=q->next;
			//发送系统信息
			if(sendto(sfd,buf,sizeof(buf),0,(struct sockaddr*)&(q->add),sizeof(q->add))<0)
			{
				ERR_MSG("sendto");
				return NULL;
			}
		}
	}

}

4)主线程实现其余功能(登录请求、群聊请求和下线请求)

char buf[128] = "";
	char str[255] ="";
	int flag=0;
	int i=0;
	while(1)
	{
		flag=0;   //用于判断用户是否已经在线
		i=0;      //定位用户的位置
		bzero(buf, sizeof(buf));
		//接收
		if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, &addrlen) < 0)
		{
			ERR_MSG("recvfrom");
			return -1;
		}

		//遍历链表查看发送消息的用户是否已经在线
		Linklist *q = L;
		while(q != NULL)
		{
			if(q->add.sin_addr.s_addr==cin.sin_addr.s_addr && q->add.sin_port==cin.sin_port)
			{
				flag=1;      //查找到有该用户时将flag的值进行改变
				break;
			}
			q = q->next;
			i++;
		}
		if(flag==0)   //flag的值未改变表示该次信息是用户的登录
		{
			bzero(str,sizeof(str));
			//将新用户的信息打印到服务器终端
			printf("%s [%s : %d]登录\n",buf,inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
			//调用尾插函数
			list_intsrt_tail(L,cin,buf);

			//使用sprintf函数将上线提醒消息打包
			int res=sprintf(str,"%s上线了",buf);
			if (res<0)
			{
				ERR_MSG("sprintf");
				return -1;
			}

			//因为链表结点的增加是使用尾插实现
			//所以发送上线提醒只需遍历到最后第二个结点
			Linklist *q2 = L->next;
			while(q2->next != NULL)
			{
				//发送上线提醒
				if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(q2->add),sizeof(q2->add))<0)
				{
					ERR_MSG("sendto");
					return -1;
				}
				q2=q2->next;
			}
		}
		else
		{
			if(strcasecmp(buf,"quit") == 0)
			{
				//遍历链表找到存放申请下线的用户的节点和前驱节点
				Linklist *k = L->next;    //用户信息节点
				Linklist *kq = L;         //前驱节点
				bzero(str,sizeof(str));
				for(int j=0; j<i-1; j++)
				{
					k=k->next;
					kq=kq->next;
				}
				int res=sprintf(str,"%s下线了",k->name);
				if (res<0)
				{
					ERR_MSG("sprintf");
					return -1;
				}
				printf("%s [%s : %d]下线\n",k->name,inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
				
				//利用前驱节点删除该用户的信息节点
				kq->next = k->next;
				free(k);
				k=NULL;
				L->len--;

				//删除该用户节点后将下线信息发送给剩余用户
				Linklist *kp = L;
				while(kp->next != NULL)
				{
					kp=kp->next;
					if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(kp->add),sizeof(kp->add))<0)
					{
						ERR_MSG("sendto");
						return -1;
					}

				}
			}
			else
			{
				//将用户传输来的信息的格式做转换
				Linklist *k = L;
				bzero(str,sizeof(str));
				for(int j=0; j<i; j++)
				{
					k=k->next;
				}
				int res=sprintf(str,"%s说:%s",k->name,buf);
				if (res<0)
				{
					ERR_MSG("sprintf");
					return -1;
				}

				//遍历链表
				Linklist *k2 = L;
				while(k2->next!=NULL)
				{
					k2=k2->next;
					//跳过发送信息的用户本身
					if(k2->add.sin_addr.s_addr==cin.sin_addr.s_addr && k2->add.sin_port==cin.sin_port)
					{
						continue;
					}
					//给其他用户各发一份转换后的信息
					if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(k2->add),sizeof(k2->add))<0)
					{
						ERR_MSG("sendto");
						return -1;
					}
				}
			}
		}
	}

5)服务器完整代码

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <net/if.h>
#include <pthread.h>

//打印错误信息的宏函数
#define ERR_MSG(msg)  do{\
	fprintf(stderr, " __%d__ ", __LINE__);\
	perror(msg);\
}while(0)

//链表所用的结构体
typedef struct Node
{
	union{
		struct sockaddr_in add;
		int len;
	};
	char name[30];
	struct Node *next;
}Linklist;

//将套接字变量和链表头设置成全局变量便于在子线程使用
int sfd; 
Linklist* L; 


//申请节点
Linklist * node_buy(struct sockaddr_in e,char name[])
{
	Linklist *p = (Linklist *)malloc(sizeof(Linklist));
	if(NULL == p)
	{
		printf("节点申请失败\n");
		return NULL;
	}
	p->add = e;
	strcpy(p->name,name);
	p->next = NULL;
	return p;
}

//判空
int list_empty(Linklist *L)
{
	//1表示空  0表示非空
	return NULL == L->next ? 1:0;
}

//尾插
int list_intsrt_tail(Linklist *L,struct sockaddr_in e,char name[])
{
	if(NULL==L)
	{
		printf("所给的链表不合法\n");
		return -1;
	}
	Linklist *p = node_buy(e,name);
	Linklist *q = L;
	while(q->next != NULL)
	{
		q = q->next;
	}
	q->next = p;
	L->len++;
	return 0;
}

//头删
int list_delete_head(Linklist *L)
{
	//判断逻辑
	if(NULL==L || list_empty(L))
	{
		printf("删除失败\n");
		return -1;
	}
	//头删
	Linklist *p = L->next;   //标记
	L->next = p->next;       //孤立
	free(p);                 //踢开
	p=NULL;
	//表的变化
	L->len--;
	printf("头删成功\n");
	return 0;
}
//销毁表
void list_delete_all(Linklist *L)
{
	if(NULL==L)
	{
		return;
	}
	//不断调用头删,将节点进行删除
	while(L->next != NULL)
	{
		list_delete_head(L);
	}
	//将头节点释放
	free(L);
	L=NULL;
	return;
}

//子线程用于发送系统信息
void* callBack(void* arg)
{
	char str[128] = "";
	char buf[140] = "";
	while(1)
	{
		Linklist *q = L;    //遍历指针
		//从终端读取要发送的信息
		bzero(str, sizeof(str));
		fgets(str, sizeof(str), stdin);
		buf[strlen(str)-1] = 0;

		//转换格式
		bzero(buf, sizeof(buf));
		int res = sprintf(buf,"**system**%s",str);
		if(res < 0)
		{
			ERR_MSG("sprintf");
			return NULL;
		}

		while(q->next!=NULL)
		{
			q=q->next;
			//发送系统信息
			if(sendto(sfd,buf,sizeof(buf),0,(struct sockaddr*)&(q->add),sizeof(q->add))<0)
			{
				ERR_MSG("sendto");
				return NULL;
			}
		}
	}

}

int main(int argc, const char *argv[])
{
	//创建链表头
	L = (Linklist*)malloc(sizeof(Linklist));
	if(NULL == L)
	{
		printf("创建失败\n");
		return -1;
	}
	//初始化
	L->len=0;
	L->next=NULL;
	printf("聊天室服务器创建成功\n");

	//判断传参个数的合法性
	if(argc < 3)
	{
		printf("请输入IP和端口号");
	}

	//创建报式套接字
	sfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sfd < 0)
	{
		ERR_MSG("socket");
		return -1;
	}

	//验证端口号的合法性
	int port = atoi(argv[2]);
	if(port < 1024 || port > 49151)
	{
		printf("端口号不正确\n");
		return -1;
	}
	
	//填充服务器的IP地址以及端口号
	struct sockaddr_in sin;
	sin.sin_family 		= AF_INET;
	sin.sin_port 		= htons(port);
	sin.sin_addr.s_addr = inet_addr(argv[1]);
	

	//绑定服务器的地址信息结构体
	if(bind(sfd, (struct sockaddr*)&sin, sizeof(sin)) < 0)
	{
		ERR_MSG("bind");
		return -1;
	}

	struct sockaddr_in cin; 	//存储接收到的数据包来自哪里
	socklen_t addrlen = sizeof(cin);

	//创建一个线程
	pthread_t tid;
	if(pthread_create(&tid,NULL,callBack,NULL)!=0)
	{
		perror("pthread_create");
		return -1;
	}

	char buf[128] = "";
	char str[255] ="";
	int flag=0;
	int i=0;
	while(1)
	{
		flag=0;   //用于判断用户是否已经在线
		i=0;      //定位用户的位置
		bzero(buf, sizeof(buf));
		//接收
		if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&cin, &addrlen) < 0)
		{
			ERR_MSG("recvfrom");
			return -1;
		}

		//遍历链表查看发送消息的用户是否已经在线
		Linklist *q = L;
		while(q != NULL)
		{
			if(q->add.sin_addr.s_addr==cin.sin_addr.s_addr && q->add.sin_port==cin.sin_port)
			{
				flag=1;      //查找到有该用户时将flag的值进行改变
				break;
			}
			q = q->next;
			i++;
		}
		if(flag==0)   //flag的值未改变表示该次信息是用户的登录
		{
			bzero(str,sizeof(str));
			//将新用户的信息打印到服务器终端
			printf("%s [%s : %d]登录\n",buf,inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
			//调用尾插函数
			list_intsrt_tail(L,cin,buf);

			//使用sprintf函数将上线提醒消息打包
			int res=sprintf(str,"%s上线了",buf);
			if (res<0)
			{
				ERR_MSG("sprintf");
				return -1;
			}

			//因为链表结点的增加是使用尾插实现
			//所以发送上线提醒只需遍历到最后第二个结点
			Linklist *q2 = L->next;
			while(q2->next != NULL)
			{
				//发送上线提醒
				if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(q2->add),sizeof(q2->add))<0)
				{
					ERR_MSG("sendto");
					return -1;
				}
				q2=q2->next;
			}
		}
		else
		{
			if(strcasecmp(buf,"quit") == 0)
			{
				//遍历链表找到存放申请下线的用户的节点和前驱节点
				Linklist *k = L->next;    //用户信息节点
				Linklist *kq = L;         //前驱节点
				bzero(str,sizeof(str));
				for(int j=0; j<i-1; j++)
				{
					k=k->next;
					kq=kq->next;
				}
				int res=sprintf(str,"%s下线了",k->name);
				if (res<0)
				{
					ERR_MSG("sprintf");
					return -1;
				}
				printf("%s [%s : %d]下线\n",k->name,inet_ntoa(cin.sin_addr),ntohs(cin.sin_port));
				
				//利用前驱节点删除该用户的信息节点
				kq->next = k->next;
				free(k);
				k=NULL;
				L->len--;

				//删除该用户节点后将下线信息发送给剩余用户
				Linklist *kp = L;
				while(kp->next != NULL)
				{
					kp=kp->next;
					if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(kp->add),sizeof(kp->add))<0)
					{
						ERR_MSG("sendto");
						return -1;
					}

				}
			}
			else
			{
				//将用户传输来的信息的格式做转换
				Linklist *k = L;
				bzero(str,sizeof(str));
				for(int j=0; j<i; j++)
				{
					k=k->next;
				}
				int res=sprintf(str,"%s说:%s",k->name,buf);
				if (res<0)
				{
					ERR_MSG("sprintf");
					return -1;
				}

				//遍历链表
				Linklist *k2 = L;
				while(k2->next!=NULL)
				{
					k2=k2->next;
					//跳过发送信息的用户本身
					if(k2->add.sin_addr.s_addr==cin.sin_addr.s_addr && k2->add.sin_port==cin.sin_port)
					{
						continue;
					}
					//给其他用户各发一份转换后的信息
					if(sendto(sfd,str,sizeof(str),0,(struct sockaddr*)&(k2->add),sizeof(k2->add))<0)
					{
						ERR_MSG("sendto");
						return -1;
					}
				}
			}
		}
	}
	//关闭套接字
	close(sfd);
	//调用函数销毁链表
	list_delete_all(L);
	return 0;
}

4.客户端的搭建

 1)宏函数及全局变量

#define ERR_MSG(msg)  do{\
	fprintf(stderr, " __%d__ ", __LINE__);\
	perror(msg);\
}while(0)

int sfd;

2)子线程用于接收服务器发送的信息并将其打印到终端上

void* callBack(void* arg)
{
	char buf[128]="";
	struct sockaddr_in* sin=(struct sockaddr_in*)arg; 
	socklen_t addrlen = sizeof(*sin);

	while(1)
	{
		bzero(buf, sizeof(buf));
		//接收信息
		if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)sin,&addrlen) < 0)
		{
			ERR_MSG("recvfrom");
			return NULL;
		}
		fprintf(stderr,"%s\n",buf);
	}
}

3)发送上线申请

    char buf[128] = "";

	printf("请输入姓名>>>");
	fgets(buf, sizeof(buf), stdin);
	buf[strlen(buf)-1] = 0;

	//将数据包发送给服务器,所以地址信息结构体需要填服务器的信息
	if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
	{
		ERR_MSG("sendto");
		return -1;
	}
	printf("欢迎进入聊天室\n");

4)主线程发送聊天内容

    while(1)
	{
		//从终端读取要发送的信息
		bzero(buf, sizeof(buf));
		fgets(buf, sizeof(buf), stdin);
		buf[strlen(buf)-1] = 0;

		//将数据包发送给服务器,所以地址信息结构体需要填服务器的信息
		if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
		{
			ERR_MSG("sendto");
			return -1;
		}
		//判断输入的是否为quit,是则退出循环
		if(strcasecmp(buf,"quit") == 0)
		{
			printf("准备退出聊天室\n");
			break;
		}

		printf("信息已发送\n");
	}

5)客户端完整代码

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

//打印错误信息的宏函数
#define ERR_MSG(msg)  do{\
	fprintf(stderr, " __%d__ ", __LINE__);\
	perror(msg);\
}while(0)

int sfd;

//子线程用于接收服务器发送的信息并将其打印到终端上
void* callBack(void* arg)
{
	char buf[128]="";
	struct sockaddr_in* sin=(struct sockaddr_in*)arg;
	socklen_t addrlen = sizeof(*sin);

	while(1)
	{
		bzero(buf, sizeof(buf));
		//接收信息
		if(recvfrom(sfd, buf, sizeof(buf), 0, (struct sockaddr*)sin,&addrlen) < 0)
		{
			ERR_MSG("recvfrom");
			return NULL;
		}
		fprintf(stderr,"%s\n",buf);
	}
}

int main(int argc, const char *argv[])
{
	//判断传参个数的合法性
	if(argc < 3)
	{
		printf("请输入IP和端口号");
	}

	//创建报式套接字
	sfd = socket(AF_INET, SOCK_DGRAM, 0);
	if(sfd < 0)
	{
		ERR_MSG("socket");
		return -1;
	}

	//验证端口号的合法性
	int port = atoi(argv[2]);
	if(port < 1024 || port > 49151)
	{
		printf("端口号不正确\n");
		return -1;
	}

	//填充接收端的IP地址以及端口号 
	struct sockaddr_in sin;
	sin.sin_family 		= AF_INET;
	sin.sin_port 		= htons(port);
	sin.sin_addr.s_addr = inet_addr(argv[1]);

	socklen_t addrlen = sizeof(sin);

	char buf[128] = "";

	printf("请输入姓名>>>");
	fgets(buf, sizeof(buf), stdin);
	buf[strlen(buf)-1] = 0;

	//将数据包发送给服务器,所以地址信息结构体需要填服务器的信息
	if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
	{
		ERR_MSG("sendto");
		return -1;
	}
	printf("欢迎进入聊天室\n");

	//创建一个线程
	pthread_t tid;
	if(pthread_create(&tid,NULL,callBack,(void*)&sin)!=0)
	{
		perror("pthread_create");
		return -1;
	}

	while(1)
	{
		//从终端读取要发送的信息
		bzero(buf, sizeof(buf));
		fgets(buf, sizeof(buf), stdin);
		buf[strlen(buf)-1] = 0;

		//将数据包发送给服务器,所以地址信息结构体需要填服务器的信息
		if(sendto(sfd, buf, sizeof(buf), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0)
		{
			ERR_MSG("sendto");
			return -1;
		}
		//判断输入的是否为quit,是则退出循环
		if(strcasecmp(buf,"quit") == 0)
		{
			printf("准备退出聊天室\n");
			break;
		}

		printf("信息已发送\n");
	}
	//关闭套接字
	close(sfd);
	return 0;
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

命如星火

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值