epoll的简介
epoll 是 Linux 提供的一种高效的 I/O 多路复用接口,它是 select 和 poll 的增强版,用于同时监视多个文件描述符(FDs),以确定哪些 FDs 已经准备好进行 I/O 操作。epoll 相较于 select 和 poll 更加高效,因为它不需要在每次调用时重复传递和检查整个文件描述符集合,而是通过事件回调机制来通知进程哪些 FDs 已经就绪。
epoll 的核心优势
- 高效的事件通知机制:epoll 通过在内核中维护一个事件表,当 socket 状态发生变化时,epoll 能够立即通知应用程序,而不需要应用程序不断轮询检查。
- 支持大量并发连接:epoll 没有像 select 那样的文件描述符数量限制,因此能够处理大量的并发连接,这对于高并发的网络服务来说非常重要。
- 资源消耗低:epoll 通过使用内存映射(mmap)技术,减少了用户态和内核态之间的数据交换,从而降低了资源消耗。
- 两种触发模式:epoll 支持水平触发(LT)和边缘触发(ET)两种模式。LT 模式下,只要 socket 状态就绪,epoll 就会不断通知应用程序;而 ET 模式下,socket 状态变化时 epoll 只通知一次,这有助于减少不必要的通知,提高效率。
epoll的使用方法
- 创建 epoll 实例:使用 epoll_create 函数创建一个 epoll 实例,它返回一个文件描述符,用于后续的 epoll 操作。
- 添加监控的 socket:使用 epoll_ctl 函数将需要监控的 socket 添加到 epoll 实例中。
- 等待事件通知:使用 epoll_wait 函数等待事件发生,当有 socket 就绪时,epoll_wait 会返回,应用程序可以处理这些就绪的 socket。
- 处理就绪的 socket:应用程序根据 epoll_wait 返回的事件,进行相应的读写操作。
epoll与socket的关系
epoll 与 socket 的关系在于,socket 是网络通信的端点,而 epoll 是用来高效地管理多个 socket 连接的机制。在网络服务器中,通常会创建多个 socket 来处理客户端的连接请求,而 epoll 则用于高效地监控这些 socket 的状态,以便及时响应。
epoll监听socket的示例
server.cpp
#include <iostream>
#include <vector>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
const int MAX_EVENTS = 10;
const int LISTEN_PORT = 8080; // 监听端口
const char* LISTEN_IP = "127.0.0.1"; // 监听IP
// 创建并绑定套接字
int create_and_bind() {
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) {
std::cerr << "Failed to create socket" << std::endl;
return -1;
}
int opt = 1;
if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0)
std::cerr << "setsockopt(SO_REUSEADDR) failed" << std::endl;
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(LISTEN_IP);
addr.sin_port = htons(LISTEN_PORT);
if (bind(listen_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
std::cerr << "Failed to bind" << std::endl;
close(listen_fd);
return -1;
}
return listen_fd;
}
int main() {
// 创建并绑定套接字
int listen_fd = create_and_bind();
if (listen_fd == -1) {
return -1;
}
// 监听套接字
if (listen(listen_fd, SOMAXCONN) < 0) {
std::cerr << "Failed to listen" << std::endl;
close(listen_fd);
return -1;
}
// 创建epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd < 0) {
std::cerr << "Failed to create epoll instance" << std::endl;
close(listen_fd);
return -1;
}
// 将监听套接字添加到epoll
struct epoll_event event;
event.data.fd = listen_fd;
event.events = EPOLLIN;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event) < 0) {
std::cerr << "Failed to add listen_fd to epoll" << std::endl;
close(listen_fd);
close(epoll_fd);
return -1;
}
// 定义事件数组
struct epoll_event events[MAX_EVENTS];
// 等待并处理事件
while (true) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds < 0) {
std::cerr << "epoll_wait error" << std::endl;
break;
}
for (int i = 0; i < nfds; ++i) {
if (events[i].events & EPOLLIN) {
if (events[i].data.fd == listen_fd) {
// 接受新的连接
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr *)&client_addr, &client_addr_len);
if (client_fd < 0) {
std::cerr << "Failed to accept connection" << std::endl;
continue;
}
const char* message = "from, server!";
if (send(client_fd, message, strlen(message), 0) < 0) {
std::cerr << "Failed to send message to the client" << std::endl;
close(client_fd);
return -1;
}
std::cout << "Accepted new connection on FD " << client_fd << std::endl;
// 添加新的客户端连接到epoll
event.data.fd = client_fd;
event.events = EPOLLIN;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) < 0) {
std::cerr << "Failed to add client_fd to epoll" << std::endl;
close(client_fd);
}
} else {
// 读取数据
char buffer[1024];
ssize_t count = read(events[i].data.fd, buffer, sizeof(buffer));
if (count < 0) {
std::cerr << "Error reading from socket" << std::endl;
} else if (count == 0) {
std::cout << "Socket " << events[i].data.fd << " disconnected" << std::endl;
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
close(events[i].data.fd);
} else {
std::cout << "Data from socket " << events[i].data.fd << ": " << buffer << std::endl;
}
}
}
}
}
// 清理
close(listen_fd);
close(epoll_fd);
return 0;
}
client.cpp
#include <iostream>
#include <vector>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <cstring>
#include <arpa/inet.h>
const int MAX_EVENTS = 10;
// 函数用于创建并连接到服务器的套接字
int create_and_connect(const char* server_ip, int server_port) {
int sock = socket(AF_INET, SOCK_STREAM, 0);
if (sock < 0) {
std::cerr << "Failed to create socket" << std::endl;
return -1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(server_port);
inet_pton(AF_INET, server_ip, &server_addr.sin_addr);
if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
std::cerr << "Failed to connect to the server" << std::endl;
close(sock);
return -1;
}
std::cout << "Connected to server at " << server_ip << ":" << server_port << std::endl;
// 设置非阻塞模式
fcntl(sock, F_SETFL, O_NONBLOCK);
return sock;
}
int main() {
const char* server_ip = "127.0.0.1";
const int server_port = 8080;
const int num_clients = 5; // 假设我们有5个客户端连接
// 创建epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd < 0) {
std::cerr << "Failed to create epoll instance" << std::endl;
return -1;
}
// 创建多个客户端连接并添加到epoll
std::vector<int> client_sockets;
for (int i = 0; i < num_clients; ++i) {
int sock = create_and_connect(server_ip, server_port);
if (sock < 0) {
continue; // 错误处理可以更精细
}
client_sockets.push_back(sock);
struct epoll_event event;
event.data.fd = sock;
event.events = EPOLLIN; // 我们对读事件感兴趣
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock, &event) < 0) {
std::cerr << "Failed to add socket to epoll" << std::endl;
// 错误处理
}
}
// 定义事件数组
struct epoll_event events[MAX_EVENTS];
// 等待并处理事件
while (true) {
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds < 0) {
std::cerr << "epoll_wait error" << std::endl;
break;
}
for (int i = 0; i < nfds; ++i) {
if (events[i].events & EPOLLIN) {
int sock = events[i].data.fd;
char buffer[1024];
ssize_t count = read(sock, buffer, sizeof(buffer));
if (count < 0) {
std::cerr << "Error reading from socket" << std::endl;
} else if (count == 0) {
std::cout << "Socket " << sock << " disconnected" << std::endl;
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, sock, NULL);
close(sock);
} else {
std::cout << "Data from socket " << sock << ": " << buffer << std::endl;
}
}
}
}
// 清理
close(epoll_fd);
return 0;
}
client与server通信的结果
$./server
Accepted new connection on FD 5
Accepted new connection on FD 6
Accepted new connection on FD 7
Accepted new connection on FD 8
Accepted new connection on FD 9
$./client
Connected to server at 127.0.0.1:8080
Connected to server at 127.0.0.1:8080
Connected to server at 127.0.0.1:8080
Connected to server at 127.0.0.1:8080
Connected to server at 127.0.0.1:8080
Data from socket 4: from, server!
Data from socket 5: from, server!
Data from socket 6: from, server!
疑问
- 为什么服务端接收者的client端的fd为5,6,7,8,9. client端显示生成的fd为4,5,6,7,8?
在网络编程中,当客户端和服务端建立连接时,每个端点都会获得一个文件描述符(fd),用于标识和管理该连接。服务端接收到客户端的连接请求后,会调用 accept 函数来接受连接,并返回一个新的文件描述符,用于后续的读写操作。
服务端接收到的客户端文件描述符(例如5, 6, 7, 8, 9)和客户端生成的文件描述符(例如4, 5, 6, 7, 8)之间的关系如下:
- 客户端文件描述符:当客户端调用 socket 函数创建一个套接字时,它会获得一个文件描述符,这个文件描述符用于标识客户端的套接字。客户端使用这个文件描述符来执行 connect 调用,尝试与服务端建立连接。
- 服务端文件描述符:服务端监听某个端口,当 accept 被调用时,它会阻塞等待客户端的连接请求。一旦有客户端连接,accept 会返回一个新的文件描述符,这个文件描述符是服务端为这次连接创建的新套接字,专门用于与该客户端通信。
- 文件描述符的编号:文件描述符是一个整数,用于索引进程的文件描述符表。表中的每个条目都包含了一个打开文件或套接字的状态信息。文件描述符的编号是操作系统动态分配的,它不一定需要在客户端和服务端之间保持一致。换句话说,客户端的文件描述符编号(如4, 5, 6, 7, 8)和服务端接收到的文件描述符编号(如5, 6, 7, 8, 9)可以是不同的,它们只是在各自进程的文件描述符表中的索引。
- 文件描述符的分配:文件描述符的分配是由操作系统管理的,当一个新的文件或套接字被打开时,操作系统会分配一个当前未使用的最小文件描述符。因此,客户端和服务端的文件描述符编号可能会不同,这取决于各自的文件描述符表中当前已分配的情况。
- 显示的文件描述符:客户端显示的文件描述符是它自己用于 connect 调用的套接字的文件描述符。而服务端显示的文件描述符是 accept 返回的新套接字的文件描述符,用于与特定的客户端通信。
总结来说,客户端和服务端的文件描述符编号不需要匹配,它们只是在各自的上下文中用于标识套接字的整数。服务端的 accept 调用返回的文件描述符是专门用于与已连接的客户端通信的,而客户端的文件描述符是它最初创建套接字时获得的。