当前位置: 首页 > news >正文

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内核网络协议栈的相关知识有了更深的理解。同时也感谢孟老师的悉心教导,让我们初学者也能进行实践学习,完成一个个实验。

http://www.xdnf.cn/news/822979.html

相关文章:

  • 数据挖掘(七) DBSCAN聚类算法
  • 魔兽世界宏命令完全指南
  • clamav使用指南
  • 白盒测试(超详细整理)
  • mpi编程基础
  • HotSpot与JVM概述
  • 【C++二分查找 】1477. 找两个和为目标值且不重叠的子数组|1850
  • 高数——偏导数
  • 如何搭建Z-blog网站并结合内网穿透实现无公网ip访问本地站点
  • JavaScript基本功之迭代器(iterator)的使用和原理
  • 微软 SDL 安全研发生命周期详解
  • TightVNC二次开发(1) 软件安装与测试
  • posix是什么
  • jrebel debug 启动不起来
  • 【LUT技术专题】图像自适应3DLUT代码讲解
  • 一文读懂UML用例图
  • Webshell 网络安全应急响应
  • 【TIFF】一.TIFF 格式详解
  • 源码、反码、补码(超详细解析)
  • 深度神经网络(Deep Neural Networks,DNN)模型
  • RAID磁盘阵列详解
  • white-space几种属性的用法(处理空格)
  • BeanUtils工具类下copyProperties拷贝对象的用法
  • 51单片机下载不进去程序?(pcb的设计问题)
  • OTN技术
  • C语言回调函数详解(全网最全)
  • 预警功能深度测评:系统如何降低设备突发故障率?
  • Redis 复制(replica)
  • Web前端 | HTML表单form
  • c语言中atoi函数用法以及功能