I/O多路复用是一种技术,允许一个进程同时监控多个文件描述符,并在其中任何一个文件描述符准备好进行I/O操作时通知进程。这种技术可以提高程序的性能和效率,特别是在需要同时处理多个网络连接或文件描述符的情况下。

select

函数原型:使用select这种IO多路转接方式需要调用一个同名函数select,这个函数是跨平台的,LinuxWindowsMacOS等操作系统都是支持的。该函数可以委托内核帮助我们检测若干个文件描述符的状态,其实就是检测这些文件描述符对应的读写缓冲区的状态。

  • 读缓冲区状态:检测里面是否有数据,如果有数据该缓冲区对应的文件描述符就绪
  • 写缓冲区状态:检测里面是否还有空闲空间,如果有空闲空间该缓冲区对应的文件描述符就绪
  • 读取异常状态:检测是否发生异常,比如网络异常、文件异常等,如果发生异常该缓冲区对应的文件描述符就绪

委托检测的文件描述符遍历检测完毕之后,已就绪的这些满足的文件描述符会通过select()的参数分为3个集合传出,程序员得到这几个集合依次分情况依次处理即可。

函数原型:

#include <sys/select.h>
struct timeval {
    time_t tv_sec; /* 秒 */
    suseconds_t tv_usec; /* 微秒 */
}

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:需要检测的文件描述符的范围,即检测的文件描述符从0到nfds-1nfds的值通常为待检测的最大文件描述符加1。(内核允许最大一般是1024)
    • 内核需要线程遍历这些集合中的文件描述符,这个值是循环结束的条件。
    • 在windows中,该参数是无效的,指定为-1即可。
  • readfds:需要检测读状态的文件描述符集合,即检测这些文件描述符对应的读缓冲区状态。
    • 传入传出参数,读集合一般情况下是需要检测的,这样测能直到通过哪些文件描述符接收数据。
  • writefds:需要检测写状态的文件描述符集合,即检测这些文件描述符对应的写缓冲区状态。
    • 传入传出参数,如果不需要使用该参数可以指定为NULL。
  • exceptfds:需要检测异常状态的文件描述符集合,即检测这些文件描述符对应的异常状态。
    • 传入传出参数,如果不需要使用该参数可以指定为NULL。
  • timeout:超时时间,即检测文件描述符状态的超时时间。
  • 函数返回值:
    • 大于0:成功,返回集合中已就绪的文件描述符的个数。
    • 等于-1:则表示出错。
    • 等于0:则表示超时。

另外初始化fd_set类型的参数还需要使用相关的一些列操作函数,具体如下:

void FD_ZERO(fd_set *set); // 将set集合中的所有文件描述符对应的标志位都设置为0,表示集合中没有文件描述符
void FD_SET(int fd, fd_set *set); // 将set集合中的fd文件描述符对应的标志位设置为1,表示集合中有该文件描述符
void FD_CLR(int fd, fd_set *set); // 将set集合中的fd文件描述符对应的标志位设置为0,表示集合中没有该文件描述符
void FD_ISSET(int fd, fd_set *set); // 判断set集合中的fd文件描述符对应的标志位是否为1,如果为1则表示集合中有该文件描述符,否则表示集合中没有该文件描述符

poll

函数原型:

#include <poll.h>
struct pollfd {
    int fd; /* 文件描述符 */
    short events; /* 监控的事件 */
    short revents; /* 实际发生的事件 */
};
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • fds:需要检测的文件描述符集合,即检测这些文件描述符对应的读缓冲区状态、写缓冲区状态、异常状态。
    • 传入传出参数,读集合一般情况下是需要检测的,这样测能直到通过哪些文件描述符接收数据。
    • pollfd结构体数组,每个pollfd结构体表示一个文件描述符的检测信息,包括文件描述符、检测的事件、实际发生的事件。
  • nfds:需要检测的文件描述符的个数,即检测的文件描述符从0到nfds-1nfds的值通常为待检测的最大文件描述符加1。
    • 内核需要线程遍历这些集合中的文件描述符,这个值是循环结束的条件。
  • timeout:超时时间,即检测文件描述符状态的超时时间。
  • 函数返回值:
    • 大于0:成功,返回集合中已就绪的文件描述符的个数。
    • 等于-1:则表示出错。
    • 等于0:则表示超时。

创建监听的套接字 lfd = socket();
将监听的套接字和本地的IP和端口绑定 bind()
给监听的套接字设置监听 listen()
创建一个文件描述符集合 fd_set,用于存储需要检测读事件的所有的文件描述符
通过 FD_ZERO() 初始化
通过 FD_SET() 将监听的文件描述符放入检测的读集合中
循环调用select(),周期性的对所有的文件描述符进行检测
select() 解除阻塞返回,得到内核传出的满足条件的就绪的文件描述符集合
通过FD_ISSET() 判断集合中的标志位是否为 1
如果这个文件描述符是监听的文件描述符,调用 accept() 和客户端建立连接
将得到的新的通信的文件描述符,通过FD_SET() 放入到检测集合中
如果这个文件描述符是通信的文件描述符,调用通信函数和客户端通信
如果客户端和服务器断开了连接,使用FD_CLR()将这个文件描述符从检测集合中删除
如果没有断开连接,正常通信即可

服务端并发实现:

#include <stdio.h>
#include <stdlib.h>
#include<unistd.h>
#include<string.h>
#include<arpa/inet.h>
#include <sys/select.h>
int main(){
    // 1. 创建监听的套接字
    int fd = socket(AF_INET, SOCK_STREAM, 0); // IPV4, TCP, 默认协议
    if(fd == -1){
        perror("socket");
        exit(-1);
    }
    // 2. 绑定IP和端口
    struct sockaddr_in saddr;

    saddr.sin_family = AF_INET; // 初始化IPV4
    saddr.sin_port = htons(9999); // 初始化 端口
    saddr.sin_addr.s_addr = INADDR_ANY; // 0=0.0.0.0 
    int ret = bind(fd,(struct sockaddr *)&saddr,sizeof(saddr));
    if(ret == -1){
        perror("bind");
        return -1;
    }

    // 设置监听
    ret = listen(fd,128);
    if(ret == -1){
        perror("listen");
        return -1;
    }

    fd_set redset;
    FD_ZERO(&redset);
    FD_SET(fd,&redset);

    int maxfd = fd;
    while(1){
        fd_set temp = redset;
        int ret = select(maxfd + 1, &temp, NULL,NULL, NULL);
        // 判断是不是监听fd
        if(FD_ISSET(fd,&temp)){
            // 接受客户端的连接
            int cfd = accept(fd,NULL,NULL);
            FD_SET(cfd, & redset);
            maxfd = cfd > maxfd ?cfd:maxfd ;
        }
        for(int i = 0; i<= maxfd;++i){
            if(i!=fd && FD_ISSET(i,&temp)){
                // 接收数据
                char buf[1024];
                int len = recv(i,buf,sizeof(buf),0);
                if(len>0){
                    printf("client say : %s\n",buf);
                    send(i,buf,len,0);
                }
                else if(len == 0){
                    printf("客户端断开连接。。\n");
                    FD_CLR(i,&redset);
                    close(i);
                    break;
                }
                else{
                    perror("recv");
                    break;
                }
            }
        }
    }

    // 关闭文件描述符
    close(fd);

    return 0;
}

select 与 poll 的并发性能比较:

  • select函数的fd_set类型参数,在每次调用select函数时,都需要重新设置,而poll函数的pollfd类型参数,只需要在第一次调用poll函数时设置一次,之后每次调用poll函数时,只需要修改需要检测的文件描述符的pollfd结构体即可。