0voice-2.1.1-io多路复用select/poll/epoll

select

  1. 之前的模式:\(1\) 请求 , \(1\) 线程

    • 好处:代码逻辑简单
    • 缺点:不利于并发, \(1 \ k\) 并发量左右
  2. 连接三种集合代表的行为模式 (readfdswritefdsexceptfds

    • readfds :

      • 监听套接字 ( listening socket ) 可读:
        表示有新的客户端连接请求到达了服务器的监听队列,可以调用 accept() 来接受这个新连接了。
      • 连接套接字 (connected socket) 可读:
        表示套接字接收缓冲区中有数据可读。你可以调用 read()recv() 函数来从这个套接字中读取数据,而且调用不会阻塞(或者至少不会无限期阻塞)。
      • 远程关闭连接:如果连接的另一端(客户端)正常关闭了连接(发送了 FIN 包),那么这个套接字也会被标记为可读,此时 read()recv() 会返回 0。
      • readfds 关注的是是否有数据可以从 FD 中读取,或者是否有新的连接可以建立。
    • writefds :

      • 连接套接字 (connected socket) 可写:
        表示套接字的发送缓冲区有足够的空间来写入数据。你可以调用 write()send() 函数来向这个套接字中写入数据,而且调用不会阻塞(或者至少不会无限期阻塞)。通常,在 TCP 连接建立后,连接套接字大部分时间都是可写的,除非你正在发送大量数据,导致发送缓冲区被填满。
      • writefds 关注的是是否可以向 FD 中写入数据而不会阻塞。
    • exceptfds :

      • 套接字接收到带外数据 (Out-of-Band Data - OOB):
        这是最常见的用途。TCP 协议支持发送“带外数据”,它是一种紧急数据,优先级高于普通数据。当套接字接收到 OOB 数据时,它会被标记为异常。你可以使用 MSG_OOB 标志的 recv() 来读取它。
      • 连接错误:在某些情况下,如果套接字发生了一些严重的错误(例如,连接的另一端重置了连接 RST ),可能会在 exceptfds 中报告(尽管更多时候,这些错误也会在 readfdswritefds 上报告,导致 read/write 返回错误。
      • exceptfds 关注的是是否有带外数据到达,或连接是否发生了某些不寻常的、可能需要立即处理的错误或特殊条件。
  3. 理解 select 语句

    int nready = select(参数1 , 参数2 , 参数3 , 参数4 , 参数5);

    • 参数 \(2\) : 前文提及的 readfds ,读集合 ,注意它是一个 fd 的集合形式。
    • 参数 \(3\) : 前文提及的 writefds , 写集合。
    • 参数 \(4\) : 前文提及的 exceptfds , 异常集合。
    • 参数 \(5\) : 阻塞时长,等待触发,指的是三个集合中的待定元素,有触发的现象。
      • NULL 代表着一直阻塞。
    • 参数 \(1\) : 三个集合中最大 \(fd\) 编号 (\(maxfd\)) + \(1\) , 注意 \(maxfd\) ,由于有 \(fd\) 的创建和回收的机制 ,需要我们写代码动态去更新的。
    • nready : 代表三个集合中有触发次数的总数。
    • 注意每次 select 过后,三个集合都会被修改的,只有真正触发对应事件的 \(fd\) 才会被留下。
  4. 主要代码

fd_set rfds,rset;
    FD_ZERO(&rfds);
    FD_SET(sockfd, &rfds);

    int maxfd = sockfd;  //集合fd最大值
    while (1) {
        
        //rfds 主集合或持久化集合的角色
        //rset 工作集合或临时集合的角色
        rset = rfds;

        //最大fd + 1 , 可读集合 , 可写集合 , 出错集合 , timeout
        int nready = select(maxfd + 1 , &rset , NULL , NULL , NULL);

        //accept
        if (FD_ISSET(sockfd , &rset)) { //在 sockfd 的位置上有没有被设置 (ISSET)
            
            int clientfd = accept(sockfd , (struct sockaddr*)&clientaddr , &len);
            printf("accept finished: %d\n",clientfd);

            FD_SET(clientfd, &rfds);
            
            if (clientfd > maxfd) {
                maxfd = clientfd;
            }
        }

        //recv
        int i = 0;
        for (i = sockfd + 1 ; i <= maxfd ; ++i) {

            if (FD_ISSET(i , &rset)) {
                
                char buffer[1024] = {0};
                int count = recv(i , buffer , 1024 , 0);
                printf("RECV %s\n", buffer);
        
                if (count == 0) { //discount
                    printf("client disconnect: %d\n",i);
                    close(i);
                    FD_CLR(i , &rfds);
                    continue;
                }
                
                count = send(i , buffer , count , 0);
                printf("SEND: %d\n", count);
            
            }

        }

    }
  • 注意 rfdsrset 两个集合的含义
    • 注意判断都是用 rset , 因为只在乎 这次循环 触发事件的 fd
    • rfdsfd 的增加和回收中会及时变化的。
    • rsetselect 中是要进入内核的。
  • 思考为什么能并行 ?
    • 阻塞 ( NULL 状态下) 的语句就只有 select,而且一旦有对应的事件发生就进行。
    • 集合的作用。不在关注服务个体,只有在对应触发事件的集合下,都会被遍历到发生,这样我们就不会像一服务一线程一样,仅关注一个服务上触发的事件。
  • maxfd 可以无限大吗?
    • fd_setc++ 中的 bitset 一样,它是位图,肯定有大小限制的,有默认大小。
  • 上述代码能用 nready 优化
    • 每次完成一个触发事件,nready-- ,nready 变成 0 时,不在循环找触发事件。

poll

  1. select 的优点与缺点

    • 实现了 io 多路复用
    • 参数太多
  2. 参数解析

struct pollfd {
    int fd;
    short events;
    short revents;
}
  • events : 想要监听的事件 , 如 POLLIN (可读事件)
  • revents : 实际触发的事件 (由 poll 返回)
poll(参数1,参数2,参数3)
  • 参数 \(1\) : fd 集合
  • 参数 \(2\) : 集合大小
  • 参数 \(3\) : Timeout , \(-1\) 意为着一直等待
  1. 代码
    struct pollfd fds[1024] = {0};
    fds[sockfd].fd = sockfd;
    fds[sockfd].events = POLLIN;

    int maxfd = sockfd;

    while (1) {

        int nready = poll(fds , maxfd + 1 , -1); //fds copy到内核里面

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

            fds[clientfd].fd = clientfd;
            fds[clientfd].events = POLLIN;
            
            if (clientfd > maxfd) {
                maxfd = clientfd;
            }
        }

        //recv
        int i = 0;
        for (i = sockfd + 1 ; i <= maxfd ; ++i) {

            if (fds[i].revents & POLLIN) {
                
                char buffer[1024] = {0};
                int count = recv(i , buffer , 1024 , 0);
                printf("RECV %s\n", buffer);
        
                if (count == 0) { //discount
                    printf("client disconnect: %d\n",i);
                    close(i);
                    
                    fds[i].fd = -1;
                    fds[i].events = 0;
                    continue;
                }
                
                count = send(i , buffer , count , 0);
                printf("SEND: %d\n", count);
            
            }

        }
    }
  • select 的限制:在 select 中,文件描述符的数量是有限制的,通常FD_SETSIZE 定义(在许多系统中,默认值为 1024)。这意味着,如果有超过 1024 个文件描述符需要监听,select 就不再适用,需要手动调整编译时参数或者使用其他方法。

  • poll 没有这个限制:poll 使用一个数组来存储待监听的文件描述符,数组的大小是动态的,不受固定的限制。因此,可以轻松地监听成千上万的文件描述符。

  • poll 本身会检测套接字的 可写 状态:如果套接字的发送缓冲区已经有足够的空间,poll 会自动把该套接字标记为可写(POLLOUT)。因此你不需要手动设置 POLLOUT,只要你在 pollfd 中监听了 POLLOUT 事件,poll 会在合适的时候通知你该套接字可以写数据。(解释为什么没有在 send 的时候设置可写状态)。


epoll

    int epfd =  epoll_create(1);

    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = sockfd;
    epoll_ctl(epfd , EPOLL_CTL_ADD , sockfd , &ev);

    while (1) {

        struct epoll_event events[1024] = {0};
        int nready = epoll_wait(epfd , events, 1024 , -1);
        //events 数组存放返回的事件信息,注意它也是 

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

                int clientfd = accept(sockfd , (struct sockaddr*)&clientaddr , &len);
                printf("accept finished: %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);
                printf("RECV %s\n", buffer);
        
                if (count == 0) { //discount
                    printf("client disconnect: %d\n",connfd);
                    close(i);
                    
                    epoll_ctl(epfd , EPOLL_CTL_DEL , connfd , &ev);
                    continue;
                }
                
                count = send(connfd , buffer , count , 0);
                printf("SEND: %d\n", count);

            }
        }
        
    }
  1. int epfd = epoll_create(int size)

    • 这里的 size 参数不等于 0 均可,基本没用,为的是兼容旧版本。
    • epoll_create 它会在内核里创建一个 epoll 实例,它这个 epoll 实例里面,可以保存很多fd
    • 以后你要对某个 socket 关注 EPOLLIN(可读)、EPOLLOUT(可写),都要先把它“放进 epoll 实例里”。
    • epfd 是内核中那张 epoll 事件表的“编号”。
  2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *_Nullable event);

    • op : epoll_ctl_add (添加)、 epoll_ctl_mod (修改) 和
      epoll_ctl_del (删除)。
  3. int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

    • 等待 epfd 中的事件响应
  4. epoll_ctl(epfd , EPOLL_CTL_ADD , sockfd , &ev);

    • 将套接字 sockfd 添加到 epoll 监听集合

    • epfdev 的编号

    • EPOLL_CTL_ADD 添加 fd , EPOLL_CTL_DEL 删除 fd , EPOLL_CTL_MOD 修改 fd 的监听事件。

  5. 相比较于 select 而言,对于大并发的优势?

    • select 需要把整个集合 copy 进内核里面去。
    • 积累起来的,就绪就 ok
    • 就绪是我们需要处理的事件。
  6. selectpollepoll

    • 均解决 io 事件触发
    • 未涉及到应用层
  7. io 的生命周期内,无数个 io 事件组成。

posted @ 2025-09-21 16:31  xqy2003  阅读(8)  评论(0)    收藏  举报