解决频繁I/O之select以及select+TCP的服务器代码实现

前面写了关于IO的5种常见模型:五种基本常见IO
今天,我们来学习一下select系统调用。

**select作用:让程序监视多个文件描述符的状态变化.**

函数原型:

#include <sys/select.h>
int select(int nfds,fd_set *readfds,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);
参数:
nfds:需要监视的最大文件描述符+1
readfds:读文件描述符集合
writefds:写文件描述符集合
exceptfds:异常文件描述符集合
timeout:表示超时时间.若设置为0:表示非阻塞;设置为NULL:表示阻塞,知道某个文件描述符就绪;设置为大于0的值,表示等待的时间,若在等待的这段时间内有文件描述符就绪了,那么就立即返回.若在这段时间内没有就会的文件描述符,也会在时间结束时返回(超时返回).

关于timeval结构体:

struct timeval
{
    _time_t tv_sec;    //单位为秒
    _suseconds_t tv_usec;    //单位为微秒
};

关于select的返回值:

  • 执行成功且返回值大于0,返回值即表示就绪的文件描述符的个数.
  • 执行成功但是返回值为0,那么就表示所有的文件描述符都没有就绪.
  • 当有错误发生时,就返回-1,并且错误原因errno中.

研究select的执行过程:
对于fd_set到底有多大,我们可以在程序中进行简单的测试.
所以在这里我们只取其1个字节(8bit)进行模拟select执行过程.

  1. 首先进行初始化:FD_ZERO(&set);(set表示:0000 0000)
  2. 若fd = 3,执行FD_SET(fd,&set)后,set为:0000 1000(注意:是从0开始数的)
  3. 再加入fd = 5,FD_SET(fd,&set);fd = 2,FD_SET(fd,&set)后set表示:0010 1100
  4. 此时系统函数select执行:select(5+1,&set,NULL,NULL,NULL)设置为阻塞等待.
  5. 若fd = 3 和 fd = 5都发生了可读事件.那么select就会返回2.并且此时set表示为:0010 1000.注意:此时没有发生任何事件的fd = 2就会被清空,可是如果2号文件描述符并没有关闭,还需要进行监视,那么就要重新设置set

关于fd_set:就是设置文件描述符的相关属性

在操作系统内核中,fd_set其实是一个位图.可以根据位图对应的位来表示监视的文件描述符的状态.
下面是fd_set常见的几个接口:

void FD_CLR(int fd,fd_set *set); //清除集合set中的fd位
void FD_ISSET(int fd,fd_set *set); //判断set集合中的fd位是否为真
void FD_SET(int fd,fd_set *set); //将set集合中的fd位进行设置
void FD_ZERO(fd_set *set); //用来清除set集合中的全部位

select就绪条件:

读就绪:

  • socket内核中,接受缓冲区有一个阈值,当接受缓冲区的字节数大于这个阈值时就可以进行无阻塞的读取.
  • socket的TCP通信中,对端关闭连接,若依旧对socket读,就返回0;
  • 监听的socket上有新的连接请求.(可理解为有数据过来要进行读取)
  • socket上有未处理的请求.

写就绪:

  • socket内核中,发送缓冲区也有一个空闲的阈值,当发送缓冲区的空闲区大于这个阈值时,就可以进行无阻塞的写,并且返回值大于0;
  • 当socket的写操作被关闭时,对这个写操作被关闭的socket进行写操作,就会触发一个SIGPIPE信号.
  • socket使用非阻塞connect连接成功或失败之后.

使用select来监视标准输入输出:
代码如下:

#include<stdio.h>
#include<unistd.h>
#include<sys/select.h>
#include<sys/time.h>

int main()
{
    struct timeval t;
    while(1)
    {
        fd_set read_fds;
        FD_ZERO(&read_fds);
        FD_SET(0,&read_fds);
        //每次都要初始化,当文件描述符变多时,将该初始化放在外面,就会存在有的描述符可能会越来越少.
        t.tv_sec = 2;
        t.tv_usec = 20;
        printf(">");
        fflush(stdout);
        int ret = select(1,&read_fds,NULL,NULL,&t);
        //select:第一个参数表示有效的文件描述符的最大值+1
        //返回的是有效的描述符的个数
        //返回0,表示超时
        //返回-1,表示出现了致命的错误
        if(ret == 0)
        {
            //如果超时了就会返回0
            printf("ret == 0\n");
        }
        if(ret < 0)
        {
            perror("select");
            return 1;
        }
        if(FD_ISSET(0,&read_fds))
        {
            //0号描述符读就绪
            char buf[1024] = {0};
            ssize_t read_size  = read(0,buf,sizeof(buf) - 1);
            if(read_size < 0)
            {
                perror("read");
                continue;
            }
            if(read_size == 0)
            {
                printf("read done\n");
                return 0;
            }
            printf("stdout:%s",buf);
        }
    }
    return 0;
}

执行结果如下:
结果
可以看出:超时后会立即返回.

使用select实现一个基于TCP的回显服务器:(要求这个服务器可以同时监控多个客户端)
服务器的代码如下:
若想看所有的服务器+客户端的代码:请戳:select的相关代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <arpa/inet.h>

typedef struct sockaddr sockaddr;
typedef struct sockaddr_in sockaddr_in;
//获取最大的文件描述符
typedef struct fds
{
    fd_set fd;
    int max_fd;   //当前文件描述符中最大的文件描述符
}fds;

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

void Set_Add(fds *set,int fd)
{
    if(set == NULL)
    {
        return;
    }
    FD_SET(fd,&set->fd);
    if(fd > set->max_fd)
    {
        set->max_fd = fd;
    }
}

void Del_Set(fds *set,int fd)
{
    if(set == NULL)
    {
        return;
    }
    FD_CLR(fd,&set->fd);
    int max = -1;
    int i = 0;
    for(i = 0;i <= set->max_fd;++i)
    {
        if(!FD_ISSET(i,&set->fd))
        {
            continue;
        }
        if(max < i)
        {
            max = i;
        }
    }
    set->max_fd = max;
}

int start_server(const char* ip,short port)
{
    int sock = socket(AF_INET,SOCK_STREAM,0);
    if(sock < 0)
    {
        perror("socket");
        return -1;
    }

    sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip);
    addr.sin_port = htons(port);

    //绑定
    int ret_bind = bind(sock,(sockaddr*)&addr,sizeof(addr));
    if(ret_bind < 0)
    {
        perror("bind");
        return -1;
    }

    //将服务器变为被动形式,进行监听
    int ret_listen = listen(sock,5);
    if(ret_listen < 0)
    {
        perror("listen");
        return -1;
    }
    return sock;
}
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        printf("Usage:./select_server [ip] [port]\n");
        return 1;
    }
    //初始化服务器
    int ret = start_server(argv[1],atoi(argv[2]));
    if(ret < 0)
    {
        printf("start failed\n");
        return 1;
    }
    printf("server success\n");
    //进入循环事件
    fds rfd;
    FD_SET_init(&rfd);
    Set_Add(&rfd,ret);
    while(1)
    {
        //为了保证accept返回的也可以添加到读就绪中.
        fds fd = rfd;
        //原来写的服务器的阻塞等待是由read和write完成的.现在使用select来完成这个过程.
        //该服务器仍是属于回显服务器,接收到什么就回复什么
        int ret_sel = select(fd.max_fd + 1,&fd.fd,NULL,NULL,NULL);
        if(ret_sel < 0)
        {
            perror("select");
            continue;
        }
        if(ret_sel == 0)
        {
            printf("timeout\n");
            continue;
        }
        //说明现在有就绪的文件描述符
        if(FD_ISSET(ret,&fd.fd))
        {
            //并且就绪的文件描述符是ret读就绪
            //此时就要调用accept,获取连接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int ret_accept = accept(ret,(struct sockaddr*)&peer,&len);
            if(ret_accept < 0)
            {
                perror("accept");
                continue;
            }
            //如果ret就绪,就要将它添加到读继续中
            Set_Add(&rfd,ret_accept);
            printf("client %d id connected\n",ret_accept);
        }
        else    //这里有坑!!!!!!,在后面进行坑的讲解
        {
            //此时返回的是accept的就绪
            int i = 0;
            char buff[1024] = {0};
            for(i = 0;i <= fd.max_fd;++i)
            {
                if(!FD_ISSET(i,&fd.fd))
                {
                    continue;
                }
                //在这儿不使用循环读写,因为等待的过程都是有select完成
                ssize_t read_size = read(i,buff,sizeof(buff) - 1);
                if(read_size < 0)
                {
                    perror("read");
                    continue;
                }
                if(read_size == 0)
                {
                    //如果读完了就要关闭该文件描述符
                    printf("%dread done,goodbye\n",i);
                    close(i);
                    Del_Set(&rfd,i);
                }
                else
                {
                    printf("文件描述符%d,说%s\n",i,buff);
                    write(i,buff,strlen(buff));
                }
            }//end else(for)
        }//end else
    }//end while
    return 0;
}

上述代码中有个地方需要注意一下:当select返回的结果大于0时,就说明有就绪的文件描述符.那么接下来就要进行判断时socket就绪要进行accept还是accept就绪要进行读写.在程序中我们使用了if-else语句.当socket就绪要进行accept时就进入if条件中,而
accept就绪要进行读写时则进入else条件.那么如若此时就绪的文件描述符中这两种情况都同时包含,那么首先会进入if条件中执行,那么accept就绪要进行读写的文件描述符就只能等待下次去执行了,这样并不会出现错误,只是可能会效率比较低.但是能保证所有的文件描述符一定会被处理.这样的处理方式叫做水平触发,与之相对的还有一个边缘触发.在后面再进行讲解.

select的缺点(就是epoll的优点):
1. 每次调用select,都要手动设置fd_set的集合,使用很比方便.
2.每次调用select,都要把fd_set集合(3个集合,读,写,异常)从用户态拷贝到内核态,那么如果频繁的调用select,开销就会很大.同样在内核和用户要遍历fd_set该集合,已返回就绪的文件描述符,同样是一种很大的开销
3.select的文件描述符数量太小.

猜你喜欢

转载自blog.csdn.net/Yinghuhu333333/article/details/81137276