epoll并发编程


一、epoll介绍

epoll Linux 提供的一种事件通知机制,用于处理大量文件描述符的 I/O 事件。它是 Linux I/O 多路复用的一种机制,相比于传统的 select pollepoll 在处理大量并发连接时表现更为高效。

优点如下:效率高: epoll 使用回调机制,只有在事件发生时才调用相应的回调函数,避免了轮询的开销。

支持大量文件描述符: epoll 不受文件描述符数量的限制,能够高效处理大量的并发连接。

不会因为文件描述符数量增加而导致效率下降: selectpoll 的效率在文件描述符数量增加时会降低,而 epoll 不受此影响。

1.1epoll 的系统调用

epoll 提供了三个主要的系统调用,用于创建 epoll 实例、控制事件的添加和删除,以及等待就绪事件的发生。

1.1.1epoll_create:

int epoll_create(int size);

创建一个新的 epoll 实例,并返回一个文件描述符。

size 参数是一个暗示内核初始分配多少空间用于保存文件描述符的数量。通常,这个值并不影响 epoll 的性能,因为内核会动态调整。

返回值:成功时返回一个非负的文件描述符,表示新创建的 epoll 实例;失败时返回 -1,并设置 errno。

1.1.2epoll_ctl:

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

用于在 epoll 实例上注册或注销文件描述符,并设置关联的事件。

epfd 是 epoll 实例的文件描述符。

op 是操作类型,可以是 EPOLL_CTL_ADD(添加)、EPOLL_CTL_MOD(修改)或 EPOLL_CTL_DEL(删除)。

fd 是要操作的文件描述符。

event 是一个指向 struct epoll_event 结构体的指针,表示要关联的事件。

返回值:成功时返回 0;失败时返回 -1,并设置 errno。

1.1.3epoll_wait:

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

用于等待就绪事件的发生。

epfd 是 epoll 实例的文件描述符。

events 是一个数组,用于存储就绪事件的结果。

maxevents 是 events 数组的最大容量,表示最多可以存储多少个就绪事件。

timeout 是超时时间(以毫秒为单位)。传入负值表示无限等待,传入 0 表示非阻塞,传入正值表示等待指定时间。

返回值:成功时返回就绪事件的数量;失败时返回 -1,并设置 errno。

1.2epoll 的触发模式

epoll 提供了两种触发模式:水平触发(Level-Triggered,简称 LT)和边缘触发(Edge-Triggered,简称 ET)。这两种模式决定了当文件描述符上的事件发生时,epoll 如何通知应用程序。

1.2.1水平触发(LT)

在水平触发模式下,当文件描述符上的事件发生时,epoll_wait 会通知应用程序,即使应用程序没有对文件描述符进行操作。如果应用程序没有完全处理事件,下次 epoll_wait 仍然会通知。

在使用水平触发模式时,通常需要使用非阻塞 I/O,以避免长时间的阻塞操作。因为水平触发会在文件描述符上的任何数据变化时通知应用程序,而不仅仅是在数据变为可读或可写时。

使用水平触发时,可以通过设置 struct epoll_event 结构体中的 events 字段为 EPOLLINEPOLLOUT,以监听可读或可写事件。

1.2.2边缘触发(ET)

在边缘触发模式下,epoll_wait 只在文件描述符上的事件发生时通知一次。如果应用程序没有完全处理事件,下次 epoll_wait 不会再次通知,直到文件描述符上的状态再次发生变化。

边缘触发模式需要更为精细的控制,因为应用程序需要确保在每次事件通知后,完全处理文件描述符上的数据,直到发生下一次状态变化。

在使用边缘触发模式时,可以通过设置 struct epoll_event 结构体中的 events 字段为 EPOLLIN | EPOLLETEPOLLOUT | EPOLLET,以监听可读或可写事件,并启用边缘触发。

1.3epoll_event 结构体

struct epoll_event {
    uint32_t events;    // 事件类型,如 EPOLLIN、EPOLLOUT 等
    epoll_data_t data;   // 用户数据,可以是文件描述符或指针等
};

typedef union epoll_data {
    void *ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;
 

二、通过epoll实现并发聊天室

本实验基于简单的客户端-服务器模型,使用套接字和 epoll 实现了基本的并发处理。客户端和服务器通过套接字通信,服务器通过 epoll 实例管理多个并发连接,从而实现同时处理多个客户端的消息。

  服务端

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <vector>

constexpr int MAX_EVENTS = 10;
constexpr int PORT = 8888;

struct Client {
    int fd;
    std::string name;
};

std::vector<Client> clients;

// 根据文件描述符获取客户端名称
std::string getClientNameByFd(int fd) {
    for (const auto& client : clients) {
        if (client.fd == fd) {
            return client.name;
        }
    }
    return "Unknown";
}

// 将消息发送给所有客户端
void sendToAllClients(const std::string& sender, const std::string& message) {
    for (const auto& client : clients) {
        if (send(client.fd, (sender + ": " + message).c_str(), strlen((sender + ": " + message).c_str()), 0) == -1) {
            perror("send");
        }
    }
}

int main() {
    // 创建服务器套接字
    int serverSock = socket(AF_INET, SOCK_STREAM, 0);
    if (serverSock == -1) {
        perror("socket");
        exit(EXIT_FAILURE);
    }

    // 设置服务器地址结构
    sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(PORT);
    serverAddr.sin_addr.s_addr = INADDR_ANY;

    // 将服务器套接字绑定到地址
    if (bind(serverSock, reinterpret_cast<sockaddr*>(&serverAddr), sizeof(serverAddr)) == -1) {
        perror("bind");
        exit(EXIT_FAILURE);
    }

    // 监听连接
    if (listen(serverSock, SOMAXCONN) == -1) {
        perror("listen");
        exit(EXIT_FAILURE);
    }

    // 创建 epoll 实例
    int epollFd = epoll_create1(0);
    if (epollFd == -1) {
        perror("epoll_create1");
        exit(EXIT_FAILURE);
    }

    // 设置 epoll 事件
    epoll_event event{};
    event.events = EPOLLIN;
    event.data.fd = serverSock;
    if (epoll_ctl(epollFd, EPOLL_CTL_ADD, serverSock, &event) == -1) {
        perror("epoll_ctl");
        exit(EXIT_FAILURE);
    }

    std::cout << "Server is listening on port " << PORT << std::endl;

    while (true) {
        // 等待 epoll 事件
        std::vector<epoll_event> events(MAX_EVENTS);
        int numEvents = epoll_wait(epollFd, events.data(), MAX_EVENTS, -1);
        if (numEvents == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }

        for (int i = 0; i < numEvents; ++i) {
            if (events[i].data.fd == serverSock) {
                // 有新的客户端连接
                sockaddr_in clientAddr{};
                socklen_t clientAddrLen = sizeof(clientAddr);
                int clientSock = accept(serverSock, reinterpret_cast<sockaddr*>(&clientAddr), &clientAddrLen);
                if (clientSock == -1) {
                    perror("accept");
                } else {
                    // 设置新连接的 epoll 事件
                    event.events = EPOLLIN | EPOLLET;
                    event.data.fd = clientSock;
                    if (epoll_ctl(epollFd, EPOLL_CTL_ADD, clientSock, &event) == -1) {
                        perror("epoll_ctl");
                        exit(EXIT_FAILURE);
                    }

                    std::cout << "New client connected. " << std::endl;

                    // 接收客户端名称
                    char buffer[256];
                    ssize_t bytesReceived = recv(clientSock, buffer, sizeof(buffer), 0);
                    if (bytesReceived <= 0) {
                        perror("recv");
                        close(clientSock);
                    } else {
                        buffer[bytesReceived] = '\0';
                        clients.push_back({clientSock, buffer});
                        std::cout << "Client '" << buffer << "' is online." << std::endl;
                    }
                }
            } else {
                // 有数据从客户端接收
                char buffer[256];
                ssize_t bytesReceived = recv(events[i].data.fd, buffer, sizeof(buffer), 0);
                if (bytesReceived <= 0) {
                    // 客户端断开连接
                    perror("recv");
                    close(events[i].data.fd);
                } else {
                    buffer[bytesReceived] = '\0';
                    std::cout << "Received from " << getClientNameByFd(events[i].data.fd) << ": " << buffer << std::endl;
                    sendToAllClients(getClientNameByFd(events[i].data.fd), buffer);
                }
            }
        }
    }

    // 关闭服务器套接字
    close(serverSock);
    return 0;
}

客户端

#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <arpa/inet.h>
#include <thread>

constexpr int PORT = 8888;
constexpr int BUFFER_SIZE = 256;

// 接收消息的线程函数
void receiveMessages(int socket) {
    char buffer[BUFFER_SIZE];
    while (true) {
        ssize_t bytesReceived = recv(socket, buffer, sizeof(buffer), 0);
        if (bytesReceived <= 0) {
            std::cerr << "服务器关闭了连接。" << std::endl;
            break;
        } else {
            buffer[bytesReceived] = '\0';
            std::cout << buffer << std::endl;
        }
    }
}

int main(int argc, char *argv[]) {
    if (argc != 2) {
        std::cerr << "用法: " << argv[0] << " <用户名>" << std::endl;
        return EXIT_FAILURE;
    }

    const char *name = argv[1];

    // 创建客户端套接字
    int clientSock = socket(AF_INET, SOCK_STREAM, 0);
    if (clientSock == -1) {
        perror("socket");
        return EXIT_FAILURE;
    }

    // 设置服务器地址结构
    sockaddr_in serverAddr{};
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_port = htons(PORT);
    inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);

    // 连接到服务器
    if (connect(clientSock, reinterpret_cast<sockaddr*>(&serverAddr), sizeof(serverAddr)) == -1) {
        perror("connect");
        close(clientSock);
        return EXIT_FAILURE;
    }

    // 发送客户端名称到服务器
    if (send(clientSock, name, strlen(name), 0) == -1) {
        perror("send");
        close(clientSock);
        return EXIT_FAILURE;
    }

    std::cout << "已连接到服务器。您可以开始输入消息。" << std::endl;

    // 创建接收消息的线程
    std::thread receiveThread(receiveMessages, clientSock);

    // 主循环用于发送消息
    while (true) {
        std::string message;
        std::getline(std::cin, message);

        // 发送消息到服务器
        if (send(clientSock, message.c_str(), message.length(), 0) == -1) {
            perror("send");
            break;
        }
    }

    // 等待接收线程结束
    receiveThread.join();

    // 关闭客户端套接字
    close(clientSock);
    return 0;
}

 结果演示

启动服务端

 启动多个客户端,并发送消息

可以看到在服务端和客户端均能打印消息 ,程序可以正常工作


总结

本实验使用epoll和套接字实现了基本的并发聊天功能。通过本学期网络程序设计的学习,我对Javascript网络编程,Socket API,网络协议设计及RPC,Linux内核网络协议栈的相关知识有了更深的理解。同时也感谢孟老师的悉心教导,让我们初学者也能进行实践学习,完成一个个实验。

  • 26
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值