项目中文名称:群英阁
项目英文名称:galaxyHub
利用UDP通信实现局域网内的多人在线聊天(即群聊),即所有用户处在同一局域网下,多个(大于等于2)用户在客户端登录系统,用户发送消息之后其他用户都可以在其终端收到发送者的用户信息极其发送的消息。我们知道QQ群聊中不仅有群聊的功能,还可以限定群中某个用户进行私聊(指定在线用户私法消息),以及群通知的功能,该项目也仿照QQ添加了私聊以及系统通知的功能。
该项目的实现可以对UDP通信,数据结构,进程线程进行综合运用。
为什么采用UDP通信实现? 采用TCP当然也是可以的。TCP通信是面向连接的,有三次握手和四次挥手机制,且能提供高可靠性通信,但是要实现一个服务端响应多个客户端(即实现并发服务器)相对繁琐。其中并发服务器的实现有如下几种实现方式:
多进程
。服务端每来一个客户端连接,便开一个子进程来专门处理客户端的数据,实现简单,但是系统开销相对较大。多线程
。每来一个客户端连接,开一个子线程来专门处理客户端的数据,实现简单,占用资源较少,属于使用比较广泛的模型。IO多路复用
。借助select、poll、epoll机制,将新连接的客户端描述符增加到描述符表中,只需要一个线程即可处理所有的客户端连接,在嵌入式开发中应用广泛,不过代码写起了稍显繁琐。UDP通信是面向无连接的,没有三次握手和四次挥手机制,客户端不需要对服务端进行绑定连接,一个服务端可以响应多个客户端,这样便很简单的实现并发服务器。
项目总的通信传输协议是基于UDP的,客户端和服务端是面向无连接的,一个服务端可以与多个客户端通信,从而实现并发服务器。为了保证客户端和服务器之间通信正确无误,需建立一个“通信协议”,即建立一个存放消息结构体,该结构体包含了消息类型、消息所有者、消息内容。
假设一个服务端一个客户端,客户往服务端发消息,服务端可以收到,若有多个客户端一个服务器实现群聊,客户端不知道所连接的其他客户端,不能直接群发给其他客户实现群聊,那只能通过服务端间接实现,即一个客户端往服务器发送消息,服务器收到消息后将此消息转发给其他用户,实现群聊。那么问题来了,服务器如何获取客户端信息实现消息转发呢?我们知道只要服务端通过recvfrom函数收到了客户端的一条消息便可以获取到客户端的ip和端口(存入sockaddr_in结构体中),因此就满足了服务器消息发送的条件,我们可以在客户端发送登陆消息时在服务端获取到其通信结构体,并保存下来,登陆一个保存一个。问题又来了,通过什么数据结构呢?常用的主要是顺序表和链表,顺序表的大小在建立时便确定,链表大小不确定,随用随加,考虑到我们不确定有多少客户端连接,故采用链表来存储客户端的通信结构体,登录一个客户端,便将其通信结构加入链表保存。客户端的登录信息实现了存储,自然而然群聊功能就不难了,服务端收到客户端发来的群聊类型消息后,可以遍历保存有所有已登录客户通信结构体的链表,遍历一个节点发送一个实现群聊,但要注意遍历时要剔除发送消息的客户端。
私聊的话,首先客户端指定私聊对象,要是能直接指出私聊的客户端的ip和端口最好,但是不好拿,就算拿到了客户要私聊时在终端输入一串ip和端口未免显得繁琐,私聊对象名字就相对好拿到(因为在终端可以看到其他客户端的名字对应的登录信息以及聊天信息),可以指定名字,将私聊对象名发给服务器,服务器拿到私聊对象名字就可以在链表中遍历查找(因此链表中不仅包含客户端通信结构体,还应包含其对应的登录名称),找到后直接定向转发发送私聊信息,实现了私聊功能。
有了上面的分析,系统通知功能就简单了。服务端在终端获取待发系统消息,只需遍历链表,同时对当前遍历到的节点对应的客户端发送系统消息即可。到这可以看出在客户端要有两个进程或者线程,一个进程(或线程)发送消息给服务器,另一个接收服务端消息;服务端同样要有两个进程或者线程,一个进程(或线程)发送系统消息,另一个接收客户端消息。这里要注意的是,若客户端采用两个进程,需处理好程序退出时进程资源的回收问题,避免僵尸进程或者孤儿进程的出现。
#ifndef __HEAD_H__
#define __HEAD_H__
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
//定义链表节点结构体,用来存放客户端的通信结构体
typedef struct node
{
struct sockaddr_in addr; //登录客户通信结构体
char name[32]; //登录客户用户名
struct node *next;
} link_node;
//消息对应的结构体(保证客户端服务端同一个协议)
typedef struct msg_t
{
int type; //消息类型
char name[32]; //登录客户用户名
char text[128]; //消息正文
} MSG_t;
//枚举,表示“登录、聊天、退出”三种消息类型
enum un
{
login,
chat,
quit,
};
#endif
/*
项目中文名称:群英阁(服务端)
项目英文名称:galaxyHub
完成时间:2023/8/23
作者:Sunqk5665
*/
#include "head.h"
void theForeword(); //说明
link_node *CreatEpLinkList(); //创建空的有头单向链表
void login_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p); //登录
void chat_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p); //聊天
void quit_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p); //退出
void privChat(int sockfd, MSG_t msg, struct sockaddr_in caddr, char *buf, link_node *p); //私聊
void *systemNotice(void *arg); //线程处理函数,给所有客户发送系统通知
int sockfd;
struct sockaddr_in saddr, caddr;
int main(int argc, char const *argv[])
{
if (argc != 2)
{
printf("Please input:%s \n" , argv[0]);
return 0;
}
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("sockfd err.");
return -1;
}
//填充通信结构体(IPV4)
// struct sockaddr_in saddr, caddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[1]));
saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
//bind
if (bind(sockfd, (struct sockaddr *)&saddr, sizeof(saddr)) < 0)
{
perror("bind err.");
return -1;
}
socklen_t len = sizeof(caddr);
link_node *p = CreatEpLinkList(); //创建空的有头单向链表
MSG_t msg; //创建消息结构体变量
theForeword();
//创建一个线程从终端获取消息,并向服务器发通知
pthread_t tid;
pthread_create(&tid, NULL, systemNotice, NULL);
while (1)
{
if (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&caddr, &len) < 0)
{
perror("recvfrom err.");
return -1;
}
switch (msg.type)
{
case login: //接收到的客户端消息为“登录”类型
login_fun(sockfd, msg, caddr, p); //调用登录函数
break;
case chat: //接收到的客户端消息为“聊天”类型
chat_fun(sockfd, msg, caddr, p); //调用聊天函数
break;
case quit: //接收到的客户端消息为“退出”类型
quit_fun(sockfd, msg, caddr, p); //调用退出函数
break;
default:
break;
}
}
close(sockfd);
free(p); //释放空间
p = NULL;
return 0;
}
//创建空的有头单向链表
link_node *CreatEpLinkList()
{
link_node *h = (link_node *)malloc(sizeof(link_node));
if (NULL == h)
{
printf("CreatEpLinkList error!\n");
return NULL;
}
h->next == NULL;
return h;
}
//登录函数
void login_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p)
{
sprintf(msg.text, "[%s]上线了...", msg.name); //将客户端上线信息放入消息正文中
while (p->next != NULL) //将登录信息发送给其他客户端(不包括现在登录的客户端,故先遍历发送)
{
p = p->next;
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
}
//更新已连接的客户端信息:将当前登录的客户端所建立的通信结构体插入单链表尾部
link_node *pnew = (link_node *)malloc(sizeof(link_node));
pnew->addr = caddr;
strcpy(pnew->name, msg.name);
pnew->next = NULL;
p->next = pnew;
//客户端的信息已加入链表,给发登录消息的客户端回一个"Login ok"
strcpy(msg.text, "Login ok");
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&caddr, sizeof(caddr));
printf("[%s]上线了...ip=%s,port=%d\n", msg.name, inet_ntoa(caddr.sin_addr), ntohs(caddr.sin_port));
}
//聊天函数
void chat_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p)
{
printf("[%s]>>%s\n", msg.name, msg.text); //服务端打印一下当前聊天信息
char sname[32] = {0};
char stext[128] = {0};
if (msg.text[0] == '@') //私聊
{
int i = 1, j = 0;
while (msg.text[i] != ' ')
{
sname[j] = msg.text[i]; //@abc abc
i++;
j++;
}
sname[j] = '\0';
strcpy(stext, msg.text + i + 1);
strcpy(msg.text, "(私聊信息)");
strcat(msg.text, stext);
privChat(sockfd, msg, caddr, sname, p);
}
else // 群聊
{
//将当前发消息的客户端的消息发送给其他客户端
while (p->next != NULL)
{
p = p->next;
if (memcmp(&caddr, &(p->addr), sizeof(caddr)) != 0) //相等返回0,这样保证消息不发给发送消息的客户端
{
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
}
}
}
}
//私聊函数
void privChat(int sockfd, MSG_t msg, struct sockaddr_in caddr, char *name, link_node *p)
{
while (p->next != NULL) // 查找私聊对象
{
p = p->next;
if (strcmp(name, p->name) == 0) //相等返回0
{
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
}
}
}
//退出函数
void quit_fun(int sockfd, MSG_t msg, struct sockaddr_in caddr, link_node *p)
{
printf("[%s]下线了...\n", msg.name); //服务端打印一下当前推出聊天室的客户端信息
link_node *pre, *pdel; //pre:指向p的前一个结点,pdel:指向要删除结点
sprintf(msg.text, "[%s]下线了...", msg.name); //将客户端下线信息放入消息正文中,以便下面的消息推送
while (p->next != NULL)
{
//在链表中找到当前退出的客户端的caddr进行删除,并向其他在线的客户端推送消息
pre = p;
p = p->next;
if (!memcmp(&caddr, &(p->addr), sizeof(caddr))) //是要退出的客户端,删除
{
pdel = p;
pre->next = p->next;
free(pdel);
}
else //不是退出的客户端的通信结构体,则将退出的客户端的消息发送给其他客户端
{
//将退出的客户端信息发送给其他客户端
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&(p->addr), sizeof(p->addr));
}
}
}
void *systemNotice(void *arg)
{
MSG_t msg;
msg.type = chat;
strcpy(msg.name, "<系统通知>@所有人");
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 *)&saddr, sizeof(saddr));
}
return NULL;
}
void theForeword()
{
printf("说明:你作为超级管理员,你可以向群聊用户发送「系统通知」,\n只需在终端输入消息,回车即可发送\n\n");
printf("**********************************************\n");
printf("* This project has been created by Sunqk5665 *\n");
printf("**********************************************\n\n");
}
/*
项目中文名称:群英阁(客户端)
项目英文名称:galaxyHub
完成时间:2023/8/23
作者:Sunqk5665
*/
#include "head.h"
void menuShow(); //菜单
void theForeword(); //说明
int main(int argc, char const *argv[])
{
if (argc != 3)
{
printf("Please input:%s \n" , argv[0]);
return 0;
}
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
if (sockfd < 0)
{
perror("sockfd err.");
return -1;
}
//填充服务端的通信结构体
struct sockaddr_in saddr;
saddr.sin_family = AF_INET;
saddr.sin_port = htons(atoi(argv[2])); //port
saddr.sin_addr.s_addr = inet_addr(argv[1]); //ip
socklen_t len = sizeof(saddr);
MSG_t msg; //创建消息结构体变量
char buf[32] = {0};
//客户端上来先登录。给服务端发送登录消息,以便服务端获取到当前客户端的连接信息
//登录-只登录一次(消息只发送一次)+判断是否登录成功,没有成功继续登录
theForeword();
menuShow();
printf("请输入login登录!!!\n");
while (1)
{
fgets(buf, 32, stdin);
if (buf[strlen(buf) - 1] == '\n') //剔除换行符,改为'\0'
buf[strlen(buf) - 1] = '\0';
if (strcmp(buf, "login") == 0)
{
printf("请输入用户名:");
fgets(msg.name, 32, stdin);
if (msg.name[strlen(msg.name) - 1] == '\n') //剔除换行符,改为'\0'
msg.name[strlen(msg.name) - 1] = '\0';
msg.type = login; //将消息类型设置为login
if (sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, len) < 0)
{
perror("sendto login err.");
return -1;
}
// 接收服务端发来的客户端登录成功的消息,确保客户端成功登录
if (recvfrom(sockfd, &msg, sizeof(msg), 0, NULL, NULL) < 0)
{
perror("recvfrom confirm err");
printf("请输入login重新登录!!!\n");
continue;
}
if (strcmp(msg.text, "Login ok") == 0) //收到了服务端发来的客户端登录成功的消息
{
printf("login ok...\n\n");
break;
}
else //未收到重新登录
{
printf("请输入login重新登录!!!\n");
continue;
}
}
else if(strcmp(buf, "quit")==0)
{
exit(-1);
}
else if(strcmp(buf, "help")==0)
{
menuShow();
printf("请输入login重新登录!!!\n");
continue;
}
else
{
printf("请输入login重新登录!!!\n");
continue;
}
}
pid_t pid = fork(); //创建子进程
if (pid < 0)
{
perror("fork err.");
return -1;
}
else if (pid == 0) //子进程接收server端消息
{
while (1)
{
recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, &len);
if (msg.type == login)
printf("(上线提醒)%s\n", msg.text);
else if (msg.type == chat)
printf("[%s]:%s\n", msg.name, msg.text);
else if (msg.type == quit)
printf("(下线提醒)%s\n", msg.text);
}
}
else //父进程向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';
if (strcmp(msg.text, "help") == 0)
{
menuShow();
}
else if (strcmp(msg.text, "quit") == 0) //客户端消息为"quit"退出命令
{
msg.type = quit; //消息类型设置为“quit”
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, len);
kill(pid, SIGKILL); //杀死子进程
wait(NULL); //阻塞回收子进程
exit(-1); //父进程退出
}
else //执行到这,消息肯定为聊天类型的
{
msg.type = chat; //消息类型设置为“chat”
sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr *)&saddr, len);
}
}
}
close(sockfd);
return 0;
}
void theForeword()
{
printf("说明:用户登录成功后的状态即为「群聊模式」,要想「私聊」某一用户,每次只需作如下输入:\n");
printf(" @私聊对象用户名 消息内容\n\n");
printf("***************** Client *********************\n");
printf("* This project has been created by Sunqk5665 *\n");
printf("**********************************************\n\n");
}
void menuShow()
{
printf("=========THE MENU===========\n");
printf("* help : 查看菜单 *\n");
printf("* 默认群聊 *\n");
printf("* @<姓名> <消息> : 私聊 *\n");
printf("* quit : 退出聊天 *\n");
printf("============================\n");
}