高效网络I/O的秘诀:探索Select、Poll和Epoll的力量

目录

一、相关概念简介

1、网络I/O

2、Select

3、Poll

4、Epoll

二、代码实践

1、网络I/O

1)基础搭建

2)io与tcp连接关系

2、select

1)函数原型

2)参数解释

3)fd_set 操作

4)示例代码 

3、poll

1)函数原型

2)参数解释

3)pollfd 结构体

4)事件类型

5)示例代码

4、epoll

1)epoll_create

2) epoll_ctl

3)epoll_wait

4) epoll_event 结构

5)事件类型

6)示例代码 


一、相关概念简介

1、网络I/O

网络I/O指的是在网络编程中,数据在网络中的输入与输出过程,主要涉及到数据的发送与接收。在Linux系统中,有几种常用的I/O模型,包括阻塞I/O、非阻塞I/O、I/O复用、信号驱动I/O和异步I/O。其中,I/O复用是非常关键的一种技术,它允许单个线程同时监视多个文件描述符,以检查一个或多个文件描述符是否就绪(即它们是否已准备好进行非阻塞性读或写操作)。

2、Select

select 是最初UNIX系统支持的I/O多路复用接口。它允许程序监视多个文件描述符,等待直到一个或多个文件描述符就绪(可读、可写或异常),或者直到超时。

优点

  • 简单易用,广泛支持于各种操作系统。

缺点

  • 文件描述符数量受限于FD_SETSIZE,通常是1024。
  • 每次调用select时,都需要重新传入文件描述符集合,这增加了开销。
  • 内部实现使用线性结构存储文件描述符,因此每次都需要遍历整个集合,效率低下。

3、Poll

poll 函数与select类似,但它没有文件描述符数量的限制,因为它使用了一种不同的方式来存储和查找监视的文件描述符。

优点

  • 不受文件描述符数量限制。
  • select相比,管理文件描述符的方式更灵活。

缺点

  • 虽然解决了文件描述符数量的限制,但在文件描述符多的情况下,性能仍然不是很高,因为它依然需要遍历所有文件描述符。

4、Epoll

epoll 是Linux特有的I/O复用机制,性能比selectpoll更高。它不仅支持大量的文件描述符,而且只会激活那些真正发出I/O通知的描述符。

优点

  • 支持的文件描述符数量远大于select
  • 使用事件通知方式,只处理活跃的文件描述符,效率高。
  • 文件描述符的添加、修改和删除都有对应的API,管理更高效。

缺点

  • 仅在Linux系统上可用。

这三种技术各有利弊,选择哪一种主要取决于应用程序的需求和运行环境。对于需要处理大量连接的高性能服务器应用,epoll通常是最佳选择。

二、代码实践

1、网络I/O

1)基础搭建

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main()
{

    int socketfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    servaddr.sin_port = htons(2000);              // 0-1023

    if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
    }

    listen(socketfd, 10);

    printf("listen finshed\n");
    getchar();
    printf("exit\n");

    return 0;
}

 使用gcc编译运行

travis@Travis-Ubuntu:~/share/network-io$ gcc -o  networkio networkio.c 
travis@Travis-Ubuntu:~/share/network-io$ ./networkio 
listen finshed

 终端输入指令,查看端口2000的状态

netstat -anop | grep 2000

终端输入信息:

travis@Travis-Ubuntu:~/share/network-io$ netstat -anop | grep 2000
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        1      0 0.0.0.0:2000            0.0.0.0:*               LISTEN      8340/./networkio     关闭 (0.00/0/0)

此时再开一个终端再次运行将会绑定失败,输入如下信息:

travis@Travis-Ubuntu:~/share/network-io$ ./networkio 
bind failed: Address already in use
listen finshed

总结: 

  1. 端口被绑定之后不能再次被绑定
  2. 执行了listen,可以通过netstat看见端口的状态
  3. 进入listen就可以被连接,并且会产生新连接状态
  4. io与tcp连接

2)io与tcp连接关系

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main()
{

    int socketfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    servaddr.sin_port = htons(2000);              // 0-1023

    if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
    }

    listen(socketfd, 10);
    printf("listen finshed\n");

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);

    int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);

    char buffer[1024] = {0};
    int count = recv(clientfd, buffer, 1024, 0);
    printf("RECV: %s\n", buffer);

    send(clientfd, buffer, count, 0);

    getchar();
    printf("exit\n");

    return 0;
}

编译运行,当前代码会阻塞在accept, 因为 accept 是一个阻塞调用,它会等待客户端的连接。如果没有客户端尝试连接到服务器,程序就会在 accept 调用处停止执行,直到一个连接到来。

注意:(监听)在调用 accept 之前,需要在 socketfd 上调用 listen 函数,使其能够接受来自客户端的连接请求。否则accept将执行错误,不会阻塞。

使用NetAssist网络调试助手连接后,再次查看端口2000信息,会发现多出一条信息,

travis@Travis-Ubuntu:~/share/network-io$ netstat -anop | grep 2000
(并非所有进程都能被检测到,所有非本用户的进程信息将不会显示,如果想看到所有信息,则必须切换到 root 用户)
tcp        0      0 0.0.0.0:2000            0.0.0.0:*               LISTEN      9768/./networkio     关闭 (0.00/0/0)
tcp        0      0 192.168.1.132:2000      192.168.1.108:50385     ESTABLISHED 9768/./networkio     关闭 (0.00/0/0)

这里第一个对应的是socketfd,第二个对应的是clientfd 。

总结:fd与tcp连接信息一对一的关系

一请求一线程的方式

为了连接多个客户端,需要多次调用accept,为每一个clientfd单独开一个线程去读取接收信息

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>

void *client_thread(void *arg)
{
    int clientfd = *(int *)arg;

    while (1)
    {
        char buffer[1024] = {0};
        int count = recv(clientfd, buffer, 1024, 0);

        if (count == 0)
        {
            // 客户端关闭了连接
            printf("Client closed the connection\n");
            break;
        }
        else if (count < 0)
        {
            // 发生错误
            perror("Error receiving data");
            break;
        }

        send(clientfd, buffer, count, 0);
    }

    close(clientfd); // 确保关闭客户端文件描述符
    return NULL;
}

int main()
{

    int socketfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    servaddr.sin_port = htons(2000);              // 0-1023

    if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
    }

    listen(socketfd, 10);
    printf("listen finshed\n");

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);

#if 0
    int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);

    char buffer[1024] = {0};
    int count = recv(clientfd, buffer, 1024, 0);
    printf("RECV: %s\n", buffer);

    send(clientfd, buffer, count, 0);
#elif 0
    while (1)
    {
        int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);

        char buffer[1024] = {0};
        int count = recv(clientfd, buffer, 1024, 0);
        printf("RECV: %s\n", buffer);

        send(clientfd, buffer, count, 0);
    }

#else
    while (1)
    {
        int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);

        pthread_t thid;
        pthread_create(&thid, NULL, client_thread, &clientfd);
    }

#endif
    getchar();
    printf("exit\n");

    return 0;
}

在clientfd正常连接状态,每个线程中循环会阻塞在recv函数中。

当连接关闭时,recv 函数会停止阻塞并返回 0。这意味着没有数据被接收,且连接已经被对端(如客户端)正常关闭。在这种情况下,你的服务器线程应该识别到这个返回值,合适地处理这种情况,通常是通过结束循环并执行必要的清理工作,比如关闭线程使用的套接字。

在阻塞模式下,recv 主要在以下几种情况中停止阻塞:

  1. 数据接收:收到数据,recv 读取这些数据并返回读取的字节数。
  2. 连接关闭:对端关闭连接,recv 返回 0
  3. 错误发生:如果发生接收错误(例如因网络问题),recv 返回 -1 并设置 errno 以指示错误类型。

因此,当你的应用程序检测到 recv 返回 0,这通常意味着应当终止对该连接的进一步读取和写入操作,关闭套接字,并适当地管理线程的退出。

2、select

1)函数原型

select 的函数原型定义在 <sys/select.h> 头文件中,其基本形式如下:

#include <sys/select.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

2)参数解释

  • nfds: 监视的文件描述符的最大数目加一。通常设为最大文件描述符编号加一。
  • readfds: 指向 fd_set 结构的指针,该结构指定哪些文件描述符需要检测读取就绪状态。
  • writefds: 指向 fd_set 结构的指针,该结构指定哪些文件描述符需要检测写入就绪状态。
  • exceptfds: 指向 fd_set 结构的指针,该结构指定哪些文件描述符需要检测异常条件。
  • timeout: 指向 timeval 结构的指针,用于指定等待就绪文件描述符的最大时间。如果为 NULL,则无限等待。

3fd_set 操作

select 使用 fd_set 数据结构来管理文件描述符集合。有几个宏用于操作 fd_set

  • FD_ZERO(fd_set *set): 初始化文件描述符集合,将集合清空。
  • FD_SET(int fd, fd_set *set): 将指定的文件描述符加入集合。
  • FD_CLR(int fd, fd_set *set): 将指定的文件描述符从集合中删除。
  • FD_ISSET(int fd, fd_set *set): 检查指定的文件描述符是否在集合中。

4)示例代码 

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/select.h>

int main()
{

    int socketfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    servaddr.sin_port = htons(2000);              // 0-1023

    if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
    }

    listen(socketfd, 10);
    printf("listen finshed: %d\n", socketfd);

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);

    fd_set rfds, rset;

    FD_ZERO(&rfds);
    FD_SET(socketfd, &rfds);

    int maxfd = socketfd; // fd集合的最大值

    while (1)
    {
        rset = rfds;
        int nready = select(maxfd + 1, &rset, NULL, NULL, NULL);

        if (FD_ISSET(socketfd, &rset)) // accept
        {
            int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);
            printf("accept finshed: %d\n", clientfd);

            FD_SET(clientfd, &rfds);

            if (clientfd > maxfd)
                maxfd = clientfd;
        }
        // recv
        int i = 0;
        for (i = socketfd + 1; i <= maxfd; i++)
        {
            if (FD_ISSET(i, &rset))
            {
                char buffer[1024] = {0};
                int count = recv(i, buffer, 1024, 0);

                if (count == 0)
                {
                    // 客户端关闭了连接
                    printf("Client closed the connection: %d\n", i);
                    close(i);
                    FD_CLR(i, &rfds);
                    
                    continue;
                }
                else if (count < 0)
                {
                    // 发生错误
                    perror("Error receiving data");
                    break;
                }

                send(i, buffer, count, 0);
            }
        }
    }
    getchar();
    printf("exit\n");

    return 0;
}
  1. 每次調用需要把fd_set集合, 从用户空间copy到内核空间
  2. maxfd,遍历到最大的maxfd       for(int i = 0; i < maxfd + 1; i++)

3、poll

1)函数原型

poll 函数的原型定义在 <poll.h> 头文件中,其基本形式如下:

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

2)参数解释

  • fds: 指向一个 pollfd 结构数组的指针,每个结构体用于指定监视的文件描述符和感兴趣的事件。
  • nfds: 指定数组 fds 中结构体的数量,告诉 poll 监视多少个文件描述符。
  • timeout: 指定等待事件发生的超时时间(以毫秒为单位)。如果设置为 -1,则无限等待直到某个事件发生。设置为 0 则表示非阻塞模式,即 poll 调用立即返回,不管是否有事件发生。

3pollfd 结构体

pollfd 结构体定义了要监视的文件描述符和事件类型,其定义如下:

struct pollfd {
    int fd;         // 文件描述符
    short events;   // 监视的事件
    short revents;  // 实际发生的事件,由 poll() 填充
};

4)事件类型

  • POLLIN: 数据可读。
  • POLLOUT: 数据可写。
  • POLLERR: 错误条件。
  • POLLHUP: 挂起条件。
  • POLLPRI: 有紧急数据可读。

5)示例代码

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/select.h>
#include <poll.h>

int main()
{

    int socketfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    servaddr.sin_port = htons(2000);              // 0-1023

    if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
    }

    listen(socketfd, 10);
    printf("listen finshed: %d\n", socketfd);

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);


    struct pollfd fds[1024] = {0};
    fds[socketfd].fd = socketfd;
    fds[socketfd].events = POLLIN;

    int maxfd = socketfd;

    while (1)
    {
        int nready = poll(fds, maxfd + 1, -1);

        if (fds[socketfd].revents & POLLIN)
        {
            int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);
            printf("accept finshed: %d\n", clientfd);

            fds[clientfd].fd = clientfd;
            fds[clientfd].events = POLLIN;

            if (clientfd > maxfd)
                maxfd = clientfd;
        }
        int i = 0;
        for (i = socketfd + 1; i < maxfd + 1; i++)
        {
            if (fds[i].revents & POLLIN)
            {
                char buffer[1024] = {0};
                int count = recv(i, buffer, 1024, 0);

                if (count == 0)
                {
                    // 客户端关闭了连接
                    printf("Client closed the connection: %d\n", i);
                    close(i);
                    fds[i].fd = -1;
                    fds[i].events = 0;
                    continue;
                }
                else if (count < 0)
                {
                    // 发生错误
                    perror("Error receiving data");
                    break;
                }

                send(i, buffer, count, 0);
            }
        }
    }

    getchar();
    printf("exit\n");

    return 0;
}

4、epoll

1)epoll_create

  • 功能:创建一个 epoll 的实例。
  • 参数:指定 epoll 实例能处理的最大文件描述符数量。
  • 返回值:返回一个文件描述符,这个描述符用于所有后续对 epoll 接口的调用。
  • 实例代码:
    int epfd = epoll_create(256); // 创建一个新的epoll实例,可以监控最多256个描述符
    

2)epoll_ctl

  • 功能:用于控制某个文件描述符上的事件,可以注册、修改或删除。
  • 参数:
    • epfdepoll_create 返回的文件描述符。
    • op:要进行的操作,如 EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL
    • fd:相关联的文件描述符。
    • event:指向 epoll_event 结构的指针,该结构指定了对应的事件类型和用户数据。
  • 返回值:成功时返回 0,失败时返回 -1。
  • 示例代码:
    struct epoll_event ev;
    ev.events = EPOLLIN; // 监控输入事件
    ev.data.fd = sockfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev); // 添加 sockfd 到 epoll 实例中
    

3)epoll_wait

  • 功能:等待注册的文件描述符上的事件发生。
  • 参数:
    • epfdepoll_create 返回的文件描述符。
    • events:用于从内核得到事件的集合。
    • maxevents:告诉内核这个 events 数组可以接收多少事件。
    • timeout:等待的超时时间(毫秒),如果设置为 -1 表示无限等待。
  • 返回值:有事件发生的文件描述符数量,0 表示超时,-1 表示错误。
  • 示例代码:
    struct epoll_event events[20];
    int nfds = epoll_wait(epfd, events, 20, 500); // 等待直到有事件发生或500毫秒超时
    

4)epoll_event 结构

epoll_event 结构定义了事件类型和用户数据:

struct epoll_event {
    uint32_t events;    // Epoll 事件
    epoll_data_t data;  // 用户数据
};

5)事件类型

epoll 支持多种事件类型,包括:

  • EPOLLIN:表示对应的文件描述符可读(包括普通文件、TCP套接字等)。
  • EPOLLOUT:表示对应的文件描述符可写。
  • EPOLLERR:表示对应的文件描述符出现错误。
  • EPOLLHUP:表示对应的文件描述符被挂断。
  • EPOLLET:设置边缘触发模式,事件只会被报告一次。
  • EPOLLONESHOT:一个文件描述符只通知其注册的事件一次,直到重新充使。

6)示例代码 

#include <errno.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/select.h>
#include <poll.h>
#include <sys/epoll.h>

int main()
{

    int socketfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in servaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY); // 0.0.0.0
    servaddr.sin_port = htons(2000);              // 0-1023

    if (-1 == bind(socketfd, (struct sockaddr *)&servaddr, sizeof(struct sockaddr)))
    {
        printf("bind failed: %s\n", strerror(errno));
    }

    listen(socketfd, 10);
    printf("listen finshed: %d\n", socketfd);

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);


    int epfd = epoll_create(1);

    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = socketfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, socketfd, &ev);
    while (1)
    {
        struct epoll_event events[1024] = {0};
        int nready = epoll_wait(epfd, events, 1024, -1);

        int i = 0;
        for (i = 0; i < nready; i++)
        {
            int connfd = events[i].data.fd;

            if (connfd == socketfd)
            {
                int clientfd = accept(socketfd, (struct sockaddr *)&clientaddr, &len);
                printf("accept finshed: %d\n", clientfd);

                ev.events = EPOLLIN;
                ev.data.fd = clientfd;
                epoll_ctl(epfd, EPOLL_CTL_ADD, clientfd, &ev);
            }
            else if (events[i].events & EPOLLIN)
            {
                char buffer[1024] = {0};
                int count = recv(connfd, buffer, 1024, 0);

                if (count == 0)
                {
                    // 客户端关闭了连接
                    printf("Client closed the connection: %d\n", connfd);
                    close(connfd);
                    epoll_ctl(epfd, EPOLL_CTL_DEL, connfd, &ev);
                    continue;
                }
                else if (count < 0)
                {
                    // 发生错误
                    perror("Error receiving data");
                    break;
                }

                printf("RECV: %s\n", buffer);
                count = send(connfd, buffer, count, 0);
                printf("SEND: %d\n", count);
            }
        }
    }
    getchar();
    printf("exit\n");

    return 0;
}

1.整集选择什么数据结构存储

2.选择什么数据结构做就绪

并发之巅:事件驱动Reactor在高性能服务器的应用icon-default.png?t=N7T8http://t.csdnimg.cn/S5E0G

https://xxetb.xetslk.com/s/2sff5t

  • 25
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值