select函数实现高性能IO多路访问的关键示例深入解析

 更新时间:2023年09月25日 09:40:09   作者:白茶加冰  
这篇文章主要为大家介绍了select函数实现高性能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。在监听范围内的文件描述符包括从 0 到 nfds-1。这个参数的值应该是监听的所有文件描述符中的最大值加 1。

  • readfds:一个指向 fd_set 结构体的指针,用于指定需要监听可读事件的文件描述符。

  • writefds:一个指向 fd_set 结构体的指针,用于指定需要监听可写事件的文件描述符。

  • exceptfds:一个指向 fd_set 结构体的指针,用于指定需要监听异常事件的文件描述符。

  • timeout:一个指向 struct timeval 结构体的指针,用于设置 select 的超时时间。如果设置为 NULL,则 select 将会阻塞直到有事件发生。如果设置为一个指向 struct timeval 结构体的指针,则 select 会等待指定的时间,超时后返回。

fd_set 是一个文件描述符集合数据类型,用于存储需要监听事件的文件描述符。

struct timeval 是一个时间结构体,用于指定超时时间。它包含了两个成员:tv_sec 表示秒数,tv_usec 表示微秒数。

select 函数返回就绪文件描述符的总数,如果出错则返回 -1。通过检查 readfdswritefds 和 exceptfds 来确定哪些文件描述符已经就绪。

注意:在使用 select 函数之前,需要使用 FD_ZERO 和 FD_SET 宏来初始化和设置文件描述符集合。

二.select相关宏函数

在使用 select 函数进行文件描述符集合操作时,常用的宏函数有以下几个:

  • FD_ZERO:用于将文件描述符集合清空,将集合中所有位都设置为 0。
void FD_ZERO(fd_set *set);
  • FD_SET:用于将指定的文件描述符加入到集合中。
void FD_SET(int fd, fd_set *set);
  • FD_CLR:用于将指定的文件描述符从集合中移除。
void FD_CLR(int fd, fd_set *set);
  • FD_ISSET:用于检查指定的文件描述符是否在集合中已经设置。
int FD_ISSET(int fd, fd_set *set);

这些宏函数可以用于操作文件描述符集合,例如初始化集合、添加文件描述符、移除文件描述符以及检查集合中是否已经设置了特定的文件描述符。

需要注意的是,fd_set 是一个位图类型的结构,用于存储文件描述符集合的状态。宏函数配合使用可以方便地操作这个位图,以设置和判断文件描述符的状态。

另外,需要包含 <sys/select.h> 头文件来使用这些宏函数。

三.select特点

select 函数是一种多路 I/O 复用机制,具有以下特点:

  • 多路复用:select 允许在一个事件循环中同时监听多个文件描述符(比如套接字、管道等),并在这些文件描述符就绪时通知应用程序。

  • 平台兼容性:select 是一种标准的系统调用,几乎在所有的 POSIX 操作系统上都有支持,因此具有良好的跨平台性。

  • 阻塞和非阻塞模式:select 支持两种调用方式。在阻塞模式下,调用 select 会一直等待,直到至少一个文件描述符变得可读或可写。而在非阻塞模式下,调用 select 会立即返回,不管是否有文件描述符就绪。

  • 超时功能:select 允许指定一个超时时间,在等待文件描述符就绪时设定一个最长等待时间。一旦超过设定的超时时间,select 将返回,这样可以避免无限期地阻塞程序。

  • 可读性检查:select 对于读操作可用于检查一个文件描述符是否有数据可以读取。

  • 可写性检查:select 对于写操作可用于检查一个文件描述符是否可以写入数据而不会造成阻塞。

  • 异常条件检查:select 还可以检查是否有异常条件发生,例如套接字的带外数据。

然而,值得注意的是,select 在高并发情况下可能性能较差,因为在每一次调用 select 时,都需要遍历所有的文件描述符。在这种情况下,使用更高效的 I/O 复用机制,如 poll 或 epoll,可能是更好的选择。

四.select机制

select 是一种多路 I/O 复用机制,用于同时监听多个文件描述符的可读、可写和异常事件。它的机制如下:

  • 调用 select 函数时,将需要监听的文件描述符通过参数传递给 select 函数,同时设置超时时间(可选)。

  • select 函数会阻塞程序运行,直到以下三种情况之一发生:

    • 有一个或多个文件描述符可以读取(被置于可读状态)。
    • 有一个或多个文件描述符可以写入(被置于可写状态)。
    • 有一个或多个文件描述符发生了异常。
  • 当有一个或多个文件描述符就绪时,select 函数返回,程序可以继续往下执行。此时,通过检查文件描述符集合的状态(readfdswritefds 和 exceptfds),可以确定哪些文件描述符已经就绪。

  • 根据文件描述符的状态,进行相应的读取、写入或处理异常的操作。

  • 重复以上步骤,循环使用 select 函数来监听文件描述符,以实现事件驱动的编程模式。

select 的特点是可以同时监听多个文件描述符,在有事件发生时返回,并指示哪些文件描述符已经就绪,使得程序能够及时处理相关操作。它适用于需要同时处理多个 I/O 事件的情况,并且具有良好的跨平台性。然而,在高并发情况下性能可能较差,因为每次调用 select 都需要遍历所有的文件描述符。在这种情况下,可以考虑使用其他更高效的 I/O 复用机制,如 poll 或 epoll

五.select编程步骤

使用 select 函数进行 I/O 复用时,通常包含以下步骤:

  • 准备文件描述符集合:创建并初始化需要监听的文件描述符集合,例如使用 fd_set 类型的变量,并使用 FD_ZERO 宏将其清空,然后使用 FD_SET 宏将需要监听的文件描述符添加到集合中。

  • 设置超时时间:可选择是否设置超时时间。如果需要设置超时时间,在 struct timeval 结构体中设置合适的值,然后将该结构体的指针作为 select 函数的最后一个参数传递给函数。

  • 调用 select 函数:将文件描述符集合作为参数传递给 select 函数,并传递其他必要的参数,如最大文件描述符编号加 1(nfds)以及超时时间(可选)。调用 select 函数后,程序将在该函数处阻塞等待。

  • 检查就绪文件描述符:当 select 函数返回时,需要检查文件描述符集合的状态,以确定哪些文件描述符已经就绪。可以使用 FD_ISSET 宏来检查特定的文件描述符是否已经设置。

  • 处理就绪文件描述符:根据文件描述符的状态(可读、可写或异常),进行相应的处理操作。例如,如果一个文件描述符可读,可以使用 read 函数读取数据;如果一个文件描述符可写,可以使用 write 函数写入数据;如果一个文件描述符发生异常,可以进行相应的错误处理。

  • 重复步骤 3-5:根据需要,可以使用循环来多次调用 select 函数以处理更多的事件。这样可以实现事件驱动的编程模式,不断监听和处理多个文件描述符的事件。

需要注意的是,在每次循环中调用 select 函数之前需要重新设置文件描述符集合和超时时间,以反映当前需要监听的文件描述符集合和超时时间的变化。

六.自定义数组提高效率

要提高 select 函数的效率,可以考虑使用自定义的数组来替代文件描述符集合,以便更有效地管理和操作需要监听的文件描述符。

以下是使用自定义数组提高 select 函数效率的一般步骤:

  • 创建自定义数组:根据需要监听的文件描述符数量,创建一个数组来存储这些文件描述符的信息。可以使用整数型数组、结构体数组等来表示每个文件描述符的状态和其他相关信息。
  • 初始化自定义数组:在程序开始时,初始化自定义数组,设置每个文件描述符的初始状态和其他必要的信息。
  • 更新自定义数组:在程序中需要做出更改时,通过修改自定义数组中的相应元素来反映文件描述符的状态变化。
  • 使用自定义数组代替 select 函数的文件描述符集合:在调用 select 函数之前,将自定义数组转换为对应的文件描述符集合,例如使用 FD_ZERO 和 FD_SET 宏来设置相应的文件描述符集合。
  • 调用 select 函数:使用转换后的文件描述符集合作为参数调用 select 函数,并传递其他必要的参数,如最大文件描述符编号加 1(nfds)以及超时时间(可选)。
  • 检查就绪文件描述符:当 select 函数返回时,检查文件描述符集合的状态以确定哪些文件描述符已经就绪。可以通过遍历自定义数组,根据其中的状态信息来确定哪些文件描述符已经就绪。
  • 处理就绪文件描述符:根据文件描述符的状态,进行相应的处理操作,如读取、写入或处理异常等。
  • 重复步骤 3-7:根据需要,可以使用循环来多次调用 select 函数以处理更多的事件。

使用自定义数组可以更灵活地管理文件描述符,并且可以根据程序需求进行优化,以提高效率。但请注意,当需要监听的文件描述符数量过大时,自定义数组的使用可能会导致额外的内存开销。因此,在性能和内存消耗之间需要进行权衡。

七.编程实战

练习
利用select实现一个高并发服务器,当服务连接成功后,客户端发送小写字母的字符串,服务器端发送其大写形式。要求自定义一个数组,用于提高程序效率。

server.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    //1.socket创建套接字
    int socked = socket(AF_INET, SOCK_STREAM, 0);
    if (socked < 0)
    {
        perror("socket is err");
        return -1;
    }
    //2.bind绑定服务器ip地址和端口号
    struct sockaddr_in saddr, caddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[1]));
    saddr.sin_addr.s_addr = inet_addr("0.0.0.0");
    int len = sizeof(caddr);
    int ret = bind(socked, (struct sockaddr *)(&saddr), sizeof(saddr));
    if (ret < 0)
    {
        perror("bind is err");
        return -1;
    }
    //3.listen设置同时最大链接数
    ret = listen(socked, 5);
    if (ret < 0)
    {
        perror("listen is err");
        return -1;
    }
    //4.select前置工作
    fd_set readfds, tempfds; //创建表
    FD_ZERO(&readfds);       //清空表
    FD_SET(socked, &readfds); //添加文件描述符
    FD_SET(0, &readfds);
    int maxfd = socked, size = 0, maxi = -1, client[1024] = {0};
    char buf[1024] = {0};
    //初始化数组为-1
    for (int i = 0; i < 1024; ++i)
        client[i] = -1;
    while (1)
    {
        tempfds = readfds;
        int ret = select(maxfd + 1, &tempfds, NULL, NULL, NULL);
        if (ret < 0)
        {
            perror("select is err");
            break;
        }
        if (FD_ISSET(socked, &tempfds))
        {
            int fd = accept(socked, (struct sockaddr *)(&caddr), &len);
            printf("port:%d   ip:  %s\n", ntohs(caddr.sin_port), inet_ntoa(caddr.sin_addr));
            //文件描述符加入数组中
            int i;
            for (i = 0; i < 1024; ++i)
            {
                if (client[i] < 0)
                {
                    client[i] = fd;
                    break;
                }
            }
            if (i == 1024)
            {
                printf("too many clients\n");
                exit(-1);
            }
            if (i > maxi)
                maxi = i;
            FD_SET(fd, &readfds);
            if (maxfd < fd)
                maxfd = fd;
            if (--ret == 0)
                continue;
        }
        for (int i = 0; i <= maxi; ++i)
        {
            if (client[i] < 0)
                continue;
            if (FD_ISSET(client[i], &tempfds))
            {
                int flage = recv(client[i], buf, sizeof(buf), 0);
                if (flage < 0)
                {
                    perror("recv is err");
                }
                else if (flage == 0)
                {
                    printf("ip:%s is close\n", inet_ntoa(caddr.sin_addr));
                    close(i);
                    FD_CLR(i, &readfds);
                    client[i] = -1;
                }
                else
                {
                    size = strlen(buf);
                    for (int i = 0; i < size; ++i)
                    {
                        if (buf[i] >= 'a' && buf[i] <= 'z')
                            buf[i] = buf[i] + ('A' - 'a');
                        else
                            buf[i] = buf[i] + ('a' - 'A');
                    }
                    printf("%s\n", buf);
                    send(client[i], buf, sizeof(buf), 0);
                }
                if (--ret == 0)
                    break;
            }
        }
    }
    close(socked);
    return 0;
}

client.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char const *argv[])
{
    //1.socket建立文件描述符
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    if (fd < 0)
    {
        perror("socket is err");
    }
    //2.connect连接服务器
    struct sockaddr_in saddr;
    saddr.sin_family = AF_INET;
    saddr.sin_port = htons(atoi(argv[2]));
    saddr.sin_addr.s_addr = inet_addr(argv[1]);
    int flage = connect(fd, (struct sockaddr *)(&saddr), sizeof(saddr));
    if (flage < 0)
    {
        perror("connect is err");
    }
    //3.服务器端不断发送数据,接受服务器转化后的数据
    char buf[1024] = {0};
    while (1)
    {
        //memset(buf,0,sizeof(buf));
        fgets(buf, sizeof(buf), stdin);
        if (strncmp(buf,"quit#",5)==0)
        {
            break;
        }
        if (buf[strlen(buf) - 1] == '\n')
            buf[strlen(buf) - 1] = '\0';
        send(fd, buf, sizeof(buf), 0);
        flage = recv(fd, buf, sizeof(buf), 0);
        if (flage < 0)
        {
            perror("recv is err");
        }
        else
        {
            fprintf(stdout, "%s\n", buf);
        }
    }
    close(fd);
    return 0;
}

以上就是select函数实现高性能IO多路访问的关键示例深入解析的详细内容,更多关于select函数IO多路访问的资料请关注脚本之家其它相关文章!

相关文章

  • C程序实现整数的素数和分解问题

    C程序实现整数的素数和分解问题

    这篇文章主要介绍了C程序实现整数的素数和分解问题,对于算法的学习有不错的借鉴价值,需要的朋友可以参考下
    2014-09-09
  • C++中指针的详解及其作用介绍

    C++中指针的详解及其作用介绍

    这篇文章主要介绍了C++中指针的详解及其作用介绍,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-09-09
  • 一篇文章带你了解C++ static的作用,全局变量和局部变量的区别

    一篇文章带你了解C++ static的作用,全局变量和局部变量的区别

    这篇文章介绍了C++ static的作用,全局变量和局部变量的区别,需要的朋友可以过来参考下,希望能够给你带来帮助
    2021-09-09
  • C++ STL 序列式容器与配接器的简单使用

    C++ STL 序列式容器与配接器的简单使用

    本文主要介绍了C++ STL 序列式容器与配接器的简单使用,文中通过示例代码介绍的非常详细,需要的朋友们下面随着小编来一起学习学习吧
    2021-06-06
  • EasyC++单独编译

    EasyC++单独编译

    这篇文章主要介绍了EasyC++单独编译,在上一篇当中,我们编写好了头文件coordin.h,现在我们要完成它的实现。需要的小伙伴可以先学习上一篇内容然后一起与小编一起进入本篇内容一起学习吧
    2021-12-12
  • 基于C语言编写简易的英文统计和加密系统

    基于C语言编写简易的英文统计和加密系统

    这篇文章主要介绍如何基于C语言编写一个简易的英文统计和加密系统,实际上就是对字符数组的基本操作的各种使用,感兴趣的可以了解一下
    2023-05-05
  • 深入解析C++ STL中的常用容器

    深入解析C++ STL中的常用容器

    这里我们不涉及容器的基本操作之类,只是要讨论一下各个容器其各自的特点。STL中的常用容器包括:顺序性容器(vector、deque、list)、关联容器(map、set)、容器适配器(queue、stac)
    2013-09-09
  • 深入解析C++中类的多重继承

    深入解析C++中类的多重继承

    这篇文章主要介绍了深入解析C++中类的多重继承,包括多重继承相关的二义性问题,需要的朋友可以参考下
    2015-09-09
  • java string对象上的操作,常见的用法你知道吗

    java string对象上的操作,常见的用法你知道吗

    今天给大家带来的是关于Java的相关知识,文章围绕着Java String类用法展开,文中有非常详细的介绍及代码示例,需要的朋友可以参考下
    2021-08-08
  • 详解C语言-二级指针三种内存模型

    详解C语言-二级指针三种内存模型

    这篇文章主要介绍了详解C语言-二级指针三种内存模型的相关知识,文中代码非常详细,供大家参考和学习,感兴趣的朋友可以了解下
    2020-06-06

最新评论