基于Epoll实现的多人聊天室

概 述

本文介绍基于Epoll实现的多人聊天室服务端程序,有关Epoll的相关内容,可以参考博客Linux epoll ET模式实现

服务端

服务端利用List记录所有连接的客户端socket,在收到客户端消息时,广播给所有当前的客户端。
服务端需要注意如何处理客户端断开socket连接的逻辑,当客户端断开连接时,理论上服务端会触发EPOLLINEPOLLRDHUP事件,如果我们在服务端只关心EPOLLRDHUP事件,触发该事件后关闭套接字,这个逻辑是不可行的,有些系统未必会触发EPOLLRDHUP。所以服务端代码采用关心EPOLLIN事件,然后在read()时进行处理的方式,分为以下两种情况:

  • read返回0,对方正常调用close关闭连接
  • read返回-1,需要通过errno来判断,如果不是EAGAINEINTR,那么就是对方异常断开连接

(这里参考了知乎 Nov 23的回答)

Sever.h

#ifndef EPOLL_ET_SERVER_H
#define EPOLL_ET_SERVER_H

#include <list> //list
#include <string>

#define MAX_EVENT_NUMBER 5000   //Epoll最大事件数
#define BUFFER_SIZE      0xFFFF //缓存区数据大小

class Server {
public:
    explicit Server();
    bool InitServer(const std::string &Ip, const int &Port);
    void Run();

private:
    int m_socketFd;    //创建的socket文件描述符
    int m_epollFd;     //创建的epoll文件描述符
    std::list<int> m_clientsList;  //已连接的客户端socket列表

private:
    int setnonblocking(int fd); // 将文件描述符设置为非堵塞的
    void addfd(int epoll_fd, int sock_fd, bool epoll_et); // 将文件描述符上的事件注册
};

Sever.cpp

#include <iostream>
#include <fcntl.h> //fcntl()
#include <sys/epoll.h> //epoll
#include <netinet/in.h> //sockaddr_in
#include <arpa/inet.h> //inet_pton()
#include <string.h> //memset()
#include <unistd.h> //close()
#include "Server.h"

using namespace std;

Server::Server() {
}

bool Server::InitServer(const std::string &Ip, const int &Port) {
    int ret;
    struct sockaddr_in address;
    memset(&address, 0, sizeof(address)); //初始化 address
    address.sin_family = AF_INET;
    inet_pton(AF_INET, Ip.c_str(), &address.sin_addr);
    address.sin_port = htons(Port);

    m_socketFd = socket(AF_INET, SOCK_STREAM, 0); //创建socket
    if (m_socketFd < 0) {
        cout << "Server: socket error! id:" << m_socketFd << endl;
        return false;
    }

    ret = bind(m_socketFd, (struct sockaddr*)&address, sizeof(address)); //bind
    if (ret == -1) {
        cout << "Server: bind error!" << endl;
        return false;
    }

    ret = listen(m_socketFd, 20); //listen
    if (ret == -1) {
        cout << "Server: listen error!" << endl;
        return false;
    }

    m_epollFd = epoll_create(5);
    if (m_epollFd == -1) {
        cout << "Server: create epoll error!" << endl;
        return false;
    }
    addfd(m_epollFd, m_socketFd, true); //注册sock_fd上的事件

    return true;
}

int Server::setnonblocking(int fd) {
    int old_option = fcntl(fd, F_GETFL);
    int new_option = old_option | O_NONBLOCK;
    fcntl(fd, F_SETFL, new_option);
    return old_option;
}

void Server::addfd(int epoll_fd, int sock_fd, bool epoll_et) {
    epoll_event event;
    event.data.fd = sock_fd;
    event.events = EPOLLIN;
    if (epoll_et) event.events |= EPOLLET;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &event);
    setnonblocking(sock_fd);
}

void Server::Run() {
    epoll_event events[MAX_EVENT_NUMBER];

    while (1) {
        int ret = epoll_wait(m_epollFd, events, MAX_EVENT_NUMBER, -1); //epoll_wait
        if (ret < 0) {
            cout << "Server: epoll error" << endl;
            break;
        }

        char buf[BUFFER_SIZE];
        for (int i = 0; i < ret; ++i) {
            int sockfd = events[i].data.fd;
            if (sockfd == m_socketFd) { //新的socket连接
                struct sockaddr_in client_addr;
                socklen_t len = sizeof(client_addr);
                int conn_fd = accept(m_socketFd, (struct sockaddr*)&client_addr, &len);

                addfd(m_epollFd, conn_fd, true); //对 conn_fd 开启ET模式
                m_clientsList.emplace_back(conn_fd);

                cout << "Server: New connect fd:" << conn_fd << " Now client number:" << m_clientsList.size() << endl;
            } else if (events[i].events & EPOLLIN) { //可读事件
                char client_buf[BUFFER_SIZE];
                memset(&client_buf, '\0', BUFFER_SIZE);
                int recvRet = recv(sockfd, client_buf, BUFFER_SIZE - 1, 0);
                if (recvRet == 0) { //对端正常关闭socket
                    close(sockfd);
                    m_clientsList.remove(sockfd);
                    cout << "Server: Client close socket!" << endl;
                    cout << "Server: Now client number:" << m_clientsList.size() << endl;
                } else if (recvRet < 0){
                    if ((errno != EAGAIN) && (errno != EINTR)) { //对端异常断开socket
                        close(sockfd);
                        m_clientsList.remove(sockfd);
                        cout << "Server: Client abnormal close socket!" << endl;
                        cout << "Server: Now client number:" << m_clientsList.size() << endl;
                    } else { //recv error
                        cout << "Server: Recv error!" << endl;
                    }
                } else {
                    cout << "Server: Recv data: " << client_buf << endl;
                    for (auto &i : m_clientsList) {
                        if (i != sockfd) {
                            if (send(i, client_buf, BUFFER_SIZE, 0) < 0) {
                                close(sockfd);
                                m_clientsList.remove(sockfd);
                                cout << "Server: send error! Close client: " << i << endl;
                            }
                        }
                    }
                }
            } else {
                cout << "Server: socket something else happened!" << endl;
            }
        }
    }

    close(m_socketFd);
    close(m_epollFd);
}

main.cpp

#include "Epoll/Server.h"

using namespace std;

int main() {
    Server server;
    if (server.InitServer("127.0.0.1", 8888)) {
        server.Run();
    }
    return 0;
}

客户端

因为客户端对高并发的要求不高,并且select模式跨平台性更好,所以客户端代码用select来实现。有关select的原理,这里就不赘述了,大家可以百度学习。
代码是本人之前学习select时写的,这里拿来使用下,代码如下:

#include <iostream>
#include <string.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>

#define MAXSIZE 0xFFFF

using namespace std;

int main() {
    int socket_fd;
    struct sockaddr_in server_addr;
    int len;
    char send_buf[MAXSIZE];
    char get_buff[MAXSIZE];
    int recv_num;
    int fun_res;
    string str;

    fd_set rfds;
    struct timeval tv;
    int max_fd;

    memset(&server_addr,0,sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(8888);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    len = sizeof(server_addr);

    socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd < 0) {
        cout << "socket error!" << endl;
        exit(1);
    }

    fun_res = connect(socket_fd, (struct sockaddr *) &server_addr, len);
    if (fun_res < 0) {
        cout << "connect error!" << endl;
        exit(1);
    }

    while (1) {
        FD_ZERO(&rfds);
        FD_SET(0,&rfds);

        max_fd = 0;
        FD_SET(socket_fd,&rfds);
        if (max_fd < socket_fd) max_fd = socket_fd;

        tv.tv_sec = 5;
        tv.tv_usec = 0;

        fun_res = select(max_fd+1, &rfds, NULL, NULL, &tv);
        if (fun_res < 0) {
            cout << "select error!" << endl;
            exit(1);
        } else if(fun_res == 0) {
            //cout << "no msg!waiting..." << endl;
            continue;
        } else {
            if (FD_ISSET(socket_fd,&rfds)) {
                recv_num = recv(socket_fd, get_buff, sizeof(get_buff), 0);
                if (recv_num < 0) {
                    cout << "recv error!" << endl;
                    exit(1);
                } else {
                    get_buff[recv_num] = 0;
                    cout << "server msg: " << get_buff << endl;
                }
            }

            if (FD_ISSET(0,&rfds)) {
                cin >> send_buf;
                str = send_buf;

                fun_res = send(socket_fd,send_buf,strlen(send_buf),0);
                if (fun_res < 0) {
                    cout << "send error!" << endl;
                    exit(1);
                }
                if (str == "exit") exit(1);
            }
        }
    }

    close(socket_fd);
    return 0;
}

运行效果

聊天室

运行服务端后,启动多个客户端连接服务端。客户端发送数据后,服务端打印接受的数据并将数据广播给当前在线的客户端。

说明:
本服务端使用Epoll ET模式,简单的实现了多人聊天室。GitHub地址:ChatRoomServer
代码中没有使用多线程以及使用EPOLLONESHOT来避免多个线程同时操作一个socket的问题、对通信数据的处理也相对简单。日后工作之余会对代码进行完善更新。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容