IO多路转接 —— select

前言: select用的少现在,而且写起来很复杂,本篇所讲的select IO模型主要是用于处理读就绪事件的,至于写就绪不关心。


1. select的概念

select函数来实现多路复用输入/输出模型:

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变

select函数它是负责等这个过程的,等成功了就通知上层来缓冲区拷贝,这个等的方式是可以自己设置的,比如:阻塞等,非阻塞等都行。注意:它只负责等,不负责拷贝或者写入。

2. select的函数接口

在这里插入图片描述

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

函数参数:

  1. nfds 是要监测的fd中,最大的fd+1,它相当于一个边界控制。
  2. fd_set *readfds, fd_set *writefds, fd_set *exceptfds,这三个参数是输入输出型参数,输入:你告诉内核你要关心那些fd。输出:你关心的fd有谁就绪了。总共有三个参数,它们分别是可读文件描述符的集合,可写文件描述符的集 合及异常文件描述符的集合。
  3. timeout这个结构体就是一个时间线。就用它来控制等的方式,如果将它给成null,那么就是阻塞式等。如果给成{0,0},那就是非阻塞式等。如果给成{5,0},那就是5秒前为阻塞式等,五秒后为非阻塞式等(相当于超时处理)。

函数返回值:

  • 大于 0:成功,返回集合中已就绪的文件描述符的总个数
  • 等于 - 1:函数调用失败
  • 等于 0:超时,没有检测到就绪的文件描述符

fd_set: 这个类型它就是一个位图,可以通过操作位图来表示你要关心哪些fd。它的大小是128字节。128*8=1024个比特位,也就是说可以检测1024个文件描述符,这个范围其实不是很大。

看一下它的结构:

typedef struct
  {
    /* XPG4.2 requires this member name.  Otherwise avoid the name
       from the global namespace.  */
#ifdef __USE_XOPEN
    __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
    __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
  } fd_set;

但这个位图,不能直接操作,必须使用函数接口:

  • void FD_CLR(int fd, fd_set *set); // 这是用于去除位图set中的某个fd
  • int FD_ISSET(int fd, fd_set *set);// 用来检测某个fd是否在位图set中
  • void FD_SET(int fd, fd_set *set); // 把某个fd加入到位图set中
  • void FD_ZERO(fd_set *set);//清空位图

举个例子,就取fd_set的最后8个比特位:fd_set i;

(1)先是利用FD_ZERO(&i)清空位图。
在这里插入图片描述
(2)将fd = 5,设置到位图i中,FD_SET(5,&i)。那就是将第五个比特位设置为1。
在这里插入图片描述
(3)再把fd = 1,fd =2 ,都设置进去:

在这里插入图片描述
(4)那么就可以利用select进行检测,假如这些描述符组合,我只关心读事件就绪:
select(6,&i,null,null,null),为啥是6,最大文件描述符fd是5,5+1 = 6。最后一个参数设置为null,就是阻塞式等待。

(5)假如现在fd=5的读事件就绪了,但是2和1都不就绪,那也返回了,位图就变为:

在这里插入图片描述
注意了:2和1处都被抹除,只有5处为1,因为只有5的读事件就绪了。那么肯定有疑问了,我本来要关心fd=1,2,5,三个文件描述符,虽然只有5事件读就绪,你把1,2都给抹除了,那么下次我还要重新设置位图吗?还得把1和2重新设置进位图?答案是:需要,这就是select难的地方。得有一个数组,提前把你要关心的文件描述符存进去。

那么我是如何知道5事件就绪了呢?还得操作位图:FD_ISSET(5, &i),这就是帮助检测的,如果返回值是>0,那么就说明关系的fd事件已经就绪,<0,就表示未就绪。

(6) 要是不关心某个fd了,就可以用void FD_CLR(int fd, fd_set *set);

3. 基于select的简易服务器实现

那么知道了select的基本操作函数,我们就基于select来完成一个简易的读取数据服务器,客户端就不写了,我们主要看的是服务器中使用select的基本逻辑。

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

 这是在定义一个存放fd集合的数组,记忆功能
#define NUM (sizeof(fd_set) * 8)
int fd_array[NUM];

void useage()
{
    std::cout << "please use"
              << "./select_sever"
              << "+"
              << "端口号" << std::endl;
}

int main(int argv, char *argc[])
{
     启动服务器的方式
    if (argv != 2)
    {
        useage();
        exit(1);
    }

     使服务器进去listen状态

    int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0)
    {
        std::cerr << "listen_fd failed" << std::endl;
        exit(2);
    }
    std::cout << "listen_fd:" << listen_fd << std::endl;
    struct sockaddr_in my_sock;
    my_sock.sin_family = AF_INET;
    my_sock.sin_port = htons(atoi(argc[1]));
    my_sock.sin_addr.s_addr = INADDR_ANY;

    if (bind(listen_fd, (const struct sockaddr *)&my_sock, sizeof(my_sock)) < 0)
    {
        std::cerr << "bind errno" << std::endl;
        exit(3);
    }

    if (listen(listen_fd, 5) < 0)
    {
        std::cerr << "listen errno" << std::endl;
        exit(4);
    }

     请问这里可以accept吗?绝对不可以,因为accept它是从listen_fd中拿连接,这也是读取事件,所以要用select进行管理。

     初始化一下 fd_arry

    for (int i = 0; i < NUM; i++)
    {
        fd_array[i] = -1;
    }

     我们只关心读事件,那么先得有一个位图fd_Set

    fd_set rfds;
    fd_array[0] = listen_fd; // 这个是不变的,一上来就只有一个套接字要监听读事件,那就是listen_fd

    while (true)
    {
         清空位图

        FD_ZERO(&rfds);

         把关心的事件设置进位图,并且更新MAX_fd(select的第一个参数)
        int MAX_fd = fd_array[0];

        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];
            }
        }

        开始select等待
        int n = select(MAX_fd + 1, &rfds, NULL, NULL, NULL);

        switch (n)
        {
        case -1:
            std::cerr << "select error" << std::endl;
            break;

        case 0:
            std::cout << "time out" << std::endl;
            break;

        default:
            std::cout << "有读事件就绪" << std::endl;
            std::cout<<"********************************************"<<std::endl;
            // 检测有哪些事件就绪
            for (int i = 0; i < NUM; i++)
            {
                if (fd_array[i] == -1)
                    continue;

                if (FD_ISSET(fd_array[i], &rfds))
                {
                    if (fd_array[i] == listen_fd)
                    {
                        std::cout << "有新的连接" << std::endl;
                        // 进行连接
                        struct sockaddr_in user;
                        socklen_t size_sockaddr = sizeof(user);
                        int sock = accept(listen_fd, (struct sockaddr *)&user, &size_sockaddr);
                        if (sock < 0)
                        {
                            std::cerr << "accept errno" << std::endl;
                            exit(5);
                        }
                        std::cout<<"连接成功:"<<"fd = "<<sock<<std::endl;
                       std::cout<<"********************************************"<<std::endl;

                        // 那么连接成功了,可以读数据吗?不可以,注意这里不可以进行读,因为 建立好连接并不表明数据来了,
                        // 本质就是要用select进行监测sock套接字,监测它读事件是否就绪

                        // 那么怎么就绪select进行检测呢?毫无疑问,把sock添加到fd_array中,下次循环就可以检测了。
                        // 必须要找到插入的位置

                        int pos = 1;
                        for (; pos < NUM; pos++)
                        {
                            if (fd_array[pos] == -1)
                                break;
                        }
                        // 对pos位置进行管理,看pos位置是否合法:
                        if (pos < NUM)
                        {
                            fd_array[pos] = sock;
                        }
                        else
                        {
                            std::cout << "服务器满载,关闭此连接" << std::endl;
                            std::cout<<"********************************************"<<std::endl;
                            close(sock);
                        }
                    }

                    else
                    {
                         走到这里,说明就是普通的读取事件,普通套接字的读事件就绪。
                         到这可以读吗?当然可以,人家都通知你读了

                        std::cout << "套接字为" << fd_array[i] << "有读取事件就绪" << std::endl;
                        char buffer[1024] = {0};
                        ssize_t N = recv(fd_array[i], buffer, sizeof(buffer) - 1, 0);

                        if (N > 0)
                        {
                            buffer[N] = 0;
                            std::cout << "client[" << fd_array[i] << "]#" << buffer << std::endl;
                            std::cout<<"********************************************"<<std::endl;
                        }
                        else if (N == 0)
                        {
                            std::cout << "对端连接关闭" << std::endl;
                            close(fd_array[i]);

                            // 注意,我们要把它在数组中的存储一并去除,这件事很重要

                            fd_array[i] = -1;
                            std::cout<<"********************************************"<<std::endl;
                        }
                        else
                        {
                            std::cout << "读取失败"
                                      << "主动关闭连接" << std::endl;
                            close(fd_array[i]);
                            // 注意,我们要把它在数组中的存储一并去除,这件事很重要
                            fd_array[i] = -1;
                            std::cout<<"********************************************"<<std::endl;
                        }
                    }
                }
            }
            break;
        }
    }
    return 0;
}

看看结果:
(1) 服务器起来
在这里插入图片描述
(2) telnet 连接

在这里插入图片描述
(3) 看现象,这是连接成功了,并且被select管理起来的
在这里插入图片描述
(4) 客户端发送数据
在这里插入图片描述
(5) 服务器接收
在这里插入图片描述
(6) 客户端退出,服务端:
在这里插入图片描述

至于这个代码的讲解,我都在注释里讲清楚了,不好理解。

其实,里面的select默认用的是阻塞等待,如果想要实验 select 的等待方式,其实只需要把函数的最后参数 timeout 改改就可以了。

4. select优缺点分析

分析一下select IO模型,它非常依赖第三方数组,原因有两点:

  1. fd_set 位图每一次都需要重新设置,这是很尴尬的,我让你关心这些fd事件,你返回的时候只返回哪些就绪了的,其余都给我抹除了,我还得搞个第三方数组,提前把关心的事件保存起来。
  2. 你要想关心别的fd,或者不再关心某个fd,都需要第三方数组进行管理。

那么就可以总结一下它的优缺点:

  • 优点:一次可以等待多个fd,一定程度上提高了IO效率
  • 缺点:每次都要重新设置位图,还得遍历检测。select从用户层到内核,多次切换,这个频率有点高,这是有开销的。文件描述符1024个,这个数量有点小。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

动名词

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

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

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

打赏作者

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

抵扣说明:

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

余额充值