Linux下基于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;
}
上一篇:js(javascript) 继承的5种实现方式


下一篇:安装oracle