I/O多路复用 - select

了解select,首先我们先了解一下这两句话
  • select只负责等(即并不数据搬迁,不处理数据)
  • 等待文件描述符的读就绪或者写就绪
select函数

  • select系统调用是用来让我们监视多个程序的文件描述符的变化;
  • 程序会停在select这里等,知道被监视的文件描述符至少有一个达到了就绪状态;
函数原型

参数:
  • _nfds : 表示需要监视的最大文件描述符 + 1;
  • __restrict __readfds : 需要检测的可读文件描述符的集合,(输入输出型参数; 输入:用户想关心那些文件描述符上的读事件, 输出: 所关心的文件描述符那些已经读就绪了);
  • __restrict __writefds : 需要检测的可写文件描述符的集合,(输入输出型参数; 输入:用户想关心那些文件描述符上的写事件,输出: 所关心的文件描述符那些已经写就绪了);
  • __restrict __exceptfds : 需要检测的异常文件描述符的集合,,(输入输出型参数; 输入:用户想关心那些文件描述符上的异常事件,输出: 所关心的文件描述符那些已经异常就绪了);
  • __restrict __timeout : 用来设置select等待时间的
timeout的取值:
  • NULL : 表示当前select()没有timeout,select将会阻塞的等待文件描述符就绪;
  • 0 : 仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生; (非阻塞)
  • 一个特定的时间值 : 如果在指定时间内还没有等待到就绪的文件描述符,select()就会超时返回;
fd_set 结构体:

其实这个结构体就是一个位图,使用位图中的对应位表示要监视的文件描述符。
fd_set的结构:
void FD_CLR(int fd, fd_set* set);    // 将位图中的某一位置为0
void FD_SET(int fd, fd_set* set);    // 将位图中的某一位置为1
int FD_ISSET(int fd, fd_set* set;    // 检测位图中fd的位置是不是1
void FD_ZERO(fd_set* set);           // 将set中全部位置为0
禁止自己使用与或非方式操作位图!!!

timeval结构体:
这个结构体构建了一个时间, tv_sec是秒数, tv_usec是微妙, 最后时间为 tv_sec+tv_usec; 


函数返回值:
  • 执行成功返回已改变状态的文件描述符个数;
  • 0 : 表示在文件描述符状态改变之前就已经超时了,没有返回;
  • -1 : 表示有错误发生,错误原因存在于errno,此时函数的额后四个参数的值变为不可预测;
socket就绪条件

读就绪
  • socket内核中, 接收缓冲区中的字节数, ⼤于等于低⽔位标记SO_RCVLOWAT. 此时可以⽆阻塞的读该⽂件描述符, 并且返回值⼤于0;
  • socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
  • 监听的socket上有新的连接请求;
  • socket上有未处理的错误;
写就绪
  • socket内核中, 发送缓冲区中的可⽤字节数(发送缓冲区的空闲位置⼤⼩), ⼤于等于低⽔位标记 SO_SNDLOWAT, 此时可以⽆阻塞的写, 并且返回值⼤于0;
  • socket的写操作被关闭(close或者shutdown). 对⼀个写操作被关闭的socket进⾏写操作, 会触发 SIGPIPE信号;
  • socket使⽤⾮阻塞connect连接成功或失败之后;
  • socket上有未读取的错误;
异常就绪
  • socket收到带外数据(可以联想到TCP中的紧急指针URG);
select特点

  • 可监控的文件描述符个数取决于sizeof(fd_set)的值,我这边是128,每个比特位上一个文件描述符,我的服务器上最大就可表示 128*8 = 1024个;
  • 将fd加入到select监控集的同时,还需要再使用一个数据结构arr来保存select监控集中的fd
一是用于select返回后,arr作为源数据和fd进行FD_ISSET判断;
二是select返回后,会把以前加入的但是没有事件发生的fd清空,则每次开始select前都要重新从arr取得一个fd逐一加入,扫描arr的同时取得fd的maxfd,用于select的第一个参数;

代码编写: select服务器(回显)
// select_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/select.h>

// 对fd_set进行一个简单的封装
// 为了能够方便的获取到文件描述符集中的的最大文件描述符
typedef struct FdSet
{
    fd_set fds;
    int max_fd; // 当前文件描述符集中的最大文件描述符
}FdSet;

void InitFdSet(FdSet* set)
{
    if(set == NULL)
    {
        return;
    }
    set->max_fd = -1;
    FD_ZERO(&set->fds);
}

void AddFdSet(FdSet* set,int fd)
{
    FD_SET(fd,&set->fds);
    if(fd > set->max_fd)
    {
        set->max_fd = fd;
    }
}

void DelFdSet(FdSet* set,int fd)
{
    FD_CLR(fd,&set->fds);
    int max_fd = -1;
    int i = 0;
    for(; i <= set->max_fd; ++i)
    {
        if(!FD_ISSET(i,&set->fds))
        {
            continue;
        }
        if(i > max_fd)
        {
            max_fd = i;
        }
    }
    // 循环结束以后,max_fd就是当前最大的文件描述符
    set->max_fd = max_fd;
    return;
}
////////////////////////////////////////////////////  封装完成

int ServerStart(short port)
{
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd < 0)
    {
        perror("socket");
        return -1;
    }
    struct sockaddr_in local;
    local.sin_family = AF_INET;
    local.sin_addr.s_addr = htonl(INADDR_ANY);
    local.sin_port = htons(port);
    if(bind(fd,(struct sockaddr*)&local,sizeof(local)) < 0)
    {
        perror("bind");
        return -2;
    }
    if(listen(fd,3) < 0)
    {
        perror("listen");
        return -3;
    }
    return fd;
}

int ProcessRequest(int new_sock)
{
    // 完成对客户端的读写操作
    // 这里不可以向以前一样进行循环读写操作
    // 当前是单进程/线程程序,如果死循环就无法执行别的操作了
    
    // 此处只进行一次读写,因为我们要将所有的等待操作都交给select完成
    // 下一次对该 socket 读取的时机,也是由select来进行通知的
    // 也就是select下一次返回该文件描述符就绪的时候就能进行读数据了

    char buf[1024] = {0};
    ssize_t read_size = read(new_sock,buf,sizeof(buf)-1);
    if(read_size < 0)
    {
        perror("read");
        return -1;
    }
    else if(read_size == 0)
    {
        printf("[client %d] : disconnect\n",new_sock);
        return 0;
    }
    buf[read_size] = '\0';
    printf("[client %d] : %s\n",new_sock,buf);
    write(new_sock,buf,strlen(buf));
    return read_size;
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        printf("Usage: %s [port]\n",argv[0]);
        return 1;
    }
    // 1.对服务器进行初始化
    int listen_sock = ServerStart(atoi(argv[1]));
    if(listen_sock < 0)
    {
        printf("ServerStart faild\n");
        return 1;
    }
    printf("ServerStart OK\n");
    // 2.进入事件循环
    FdSet read_fds; // 输入参数: 要等待的所有文件描述符都要加到这个位图中
    InitFdSet(&read_fds);
    AddFdSet(&read_fds,listen_sock);
    while(1)
    {
        // 3.使用select完成等待
        //   创建 output_fds 的目的是: 防止select返回,把read_fds的内容给破坏掉,导致期中数据丢失
        //   read_fds 始终表示select监控的文件描述符集的内容

        FdSet output_fds = read_fds; // 输出型参数: 用来保存每一次的输出参数
        int ret = select(output_fds.max_fd+1,&output_fds.fds,NULL,NULL,NULL);
        if(ret < 0)
        {
            perror("select");
            continue;
        }
        // 4.select返回以后进行处理
        //   a) listen_sock 读就绪
        //   b) new_sock 读就绪
        if(FD_ISSET(listen_sock,&output_fds.fds))
        {// 查看listen_sock在位图中,就证明客户端连接上了,可以accept了
         // 调用 accept 获取到连接,将new_sock加入到select之中
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
            if(new_sock < 0)
            {
                perror("accept");
                return 1;
            }
            AddFdSet(&read_fds,new_sock);
            printf("[client : %d] connect \n",new_sock);
        }
        else
        {// 当前就是 new_sock 就绪了
            int i = 0;
            for(; i < output_fds.max_fd + 1; ++i)
            {
                if(!FD_ISSET(i,&output_fds.fds))
                {
                    continue;
                }
                // 到了这里,证明 i 是位图中的一位, 就可以开始读写了
                int ret = ProcessRequest(i);
                if(ret <= 0)
                {
                    // 到这里正明客户端已经断开连接了
                    // close DelFdSet 两步顺序不影响
                    DelFdSet(&read_fds,i);
                    close(i);
                } // end if(ret<=0)
            } // end for
        } // end else
    } // end while(1)
    return 0;
}
客户端的代码和之前实现tcp/udp一样
// select_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        printf("Usage: ./client [ip] [port]\n");
        return 1;
    }
    // 1. 创建 socket
    int fd = socket(AF_INET,SOCK_STREAM,0);
    if(fd < 0)
    {
        perror("socket");
        return 1;
    }
    // 2. 建立连接
    sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_addr.s_addr = inet_addr(argv[1]);
    server.sin_port = htons(atoi(argv[2]));
    int ret = connect(fd,(sockaddr*)&server,sizeof(server));
    if(ret < 0)
    {
        perror("connect");
        return 1;
    }
    // 3. 进入事件循环
    while(1)
    {
        //    a) 从标准输入读数据
        char buf[1024] = {0};
        ssize_t read_size = read(0,buf,sizeof(buf)-1);
        if(read_size < 0)
        {
            perror("read");
            return 1;
        }
        if(read_size == 0)
        {
            printf("read done\n");
            return 0;
        }
        buf[read_size] = '\0';
        //    b) 把读入的数据发送到服务器上
        write(fd,buf,strlen(buf));
        //    c) 从服务器读取相应结果
        char buf_resp[1024] = {0};
        read_size = read(fd,buf_resp,sizeof(buf_resp) - 1);
        if(read_size < 0)
        {
            perror("read");
            return 1;
        }
        else if(read_size == 0)
        {
            //对端先断开连接
            printf("server close socket\n");
            return 0;
        }
        buf_resp[read_size] = '\0';
        //    d) 把结果打印到标准输出上
        printf("server respond > %s\n",buf_resp);
    }
    return 0;
}
观察以上代码
如果当前的文件描述符是 3,5,7,9
当前 3 是 listen_sock
返回结果为 3,5
但是只处理了3,这时候 5 会怎么样呢? 会不会处理不到?
水平触发:
如果一个文件描述符就绪了,但是这一次没有处理到这个文件描述符
但是下一次select仍但会获取到这个文件描述符,把它处理掉

select缺点

  • 每次调用select都需要重新手动设置fd集合;
  • 接口使用非常不方便;
  • 每次调用select,都需要把fd集合从用户态拷贝至内核,当fd很多的时候开销会非常大;
  • 每次调用fd都需要在内核中遍历传递进来的所有fd,当fd很多的时候开销也会非常大;
  • select支持的文件描述符太少了;

猜你喜欢

转载自blog.csdn.net/J4Ya_/article/details/81063066