项目要求
***利用UDP协议,实现一套聊天室软件。服务器端记录客户端的地址,客户端发送消息后,服务器群发给各个客户端软件。
一定要根据项目要求去编写代码。。。。
理解
网络聊天室相当于我们手机QQ里的群聊,一个人发送消息,群聊中已经登录的用户都可以接受到消息。A退出群聊,群里登录的人就可以看到A已经退出群聊。B加入群聊,已经登录的用户也会看到B加入群聊。
问题思考
1.如何去实现通信?
我们学过网络间进程通信(服务器与客户端),这样我们就可以接受发数据了。
2.服务器要做的事?
收到客户端的消息,执行对应的操作。
●有几种消息类型需要实现?
登录:服务器存储新的客户端的地址。把某个客户端登录的消息发给其它客户端。
聊天:服务器只需要把某个客户端的聊天消息转发给所有其它客户端。
退出:服务器删除退出客户端的地址,并把退出消息发送给其它客户端。
3.客户端要做的事?
连接服务器成功后,给服务器发送一个消息,表示你已经加入群聊,服务器接收到消息给各个已经登录的用户发送**用户加入群聊。从终端获取你发送的消息给服务器。客户端退出时给服务器发送消息。
解决问题:
1.服务器如何存储客户端的地址?
数据结构中的链式存储。
2.如何把聊天消息转发给其他用户?
通过遍历链表发送消息。
3.客户端会不会知道其它客户端地址?
UDP客户端不会直接互连,所以不会获知其它客户端地址,所有客户端地址存储在服务器端。
4.客户端如何同时处理发送和接收?
客户端不仅需要读取服务器消息,而且需要发送消息。读取需要调用recvfrom,发送需要先调用gets,两个都是阻塞函数。所以必须使用多任务来同时处理,可以使用多进程或者多线程来处理。
重点来啦,要先画出程序流程图再去写代码
服务器端
客户端
两个结构体:
一个用于链表存储客户端的地址。
一个用于客户端消息的发送(一个是要执行的操作类型,一个是用户名,一个是消息正文)。
直接上代码
head.h(头文件)
#ifndef __HEAD_H__
#define __HEAD_H__
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
enum type_t
{
Login,
Chat,
Quit,
};
//定义消息结构体
typedef struct mag_t
{
int type;//消息类型
char name[32];//用户名
char text[128];//消息内容
}MSG_t;
//链表的节点
typedef struct node_t
{
struct sockaddr_in caddr;
struct node_t *next;
}list_t;
#endif
Server
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <pthread.h>
#include "head.h"
struct sockaddr_in serveraddr, caddr;
//创建有头单向链表
list_t *createList(void);
int login_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr);
int chat_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr);
int quit_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr);
void *pthread(void *arg)
{
MSG_t msg;
int sockfd=(*(int *)arg);
msg.type=Chat;
strcpy(msg.name,"server");
while(1)
{
fgets(msg.text,sizeof(msg.text),stdin);
if(msg.text[strlen(msg.text)-1]=='\n')
msg.text[strlen(msg.text)-1]='\0';
sendto(sockfd,&msg, sizeof(msg), 0,
(struct sockaddr *)&serveraddr,sizeof(serveraddr));
}
pthread_exit(NULL);
}
int main(int argc, char const *argv[])
{
if(argc != 2)
{
printf("please input %s <ip> <port>\n",argv[0]);
return -1;
}
//1.创建套接字
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
//填充服务器端ip和端口
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[1]));
serveraddr.sin_addr.s_addr = inet_addr("0.0.0.0");
socklen_t len = sizeof(caddr);
//2.绑定
if (bind(sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) < 0)
{
perror("bind err.");
return -1;
}
//创建链表
list_t *p=createList();
//创建线程-服务器端发送消息
pthread_t tid;
pthread_create(&tid,NULL,pthread,&sockfd);
pthread_detach(tid);
//循环收发消息
MSG_t msg;
int recvbyte;
while (1)
{
//接收 recvfrom
recvbyte = recvfrom(sockfd,&msg, sizeof(msg), 0,
(struct sockaddr *)&caddr, &len);
if (recvbyte < 0)
{
perror("recvfrom err.");
return -1;
}
switch(msg.type)
{
case Login:
login_client(sockfd,msg,p,caddr);
break;
case Chat:
chat_client(sockfd,msg,p,caddr);
break;
case Quit:
quit_client(sockfd,msg,p,caddr);
break;
}
}
close(sockfd);
return 0;
}
//创建有头单向链表
list_t *createList(void)
{
//1.创建一个无效头节点:数据域无效,指针域有效
list_t *p=(list_t *)malloc(sizeof(list_t));
if(NULL == p)
{
perror("malloc head node err.");
return NULL;
}
//2.初始化节点 空链表
p->next=NULL;
return p;
}
//1.客户端登录-服务器工作:
//1》遍历链表,将谁登录的消息发送所有已经登录的用户
//2》将新登录的客户端的ip和端口保存到链表中
int login_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr)
{
list_t *pnew=NULL;
//1》遍历链表,将谁登录的消息发送所有已经登录的用户
sprintf(msg.text,"%s login.",msg.name);
while(p->next != NULL)
{
p=p->next;
sendto(sockfd,&msg,sizeof(msg),0,\
(struct sockaddr *)&(p->caddr),sizeof(p->caddr));
}
//2》将新登录的客户端的ip和端口保存到链表中
//1-创建新节点保存
pnew=(list_t *)malloc(sizeof(list_t));
if(NULL == pnew)
{
perror("malloc new node err.");
return -1;
}
//2-初始化
pnew->caddr=caddr;
pnew->next=NULL;
//3-将节点连接到链表最后 p保存的链表最后一个节点的地址
//直接链接最后就可以
p->next=pnew;
return 0;
}
//2.聊天-服务工作:
//1》将消息发送给所有除自己已经登录的用户
int chat_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr)
{
//1》将消息发送给所有除自己已经登录的用户
//1-遍历链表,只有不是自己的ip和端口就发送消息
while(p->next != NULL)
{
p=p->next;
if(memcmp(&(p->caddr),&caddr,sizeof(caddr)) != 0)
{
sendto(sockfd,&msg,sizeof(msg),0,\
(struct sockaddr *)&(p->caddr),sizeof(p->caddr));
}
}
return 0;
}
//3。客户端推出-服务器工作
//1》将推出的用户消息发送给还登录着的用户
//2》从链表中删除推出用户的ip和端口
int quit_client(int sockfd,MSG_t msg,list_t *p,struct sockaddr_in caddr)
{
list_t *pdel=NULL;
while(p->next != NULL)
{
if(memcmp(&(p->next->caddr),&caddr,sizeof(caddr))==0)
{
pdel=p->next;
p->next=pdel->next;
free(pdel);
pdel=NULL;
}else
{
p=p->next;
sendto(sockfd,&msg,sizeof(msg),0,\
(struct sockaddr *)&(p->caddr),sizeof(p->caddr));
}
}
return 0;
}
client
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <unistd.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <string.h>
#include <signal.h>
#include "head.h"
int sockfd;
MSG_t msg;
struct sockaddr_in serveraddr;
void handler(int sig)
{
msg.type=Quit;
sendto(sockfd,&msg, sizeof(msg), 0,
(struct sockaddr *)&serveraddr,sizeof(serveraddr));
exit(-1);
}
int main(int argc, char const *argv[])
{
if(argc != 3)
{
printf("please input %s <ip> <port>\n",argv[0]);
return -1;
}
//1.创建套接字
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("socket err.");
return -1;
}
//填充服务器端ip和端口
serveraddr.sin_family = AF_INET;
serveraddr.sin_port = htons(atoi(argv[2]));
serveraddr.sin_addr.s_addr = inet_addr(argv[1]);
//ctrl + c
signal(SIGINT,handler);
//循环收发消息
int recvbyte;
//1.登录
msg.type=Login;
//从终端获取用户名
printf("please input name>>");
fgets(msg.name,sizeof(msg.name),stdin);
if(msg.name[strlen(msg.name)-1]=='\n')
msg.name[strlen(msg.name)-1]='\0';
//发送登录消息 sendto
sendto(sockfd,&msg, sizeof(msg), 0,
(struct sockaddr *)&serveraddr,sizeof(serveraddr));
pid_t pid=fork();
if(pid < 0)
{
perror("fork err.");
return -1;
}
else if(pid == 0){
//登录成功循环聊天
while (1)
{
fgets(msg.text,sizeof(msg.text),stdin);
if(msg.text[strlen(msg.text)-1]=='\n')
msg.text[strlen(msg.text)-1]='\0';
if(strncmp(msg.text,"quit",4) == 0)
{
msg.type=Quit;
sendto(sockfd,&msg, sizeof(msg), 0,
(struct sockaddr *)&serveraddr,sizeof(serveraddr));
kill(getppid(),SIGKILL);
exit(-1);
}else{
msg.type=Chat;
}
//发送消息 sendto
sendto(sockfd,&msg, sizeof(msg), 0,
(struct sockaddr *)&serveraddr,sizeof(serveraddr));
}
}else
{
//循环接受
while(1)
{
int ret=recvfrom(sockfd,&msg, sizeof(msg), 0,
NULL, NULL);
if(ret < 0)
{
perror("recvfrom err.");
return -1;
}
printf("%s:%s\n",msg.name,msg.text);
}
}
close(sockfd);
return 0;
}
补充说明
枚举:
枚举是C语言中的一种基本数据类型,它可以让数据更简洁,更易读。
枚举语法定义格式为:
enum 枚举名
{
枚举元素1,枚举元素2,…… //注意,各元素之间用逗号隔开
}; //注意,末尾有分号;
枚举是用来干嘛的?
枚举在C语言中其实是一些符号常量集。直白点说:枚举定义了一些符号,这些符号的本质就是int类型的常量,每个符号和一个常量绑定。这个符号就表示一个自定义的一个识别码,编译器对枚举的认知就是符号常量所绑定的那个int类型的数字。(总的来说就是将一个单词定义为一个数字识别码,可以通过判断数字进行操作,通常和switch case搭配使用)
一般情况下我们都不明确指定这个符号所对应的数字,而让编译器自动分配。
如:
enum type_t
{
Login, //默认定义为1
Chat, //默认定义为2
Quit, //默认定义为3
};
编译器自动分配的原则是:从0开始依次增加。如果用户自己定义了一个值,则从那个值开始往后依次增加。