自学内容网 自学内容网

epoll示例

一、服务端

下面是一个使用epoll机制在Linux上编写的简单套接字程序示例:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/epoll.h>
#include <fcntl.h>

#define MAX_EVENTS 10
#define MAX_BUFFER_SIZE 1024

int main() {
    int server_socket, client_socket;
    struct sockaddr_in server_addr, client_addr;
    socklen_t addr_size;
    char buffer[MAX_BUFFER_SIZE];
    struct epoll_event event, events[MAX_EVENTS];

    // 创建套接字
    server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket < 0) {
        perror("Error creating socket");
        exit(EXIT_FAILURE);
    }

    // 设置服务器地址
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(6868);
    //server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  //绑定127.0.0.1

    memset(server_addr.sin_zero, '\0', sizeof(server_addr.sin_zero));

    // 绑定套接字到地址
    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("Error binding socket");
        exit(EXIT_FAILURE);
    }

    // 监听
    if (listen(server_socket, 5) < 0) {
        perror("Error listening");
        exit(EXIT_FAILURE);
    }

    // 创建epoll实例
    int epoll_fd = epoll_create1(0);
    if (epoll_fd < 0) {
        perror("Error creating epoll instance");
        exit(EXIT_FAILURE);
    }

    // 设置event结构体
    event.events = EPOLLIN;
    event.data.fd = server_socket;

    // 将socket添加到epoll实例中
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &event) < 0) {
        perror("Error adding socket to epoll instance");
        exit(EXIT_FAILURE);
    }

    while (1) {
        int num_ready = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (num_ready < 0) {
            perror("Error waiting for events");
            exit(EXIT_FAILURE);
        }

        for (int i = 0; i < num_ready; i++) {
            if (events[i].data.fd == server_socket) {
                // 检测到新的客户端连接请求
                addr_size = sizeof(client_addr);
                client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &addr_size);

                // 设置client_socket为非阻塞
                int flags = fcntl(client_socket, F_GETFL, 0);
                fcntl(client_socket, F_SETFL, flags | O_NONBLOCK);

                event.events = EPOLLIN | EPOLLET;
                event.data.fd = client_socket;

                // 将客户端socket添加到epoll实例中
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket, &event) < 0) {
                    perror("Error adding client socket to epoll instance");
                    exit(EXIT_FAILURE);
                }

                printf("New client connected: %s\n", inet_ntoa(client_addr.sin_addr));
            } else {
                // 处理客户端发送的数据
                int client_fd = events[i].data.fd;
                memset(buffer, 0, MAX_BUFFER_SIZE);

                int num_bytes = recv(client_fd, buffer, MAX_BUFFER_SIZE, 0);
                if (num_bytes < 0) {
                    perror("Error receiving data");
                    close(client_fd);
                    continue;
                } else if (num_bytes == 0) {
                    // 客户端连接关闭
                    printf("Client disconnected\n");
                    close(client_fd);
                    continue;
                }

                // 处理接收到的数据
                printf("Received data from client: %s\n", buffer);

                // 将数据发送回客户端
                send(client_fd, buffer, num_bytes, 0);
            }
        }
    }

    // 关闭套接字和epoll实例
    close(server_socket);
    close(epoll_fd);

    return 0;
}

这个程序创建了一个服务器套接字,使用epoll机制监听连接请求和处理客户端发送的数据。它首先创建了一个套接字 server_socket,并将其绑定到地址。然后通过 listen 函数开始监听连接请求。

接下来,程序创建了一个epoll实例 epoll_fd,并使用 epoll_create1 函数进行创建。然后,将服务器套接字添加到epoll实例中,通过 epoll_ctl 函数实现。接下来,程序进入一个无限循环中,使用 epoll_wait 函数等待事件发生。一旦有事件发生,通过遍历 events 数组处理每个事件。

当检测到一个新的客户端连接请求时,程序通过 accept 函数接受新的客户端连接,并将新的客户端套接字设置为非阻塞模式。然后,将客户端套接字添加到epoll实例中。

当客户端发送数据时,程序通过 recv 函数接收数据,并处理接收到的数据。然后,将数据发送回客户端,使用 send 函数。

最后,在循环结束时,程序关闭服务器套接字和epoll实例。

在C语言中,`struct sockaddr_in` 是一个用于存储网络地址的结构体,其中包括IP地址和端口号。它定义在 <netinet/in.h> 头文件中。这个结构体的定义如下:

struct sockaddr_in {
    short            sin_family;   // e.g. AF_INET, AF_INET6
    unsigned short   sin_port;     // e.g. htons(3490)
    struct in_addr   sin_addr;     // see struct in_addr, below
    char             sin_zero[8];  // Zero padding to make the struct the same size as struct sockaddr
};

其中最后一个成员 sin_zero 是一个填充字段,其目的是为了保证 struct sockaddr_in 结构体的总大小和 struct sockaddr 结构体的大小相同,因为在socket API中,地址通常是通过 struct sockaddr 类型来传递的。为了确保类型兼容和内存布局的一致性,`sin_zero` 成员被添加到 struct sockaddr_in 结构体中,当使用这个结构体时,通常需要将此字段设置为全 0。
memset(server_addr.sin_zero, '\0', sizeof(server_addr.sin_zero)); 使用 memset 函数将 sin_zero 字段的内存设置为0。这里的 '\0' 是空字符(null terminator),用于表示字符串的结束,在内存中其值为0。这行代码保证了填充字段没有留下任何未定义的数据,满足某些系统和库对结构体初始化的要求。
在许多实现中,这个填充可能并不是严格必要的,因为sockaddr_in和sockaddr的转换通常都能正常工作,但按照好的编程习惯,仍然建议对这部分内存进行清零处理。 

请注意,此示例程序是一个简单的示例,为了简洁起见,没有进行错误处理和边界检查。在实际编程中,您需要根据需求进行适当的错误处理和边界检查。此外,此示例使用了阻塞的 recvsend 函数,您可以根据需要使用非阻塞的I/O函数。

二、客户端

在Linux系统中,`epoll` 是一个高效的多路复用IO接口,它可以用于同时监控多个文件描述符,来检测它们是否有IO事件发生。在网络编程中,`epoll` 常用于接收端来管理多个客户端连接。然而,`epoll` 也同样适用于发送端,特别是当程序需要管理大量的出站连接时。
在发送端使用 epoll 有若干优势:
1. 非阻塞 I/O: 可以将套接字设置为非阻塞模式,然后使用 epoll 来检测何时可以在不阻塞的情况下发送数据。
2. 效率: 当有大量的套接字需要同时发送数据时,使用 epoll 可以减少CPU时间片的浪费,并减少上下文切换,因为可以仅在写入操作能够进行时才尝试发送数据。
3. 可扩展性: epoll 比传统的多路I/O复用方法(如 select 和 poll)具有更好的可扩展性,并且当监控的文件描述符数量增加时,其性能不会显著下降。
下面是一个简单的例子代码,演示了如何在Linux环境下使用epoll来监控socket的发送情况:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>

#define MAX_EVENTS 10
#define SERVER_PORT 6868
#define SERVER_IP "127.0.0.1"

int set_non_blocking(int sockfd) {
    int flags = fcntl(sockfd, F_GETFL, 0);
    if (flags == -1) {
        return -1;
    }

    flags |= O_NONBLOCK;
    if (fcntl(sockfd, F_SETFL, flags) == -1) {
        return -1;
    }

    return 0;
}

int main() {
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
       perror("epoll_create1 failed");
       exit(EXIT_FAILURE);
    }

    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (socket_fd == -1) {
        perror("socket failed");
        exit(EXIT_FAILURE);
    }

    // 设置套接字为非阻塞模式
    if (set_non_blocking(socket_fd) == -1) {
        perror("set_non_blocking failed");
        exit(EXIT_FAILURE);
    }

    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);
    server_addr.sin_addr.s_addr = inet_addr(SERVER_IP);

    // 连接到服务器
    if (connect(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        if (errno != EINPROGRESS) { // 非阻塞socket在连接时会返回EINPROGRESS
            perror("connect failed");
            exit(EXIT_FAILURE);
        }
    }

    struct epoll_event ev, events[MAX_EVENTS];
    ev.events = EPOLLOUT | EPOLLET;  // 关注可写事件,使用边缘触发模式
    ev.data.fd = socket_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &ev) == -1) {
        perror("epoll_ctl: socket_fd");
        exit(EXIT_FAILURE);
    }

    while (1) {
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }

        for (int i = 0; i < nfds; ++i) {
            if (events[i].data.fd == socket_fd && (events[i].events & EPOLLOUT)) {
                // 套接字准备好写入,发送数据
                const char *msg = "Hello, Server!";
                ssize_t bytes_sent = send(socket_fd, msg, strlen(msg), 0);
                if (bytes_sent < 0) {
                    // 发送失败的处理
                    perror("send failed");
                    close(socket_fd);
                    exit(EXIT_FAILURE);
                } else {
                    printf("Message sent: %s\n", msg);
                    // 为了简化示例,发送成功后就退出循环
                    close(socket_fd);
                    close(epoll_fd);
                    exit(EXIT_SUCCESS);
                }
            }
        }
    }

    close(epoll_fd);
    return 0;
}

注意:这个示例假设与服务器的连接已经建立,并准备发送数据。如果服务器没有运行在端口 6868 或者服务器拒绝连接,那么 connect 调用将失败。
在运行这个代码前,确保本地的服务器正在监听端口 6868,否则 connect 调用将不会成功。此外,该例子只发送一次数据并在发送成功后立即关闭socket和epoll文件描述符,这只是为了示范目的。实际使用中,可能希望保持连接并继续根据需要进行数据发送。

三、编译运行

1. 服务端

gcc server.c -o server
./server
New client connected: 127.0.0.1
Received data from client: Hello, Server!
Client disconnected

2.客户端

gcc client.c -o client
./client
Message sent: Hello, Server!

四、多个客户端

想要在一个进程中管理多个到同一个服务器的连接,不需要为每个连接创建新的进程。而是在同一个进程中打开多个套接字,并将它们全部注册到同一个`epoll`实例。如下所示:

#include <sys/epoll.h>
#include <sys/socket.h>
// 其他必要的头文件...

#define MAX_EVENTS 10

int main() {
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
       perror("epoll_create1 failed");
       exit(EXIT_FAILURE);
    }

    struct epoll_event ev, events[MAX_EVENTS];
    int socket_fds[2];  // 假设我们有两个连接

    // 对每个套接字重复连接和设置过程
    for (int i = 0; i < 2; i++) {
        socket_fds[i] = /* 这里是创建套接字并连接到服务器的代码 */;
        ev.events = EPOLLOUT;
        ev.data.fd = socket_fds[i];
        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fds[i], &ev) == -1) {
            perror("epoll_ctl: socket_fds[i]");
            exit(EXIT_FAILURE);
        }
    }

    while (1) {
        int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (nfds == -1) {
            perror("epoll_wait");
            exit(EXIT_FAILURE);
        }

        for (int i = 0; i < nfds; ++i) {
            if (events[i].events & EPOLLOUT) {
                // 在这里根据events[i].data.fd判断是哪个套接字准备好了,然后发送数据
                send(events[i].data.fd, /* data */, /* size */, /* flags */);
                // 处理发送逻辑
            }
        }
    }

    close(epoll_fd);
    for (int i = 0; i < 2; i++) {
        close(socket_fds[i]);  // 关闭套接字
    }
    return 0;
}

在这个示例中,`socket_fds` 数组用来存储两个套接字描述符,并且都被添加到同一个`epoll`实例中。这样,在主循环中使用`epoll_wait`时可以同时监控两个套接字的事件状态。当套接字准备好写数据时,`epoll_wait`会返回并且通过检查`events[i].events` 来确定是哪个套接字准备好,并执行相应的`send`操作。

上述代码是一个示意性的框架,其中需要填充创建套接字并连接到服务器的代码,以及进行实际数据发送的代码。此外,异常处理和清理操作(如关闭套接字)在实际应用中也需要妥善处理。

五、动态添加和删除客户端套接字

1. 动态添加

为每个客户端都维护一个 epoll 实例并不是一个可扩展或高效的解决方案。事实上,`epoll` 的主要优势之一就是能够使用单个 epoll 实例来监控多个文件描述符(如socket连接)。这样,使用单个线程或者进程就能够管理大量的客户端连接,从而显著减少系统资源的使用和上下文切换的开销。

正确的做法是为所有的客户端连接使用同一个 epoll 实例。当有新的客户端连接时,可以把新的socket文件描述符添加到这个 epoll 实例中去。这个 epoll 实例会告诉哪些socket有事件需要处理,比如数据准备好读取或socket准备好写入数据。

下面是一个简单的例子,展示了如何使用单个 epoll 实例来处理来自多个客户端的连接:

#define MAX_EVENTS 1024

// 创建并初始化epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
    // 处理错误
}

struct epoll_event event, events[MAX_EVENTS];

// 通过某种方式获取到一个监听socket_fd,例如bind和listen之后的socket

event.events = EPOLLIN; // 监控可读事件
event.data.fd = listen_socket_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_socket_fd, &event) == -1) {
    // 处理错误
}

while (1) {
    // 等待事件发生
    int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    if (nfds == -1) {
        // 处理错误
    }

    for (int i = 0; i < nfds; ++i) {
        if (events[i].data.fd == listen_socket_fd) {
            // 接受新的连接
            int client_fd = accept(listen_socket_fd, NULL, NULL);
            if (client_fd == -1) {
                // 处理错误
            }

            // 设置新的socket为非阻塞模式...

            // 将新的客户端socket添加到epoll实例中
            event.events = EPOLLIN | EPOLLET; // 边缘触发模式
            event.data.fd = client_fd;
            if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &event) == -1) {
                // 处理错误
            }
        } else {
            // 处理客户端socket的事件:
            // 如果是EPOLLIN事件,读取数据
            // 如果是EPOLLOUT事件,发送数据
            // 如果有EPOLLERR或EPOLLHUP,处理断开连接
        }
    }
}

// 清理资源
close(epoll_fd);
// 关闭其他打开的sockets

这个例子中,我们通过对每个新接受的客户端连接调用 epoll_ctl,让单个 epoll 实例监控多个客户端连接。在服务端程序运行期间,`epoll_wait` 调用返回准备好的事件,然后我们根据事件类别(可读、可写、错误等)来处理每个客户端的socket。

使用这种方式,可以高效、可靠和可扩展地管理成千上万个并发连接。

2. 动态删除

在同一个 epoll 实例中动态地删除多个客户端套接字,可以通过调用 epoll_ctl 函数并指定 EPOLL_CTL_DEL 操作来实现。当决定不再监控某个文件描述符时,需要从 epoll 的监控列表中移除它,以避免无用的资源占用和可能的错误触发。

以下是一个简单的示例,说明如何删除多个套接字:

#include <sys/epoll.h>
// 其他必要的头文件...

int main() {
    int epoll_fd = epoll_create1(0);
    if (epoll_fd == -1) {
        perror("epoll_create1 failed");
        exit(EXIT_FAILURE);
    }

    // 假设我们有一个socket_fds数组,包含了要监控的所有客户端套接字文件描述符
    int socket_fds[] = { /* ... 客户端套接字文件描述符列表 ... */ };
    int num_sockets = sizeof(socket_fds) / sizeof(socket_fds[0]);

    // 将所有客户端套接字添加到epoll监控
    for (int i = 0; i < num_sockets; ++i) {
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = socket_fds[i];

        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fds[i], &ev) == -1) {
            perror("epoll_ctl: ADD");
            exit(EXIT_FAILURE);
        }
    }

    // ... 在这里进行一些IO操作 ...

    // 假设现在要移除多个客户端套接字
    for (int i = 0; i < num_sockets; ++i) {
        if (需要删除的条件) { // 这里应该是具体的逻辑条件,用来判断哪些套接字需要被删除
            if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, socket_fds[i], NULL) == -1) {
                perror("epoll_ctl: DEL");
                // 即使删除失败,你可能也希望继续尝试删除其他套接字
            }
        }
    }

    // 清理并关闭epoll实例
    close(epoll_fd);
    return 0;
}

在上面的示例中,通过循环遍历一个包含多个套接字的数组,并使用条件来判断是否应该删除某个套接字。满足条件的套接字会通过 epoll_ctl 调用与 EPOLL_CTL_DEL 操作来从 epoll 实例中移除。在 EPOLL_CTL_DEL 操作中,事件参数可以是 NULL,因为删除操作不需要事件结构的信息。

在实际的并发服务器应用程序中,可能需要对资源访问进行同步,以防止出现竞态条件。如果应用程序是多线程的,确保在访问和修改与 epoll 实例相关的共享资源时使用适当的锁机制。


原文地址:https://blog.csdn.net/eidolon_foot/article/details/135851887

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!