Linux C 基于epoll的多人聊天室

确认需求

实现的功能包含
1、私聊
2、群聊
3、打印在线信息
4、退出系统

需要注意

1、service中不能有重复名
2、发送的消息,如果对方不存在服务器需要给客户端返回错误信息
3、其他需要实现的功能,文件传输,聊天记录保存可根据自己完善。
4、代码只供参考阅读,一些可能存在的bug需要自己去修改

特此申明

作者自己本身也会进行完善代码和功能,对代码有问题,有更好想法的朋友欢迎评论区留言!!!希望共同学习,一起进步!

源码

头文件(也没封装啥,只是偷个懒,养成好习惯)
epoll_service_test.h
#ifndef NETWORK_EPOLL_SERVICE_TEST_H
#define NETWORK_EPOLL_SERVICE_TEST_H
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <sys/epoll.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#endif //NETWORK_EPOLL_SERVICE_TEST_H

服务器
epoll_service_test.c
#include "epoll_service_test.h"
#define MAX 1024
//定义消息结构体
typedef struct message{
    char type;//消息类型
    char name[30];//客户端姓名
    char dst_name[30];//目的客户姓名
    char text[MAX];//消息内容
}MSG;
MSG msg;//定义全局消息变量进行消息的接收和发送
//定义存储客户端信息链表
typedef struct  client_list{
    char name[30];//客户端的姓名
    int cfd;//对应的在树上的套接字
    struct client_list *next;
}linklist,*linkList ;
//定义头节点,设置为全局变量
linkList H;

//创建头节点函数
linkList head_init(){
  linkList h=(linkList)malloc(sizeof(linklist));
  bzero(h,sizeof(linklist));
  h->next=NULL;
  return h;
}

//创建结点插入函数
void insert_client(linkList H,MSG msg,int cfd){
    linkList p=(linkList)malloc(sizeof(linklist));
    bzero(p,sizeof(linklist));
    strncpy(p->name,msg.name,sizeof(msg.name));
    p->cfd=cfd;
    p->next=H->next;
    H->next=p;
}

int listenfd_init(int port){
    int lfd;

    struct sockaddr_in service;
    bzero(&service,sizeof(service));//清空
    service.sin_family=AF_INET;//初始化
    service.sin_port=htons(port);
    service.sin_addr.s_addr=htonl(INADDR_ANY);

    if ((lfd=socket(AF_INET,SOCK_STREAM,0))==-1){//确定TCP连接
        perror("[socket]");
        return -1;
    }

    int opt=1;//使用端口复用
    if (setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt))==-1){
        perror("[setsockopt]");
        return -1;
    }

    //绑定地址结构
    if (bind(lfd,(struct sockaddr *)&service,sizeof(service))==-1){
        perror("[bind]");
        return -1;
    }

    //设置最大监听数
    if (listen(lfd,128)==-1){
        perror("[listen]");
        return -1;
    }
    return lfd;
}

//客户端连接函数
int connectfd_init(int lfd){
    int cfd;
    struct sockaddr_in client;
    bzero(&client,sizeof(client));
    socklen_t len=sizeof(client);

    if ((cfd=accept(lfd,(struct sockaddr *)&client,&len))==-1){
        perror("[accpet]");
        return -1;
    }
    printf("Have a brother join to us!\n");
    return cfd;
}

int name_exist(linkList H ,MSG msg,int cfd){
    linkList s=H->next;
    while (s){//如果服务器中已经有这个人了,就不存,并给该客户端发送消息
        if (strncmp(msg.name,s->name,sizeof(msg.name))==0){
            return 0;
        }
        s=s->next;
    }
    return 1;
}

int dstname_exist(linkList H,MSG msg,int cfd){
    linkList t=H->next;
    while (t){
        if (!strncmp(msg.dst_name,t->name,sizeof(msg.dst_name))){//表示有这个人
            return 1;
        }
        t=t->next;
    }
    return 0;//表示没有这个人
}

int message_handler(int cfd){
    bzero(&msg,sizeof(msg));//清空
    int ret;
    ret=recv(cfd,&msg,sizeof(msg),0);
    if (ret==-1){
        perror("[recv]");
        return -1;
    }
    if (ret==0){//客户端断开连接
        printf("Has a brother disconnect!\n");
        return 0;
    }
    //对消息进行处理
    if (msg.type=='1'){//注册消息
        if (name_exist(H,msg,cfd)==0){
            bzero(msg.text,sizeof(msg.text));
            sprintf(msg.text,"The user %s has been registered!\n",msg.name);
            if (send(cfd,&msg,sizeof(msg),0)==-1){
                perror("[send_4]");
                return -1;
            }
        } else{
            insert_client(H,msg,cfd);//将这个客户添加到我们的链表中
            linkList q=H->next;
            sprintf(msg.text,"welcome to join us %s!\n",msg.name);
            while (q){
                if (send(q->cfd,&msg,sizeof(msg),0)==-1){
                    perror("[send_1]");
                }
                q=q->next;
            }
        }

    }
    if (msg.type=='2'){//私聊消息,进行转发
        //进行检查该用户是否存在
        if (dstname_exist(H,msg,cfd)){//有这个人,将消息发送给这个人
            linkList p=H->next;//指向头节点
            while (p){
                if (!strncmp(p->name,msg.dst_name,sizeof(msg.dst_name))){
                    if (send(p->cfd,&msg,sizeof(msg),0)==-1){
                        perror("[send_2]");
                        break;
                    }
                    printf("OK\n");
                }
                p=p->next;
            }
        } else{
            bzero(msg.text,sizeof(msg.text));
            sprintf(msg.text,"%s has been offline!\n",msg.dst_name);
            if (send(cfd,&msg,sizeof(msg),0)==-1){
                perror("[send_5]");
                return -1;
            }
        }
    }
    if (msg.type=='3'){//群聊,发送给每一个人
        linkList t=H->next;//创建一个临时结点用于遍历
        while (t){
            if (send(t->cfd,&msg,sizeof(msg),0)==-1){
                perror("[send_3]");
                break;
            }
            t=t->next;
        }
    }
    if (msg.type=='4'){//打印在线人员信息
        linkList u=H->next;
        while (u){
            bzero(msg.text,sizeof(msg.text));
            sprintf(msg.text,"%s is on line!\n",u->name);
            if (send(cfd,&msg,sizeof(msg),0)==-1){
                perror("[send_6]");
                return -1;
            }
            u=u->next;
        }
    }
}

void epoll_init(int lfd){
    int ret,ret_r;//用于接收epoll_wait的返回值,表示事件的个数,第二个表示接收消息的返回值
    int cfd;//表示用于客户端连接的套接字
    struct epoll_event ev;
    struct epoll_event events[20];//设置监听事件,和用于存储发生事件的表
    ev.data.fd=lfd;//初始化
    ev.events=EPOLLIN;//读事件

    int epfd;//设置存放套接字的红黑树

    if ((epfd=epoll_create(20))==-1){//表示监听的最大的数量
        perror("[epoll_create]");
        return;
    }

    if (epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,&ev)!=0){//将lfd添加上红黑树上,指定监听的事件为读事件
        perror("[epoll_ctl]");
        return;
    }

    printf("Listening.......\n");
    while (1){//进行事件的监听
        ret=epoll_wait(epfd,events,20,10);//timeout表示超时时长
        if (ret==-1){
            perror("[epoll_wait]");
            break;
        }
        for (int i = 0; i <ret ; ++i) {//循环处理事件
            if (!events[i].events & EPOLLIN){//如果不是读事件
                continue;
            }
            if (events[i].data.fd==lfd){//如果是读事件,且需要读的是lfd,表示的是需要进行连接
                cfd=connectfd_init(lfd);
                if (cfd==-1){
                    return;
                }
                ev.data.fd=cfd;//初始化事件,将cfd对应的监听事件添加到监听对象中
                ev.events=EPOLLIN;
                if (epoll_ctl(epfd,EPOLL_CTL_ADD,cfd,&ev)==-1){//将结点cfd添加上树上
                    perror("[epoll_ctl2]");
                    return;
                }
            } else{//其他套接字的读事件
                ret_r=message_handler(events[i].data.fd);
                if (ret_r==-1){
                    break;//出错直接跳出
                }
                if (ret_r==0){//客户端退出
                    if (epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,NULL)==-1){//删除这个结点,从树上
                        perror("[epoll_ctl3]");
                        continue;//进行处理下一个事件
                    }
                }
            }
        }
    }
}

int main(int argc ,char *argv[]){
    int lfd;//进行套接字的初始化
    lfd=listenfd_init(8888);
    if (lfd==-1){
        return -1;
    }
    H=head_init();
    //进行epoll的连接初始化
    epoll_init(lfd);
    close(lfd);//关闭套接字
    return 0;
}
客户端 (耐住性子,按着思路去读,不清楚的可以看注释,你也可以copy下来在编译器打开,将功能收起来方便阅读)
epoll_client_test.c
#include "epoll_service_test.h"
#define MAX 1024
typedef struct message{
    char type;//消息类型
    char name[30];//自己的名字
    char dst_name[30];//想要聊天的对象
    char text[MAX];//消息内容
}MSG;
MSG msg1;//用于消息的发送
MSG msg;//全局变量,用于子线程接收消息
int i;//定义全局变量,进行功能选择
void message_handler(int *cfd){
    int ret;
    while (1){
        bzero(&msg,sizeof(msg));
        ret=recv(*cfd,&msg,sizeof(msg),0);
        if (ret==-1){
            perror("[recv]");
            break;
        }
        if (ret==0){
            printf("Service has disconnect!\n");
            break;
        }
        if (msg.type=='1'){
            printf("Service:%s\n",msg.text);

        }
        if (msg.type=='2' || msg.type=='3'){//如果是群发消息,或者是私聊消息就进行接收
            printf("%s : %s\n",msg.name,msg.text);
        }
        if (msg.type=='4'){
            printf("%s",msg.text);
        }
    }
}

int connectfd_init(int port){
    int cfd;
    struct sockaddr_in service;
    bzero(&service,sizeof(service));
    service.sin_family=AF_INET;
    service.sin_port=htons(port);
    inet_pton(AF_INET,"192.168.200.134",&service.sin_addr.s_addr);

    if ((cfd=socket(AF_INET,SOCK_STREAM,0))==-1){
        perror("[socket]");
        return -1;
    }

    if (connect(cfd,(struct sockaddr *)&service,sizeof(service))==-1){
        perror("[connect]");
        return -1;
    }

    return cfd;
}

int main(int argc ,char *argv[]){
    printf("--------------------------------\n");
    printf("Welcome to jacky's chat room!\n");
    printf("\n");
    printf("Please register first!\n");
    printf("\n");
    printf("\n");

    int cfd;//连接套接字
    pthread_t tid;
    int ret;//用于检查返回值,i用于进行功能的选择
    cfd=connectfd_init(8888);
    if (cfd==-1){
        return -1;
    }

    //创建线程用于接收消息
    pthread_create(&tid,NULL,(void *)message_handler,(void *)&cfd);
    pthread_detach(tid);


    //进行注册
    bzero(&msg1,sizeof(msg1));
    printf("What's your name?\n");
    fgets(msg1.name,sizeof(msg1.name),stdin);
    msg1.name[strlen(msg1.name)-1]=0;
    msg1.type='1';
    if (send(cfd,&msg1,sizeof(msg1),0)==-1){
        perror("[send]");
        return -1;
    }

    while (1){//对功能进行处理,子线程进行接收消息。

        //进行功能的选择,私聊还是群聊
        printf("\n");
        printf("1.私聊\n");
        printf("2.群聊\n");
        printf("3.打印在线信息\n");
        printf("4.退出系统\n");
        printf("\n");
        printf("Please enter your choice:");
        scanf("%d",&i);

        switch (i) {
            case 1:
                getchar();//用于吸收scanf的\n
                bzero(msg1.dst_name,sizeof(msg1.dst_name));//清空对象的名字

                printf("Who you want to chat?\n");
                fgets(msg1.dst_name,sizeof(msg1.dst_name),stdin);
                msg1.dst_name[strlen(msg1.dst_name)-1]=0;

                msg1.type='2';//更改消息类型,注册用户的名字就不用进行改了,一直保留

                while (1){//进行聊天
                    bzero(msg1.text,sizeof(msg1.text));//清空消息内容
                    printf("text:");
                    fgets(msg1.text,sizeof(msg1.text),stdin);
                    msg1.text[strlen(msg1.text)-1]=0;

                    //进行消息的退出判断
                    if (!strncmp(msg1.text,"quit",4)){
                        break;
                    }

                    //进行消息的发送
                    if (send(cfd,&msg1,sizeof(msg1),0)==-1){
                        perror("[send]");
                        printf("Disconnect! Please reconnect!\n");
                        break;
                    }

                }
                break;
            case 2://进行群聊
                getchar();
                bzero(msg1.dst_name,sizeof(msg1.dst_name));//清空
                msg1.type='3';//指定消息类型为群聊

                while (1){
                    bzero(msg1.text,sizeof(msg1.text));
                    printf("text:");
                    fgets(msg1.text,sizeof(msg1.text),stdin);
                    msg1.text[strlen(msg1.text)-1]=0;

                    //进行退出判断
                    if (!strncmp(msg1.text,"quit",4)){
                        break;
                    }
                    if (send(cfd,&msg1,sizeof(msg1),0)==-1){
                        perror("[send_2]");
                        printf("Disconnect! Please reconnect!\n");
                        break;
                    }
                }
                break;
            case 3:
                getchar();//打印在线信息
                msg1.type='4';
                if (send(cfd,&msg1,sizeof(msg1),0)==-1){
                    perror("[send_3]");
                    break;
                }
                break;
            case 4:
                getchar();//退出系统
                printf("Goodbye!\n");
                exit(0);
                break;
            default:
                getchar();
                printf("Sorry, there is an error! please select again.\n");
                break;
        }
    }
    return 0;
}

结果展示

服务器工作状态
在这里插入图片描述
用户ABC通信
在这里插入图片描述

学习建议

这个注释还是非常的详细了。所以hxd们希望能坚持看下去,也非常希望大家能多多支持,给给意见一起来完善这个程序

上面更新了博客,在网络传输中,我们应该使用的字符型。而不建议使用int性。因为int型在网络传输中更容易出现错误。因为int32位,char8位嘛

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Jacky~~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值