多路复用之——select

select的作用

系统提供select函数来实现多路复用输入/输出模型。

● select系统调用是用来让我们的程序同时监视多个文件描述符的状态变化;

● 程序会停在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 用来设置 select 的等待时间。通常有以下三种设置方式:

NULL:阻塞式等待,select将一直被阻塞,直到某个文件描述符上发送了事件;

0:非阻塞式等待,调用select后检测文件描述符的状态,然后立即返回;

特定的时间值:阻塞式等待一定时间,若期间没有事件发生,select将超时返回;

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 ,将所有位清零 —— 初始化

timeval结构

● tv_sec:表示秒数;

● tv_usec:表示微秒数(10^6 微秒 = 1 秒);

函数返回值

● 大于0:执行成功,返回所有集合中,文件描述符就绪的总数;

● 0:等待超时,在timeout时间内,没有文件描述符就绪;

● -1:执行出错,错误原因可通过 errno 错误码得知;

select工作原理

select 中的 readfds,writefds,exceptfds 是输入输出型参数,我们需要根据这三个集合中的输入输出内容,来判断哪些文件描述符已经就绪。

输入时

● 将存在需要我们关心相应事件的文件描述符,设置到对应的 fd_set 中;

● fd_set 的比特位的位置,代表哪一个 sock,比特位上的内容代表是否要关心;

输出时

● 对于被关心且已经就绪的文件描述符,在相应的 fd_set 中仍被设置,用来告诉用户哪些已经就绪;

● 对于被关心但未就绪的文件描述符,在相应的 fd_set 中会被清除,来告诉用户哪些还未就绪;

select核心功能

以读为例:

● 用户告知内核,你要帮我关心哪些 fd 上的读事件就绪;

● 内核告知用户,你所关心的 fd 中,哪些已经读事件就绪;

第三方数组

在 select 返回后,我们需要遍历历史的每个 fd ,并在 fd_set 中检测该 fd 上是否有事件就绪。

另外,select 返回后会把以前加入的,但并无事件发生的 fd 清空。意味着每次开始 select ,都要对 fd_set 进行重新设置。

所以,用户需要定义数组或其它数据结构,来把历史 fd 都保存起来。对于每个 fd_set(readfds,writefds,exceptfds),都需要它们自己对应的第三方数组。

综上,第三方数组 array 的作用是:

● 用于在 select 返回后,array 作为源数据和 fd_set 进行 FD_ISSET判断;

● 用于重新设置 fd_set 的数据源(FD_ZERO最先),扫描 array 的同时取得 fd 的最大值,将其加一作为 select 的第一个参数;

select优点

可以一次等待多个 fd ,在一定程度上,提高了 IO 的效率。

select缺点

● 每次都要重新设置 fd_set ;每次 select 返回,都要遍历 array ,对有效位置做检测;

● fd_set 的大小是有限的,所以 select 同时检测的 fd 是有上限的;

● select 底层需要轮询式的检测哪些 fd 上的哪些事件的就绪状态;

● select 可能会较为高频率的进行用户到内核,内核到用户的的拷贝问题;

基于select的服务器示例

Sock.hpp

#pragma once
​
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdlib>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
​
​
class Sock
{
public:
    static int Socket()
    {
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if(sock < 0)
        {
            std::cerr << "socket error" << std::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;
        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)
        {
            std::cerr << "bind error" << std::endl;
            exit(3);
        }
    }
​
    static void Listen(int sock)
    {
        if(listen(sock, 5) < 0)
        {
            std::cerr << "Listen error" << std::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, std::string port)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
​
        server.sin_family = AF_INET;
        server.sin_port = htons((stoi(port)));
        server.sin_addr.s_addr = inet_addr(ip.c_str());
​
        if(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0)
        {
            std::cout << "Connect Success!" << std::endl;
        }
        else
        {
            std::cout << "Connect Failed!" << std::endl;
            exit(5);
        }
    }
};

select_server.cc

#include <iostream>
#include <sys/select.h>
#include <string>
​
#include "Sock.hpp"
​
#define NUM (sizeof(fd_set) * 8)
​
int fd_array[NUM];
​
void Usage(std::string proc)
{
    std::cout << "Usage: "
              << "\n\t" << proc << " port" << std::endl;
}
​
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_array
    for (int i = 0; i < NUM; ++i)
    {
        fd_array[i] = -1;
    }
​
    //事件循环
    fd_set rfds;
    fd_array[0] = listen_sock;
    for (;;)
    {
        FD_ZERO(&rfds);
        int max_fd = fd_array[0];
​
        //每次都需要重新填充fd_set
        for (int i = 0; i < NUM; ++i)
        {
            if (fd_array[i] == -1)
            {
                continue;
            }
​
            FD_SET(fd_array[i], &rfds);
            if (max_fd < fd_array[i])
            {
                max_fd = fd_array[i];
            }
        }
​
        // struct timeval timeout = {5, 0};
        int n = select(max_fd + 1, &rfds, nullptr, nullptr, nullptr); //阻塞等待
        switch (n)
        {
        case -1:
            std::cerr << "select error" << std::endl;
            break;
        case 0:
            std::cout << "select timeout" << std::endl;
            break;
        default:
            std::cout << "some fds have already" << std::endl;
            for (int i = 0; i < NUM; ++i)
            {
                if (fd_array[i] == -1)
                {
                    continue;
                }
                //对有效位置做检测
                if (FD_ISSET(fd_array[i], &rfds))
                {
                    std::cout << "sock: " << fd_array[i] << " 上面的读事件已经就绪" << std::endl;
                    //此时调用accept,read, recv时不会被阻塞
                    // listen套接字就绪
                    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;
​
                            //在fd_array中找一个空位置存储新来的连接
                            int pos = 1;
                            for (; pos < NUM; ++pos)
                            {
                                if (fd_array[pos] == -1)
                                    break;
                            }
                            // fd_array中还有位置
                            if (pos < NUM)
                            {
                                std::cout << "新连接: " << sock << " 已经被添加到fd_array[" << pos << "]的位置" << std::endl;
                                fd_array[pos] = sock;
                            }
                            // fd_array已满
                            else
                            {
                                std::cout << "服务器已经满载,关闭新连接" << std::endl;
                                close(sock);
                            }
                        }
                        //获取新连接失败
                        else
                        {
                            std::cout << "accept failed!" << std::endl;
                        }
                    }
                    //普通套接字就绪
                    else
                    {
                        //此时可以使用read, recv来读
                        //但本次读取不一定能将数据读完,因为存在粘包问题
                        std::cout << "普通连接: " << fd_array[i] << " 上有数据读取" << std::endl;
                        char recv_beffer[1024] = {0};
                        ssize_t s = recv(fd_array[i], recv_beffer, sizeof(recv_beffer) - 1, 0);
                        //读取数据成功
                        if (s > 0)
                        {
                            recv_beffer[s] = '\0';
                            std::cout << "client[" << fd_array[i] << "]# " << recv_beffer << std::endl;
                        }
                        //对方关闭连接
                        else if (s == 0)
                        {
                            std::cout << "sock: " << fd_array[i] << " 已关闭连接" << std::endl;
                            close(fd_array[i]);
                            std::cout << "已将fd_array[" << i << "]"
                                      << " 上的普通sock: " << fd_array[i] << " 去掉了" << std::endl;
                            fd_array[i] = -1;
                        }
                        //读取失败
                        else
                        {
                            std::cout << "recv: " << fd_array[i] << " 失败" << std::endl;
                            close(fd_array[i]);
                            std::cout << "已将fd_array[" << i << "]"
                                      << " 上的普通sock: " << fd_array[i] << " 去掉了" << std::endl;
                            fd_array[i] = -1;
                        }
                    }
                }
            }
            break;
        }
    }
    return 0;
}
  • 3
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
select函数是Linux C编程中的一种多路复用机制,用于监视并等待多个文件描述符的属性发生变化。它可以同时监视多个文件描述符的可读、可写和异常状态。select函数的原型如下: ```c #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参数指定等待时间,当超过指定时间后,select函数会返回。当select函数返回后,可以通过遍历fd_set集合来确定哪些文件描述符已经就绪。 使用select函数可以实现多个文件描述符的并发处理,提高程序的效率和响应速度。 #### 引用[.reference_title] - *1* [IO多路复用之——select详解](https://blog.csdn.net/weixin_60954394/article/details/127062613)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [Linux下C语言多路复用——select函数](https://blog.csdn.net/qq_45097019/article/details/105166595)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

waywt1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值