前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >IO多路转接之select

IO多路转接之select

作者头像
二肥是只大懒蓝猫
发布2023-10-13 11:36:31
2350
发布2023-10-13 11:36:31
举报
文章被收录于专栏:热爱C嘎嘎热爱C嘎嘎

本文分享的是IO多路转接中的select,其中包括select函数如何去使用,以及使用相关代码实现客户端向服务端发送消息的服务,从而更好地理解多路转接的select。

多路转接

多路转接是IO模型的一种,这种IO模型通过select函数进行IO等待,并且select函数能够同时等待多个文件描述符的就绪状态,单个文件描述符的等待与阻塞IO类似。

select

系统提供select函数来实现多路复用输入/输出模型。select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变。

通俗的来讲,select函数,就是负责等待,得到文件描述符就绪后,通知上层进行读取或写入。select没有读取或写入数据的功能,并且select能够同时等待多个文件描述符。

select函数原型

select的函数原型:
代码语言:javascript
复制
#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前,先解释它们的类型fd_set类型。

fd_set类型

fd_set是一个整数数组, 更严格的说, 是一个 "位图"。使用位图中对应的位来表示要监视的文件描述符。

在fd_set位图结构中,使用比特位的“位置”来表示某一个sock

而对于比特位的“内容”,首先我们需要知道的是,readfds、writefds和exceptfds三个参数都是输入输出型参数。

以readfds读为例:

用户在使用该参数进行输入时,实质上是用户告诉内核,内核你要帮我关心一下哪些文件描述符上的读事件就绪。 内核进行输出时,实质上是告诉用户,用户你所关心的那些文件描述符上的读事件已经就绪。

于是,对于比特位的“内容”,首先是输入时,是用户想要内核帮忙关心的文件描述符的合集。在输出时,是内核要告诉用户已经就绪的文件描述符的合集

比如,输入时,我们规定用户想要关心的文件描述,在位图结构中,其比特位的位置位1,3,5,于是在输入时,将其内容置为1,表示我们需要让select帮我们关心1,3,5文件描述符。那么在输出时,假设这些文件描述符1,5都已经就绪,输出回来时,这个合集中的1,5比特位的位置上的内容为1,而3由于没有就绪,就为0。需要注意的是,输入输出的都是同一个位图,是同一个!

提供了一组操作fd_set的接口, 来比较方便的操作位图:

void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位。 int 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的全部位。

②readfds、writefds和exceptfds三个参数:分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。

在解释参数timeout前,我们先来解释struct timeval结构。

timeval结构

timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。

 函数返回值:

执行成功则返回文件描述词状态已改变的个数。 如果返回0代表在描述词状态改变前已超过timeout时间,没有返回。 当有错误发生时则返回-1,错误原因存于errno,此时参数readfds, writefds, exceptfds和timeout的值变成不可预测。

错误值可能为:

EBADF 文件描述词为无效的或该文件已关闭。 EINTR 此调用被信号所中断。 EINVAL 参数n 为负值。 ENOMEM 核心内存不足。

③参数timeou:参数timeout为结构timeval,用来设置select()的等待时间。一般timeou参数的取值有三种:

NULL:填入nullptr或者NULL时,表示阻塞。即表示select()没有timeout, select将一直被阻塞,直到某个文件描述符上发生了事件,即只要不就绪,就不返回。 0:当struct timeval timeout={0,0},即为0时,表示非阻塞。仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生,即只要不就绪,立马返回。 特定的时间值:当struct timeval timeout={5,0}。表示,在5秒内阻塞,5秒后非阻塞。如果在指定的时间段里没有事件发生, select将超时返回。

④select函数返回值

当返回值ret>0:表示已有几个fd已经就绪。比如ret = 2,就有2个fd就绪。 当返回值ret==0,表示超时返回 当返回值ret<0,select调用失败

理解select执行过程

理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节, fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。

*(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。 *(2)若fd= 5,执行FD_SET(fd,&set).后set变为0001,0000(第5位置为1)。 *(3)若再加入fd= 2, fd=1,则set变为0001,0011。 *(4)执行select(6,&set,0,0,0)阻塞等待。 *(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。 *   注意:没有事件发生的fd=5被清空。

需要注意的是,因为select使用输入输出型参数标识不同的含义,因此每一此都会被清空,这意味着,每一次都需要对fd_set进行重新设置!并且,因为需要重新设置,我们需要通过第三方数组来对这些文件描述符进行保存!

代码简单实现多路转换

使用select实现一个简单服务器,客户端可以向服务端发送消息,服务端读取数据。

代码思路:代码分五步:

①创建监听套接字,端口号,绑定,进入监听状态一系列动作。进入监听状态后,不能马上进行accept,因为accept便是阻塞状态,监听套接字本身就可以看作是读事件就绪了。

②准备好一个数组,用于存放套接字。

③select等待前的准备:创建fd_ser类型的变量,并设置相关参数。

④使用select进行等待。在等待后,需要分情况,其返回值是如何。

⑤如果select成功返回读事件已经就绪的文件描述符个数,那么开始进行读取。当然,到达这一步,就证明现在的文件描述符是合法的,然而需要查看在数组中,哪些文件描述符是就绪的了。

找到已经就绪的文件描述符后,还不能马上进行读取,因为有可能该文件描述符是监听套接字,需要进行accept。

确定是用于通信的套接字后,就可以进行读取了。

代码语言:javascript
复制
#include <iostream>
#include <string>
#include <sys/select.h>
#include "Sock.hpp"

//一、创建监听套接字,端口号,绑定,进入监听状态一系列动作!

//NUM为数组的大小,含义是能够包含NUM个fd,一个fd一个bit
#define NUM (sizeof(fd_set) * 8)//fd_set类型大小为128字节

int fd_array[NUM]; //内容>=0,合法的fd,如果是-1,该位置没有fd

static void Usage(std::string proc)
{
    std::cout << "Usage: " << proc << " port" << std::endl;
}

//需要输入格式: ./select_server 8080
int main(int argc, char *argv[])
{
    //不符合格式
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(1);
    }
    //符合格式
    uint16_t port = (uint16_t)atoi(argv[1]);//端口号
    int listen_sock = Sock::Socket();//创建监听套接字
    Sock::Bind(listen_sock, port);//绑定端口号
    Sock::Listen(listen_sock);//服务器进入监听状态


//二、准备好存放fd的数组

    //先将存放fd的数组,全部置为-1。-1表示不合法
    for (int i = 0; i < NUM; i++)
    {
        fd_array[i] = -1;
    }

    // 不会在这里进行accept,accept的本质叫做通过listen_sock获取新链接
    //accept是阻塞式等待
    //站在多路转接的视角,我们认为,链接到来,对于listen_sock,就是读事件就绪!!!
    //对于所有的服务器,最开始的时候,只有listen_sock


//三、select等待前的准备:创建fd_ser类型的变量,并设置相关参数

    //事件循环
    //创建fd_set结构的位图:使用位图中对应的位来表示要监视的文件描述符
    fd_set rfds;
    //将fd数组中的第一个元素,存放为监听套接字
    fd_array[0] = listen_sock;
    //进入循环
    for (;;)
    {
        //用来清除描述词组set的全部位:将位图全部置0,全部清除。
        FD_ZERO(&rfds);
        //创建最大的文件描述符,用于后续select中的第一个参数的设置
        int max_fd = fd_array[0];

        for (int i = 0; i < NUM; i++)
        {
            //不合法,继续
            if (fd_array[i] == -1)
                continue;
            //下面的都是合法的fd
            //FD_SET:用来设置描述词组set中相关fd的位
            FD_SET(fd_array[i], &rfds); //所有要关心读事件的fd,添加到rfds中
            if (max_fd < fd_array[i])
            {
                max_fd = fd_array[i]; //更新最大fd
            }
        }

        struct timeval timeout = {0, 0}; // 5s
        // 我们的服务器上的所有的fd(包括listen_sock),都要交给select进行检测!!
        // recv,read,write,send,accept : 只负责自己最核心的工作:真正的读写(listen_sock:accept)

//四、使用select进行等待

        int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr); //暂时阻塞
        switch (n)
        {
        case -1: //错误发生时则返回-1
            std::cerr << "select error" << std::endl;
            break;
        case 0:   //返回0代表在描述词状态改变前已超过timeout时间,没有返回
            std::cout << "select timeout" << std::endl;
            break;
        default:  //执行成功则返回文件描述词状态已改变的个数
            std::cout << "有fd对应的事件就绪啦!" << std::endl;

//五、成功返回个数,开始进行
        
        //5.1查看是否是就绪fd
            for (int i = 0; i < NUM; i++)
            {
                if (fd_array[i] == -1)
                    continue;
                //下面的fd都是合法的fd,合法的fd不一定是就绪的fd
                //FD_ISSET:用来测试描述词组set中相关fd 的位是否为真
                if (FD_ISSET(fd_array[i], &rfds))
                {
                    std::cout << "sock: " << fd_array[i] << " 上面有了读事件,可以读取了" << std::endl;
                    // 一定是读事件就绪了!!!
                    // 就绪的fd就在fd_array[i]保存!
                    // read, recv时,一定不会被阻塞!
                    // 读事件就绪,就一定是可以recv,read吗??不一定!!

                    //看看数组中的文件描述符,是属于监听套接字还是普通套接字。
                    //如果是监听套接字,那就需要accept
                    if (fd_array[i] == listen_sock)//
                    {
                        std::cout << "listen_sock: " << listen_sock << " 有了新的链接到来" << std::endl;
                        // accept
                        int sock = Sock::Accept(listen_sock);
                        if (sock >= 0)
                        {
                            std::cout << "listen_sock: " << listen_sock << " 获取新的链接成功" << std::endl;
                            // 获取成功
                            // recv,read了呢?绝对不能!
                            // 新链接到来,不意味着有数据到来!!直接读的话被阻塞!什么时候数据到来呢?不知道
                            // 可是,谁可以最清楚的知道那些fd,上面可以读取了?select!
                            // 无法直接将fd设置进select,但是,好在我们有fd_array[]!
                            int pos = 1;
                            for (; pos < NUM; pos++)
                            {
                                if (fd_array[pos] == -1)
                                    break;
                            }
                            // 1. 找到了一个位置没有被使用
                            if (pos < NUM)
                            {
                                std::cout << "新链接: " << sock << " 已经被添加到了数组[" << pos << "]的位置" << std::endl;
                                fd_array[pos] = sock;
                            }
                            else
                            {
                                // 2. 找完了所有的fd_array[],都没有找到没有被使用位置
                                // 说明服务器已经满载,没法处理新的请求了
                                std::cout << "服务器已经满载了,关闭新的链接" << std::endl;
                                close(sock);
                            }
                        }
                    }
                    else  //用于通信的套接字,可以读了
                    {
                        // 普通的sock,读事件就绪啦!
                        // 可以进行读取啦,recv,read
                        // 可是,本次读取就一定能读完吗?读完,就一定没有所谓的数据包粘包问题吗?
                        // 但是,我们今天没法解决!我们今天没有场景!仅仅用来测试
                        std::cout << "sock: " << fd_array[i] << " 上面有普通读取" << std::endl;
                        char recv_buffer[1024] = {0};
                        ssize_t s = recv(fd_array[i], recv_buffer, sizeof(recv_buffer) - 1, 0);
                        if (s > 0)
                        {
                            recv_buffer[s] = '\0';
                            std::cout << "client[ " << fd_array[i] << "]# " << recv_buffer << std::endl;
                        }
                        else if (s == 0)
                        {
                            std::cout << "sock: " << fd_array[i] << "关闭了, client退出啦!" << std::endl;
                            //对端关闭了链接
                            close(fd_array[i]);
                            std::cout << "已经在数组下标fd_array[" << i << "]"
                                      << "中,去掉了sock: " << fd_array[i] << std::endl;
                            fd_array[i] = -1;
                        }
                        else
                        {
                            //读取失败
                            close(fd_array[i]);
                            std::cout << "已经在数组下标fd_array[" << i << "]"
                                      << "中,去掉了sock: " << fd_array[i] << std::endl;
                            fd_array[i] = -1;
                        }
                    }
                }
            }
            break;
        }
    }

    return 0;
}

封装套接字相关接口:

代码语言:javascript
复制
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

using namespace std;

class Sock
{
public:
    static int Socket()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock < 0)
        {
            cerr << "socket error" << endl;
            exit(2);
        }
        int opt = 1;
        setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
        return sock;
    }

    static void Bind(int sock, uint16_t port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            cerr << "bind error!" << endl;
            exit(3);
        }
    }

    static void Listen(int sock)
    {
        if (listen(sock, 5) < 0)
        {
            cerr << "listen error !" << endl;
            exit(4);
        }
    }

    static int Accept(int sock)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int fd = accept(sock, (struct sockaddr *)&peer, &len);
        if(fd >= 0){
            return fd;
        }
        return -1;
    }

    static void Connect(int sock, std::string ip, uint16_t port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));

        server.sin_family = AF_INET;
        server.sin_port = htons(port);
        server.sin_addr.s_addr = inet_addr(ip.c_str());

        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
        {
            cout << "Connect Success!" << endl;
        }
        else
        {
            cout << "Connect Failed!" << endl;
            exit(5);
        }
    }
};

select的特点

可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)= 512,每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096。将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd,一是用于再select 返回后, array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。

select缺点

每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便。 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。 select支持的文件描述符数量太小。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 多路转接
  • select
    • select函数原型
      • select的函数原型:
      • 参数解释:
      • fd_set类型
      • timeval结构
    • 理解select执行过程
    • 代码简单实现多路转换
    • select的特点
    • select缺点
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档