https://download.csdn.net/download/weixin_43042683/86949831?spm=1001.2014.3001.5503
本次实习设计的单主机聊天室需要满足以下功能需求:
(1)提供客户端和服务端,所有用户通过客户端进行聊天,每个用户可以看到所有聊天信息。
(2)要求所有登录的系统用户可以登录服务器进行聊天,服务器为守护进程。
(3)使用消息队列完成。服务器启动后使用特定的key或目录(将目录转换为key)建立消息队列,监听客户程序发出的消息(登录消息、退出消息、聊天消息)并完成消息的转发,对特定信息进行捕获来退出服务,退出服务是要通知客户端退出;客户端程序启动时检查特定key的消息队列是否建立,并通过位置参数获取用户名,若消息队列没有建立,则发送登录消息,然后准备发送消息,同事监听消息队列读取服务程序转发的聊天消息,当用户发出特定的字符时退出聊天。
单机聊天室采用客户端,服务器的模式。通过消息队列存储客户端之间的信息交流,所有用户可以登录服务器进入聊天室参与聊天,服务器采用守护进程。
客户端的功能如下:
服务器功能如下:


设计如下:
1,客户端之间的收发消息通过子进程读消息,父进程发消息来实现。发消息时通过msgsnd函数将包含消息的结构体加入消息队列,收消息时通过msgrcv函数读取消息队列信息。
- fgets(msg.mtext,MSG_SIZE,stdin);
- struct timeval tv;
- gettimeofday(&tv, NULL);
- t = tv.tv_sec;
- struct tm*p_time = localtime(&t);
- strftime(str, 26, "%Y-%m-%d %H:%M:%S", p_time);
- strcpy(msg.msg_time, str);//记录时间信息
-
- msgsnd(msgid,&msg,MSG_LEN,0);//发送结构体
以上代码实现消息的发送功能,在结构体发送给消息队列时记录发送时间,达到对消息计时的功能。
2,客户端收到新用户加入和离开聊天室的信息通过服务器的广播功能实现,用户进入和离开聊天室时将进入、离开信息发送给消息队列,服务器将这些消息广播给所有客户端。
- if(strncmp(msg.mtext,"exit",4)==0){
- msg.subtype = 3;
-
- struct timeval tv;
- gettimeofday(&tv, NULL);
- t = tv.tv_sec;
- struct tm*p_time = localtime(&t);
- strftime(str, 26, "%Y-%m-%d %H:%M:%S", p_time);
- strcpy(msg.msg_time, str);//记录时间信息
-
- msgsnd(msgid,&msg,MSG_LEN,0);//发送退出信息
- fflush(stdout);//刷新缓冲区
- sleep(2);
- kill(pid,SIGKILL);
- exit(0);
- }
以上代码实现离开聊天室通知所有客户端的功能,将包含客户端退出聊天室的结构体发送给消息队列后,必须要刷新缓冲区,不然不能及时输出客户端的退出信息。
3,消息的时间记录,通过Linux中时间结构体来记录,在发送结构体给消息队列时及时记录时间信息。
计功能如下:
服务器生成一个采用链表的形式生成消息队列链表结构如下:
- struct msgbuf{
- long mtype;
- int subtype; //消息请求类型
- int pid; //发送消息进程号
- char user_name[MSG_SIZE];
- char mtext[MSG_SIZE];
- char msg_time[MSG_SIZE];
- }msg;
结构体中包含进程号、消息请求类型(设计时采用switch选择语句实现消息的不同的类型便于扩展功能)、内容信息、时间信息。
1,广播消息时遍历消息队列,给每个进程发送消息。
- int i;
- for(i=0 ;i<Length(head) ;i++){
- msg->mtype = GetValue(head,i);
- if(msg->mtype != msg->pid){//mtype = pid排除将广播消息发送给发消息的进程
- msgsnd(msgid,msg,MSG_LEN,0);
- }
- }
2,解散聊天室杀死进程,在服务器接收到quit消息时杀死所有进程,删除消息队列,以此实现解散聊天室的功能,删除消息队列和杀死进程的代码如下:
- void Delete(LinkList head,int value)
- {
- LinkList p = head->next;
- while(p!=NULL){
- if(p->data == value){
- head->next = p->next;
- free(p);
- p = head->next;
- }else{
- head = p;
- p = p->next;
- }
- }
- }
以上代码实现对链表的删除
- if(strncmp(msg.mtext,"quit",4)==0){
- msgsnd(msgid,&msg,MSG_LEN,0);
- sleep(2);
- msgctl(msgid,IPC_RMID,NULL);//删除消息队列
- kill(pid,SIGKILL); //杀死子进程
- exit(0);
- }
以上代码杀死所有进程,实现模拟解散聊天室的功能,此代码虽然实现了对进程的杀死但是并没有按照本次实习的要求处理进程信号,是按照系统默认杀死进程,实习要求应该是对退出信号进行处理,在答辩时发现这个问题,所以这个单机聊天室没能完全按照要求实现,这也提醒我在今后的实习或是工作中一定要仔细审题和老师多交流,避免这种问题发生。
1.调试时首先启动服务器,直接登录客户端会提示服务器没有开启,如图所示:


2.启动服务器后客户端可以登录服务器进入聊天室。

3. 客户端登录后相互之间通过服务器的转发实现聊天功能,客户端之间的聊天如下图

4.在客户端输入exit后退出聊天室,服务器会收集客户端的提示信息并且通知所有在聊天室中的客户端有客户端退出聊天室。


5.在服务器输入quit退出服务器,这时服务器会通知所有进程服务器即将关闭,然后杀死所有进程并且删除消息队列,模拟解散聊天室;此功能的不足时没能对退出消息捕获处理而是按照系统默认处理,这不符合实际,还需优化。

以下代码是客户端子进程和父进程的相关代码实现了消息的收发。
- if((pid = fork())==-1){
- perror("fork error.");//创建进程失败
- exit(1);
- }else if(pid == 0){ //子进程负责读
- while(1){
- msgrcv(msgid,&msg,MSG_LEN,TYPE_ME,0);
- //判断接收到的消息是否为quit
- if(strncmp(msg.mtext,"quit",4) == 0){
- printf("\33[36m-------------------------服务器即将关闭-----------------------\33[0m\n");
- kill(TYPE_ME,SIGUSR1); //杀死所有进程
- exit(0);
- }
- //检测用户是否输入exit退出操作
- if(strncmp(msg.mtext,"exit",4) == 0){
- msg.subtype = 3;
- }
-
- switch(msg.subtype){
- case 1:
- printf("\033[33m\n%s\n\033[0m",msg.msg_time);//显示进入聊天室时间
- printf("\033[33m[%s 进入聊天室]\033[0m\n\n",msg.user_name);
- break;
- case 2:
- printf("\033[33m\n%s\n\033[0m", msg.msg_time);//显示发送消息的时间
- printf("\033[33m[%s]:\033[0m %s\n",msg.user_name,msg.mtext);
- break;
- case 3:
- printf("\033[33m\n%s\n\033[0m", msg.msg_time);//显示退出聊天室时间
- printf("\033[33m[%s 退出聊天室]\033[0m\n\n",msg.user_name);
- break;
- default :
- break;
- }
- }
- }else{ //父进程用于发送消息
- msg.mtype = MSG_TO_SERVER;
- msg.subtype = 2;//广播
- while(1){
- fgets(msg.mtext,MSG_SIZE,stdin);
-
- struct timeval tv;//生成当前时间信息
- gettimeofday(&tv, NULL);
- t = tv.tv_sec;
- struct tm*p_time = localtime(&t);
- strftime(str, 26, "%Y-%m-%d %H:%M:%S", p_time);
- strcpy(msg.msg_time, str);
-
- msgsnd(msgid,&msg,MSG_LEN,0);//结构体进入消息队列
- //当此进程输入exit时,表示进程推出聊天
- //由服务器通知其他进程,该进程已下线
- if(strncmp(msg.mtext,"exit",4)==0){
- msg.subtype = 3;
-
- struct timeval tv;//生成当前时间信息
- gettimeofday(&tv, NULL);
- t = tv.tv_sec;
- struct tm*p_time = localtime(&t);
- strftime(str, 26, "%Y-%m-%d %H:%M:%S", p_time);
- strcpy(msg.msg_time, str);
-
- msgsnd(msgid,&msg,MSG_LEN,0);
- fflush(stdout);//刷新缓冲区输出信息,如果不刷新,其他终端不能及时收到退出信息
- sleep(2);
- kill(pid,SIGKILL);//杀死当前进程
- exit(0);
- }
- }
- }
以下代码是服务器子进程和父进程的相关代码用于实现对客户端之间的消息转发和客户端进出聊天室的监控。
- if((pid = fork())==-1){
- perror("error to fork.");
- exit(1);
- }else if(pid == 0){ //pid=0,当前进程为子进程
- //消息队列
- LinkList head;
- head = CreateLinkList();
- while(1){
- //从消息队列中获取一条消息
- if(msgrcv(msgid,&msg,MSG_LEN,MSG_TO_SERVER,0)==-1){
- perror("msgrcv error.\n");
- exit(1);
- }else{
- //对接收到的消息,子进程进行消息类型判断
- switch(msg.subtype)
- {
- case 1: //客户端登录
- if(Insert(head,msg.pid)!=0){
- printf("insert error.\n");
- exit(1);
- }
- printf("\033[33m%s\033[0m", msg.msg_time);//输出登录时间
- printf("\033[33m[客户端 %s 登录]\033[0m\n\n",msg.user_name);
- BroadCast(head,msgid,&msg); //把收到的消息广播
- break;
- case 2: //消息广播
-
- BroadCast(head,msgid,&msg);
- if(strncmp(msg.mtext,"quit",4)==0){
- exit(0);
- }
- break;
- case 3: //客户端退出
- if(!Empty(head)){
- Delete(head,msg.mtype);
- }
- printf("\033[33m%s\033[0m", msg.msg_time);//输出退出时间
- printf("\033[33m[客户端 %s 退出]\033[0m\n\n",msg.user_name);
- break;
- default:
- break;
- }
- }
- }
- }else{//父进程用于服务器控制台
- msg.mtype = MSG_TO_SERVER;
- msg.subtype = 2;
- while(1){
- printf("\33[36m*************************************************************\n\33[0m");
- printf("\33[36m*---------------------服务器控制端--------------------------*\n\33[0m");
- printf("\33[36m*---------服务端已开启,客户可登陆聊天室进行聊天------------*\n\33[0m");
- printf("\33[36m*----------------若想关闭服务器,请输入“quit”---------------*\n\33[0m");
- printf("\33[36m*************************************************************\33[0m\n");
- fgets(msg.mtext,MSG_SIZE,stdin);
- //控制台接收到quit时,通知客户端,server结束
- if(strncmp(msg.mtext,"quit",4)==0){
-
- msgsnd(msgid,&msg,MSG_LEN,0);
- sleep(2);
- msgctl(msgid,IPC_RMID,NULL);//删除消息队列
- kill(pid,SIGKILL); //杀死子进程
- exit(0);
- }
- }
- }
- return 0;
-
- }
-
- LinkList CreateLinkList()
- {
- LinkList head;
- head = (LinkList)malloc(sizeof(ListNode));
- head->next = NULL;
- return head;
- }
- int Length(LinkList head)
- {
- int n = 0;
- head = head->next;
- while(head){
- n++;
- head = head->next;
- }
- return n;
- }
- void BroadCast(LinkList head,int msgid,struct msgbuf *msg)
- {
- int i;
- for(i=0 ;i<Length(head) ;i++){
- msg->mtype = GetValue(head,i);
- if(msg->mtype != msg->pid){//mtype = pid说明是发送消息的客户端,不用再接收自己发送的消息
-
- msgsnd(msgid,msg,MSG_LEN,0);
- }
- }
- }
-
- int Empty(LinkList head)
- {
- return (head->next == NULL);
- }
-
- int Insert(LinkList head,int value)
- {
- LinkList p;
- p = (LinkList)malloc(sizeof(ListNode));
- p->data = value;
- p->next = NULL;
- while(head->next){
- head = head->next;
- }
- head->next = p;
- return 0;
- }
-
- void Delete(LinkList head,int value)
- {
- LinkList p = head->next;
- while(p!=NULL){
- if(p->data == value){
- head->next = p->next;
- free(p);
- p = head->next;
- }else{
- head = p;
- p = p->next;
- }
- }
- }
-
- int GetValue(LinkList head,int pos)
- {
- head = head->next;
- while(pos--){
- head = head->next;
- }
- return head->data;
- }