一、I/O多路复用
在网络编程中,多路复用是指同时监控多个文件描述符(如套接字)是否有事件(如数据可读、可写或异常)发生。常见的多路复用机制有 select
、poll
和 epoll
。这些机制用于开发高并发的网络服务器,能够高效处理大量客户端连接。
二、select
select
是最早引入的 I/O 多路复用机制之一,支持监控多个文件描述符(套接字、管道等)。使用 select
监控文件描述符集,并在其中一个或多个文件描述符发生 I/O 事件时返回。
1.优点:
- 简单且广泛
2.缺点:
- 文件描述符集大小受限(默认最大 1024 个)。
- 每次调用时需要重新初始化描述符集(开销较大)。
select
需要遍历全部文件描述符集,效率较低。
3.函数原型:
#include <sys/select.h>
#include <sys/time.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
4.使用步骤:
- 始化文件描述符集
fd_set
。 - 调用
select()
监听可读、可写或异常的文件描述符。 - 遍历文件描述符集,处理准备就绪的文件描述符。
5.select 示例代码:
#include <iostream>
#include <sys/select.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr{};
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(8080);
bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(listen_fd, 5);
fd_set readfds;
int max_fd = listen_fd;
while (true) {
FD_ZERO(&readfds); // 清空描述符集
FD_SET(listen_fd, &readfds); // 将监听 socket 添加到集合中
int activity = select(max_fd + 1, &readfds, nullptr, nullptr, nullptr);
if (activity < 0) {
std::cerr << "select error\n";
break;
}
if (FD_ISSET(listen_fd, &readfds)) {
int new_socket = accept(listen_fd, nullptr, nullptr);
std::cout << "New connection accepted\n";
close(new_socket);
}
}
close(listen_fd);
return 0;
}
三、poll
poll
是 select
的改进版,克服了 select
的文件描述符集大小限制,可以监控任意数量的文件描述符。此外,poll
通过数组结构传递文件描述符及其对应的事件,避免了 select
需要每次重置文件描述符集的问题。
1.优点:
- 没有文件描述符数量的限制。
- 可以同时监控多个不同类型的 I/O 事件。
2.缺点:
- 对所有文件描述符进行线性扫描,效率不高。
- 每次调用时仍然需要重新初始化文件描述符数组。
3.函数原型:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
4.使用步骤:
- 初始化
pollfd
数组,指定文件描述符和感兴趣的事件。 - 调用
poll()
监控事件的变化。 - 遍历
pollfd
数组,处理就绪的文件描述符。
5.示例代码:
#include <iostream>
#include <poll.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr{};
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(8080);
bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(listen_fd, 5);
pollfd fds[1];
fds[0].fd = listen_fd;
fds[0].events = POLLIN;
while (true) {
int activity = poll(fds, 1, -1); // 无限等待
if (activity < 0) {
std::cerr << "poll error\n";
break;
}
if (fds[0].revents & POLLIN) {
int new_socket = accept(listen_fd, nullptr, nullptr);
std::cout << "New connection accepted\n";
close(new_socket);
}
}
close(listen_fd);
return 0;
}
四、epoll
poll
是 Linux 内核中提供的高效的 I/O 多路复用机制,专门针对大量并发连接优化。与 select
和 poll
相比,epoll
只关注活跃的文件描述符,避免了对所有文件描述符的轮询,极大提高了效率,特别是在大量文件描述符的情况下。
1.优点:
- 高效,适用于大规模并发连接。
epoll
可以以水平触发(LT)或边缘触发(ET)的模式工作,ET 模式下效率更高。- 无需每次重新初始化监控集,动态添加和删除文件描述符。
2.缺点:
- 仅适用于 Linux 系统。
3.函数原型:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
4.使用步骤:
- 创建
epoll
实例。 - 使用
epoll_ctl()
添加、修改或删除需要监控的文件描述符。 - 使用
epoll_wait()
监听事件变化。
5.示例代码:
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#define MAX_EVENTS 10
int main() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in serv_addr{};
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(8080);
bind(listen_fd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(listen_fd, 5);
// 创建 epoll 实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
std::cerr << "Failed to create epoll instance\n";
return -1;
}
// 添加监听套接字到 epoll 监视列表
epoll_event event;
event.events = EPOLLIN;
event.data.fd = listen_fd;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event);
epoll_event events[MAX_EVENTS];
while (true) {
int num_fds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
for (int i = 0; i < num_fds; ++i) {
if (events[i].data.fd == listen_fd) {
int new_socket = accept(listen_fd, nullptr, nullptr);
std::cout << "New connection accepted\n";
// 将新的连接添加到 epoll 监视列表
epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = new_socket;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &ev);
} else if (events[i].events & EPOLLIN) {
char buffer[1024] = {0};
int bytes_read = read(events[i].data.fd, buffer, sizeof(buffer));
if (bytes_read > 0) {
std::cout << "Received: " << buffer << std::endl;
} else {
// 关闭并从 epoll 列表中删除
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr);
close(events[i].data.fd);
}
}
}
}
close(listen_fd);
close(epoll_fd);
return 0;
}
五、对比与选择
select
:适合较小规模的并发连接,具有广泛的兼容性。poll
:改善了select
的文件描述符限制问题,但效率仍然较低,适合中等规模的并发连接。epoll
:在大规模并发连接中效率最高,适用于高性能