多路转接之select(fd_set介绍,参数详细介绍,优缺点),实现非阻塞式网络通信(代码+思路)

8 篇文章 0 订阅

目录

多路转接之select

引入

介绍

fd_set

函数原型

nfds

readfds / writefds / exceptfds

readfds 

总结 

fd_set操作接口 

timeout

timevalue 结构体

传入值

返回值

代码

注意点 -- 调用函数

select的参数填充 

获取新连接

注意点 -- 通信时的调用函数

辅助数组

处理函数

示例

完整代码

总结

优点

缺点


多路转接之select

引入

io本质+io效率本质,5种io模型(介绍,异步/同步区别,阻塞/非阻塞区别)-CSDN博客

以前使用的io接口,既完成等待,又完成拷贝

但在多路转接的io方式中不同,分为两个部分,需要调用两个函数来完成

介绍

select只负责等待,一次可以等待多个fd

  • 就像之前钓鱼例子中的d,他拥有多个鱼竿,就相当于等待多个fd

既然可以关注多个fd,自然参数中就要使用其他数据结构了 -- fd_set

fd_set

内核提供的一种数据类型

  • 位图

因为fd_set是一个具体的类型

  • 既然是类型,就一定有大小
  • 有大小就会有比特位的数量
  • 也就相当于可以等待的文件fd值文件数量是有上限的

使用sizeof测试fd_set的大小,得到它是1024个bit

  • 所以一次最多等待1024个文件的某个事件
  • 这个值随着系统不同会有变化,实际应该动态计算 -- sizeof(fd_set) * 8

函数原型

nfds

要等待的多个fd中的最大值+1

readfds / writefds / exceptfds

等待多个fd的关键,属于输入输出型参数

等待 -- 等待事件就绪

  • 事件 -- 一般分为 读/写/有异常
  • 只要读写事件就绪,就可以直接完成拷贝操作,不会阻塞住
  • 异常事件是例外,需要特殊处理,这里不做介绍

如果想关注某个文件上的读事件,就把该文件的fd设置进readfds

  • 其他同理
  • 可以关注同一个文件上的多个事件,也可以分顺序地关注,总之设置进相应位图中就行

接下来我们以readfds为例,详细介绍一下,其他位图同理 

readfds 

fd本身就是从0开始的数字

  • 和数组下标/位图均可以一一对应

因为是输入输出型参数:

输入时

  • 我们要告诉内核需要关注的fd集,你要帮我关心这些文件上面的读事件 + 这是个位图结构 + fd和位图可以对应
  • 所以,可以得出,位图上的比特位位置(从左向右,从0开始) 对应 文件的fd值
  • 只要该位设置为1,就是我们想让内核关注该文件
  • eg:我们要关注0,1,2,3这四个文件:

输出时

  • 内核要告诉我们,关注的fd集中有哪些fd上的读事件已经就绪 + 返回的也是个位图结构
  • 所以,对应关系依然没有变,但代表的含义不同
  • 如果该位为1,说明该文件上的读事件已经就绪
  • 内核会先将位图清零,然后将[读事件已经就绪的文件]的fd值 对应的 比特位 置1
  • eg:四个文件中,fd=2的文件的读事件就绪:
总结 

所以,总结来说,fd_set这张位图,是让用户和内核之间互相传递信息

  • 那么,在使用select函数的过程中,一定会涉及大量的位图操作
fd_set操作接口 

为了让用户更方便,内核为我们提供了接口

timeout

设置select的等待方式

每隔若干秒,timeout一次,timeout后 / 有文件就绪后函数会返回

timevalue 结构体

在gettimeofday()中也有使用这个类型作为参数:

  • 获取特定时区下的特定时间,精确到微秒级别

  • 时间戳 -- 秒单位和微妙单位 
  • 比如传入参数{5,0},代表设置时间戳为5s
传入值
  • 设置>0 -- 每隔一段时间timeout一次,比如5s
  • 设置为0 -- 非阻塞(select立即返回)
  • 设置为NULL -- 阻塞等待,直到有文件就绪

如果设置(非NULL)了该时间

  • 则为输入输出型参数
  • 如果在等待的中途有文件就绪,则返回[timeout时间-已经等待时间],也就是[距离超时时间的剩余时间 ]

返回值

  • >0 -- 有n个fd就绪
  • =0 -- 超时,等待过程中没有错误,也没有fd就绪
  • <0 -- 等待出错(要等待的某个文件已经关闭了)

代码

我们这里实现一个非阻塞版网络通信

注意点 -- 调用函数

创建好套接字后,不能直接accept  

  • accept本质就是在检测并获取listensock上面的事件
  • 但我们这里目的就是要让select去等待事件(有事件了再去通知我们来获取,这时候调用accept就不会被阻塞了)
  • 所以不能先调用accept

这里的事件:

  • = 新连接到来 = 三次握手完成,系统把新连接投递到全连接队列里 = select里的读事件
  • 所以我们先调用select等待读事件

select的参数填充 

这里是服务器刚启动时,是我们需要让listensocket检测并获取新连接(新客户端与当前服务器通信)

  • 所以,等待的是listensocket上的读事件,并且当前只有这一个套接字
  • 所以,max_fd=listensocket_fd+1
  • 等有客户端连接后,会有新的套接字被创建(通信时使用的套接字),就需要添加检测这些套接字上的读写事件了(后面会细说)

因为timeout是输入输出型参数

  • 一旦超时/当前有事件就绪,就会修改timeout的值
  • 所以,为了不影响下一次的等待方式,需要重复设置timeout参数

三个位图集也是同理,需要重复设置

  • 不然会被修改成已经就绪的,而不代表需要内核关注的fd集
FD_SET(listen_fd, &readfds);
struct timeval timeout = {1, 0}; // 1秒0微秒
int ret = select(listen_fd+1, &readfds, nullptr, nullptr, &timeout);

获取新连接

如果事件就绪,上层却不处理,select会一直通知

  • 所以需要我们手动调用accept()去把新连接拿走(这个操作在我们新的处理函数中)

当然,我们无法确定是哪个fd就绪了

  • 所以需要先判断
  • 判断完成后,就可以拿到新连接,创建新套接字了 
void handle(fd_set &readfds)
    {
        if (FD_ISSET(listen_socket_.get_fd(), &readfds)) // 新连接就绪
        {
            std::string clientip;
            uint16_t clientport;
            int sock = listen_socket_.Accept(clientip, clientport);
            ...
        }
    }

注意点 -- 通信时的调用函数

接下来要开始通信了,原先我们的服务器是直接read,但这里不行

  • 因为read是阻塞式等待,而我们要实现非阻塞式
  • 而且一旦阻塞在这里,就无法获取新连接以及与其他客户端通信了(因为我们写的是单进程)
  • 所以,还是需要使用select

辅助数组

当然,我们不能调用新的select

  • 为什么?
  • 一般都是在主循环处持续调用select,高效且简洁
  • 如果使用多个select,会导致代码逻辑复杂化,也难以管理

所以,需要我们把这个新套接字的fd设置进刚才的select的位图

  • 这一过程就相当于d在不断增加自己鱼竿的数量

但是,这两个数据在不同的函数中(我们在处理函数中获取新连接,而select的使用在主逻辑函数中),如何传递呢?

  • 因为这两个函数都在类中,所以我们搞一个类内变量 -- 辅助数组
  •     int fds_[def_max_num]; // 辅助数组
  • 让新增的fd都添加进辅助数组中,然后让select每次动态设置max_fd,以及三个位图(新增操作在"处理函数"中介绍)

可以固定监听套接字(也就是我们创建的第一个套接字)作为数组的第一项

  • 方便我们后续区分[获取新连接] 和 [读写事件]

因为在过程中,可能会陆陆续续关掉一些文件

  • 所以原本添加进的连续fd,会变成零零星星的
  • 所以,需要我们每次都重新整理一下这个数组,把有效的fd统一放在左侧

我们每次在循环开头就处理数组中的值

  • 合法的fd就让它设置进位图中
  • 不仅如此,在这个过程中,我们还可以找到fd中的最大值,来填充select参数
void start()
    {
        listen_socket_.Socket();
        listen_socket_.Bind(def_port);
        listen_socket_.Listen();

        // 初始化辅助数组
        for (int i = 0; i < def_max_num; ++i)
        {
            fds_[i] = -1;
        }

        int listen_fd = listen_socket_.get_fd();
        fds_[0] = listen_fd; // 固定第一项为监听套接字fd

        while (true)
        {
            int max_fd = -1;
            fd_set readfds;
            FD_ZERO(&readfds);
            for (int i = 0; i < def_max_num; ++i)
            {
                if (fds_[i] != -1)
                {
                    FD_SET(fds_[i], &readfds);
                }
                if (fds_[i] > max_fd)
                {
                    max_fd = fds_[i];
                }
            }
            struct timeval timeout = {1, 0}; // 1秒0微秒

            int ret = select(max_fd + 1, &readfds, nullptr, nullptr, &timeout);
            if (ret > 0) // 有事件就绪
            {
                handle(readfds);
            }
            else if (ret == 0) // 超时
            {
                continue;
            }
            else
            {
                perror("select");
                break;
            }
        }
        listen_socket_.Close();
    }

知道了如何解决添加新fd的问题后,接下来回到处理函数,开始具体处理:

处理函数

当我们识别到有事件就绪,获取连接后获得新套接字fd,之后就该将该fd设置进辅助数组中

  • 需要我们遍历数组,找到空位(值为-1/其他你设定的[数组内的初始值]),然后添加进去
  • 但是要注意位图还有没有空位置(别忘了位图是有上限的)
  • 所以,还需要加个判断
    void handle(fd_set &readfds)
    {
        if (FD_ISSET(listen_socket_.get_fd(), &readfds)) // 新连接就绪
        {
            std::string clientip;
            uint16_t clientport;
            int sock = listen_socket_.Accept(clientip, clientport);
            if (sock == -1)
            {
                return;
            }
            else // 把新fd加入位图
            {
                int i = 1;
                for (; i < def_max_num; ++i)
                {
                    if (fds_[i] == def_data) // 找到空位,但不能直接添加
                    {
                        break;
                    }
                }
                if (i != def_max_num)
                {
                    fds_[i] = sock;
                }
                else  // 满了
                { 
                    lg(WARNING, "server is full,close %d now", sock);
                    close(sock);
                }
            }
        }
    }

因为select一次可以等待多个fd,随着客户端的增加,新连接也在增加

如何区分fd集上的事件就绪究竟是[新连接]还是[读写事件]呢?

  • 前面我们提到,将监听套接字固定在数组第一项,就是为了区分两者,所以写个判断语句就行

一次有多个文件上的事件就绪时,如何批量处理呢?

  • 我们只能一个一个判断,也就是在[需要关注的fd集]=辅助数组中遍历fd,然后判断是否在readfds中
  • 如果在,那我们需要对该文件做处理
  • 而处理也需要分类 -- 获取新连接 和 io事件(因为这里我们只填充了readfds,所以只处理读)
  • 处理连接就是我们前面写的这部分代码,处理读其实就是手动调用read

具体细节看代码吧

#include "Log.hpp"
#include "socket.hpp"
#include <sys/select.h>

const int def_port = 8080;
const int def_max_num = (sizeof(fd_set) * 8); // fd_set最多能容纳的fd数量
const int def_data = -1;

class select_server
{
public:
    select_server() {}
    ~select_server() {}
    void start()
    {
        listen_socket_.Socket();
        listen_socket_.Bind(def_port);
        listen_socket_.Listen();

        // 初始化辅助数组
        for (int i = 0; i < def_max_num; ++i)
        {
            fds_[i] = -1;
        }

        int listen_fd = listen_socket_.get_fd();
        fds_[0] = listen_fd; // 固定第一项为监听套接字fd

        while (true)
        {
            int max_fd = -1;
            fd_set readfds;
            FD_ZERO(&readfds);
            for (int i = 0; i < def_max_num; ++i)
            {
                if (fds_[i] != -1)
                {
                    FD_SET(fds_[i], &readfds);
                }
                if (fds_[i] > max_fd)
                {
                    max_fd = fds_[i];
                }
            }
            // FD_SET(listen_fd, &readfds);
            struct timeval timeout = {1, 0}; // 1秒0微秒

            // int ret = select(listen_fd+1, &readfds, nullptr, nullptr, &timeout);
            int ret = select(max_fd + 1, &readfds, nullptr, nullptr, &timeout);
            if (ret > 0) // 有事件就绪
            {
                handle(&readfds);
            }
            else if (ret == 0) // 超时
            {
                continue;
            }
            else
            {
                perror("select");
                break;
            }
        }
        listen_socket_.Close();
    }

private:
    void handle(fd_set *p_readfds)
    {
        for (int i = 0; i < def_max_num; ++i) // 遍历数组
        {
            int fd = fds_[i];
            if (fd != def_data) // 有效fd
            {
                if (FD_ISSET(fd, p_readfds)) // 有事件就绪
                {
                    if (fd == listen_socket_.get_fd()) // 获取新连接
                    {
                        std::string clientip;
                        uint16_t clientport;
                        int sock = listen_socket_.Accept(clientip, clientport);
                        if (sock == -1)
                        {
                            continue;
                        }
                        else // 把新fd加入位图
                        {
                            int pos = 1;
                            for (; pos < def_max_num; ++pos)
                            {
                                if (fds_[pos] == def_data) // 找到空位,但不能直接添加
                                {
                                    break;
                                }
                            }
                            if (pos != def_max_num)
                            {
                                fds_[pos] = sock;
                            }
                            else // 满了
                            {
                                lg(WARNING, "server is full,close %d now", sock);
                                close(sock);
                            }
                        }
                    }
                    else // 读事件
                    {
                        char in_buff[1024];
                        int n = read(fd, in_buff, sizeof(in_buff) - 1);
                        if (n > 0)
                        {
                            in_buff[n - 1] = 0;
                            std::cout << "get message: " << in_buff << std::endl;
                        }
                        else if (n == 0) // 客户端关闭连接
                        {
                            close(fd);
                            fds_[i] = -1; // 重置数组内的值
                        }
                        else
                        {
                            lg(ERROR, "fd: %d ,read error");
                        }
                    }
                }
            }
        }
    }

private:
    MY_SOCKET listen_socket_;
    int fds_[def_max_num]; // 辅助数组
};

我们也可以把处理过程单拎出来封装成两个函数

  • 就相当于我们把收到的事件根据类型不同,派发给不同的模块进行处理
  •    void receiver(int fd, int i)
        {
            char in_buff[1024];
            int n = read(fd, in_buff, sizeof(in_buff) - 1);
            if (n > 0)
            {
                in_buff[n - 1] = 0;
                std::cout << "get message: " << in_buff << std::endl;
            }
            else if (n == 0) // 客户端关闭连接
            {
                close(fd);
                fds_[i] = -1; // 重置数组内的值
            }
            else
            {
                lg(ERROR, "fd: %d ,read error");
            }
        }
        void accepter()
        {
            std::string clientip;
            uint16_t clientport;
            int sock = listen_socket_.Accept(clientip, clientport);
            if (sock == -1)
            {
                return;
            }
            else // 把新fd加入位图
            {
                int pos = 1;
                for (; pos < def_max_num; ++pos)
                {
                    if (fds_[pos] == def_data) // 找到空位,但不能直接添加
                    {
                        break;
                    }
                }
                if (pos != def_max_num)
                {
                    fds_[pos] = sock;
                }
                else // 满了
                {
                    lg(WARNING, "server is full,close %d now", sock);
                    close(sock);
                }
            }
        }
        void handle(fd_set *p_readfds)
        {
            for (int i = 0; i < def_max_num; ++i) // 遍历数组
            {
                int fd = fds_[i];
                if (fd != def_data) // 有效fd
                {
                    if (FD_ISSET(fd, p_readfds)) // 有事件就绪
                    {
                        if (fd == listen_socket_.get_fd()) // 获取新连接
                        {
                            accepter();
                        }
                        else // 读事件
                        {
                            receiver(fd, i);
                        }
                    }
                }
            }
        }
示例

我们这里写的只是个demo版本,因为read那里会有粘包问题->自定义协议->序列化和反序列化,之前在编写网络计算器那里有解决 -- 

tcp协议的面向字节流介绍,粘包问题(解决的本质)-CSDN博客

网络通信中字节流存在的问题,tcp协议特点,自定义协议(引入+介绍,序列化反序列化介绍,实现思路)_自定义tcp通信协议-CSDN博客网络计算器代码编写+注意点(序列化,反序列化,报头封装和解包,服务端和客户端,计算),客户端和服务端数据传递流程图,守护进程化+日志重定向到文件_计算器封装-CSDN博客

完整代码

server.hpp

#include "Log.hpp"
#include "socket.hpp"
#include <sys/select.h>

const int def_port = 8080;
const int def_max_num = (sizeof(fd_set) * 8); // fd_set最多能容纳的fd数量
const int def_data = -1;

class select_server
{
public:
    select_server() {}
    ~select_server() {}
    void start()
    {
        listen_socket_.Socket();
        listen_socket_.Bind(def_port);
        listen_socket_.Listen();

        // 初始化辅助数组
        for (int i = 0; i < def_max_num; ++i)
        {
            fds_[i] = -1;
        }

        int listen_fd = listen_socket_.get_fd();
        fds_[0] = listen_fd; // 固定第一项为监听套接字fd

        while (true)
        {
            int max_fd = -1;
            fd_set readfds;
            FD_ZERO(&readfds);
            for (int i = 0; i < def_max_num; ++i)
            {
                if (fds_[i] != -1)
                {
                    FD_SET(fds_[i], &readfds);
                }
                if (fds_[i] > max_fd)
                {
                    max_fd = fds_[i];
                }
            }
            // FD_SET(listen_fd, &readfds);
            struct timeval timeout = {1, 0}; // 1秒0微秒

            // int ret = select(listen_fd+1, &readfds, nullptr, nullptr, &timeout);
            int ret = select(max_fd + 1, &readfds, nullptr, nullptr, &timeout);
            if (ret > 0) // 有事件就绪
            {
                handle(&readfds);
            }
            else if (ret == 0) // 超时
            {
                continue;
            }
            else
            {
                perror("select");
                break;
            }
        }
        listen_socket_.Close();
    }

private:
    void receiver(int fd, int i)
    {
        char in_buff[1024];
        int n = read(fd, in_buff, sizeof(in_buff) - 1);
        if (n > 0)
        {
            in_buff[n - 1] = 0;
            std::cout << "get message: " << in_buff << std::endl;
        }
        else if (n == 0) // 客户端关闭连接
        {
            close(fd);
            lg(DEBUG, "%d quit", fd);
            fds_[i] = -1; // 重置数组内的值
        }
        else
        {
            lg(ERROR, "fd: %d ,read error");
        }
    }
    void accepter()
    {
        std::string clientip;
        uint16_t clientport;
        int sock = listen_socket_.Accept(clientip, clientport);
        if (sock == -1)
        {
            return;
        }
        else // 把新fd加入位图
        {
            int pos = 1;
            for (; pos < def_max_num; ++pos)
            {
                if (fds_[pos] == def_data) // 找到空位,但不能直接添加
                {
                    break;
                }
            }
            if (pos != def_max_num)
            {
                fds_[pos] = sock;
            }
            else // 满了
            {
                lg(WARNING, "server is full,close %d now", sock);
                close(sock);
            }
        }
    }
    void handle(fd_set *p_readfds)
    {
        for (int i = 0; i < def_max_num; ++i) // 遍历数组
        {
            int fd = fds_[i];
            if (fd != def_data) // 有效fd
            {
                if (FD_ISSET(fd, p_readfds)) // 有事件就绪
                {
                    if (fd == listen_socket_.get_fd()) // 获取新连接
                    {
                        accepter();
                    }
                    else // 读事件
                    {
                        receiver(fd, i);
                    }
                }
            }
        }
    }

private:
    MY_SOCKET listen_socket_;
    int fds_[def_max_num]; // 辅助数组
};

总结

总的来说,select最重要的就是思维方式 -- 我们要将所有等待的过程都交给select

并且优缺点很明显

优点

  • 确实实现了多路转接,可以等待多个fd
  • 代码简单明了

缺点

  • 等待的fd数量有上限
  • 输入输出型参数较多,需要频繁进行用户和内核之间的数据拷贝操作 , 以及每次都要重复设置
  • 使用了第三方数组,用户层在读取fd时需要很多次遍历
  • 除了用户层,内核层也要经过多次遍历 -- 内核需要知道自己要关注的fd(遍历位图),关注的过程其实就是在遍历文件描述符表(遍历范围上限=max_fd+1,因为fd本身就是下标,+1就是数量了)

  • 24
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值