一、epoll介绍
epoll
是 Linux 提供的一种事件通知机制,用于处理大量文件描述符的 I/O 事件。它是 Linux 中 I/O 多路复用的一种机制,相比于传统的 select
和 poll
,epoll
在处理大量并发连接时表现更为高效。
优点如下:效率高: epoll
使用回调机制,只有在事件发生时才调用相应的回调函数,避免了轮询的开销。
支持大量文件描述符: epoll
不受文件描述符数量的限制,能够高效处理大量的并发连接。
不会因为文件描述符数量增加而导致效率下降: select
和 poll
的效率在文件描述符数量增加时会降低,而 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
字段为 EPOLLIN
或 EPOLLOUT
,以监听可读或可写事件。
1.2.2边缘触发(ET)
在边缘触发模式下,epoll_wait
只在文件描述符上的事件发生时通知一次。如果应用程序没有完全处理事件,下次 epoll_wait
不会再次通知,直到文件描述符上的状态再次发生变化。
边缘触发模式需要更为精细的控制,因为应用程序需要确保在每次事件通知后,完全处理文件描述符上的数据,直到发生下一次状态变化。
在使用边缘触发模式时,可以通过设置 struct epoll_event
结构体中的 events
字段为 EPOLLIN | EPOLLET
或 EPOLLOUT | 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内核网络协议栈的相关知识有了更深的理解。同时也感谢孟老师的悉心教导,让我们初学者也能进行实践学习,完成一个个实验。