Linux下基于epoll实现的群聊天系统

简介: 前面有两篇文章介绍了select、poll、epoll的函数使用方法并且也编写了群聊天系统的案例。前面编写的群聊天系统服务器端是采用多线程的方式处理客户端的请求,这篇文章讲服务器端的多线程去除掉,使用epoll来替代。完成新客户度上线的处理,接收客户端数据处理,用户上线、下线的处理。

一、功能介绍

前面有两篇文章介绍了select、poll、epoll的函数使用方法并且也编写了群聊天系统的案例。前面编写的群聊天系统服务器端是采用多线程的方式处理客户端的请求,这篇文章讲服务器端的多线程去除掉,使用epoll来替代。完成新客户度上线的处理,接收客户端数据处理,用户上线、下线的处理。

整体的代码分为两个部分:

  1. 服务器代码: 监听连接上来的客户端(用户),将信息保存在链表里,如果有客户端(用户)发来消息,上线、或者下线,就像这条信息转发给在线的其他客户端(好友),整体使用epoll来完成监听,没有加入线程,系统开销很小。
  2. 客户端代码(用户端): 客户端代码可以运行多个,每一个就是单独的用户。接收到服务器的信息之后,打印到终端显示出来。

可以实现的效果: 好友的消息显示、好友下线提醒、好友上线提醒、当前在线好友人数。

二、代码实现

2.1 服务器端代码

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/epoll.h>

int sockfd;
//消息结构体
struct MSG_DATA
{
    char type; //消息类型.  0表示有聊天的消息数据  1表示好友上线  2表示好友下线
    char name[50]; //好友名称
    int number;   //在线人数的数量
    unsigned char buff[100];  //发送的聊天数据消息
};

//存放当前服务器连接的客户端套接字
struct CLIENT_FD
{
    int fd;
    char name[50]; //名称
    struct CLIENT_FD *next;
};

//定义链表头
struct CLIENT_FD *list_head=NULL;
struct CLIENT_FD *List_CreateHead(struct CLIENT_FD *list_head);
void List_AddNode(struct CLIENT_FD *list_head,int fd);
void List_DelNode(struct CLIENT_FD *list_head,int fd);
int List_GetNodeCnt(struct CLIENT_FD *list_head);
void Server_SendMsgData(struct CLIENT_FD *list_head,struct MSG_DATA *msg_data,int client_fd);
void List_SaveName(struct CLIENT_FD *list_head,struct MSG_DATA *msg_data,int client_fd);
void List_GetName(struct CLIENT_FD *list_head,struct MSG_DATA *msg_data,int client_fd);

#define MAX_EPOLL_FD 100
struct epoll_event events[MAX_EPOLL_FD];
struct epoll_event event;
int epfd;
int nfd;
struct MSG_DATA msg_data;

/*信号工作函数*/
void signal_work_func(int sig)
{
    close(sockfd);
    exit(0); //结束进程
}

int main(int argc,char **argv)
{   
    if(argc!=2)
    {
        printf("./app <端口号>\n");
        return 0;
    }
    signal(SIGPIPE,SIG_IGN); //忽略 SIGPIPE 信号--防止服务器异常退出
    signal(SIGINT,signal_work_func);

    //创建链表头
    list_head=List_CreateHead(list_head);

    /*1. 创建socket套接字*/
    sockfd=socket(AF_INET,SOCK_STREAM,0);

    //设置端口号的复用功能
    int on = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    /*2. 绑定端口号与IP地址*/
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(atoi(argv[1])); // 端口号0~65535
    addr.sin_addr.s_addr=INADDR_ANY;    //inet_addr("0.0.0.0"); //IP地址
    if(bind(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr))!=0)
    {
        printf("服务器:端口号绑定失败.\n");
    }
    /*3. 设置监听的数量*/
    listen(sockfd,20);
    /*4. 等待客户端连接*/
    int client_fd;
    struct sockaddr_in client_addr;
    socklen_t addrlen;
    int i;
    int cnt;

    /*5. 创建epoll相关的接口*/
    epfd=epoll_create(MAX_EPOLL_FD);
    event.events=EPOLLIN;  //监听的事件
    event.data.fd=sockfd; //监听的套接字
    epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);
    while(1)
    {
        //等待事件发生
        nfd=epoll_wait(epfd,events,MAX_EPOLL_FD,-1);
        for(i=0;i<nfd;i++)
        { 
            if(events[i].data.fd==sockfd)  //表示有新的客户端连接上服务器
            {
                client_fd=accept(sockfd,(struct sockaddr *)&client_addr,&addrlen);
                printf("连接的客户端IP地址:%s\n",inet_ntoa(client_addr.sin_addr));
                printf("连接的客户端端口号:%d\n",ntohs(client_addr.sin_port));
                //保存已经连接上来的客户端
                List_AddNode(list_head,client_fd);
                //将新连接的客户端套接字添加到epoll函数监听队列里
                event.data.fd=client_fd; //监听的套接字
                epoll_ctl(epfd,EPOLL_CTL_ADD,client_fd,&event);
            }
            else  //表示客户端给服务器发送了消息-----实现消息的转发
            {
                 //读取客户端发送的消息
                cnt=read(events[i].data.fd,&msg_data,sizeof(struct MSG_DATA));
                if(cnt<=0)  //表示当前客户端断开了连接
                {
                    //获取名称
                    List_GetName(list_head,&msg_data,events[i].data.fd);
                    //删除节点
                    List_DelNode(list_head,events[i].data.fd);
                    msg_data.type=2;

                    //将断开连接的客户端套接字从epoll函数监听队列里删除调用
                    event.data.fd=events[i].data.fd; //监听的套接字
                    epoll_ctl(epfd,EPOLL_CTL_DEL,events[i].data.fd,&event);
                    close(event.data.fd);
                }
                if(msg_data.type==1) //好友上线的时候保存一次名称
                {
                    //保存名称
                    List_SaveName(list_head,&msg_data,events[i].data.fd);
                }
                //转发消息给其他好友
                msg_data.number=List_GetNodeCnt(list_head); //当前在线好友人数
                Server_SendMsgData(list_head,&msg_data,events[i].data.fd);
            }
        }
    } 
    //退出进程
    signal_work_func(0);
    return 0;
}


/*
函数功能: 创建链表头
*/
struct CLIENT_FD *List_CreateHead(struct CLIENT_FD *list_head)
{
    if(list_head==NULL)
    {
        list_head=malloc(sizeof(struct CLIENT_FD));
        list_head->next=NULL;
    }
    return list_head;
}

/*
函数功能: 添加节点
*/
void List_AddNode(struct CLIENT_FD *list_head,int fd)
{
    struct CLIENT_FD *p=list_head;
    struct CLIENT_FD *new_p;
    while(p->next!=NULL)
    {
        p=p->next;
    }
    new_p=malloc(sizeof(struct CLIENT_FD));
    new_p->next=NULL;
    new_p->fd=fd;
    p->next=new_p;
}

/*
函数功能: 删除节点
*/
void List_DelNode(struct CLIENT_FD *list_head,int fd)
{
    struct CLIENT_FD *p=list_head;
    struct CLIENT_FD *tmp;
    while(p->next!=NULL)
    {
        tmp=p;
        p=p->next;
        if(p->fd==fd) //找到了要删除的节点
        {
            tmp->next=p->next;
            free(p);
            break;
        }
    }
}

/*
函数功能: 获取当前链表中有多少个节点
*/
int List_GetNodeCnt(struct CLIENT_FD *list_head)
{
    int cnt=0;
    struct CLIENT_FD *p=list_head;
    while(p->next!=NULL)
    {
        p=p->next;
        cnt++;
    }
    return cnt;
}

/*
函数功能: 转发消息
*/
void Server_SendMsgData(struct CLIENT_FD *list_head,struct MSG_DATA *msg_data,int client_fd)
{
    struct CLIENT_FD *p=list_head;
    while(p->next!=NULL)
    {
        p=p->next;
        if(p->fd!=client_fd)
        {
            write(p->fd,msg_data,sizeof(struct MSG_DATA));
        }
    }
}

/*
函数功能: 保存好友的名称
*/
void List_SaveName(struct CLIENT_FD *list_head,struct MSG_DATA *msg_data,int client_fd)
{
    struct CLIENT_FD *p=list_head;
    while(p->next!=NULL)
    {
        p=p->next;
        if(p->fd==client_fd) //找到在链表里的当前套接字
        {
            strcpy(p->name,msg_data->name);
        }
    }
}


/*
函数功能: 获取好友的名称
*/
void List_GetName(struct CLIENT_FD *list_head,struct MSG_DATA *msg_data,int client_fd)
{
    struct CLIENT_FD *p=list_head;
    while(p->next!=NULL)
    {
        p=p->next;
        if(p->fd==client_fd) //找到在链表里的当前套接字
        {
            strcpy(msg_data->name,p->name);
        }
    }
}

客户端的代码-也就是用户

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <stdlib.h>
#include <pthread.h>
#include <semaphore.h>
#include <signal.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/select.h>
#include <sys/time.h>
#include <poll.h>
#include <sys/epoll.h>

//消息结构体
struct MSG_DATA
{
    char type; //消息类型.  0表示有聊天的消息数据  1表示好友上线  2表示好友下线
    char name[50]; //好友名称
    int number;   //在线人数的数量
    unsigned char buff[100];  //发送的聊天数据消息
};
struct MSG_DATA msg_data;

#define MAX_EVENTS 2
struct epoll_event ev, events[MAX_EVENTS];
int epollfd;
int nfds;

//文件接收端
int main(int argc,char **argv)
{   
    if(argc!=4)
    {
        printf("./app  <IP地址> <端口号> <名称>\n");
        return 0;
    }
    int sockfd;
    //忽略 SIGPIPE 信号--方式服务器向无效的套接字写数据导致进程退出
    signal(SIGPIPE,SIG_IGN); 

    /*1. 创建socket套接字*/
    sockfd=socket(AF_INET,SOCK_STREAM,0);
    /*2. 连接服务器*/
    struct sockaddr_in addr;
    addr.sin_family=AF_INET;
    addr.sin_port=htons(atoi(argv[2])); // 端口号0~65535
    addr.sin_addr.s_addr=inet_addr(argv[1]); //IP地址
    if(connect(sockfd,(const struct sockaddr *)&addr,sizeof(struct sockaddr_in))!=0)
    {
        printf("客户端:服务器连接失败.\n");
        return 0;
    }

    /*3. 发送消息表示上线*/
    msg_data.type=1;
    strcpy(msg_data.name,argv[3]);
    write(sockfd,&msg_data,sizeof(struct MSG_DATA));

    int cnt;
    int i;
    //创建专用文件描述符
    epollfd = epoll_create(10);
    //添加要监听的文件描述符
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;
    epoll_ctl(epollfd, EPOLL_CTL_ADD, sockfd, &ev);

    ev.events = EPOLLIN;
    ev.data.fd = 0; //标准输入文件描述符
    epoll_ctl(epollfd, EPOLL_CTL_ADD, 0, &ev);

    while(1)
    {
        //监听事件
        nfds=epoll_wait(epollfd,events,MAX_EVENTS,-1);
        if(nfds)
        {
            for(i=0;i<nfds;i++)
            {
                if(events[i].data.fd==sockfd) //判断收到服务器的消息
                {
                    cnt=read(sockfd,&msg_data,sizeof(struct MSG_DATA));
                    if(cnt<=0) //判断服务器是否断开了连接
                    {
                        printf("服务器已经退出.\n");
                        goto SERVER_ERROR;
                    }
                    else if(cnt>0)
                    {
                        if(msg_data.type==0)
                        {
                            printf("%s:%s  在线人数:%d\n",msg_data.name,msg_data.buff,msg_data.number);
                        }
                        else if(msg_data.type==1)
                        {
                            printf("%s 好友上线. 在线人数:%d\n",msg_data.name,msg_data.number);
                        }
                        else if(msg_data.type==2)
                        {
                            printf("%s 好友下线. 在线人数:%d\n",msg_data.name,msg_data.number);
                        }
                    }
                }
                else if(events[i].data.fd==0) //表示键盘上有数据输入
                {
                    gets(msg_data.buff); //读取键盘上的消息
                    msg_data.type=0; //表示正常消息
                    strcpy(msg_data.name,argv[3]); //名称
                    write(sockfd,&msg_data,sizeof(struct MSG_DATA));
                }
            }
        }
    }
SERVER_ERROR:
    close(sockfd);
    return 0;
}
目录
相关文章
|
4月前
|
网络协议 Linux C++
Linux C/C++ 开发(学习笔记十二 ):TCP服务器(并发网络编程io多路复用epoll)
Linux C/C++ 开发(学习笔记十二 ):TCP服务器(并发网络编程io多路复用epoll)
96 0
|
4月前
|
网络协议 Linux Python
Python网络编程基础(Socket编程)epoll在Linux下的使用
【4月更文挑战第12天】在上一节中,我们介绍了使用`select`模块来实现非阻塞IO的方法。然而,`select`模块在处理大量并发连接时可能会存在性能问题。在Linux系统中,`epoll`机制提供了更高效的IO多路复用方式,能够更好地处理大量并发连接。
|
11月前
|
监控 网络协议 Java
I/O多路复用【Linux/网络】(C++实现select、poll和epoll服务器)(上)
I/O多路复用【Linux/网络】(C++实现select、poll和epoll服务器)
190 0
|
3月前
|
消息中间件 存储 监控
实战Linux I/O多路复用:借助epoll,单线程高效管理10,000+并发连接
本文介绍了如何使用Linux的I/O多路复用技术`epoll`来高效管理超过10,000个并发连接。`epoll`允许单线程监控大量文件描述符,显著提高了资源利用率。文章详细阐述了`epoll`的几个关键接口,包括`epoll_create`、`epoll_ctl`和`epoll_wait`,以及它们在处理并发连接中的作用。此外,还探讨了`epoll`在高并发TCP服务场景的应用,展示了如何通过`epoll`和线程/协程池来构建服务框架。
394 9
|
2月前
|
Linux 网络安全 虚拟化
Ngnix04系统环境准备-上面软件是免费版的,下面是收费版的,他更快的原因使用了epoll模型,查看当前Linux系统版本, uname -a,VMWARE建议使用NAT,PC端电脑必须使用网线连接
Ngnix04系统环境准备-上面软件是免费版的,下面是收费版的,他更快的原因使用了epoll模型,查看当前Linux系统版本, uname -a,VMWARE建议使用NAT,PC端电脑必须使用网线连接
|
4月前
|
资源调度 JavaScript Ubuntu
Linux系统之部署briefing视频聊天系统
【4月更文挑战第21天】Linux系统之部署briefing视频聊天系统
83 2
|
4月前
|
监控 Linux
Linux的epoll用法与数据结构data、event
Linux的epoll用法与数据结构data、event
51 0
|
11月前
|
存储 监控 网络协议
I/O多路复用【Linux/网络】(C++实现select、poll和epoll服务器)(下)
I/O多路复用【Linux/网络】(C++实现select、poll和epoll服务器)
190 0
|
4月前
|
监控 网络协议 Linux
Linux I/O多路复用深入解析:从select到epoll的演进之路
Linux I/O多路复用深入解析:从select到epoll的演进之路
378 0
|
4月前
|
NoSQL Java Linux
【Linux IO多路复用 】 Linux 网络编程 认知负荷与Epoll:高性能I-O多路复用的实现与优化
【Linux IO多路复用 】 Linux 网络编程 认知负荷与Epoll:高性能I-O多路复用的实现与优化
157 0