Linux网络编程(多路IO复用select函数使用)


前言

本篇文章带大家来学习一下多路IO复用select函数的使用。

一、什么是多路IO复用

1.多路I/O复用(Multiplexing I/O)是一种用于同时监视和处理多个输入/输出(I/O)源的技术。它允许一个进程可以同时监听和处理多个文件描述符(sockets、文件、管道等),从而实现高效的事件驱动的编程模型。

2.在传统的I/O模型中,通常采用阻塞I/O或非阻塞I/O方式进行读写操作,为每个I/O源(例如一个socket连接)都创建一个线程或进程来处理。这种方式在高并发场景下,会导致线程或进程数目的剧增,系统资源浪费,且上下文切换开销大。

3.而多路I/O复用通过使用操作系统提供的机制,如select、poll、epoll(在Linux中),允许程序同时监听多个I/O源,实现了异步I/O操作。应用程序可以将多个I/O源注册到多路复用器中,并在多路复用器上等待事件的发生,一旦有事件就绪(如读写准备就绪),程序就可以针对这些就绪的事件进行相应的操作。

多路I/O复用的主要优点如下:

1.高效:通过事件驱动的方式,避免了线程和进程的频繁切换,减小了系统开销。

2.节省资源:使用较少的线程或进程来处理多个I/O源,节省了系统资源,并能够处理大量的并发连接。

3.简化编程模型:与传统的多线程或多进程编程方式相比,使用多路I/O复用可以简化编程模型,使得代码更加清晰和易于维护。

需要注意的是,多路I/O复用并非适用于所有情况,特别是在处理高延迟和大数据量的场景下可能不太适合。此外,不同的操作系统上多路复用器的机制和性能表现也有所不同。

二、select函数讲解

select()函数是一个用于多路复用的I/O模型,可以同时监视多个文件描述符,并在其中任何一个文件描述符就绪时进行相应的处理。它被广泛用于实现高效的事件驱动编程。

select()函数的原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数说明:

nfds:监视的文件描述符的最大值加1。

readfds:指向可读文件描述符集合的指针。

writefds:指向可写文件描述符集合的指针。

exceptfds:指向异常文件描述符集合的指针。

timeout:指定超时时间,选择阻塞的时长。

fd_set是一个文件描述符集合类型,通过宏定义和操作函数进行操作。

使用select()函数的步骤如下:

创建并初始化文件描述符集合。

将需要监视的文件描述符添加到对应的文件描述符集合中。

调用select()函数并传递文件描述符集合。

检查返回值,若为负数则表示出错,若为0则表示超时,若大于0则表示有文件描述符就绪。

使用FD_ISSET()宏检查具体哪些文件描述符就绪,并进行相应的处理。

下面是一个简单的示例代码:

#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    
    
    fd_set readfds;
    int max_fd, ret;

    FD_ZERO(&readfds);
    FD_SET(STDIN_FILENO, &readfds);  // 监视标准输入

    max_fd = STDIN_FILENO + 1;

    struct timeval timeout;
    timeout.tv_sec = 5;  // 设置超时时间为5秒
    timeout.tv_usec = 0;

    ret = select(max_fd, &readfds, NULL, NULL, &timeout);

    if (ret == -1) {
    
    
        perror("select");
        exit(EXIT_FAILURE);
    } else if (ret == 0) {
    
    
        printf("Timeout\n");
    } else {
    
    
        if (FD_ISSET(STDIN_FILENO, &readfds)) {
    
    
            printf("Stdin is ready for reading\n");

            // 读取标准输入数据
            char buffer[100];
            fgets(buffer, sizeof(buffer), stdin);
            printf("Received: %s", buffer);
        }
    }

    return 0;
}

三、使用select编程并发服务器

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/select.h>



int main()
{
    
    
    int server = 0;
    struct sockaddr_in saddr = {
    
    0};
    int client = 0;
    struct sockaddr_in caddr = {
    
    0};
    socklen_t asize = 0;
    int len = 0;
    char buf[32] = {
    
    0};
    int maxfd;//最大文件描述符
    int ret = 0;
    int i = 0;

    server = socket(PF_INET, SOCK_STREAM, 0);

    if( server == -1 )
    {
    
    
        printf("server socket error\n");
        return -1;
    }

    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = htonl(INADDR_ANY);
    saddr.sin_port = htons(8888);

    if( bind(server, (struct sockaddr*)&saddr, sizeof(saddr)) == -1 )
    {
    
    
        printf("server bind error\n");
        return -1;
    }

    if( listen(server, 128) == -1 )
    {
    
    
        printf("server listen error\n");
        return -1;
    }

    printf("server start success\n");

    maxfd = server;

    fd_set rets;
    fd_set allrets;

    FD_ZERO(&allrets);
    FD_SET(server, &allrets);

    while( 1 )
    {
    
            
        rets = allrets;
        ret = select(maxfd + 1, &rets, NULL, NULL, NULL);//使用select函数进行监听
        if(ret > 0)
        {
    
    
            if(FD_ISSET(server, &rets))
            {
    
    
                /*有客户端连接上来了*/
                asize = sizeof(caddr);  
                client = accept(server, (struct sockaddr*)&caddr, &asize);
                FD_SET(client, &allrets);//将连接上来的客户端设置进去
                if(maxfd < client)
                {
    
    
                    /*更新最大文件描述符*/
                    maxfd = client;
                }
            }
            else if(ret == -1)
            {
    
    
                for(i = server + 1; i <= maxfd; i++)
                {
    
    
                    if(FD_ISSET(i, &rets))//判断是哪一个客户端有信息
                    {
    
    
                        len = read(i, buf, 1024);
                        if(len == 0)//客户端断开了连接
                        {
    
    
                            FD_CLR(i, &allrets);//将断开连接的客户端清除出去
                            close(i);//关闭客户端
                        }
                        else
                        {
    
    
                            printf("read len : %d read buf : %s\n", len, buf);
                            write(i, buf, len);
                        }
                    }
                }
            }
        }
        else
        {
    
    
            printf("select is err\n");
        }                

    }
    
    close(server);

    return 0;
}

注意点:

使用select()函数时,传入和传出的readfds等参数可能是不一样的。这是因为select()函数会修改传入的文件描述符集合,以指示哪些文件描述符已经就绪。

所以在使用select时需要先将readfds做一个备份,以免丢失一些文件描述符。

四、select函数的缺点

1.低效性:select()函数在功能上存在一些限制。它使用线性扫描的方式来遍历监视的文件描述符集合,因此在大量文件描述符的情况下效率较低。每次调用select()都需要将整个文件描述符集合传递给内核,并在返回时重新检查整个集合以确定哪些文件描述符已就绪。这种线性扫描的开销随着监视的文件描述符数量的增加而增加。

2.文件描述符数量限制:select()函数所能监视的文件描述符数量存在限制,在某些系统上,这个限制可能是比较小的,例如1024。因此,如果要监视的文件描述符数量超过了限制,就需要采用其他方法来解决。

3.阻塞模式:select()函数是一种阻塞式调用,即当没有任何文件描述符就绪时,它会一直阻塞等待。这会导致程序无法进行其他操作。虽然可以通过设置超时时间来解决这个问题,但超时时间过短可能导致错误的超时,而超时时间过长则会影响程序的响应性。

4.受限的事件类型:select()函数只能监视文件描述符的可读性、可写性和异常情况。如果需要监视其他类型的事件,例如定时器事件或信号事件,就无法使用select()函数。

5.每次调用重新初始化:每次调用select()函数之前,需要重新初始化文件描述符集合,将需要监视的文件描述符重新添加到集合中。这样的初始化过程比较繁琐,尤其当文件描述符集合发生变化时,需要手动更新集合。

总结

本篇文章主要讲解了使用select函数进行多路IO服用实现了并发服务器程序的编写。

猜你喜欢

转载自blog.csdn.net/m0_49476241/article/details/132351251