23. IO多路复用

一、什么是I/O多路复用

  I/O 多路复用(I/O Multiplexing)是 Linux 中用于处理多个 I/O 操作的机制,使得单个线程或进程可以同时监视多个文件描述符,以处理多路 I/O 请求。我们使用 I/O 多路复用省去了进程或线程上下文切换的开销,提升了处理效率,减少了系统资源(如内存和 CPU 时间)的消耗,从而提供了应用程序的整体性能和响应速度。

二、I/O多用复用的系统调用

  在 Linux 中主要通过以下系统调用实现:select、poll 和 epoll。

/**
 * @brief 
 * 
 * @param __nfds 要监视的文件描述符最大值加 1
 * @param __readfds 要监视的可读的文件描述符集合
 * @param __writefds 要监视的可写的文件描述符集合
 * @param __exceptfds 要监视的异常的文件描述符集合
 * @param __timeout 设置超时时间
 * @return int 返回就绪的文件描述符个数,超时返回0,失败返回-1
 */
int select(int __nfds, fd_set *__restrict __readfds,
            fd_set *__restrict __writefds,
            fd_set *__restrict __exceptfds,
            struct timeval *__restrict __timeout);

  select 是早期的 I/O 多路复用机制,它允许程序监视多个文件描述符,判断时候可以进行 I/O 操作。程序通过提供三个文件描述符集(读、写、异常)和一个超时时间来调用 select,在任何一个文件描述符变得可读、可写或出现错误时返回。

/**
 * @brief 
 * 
 * @param __fds pollfd 结构体数组,包含文件描述符和要监视的事件信息
 * @param __nfds 数组元素数量
 * @param __timeout 超时时间,单位为毫秒,-1表示永久阻塞
 * @return int 返回就绪的文件描述符个数,超时返回0,失败返回-1
 */
int poll(struct pollfd *__fds, nfds_t __nfds, int __timeout);

  pool 与 select 函数类似,但它使用一个包含文件描述符和事件的结构体数组来代替三个文件描述符集。它可以处理更多文件描述符,并且更容易管理。

/**
 * @brief 生成一个 epoll 专用的文件描述符
 * 
 * @param __flags 标记位,用以获得不同的行为
 * @return int 返回重新指向新的epoll的文件描述符,如果失败则返回-1
 */
int epoll_create1(int __flags);

/**
 * @brief epoll 的事件注册函数
 * 
 * @param __epfd epoll 专用的文件描述符
 * @param __op 表示动作,用三个宏来表示
 *              EPOLL_CTL_ADD:注册新的 fd 到 epfd 中
 *              EPOLL_CTL_MOD:修改已经注册的fd的监听事件
 *              EPOLL_CTL_DEL:从 epfd 中删除一个fd
 * @param __fd 需要监听的文件描述符
 * @param __event 告诉内核要监听什么事件,可以是以下几个宏的集合
 *                  EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭)
 *                  EPOLLOUT:表示对应的文件描述符可以写
 *                  EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
 *                  EPOLLERR:表示对应的文件描述符发生错误
 *                  EPOLLHUP:表示对应的文件描述符被挂断
 *                  EPOLLET :将 EPOLL 设为边缘触发(Edge Trigger)模式,这是相对于水平触发(Level Trigger)来说的
 *                  EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
 * @return int 0表示成功,-1表示失败
 */
int epoll_ctl(int __epfd, int __op, int __fd, struct epoll_event *__event);

/**
 * @brief 等待事件的产生
 * 
 * @param __epfd epoll 专用的文件描述符
 * @param __events 分配好的 epoll_event 结构体数组
 * @param __maxevents maxevents 告之内核这个 events 有多少个
 * @param __timeout 超时时间,单位为毫秒,为 -1 时,函数为阻塞
 * @return 返回需要处理的事件数目,超时返回0,失败返回-1
 */
int epoll_wait(int __epfd, struct epoll_event *__events, int __maxevents, int __timeout);

  epoll 是 Linux 特有的、性能优化的 I/O 多路复用机制。它比 select 和 poll 更高效,特别适用于大规模并发连接。epoll 提供了两种工作模式:水平触发(Level Triggered,LT)和 边沿触发(Edge Triggered,ET)。ET 模式下,epoll 只能在状态变化是通知,因此更高效,但也更复杂。

  • 水平触发:该模式 epoll 的默认模式,在这种模式下,只要文件描述符上有未处理的事件,epoll 就会不断通知应用程序。
  • 边沿触发模式:在该模式下,当文件描述符从未就绪状态转变未就绪状态时,epoll 会通知应用程序。如果应用程序没有在通知后及时处理事件,epoll 不会在再次通知,除非文件描述符再次从未就绪变成就绪状态,即只在状态变化时通知一次。

三、epoll的使用

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

#define TCP_SERVER_IP           INADDR_ANY
#define TCP_SERVER_PORT         8000
#define TCP_MAX_CONNECT_COUNT   5

#define MAX_EVENT_COUNT         5

void set_fd_mode(int fd, int mode);

int main(void)
{
    struct sockaddr_in server_address = {0}, client_address = {0};
    int server_socket_fd = 0, client_socket_fd = 0;
    socklen_t client_address_length = sizeof(client_address);

    int epoll_fd = 0;
    struct epoll_event event = {0}, events[MAX_EVENT_COUNT] = {0}; 
    int nfds = 0;

    time_t raw_time = 0;
    struct tm *time_info = NULL;

    char data[1024] = {0};
    ssize_t length = 0;

    // 填写服务端地址
    server_address.sin_family = AF_INET;                        // 使用IPv4协议
    server_address.sin_addr.s_addr = htonl(TCP_SERVER_IP);      // 填写服务端的IP地址为本地的IP地址
    server_address.sin_port = htons(TCP_SERVER_PORT);           // 填写服务端的端口号

    // 创建socket
    if ((server_socket_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("create server socket failed.");
        exit(EXIT_FAILURE);
    } 

    // 绑定地址
    if (bind(server_socket_fd, (struct sockaddr *)&server_address, sizeof(server_address)) == -1)
    {
        perror("bind server address failed.");
        exit(EXIT_FAILURE);
    }

    // 进入监听状态
    if (listen(server_socket_fd, TCP_MAX_CONNECT_COUNT) == -1)
    {
        perror("server listen failed.");
        exit(EXIT_FAILURE);
    }

    // 将服务端套接字设置为非阻塞状态
    set_fd_mode(server_socket_fd, O_NONBLOCK);

    // 生成一个 epoll 专用的文件描述符
    if ((epoll_fd = epoll_create1(0)) == -1)
    {
        perror("create epoll fd failed.");
        exit(EXIT_FAILURE);
    }

    // 将服务端文件描述符的可读加入到列表中
    // 当有客户端连接过来的时候会被触发
    event.data.fd = server_socket_fd;
    event.events = EPOLLIN;   
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket_fd, &event) == -1)
    {
        perror("add event list failed.");
        exit(EXIT_FAILURE);
    }

    while (1)
    {
        // 等待可读信息
        // 第一次循环时,events中的元素只有一个,只等待客户端连接进来
        // 后续的循环既有新的客户端连接进来,也有旧的客户端发送的数据
        // nfds表示有多少个客户端连接
        if ((nfds = epoll_wait(epoll_fd, events, MAX_EVENT_COUNT, -1)) == -1)
        {
            perror("client connect server failed.");
            exit(EXIT_FAILURE);
        }

        for (int i = 0; i < nfds; i++)
        {
            // 第一次循环只会走这个逻辑
            // 后续循环中有新的连接时也会走这个逻辑
            if (events[i].data.fd == server_socket_fd)
            {
                // 因为里面有客户端连接了,直接获取连接
                if ((client_socket_fd = accept(server_socket_fd, (struct sockaddr *)&client_address, &client_address_length)) == -1)
                {
                    perror("server accept failed.");
                    exit(EXIT_FAILURE);
                }

                // 将客户端连接也设置为非阻塞状态
                set_fd_mode(client_socket_fd, O_NONBLOCK);
                printf("establish a connection with the client (%s:%d).\n", inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));

                // 将客户端的文件描述符的可读和可写添加到列表中
                event.data.fd = client_socket_fd;
                event.events = EPOLLIN | EPOLLET;   
                if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket_fd, &event) == -1)
                {
                    perror("add event list failed.");
                    exit(EXIT_FAILURE);
                }
            }
            // 当旧的客户端连接发送数据时,会走这个逻辑
            else if (events[i].events & EPOLLIN)
            {
                client_socket_fd = events[i].data.fd;

                getpeername(client_socket_fd, (struct sockaddr *)&client_address, &client_address_length);
                memset(data, 0, sizeof(data));

                while ((length = recv(client_socket_fd, data, sizeof(data), 0)) > 0)
                {
                    data[length] = '\0';
                    printf("receiving the data sent by the client (%s:%d): \n", 
                            inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));
                    printf("%s\n", data);

                    // 服务端返回数据
                    time(&raw_time);
                    time_info = localtime(&raw_time);
                    char date[128] = {0};
                    sprintf(date, 
                            "(%d-%d-%d %d:%d:%d)\n",
                            time_info->tm_year + 1900, time_info->tm_mon + 1, time_info->tm_mday, 
                            time_info->tm_hour, time_info->tm_min, time_info->tm_sec);
                    int date_length = strlen(date);
                    for (int i = length; i >= 0; i--)
                    {
                        data[i + date_length] = data[i];
                    }
                    memcpy(data, date, strlen(date));
                    send(client_socket_fd, data, strlen(data), 0);
                }

                if (length == 0)
                {
                    // 指定到这,客户端请求断开连接
                    printf("client (%s:%d) requested to disconnect.\n",
                        inet_ntoa(client_address.sin_addr), ntohs(client_address.sin_port));

                    // 当前前的客户端文件描述符从列表中去除
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, client_socket_fd, NULL);

                    // 关闭客户端
                    close(client_socket_fd);
                }
            }
        }
    }

    // 关闭服务端
    close(server_socket_fd);

    return 0;
}

void set_fd_mode(int fd, int mode)
{
    int opts = 0;

    if ((opts = fcntl(fd, F_GETFL)) < 0)          // 获取文件的权限模式和状态标志
    {
        perror("get server socket status failed.");
        exit(EXIT_FAILURE);
    }

    opts |= mode;
  
    if (fcntl(fd, F_SETFL, opts) < 0)             // 设置文件的权限模式和状态标志
    {
        perror("set server socket status failed.");
        exit(EXIT_FAILURE);
    }
}
posted @   星光映梦  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· Obsidian + DeepSeek:免费 AI 助力你的知识管理,让你的笔记飞起来!
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
历史上的今天:
2023-02-21 05. 数据输入输出
点击右上角即可分享
微信分享提示