多路复用 select

目录

1. select 介绍

1.1 select 函数介绍

2. 使用 select 实现 TCP 回显服务器

2.1 前置代码

2.1.1 互斥锁模块封装

2.1.2 日志类封装

2.1.3 Common 文件

2.1.4 InetAddr 类

2.1.5 Socket 类封装

2.2 select 版 TCP 回显服务器

2.2.1 服务器处理流程

2.2.2 代码实现

3. select 的优缺点


1. select 介绍

        IO 过程分为等待数据拷贝数据两个过程,而 select 只负责等待数据。系统提供 select 函数来实现多路复用(多路转接)输入/输出模型。select 系统调用是用来让我们的程序监视多个文件描述符的状态变化的,程序会在 select 这里等待,直到被监视的文件描述符有一个或者多个发生状态改变。select 是一种通过等待多个文件描述就绪的事件通知机制。从而避免为每个 FD 单独创建线程/进程,提升 I/O 效率。

        可读 == 底层缓冲区有数据,读事件就绪。可写 == 底层缓冲区有空间,写事件就绪。对于一开始的文件描述符,接收缓冲区和发送缓冲区都是空的,则默认为读事件不就绪,写事件就绪。

1.1 select 函数介绍

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

头文件:
    #include <sys/select.h>

参数:
    nfds:需要监控的 “最大文件描述符 + 1”,select 会扫描 0 ~ nfds-1 的所有 fd。
    readfds:输入输出型参数,监控“可读”事件的 fd 集合,输入为待监控的 fd 集合,输出为就绪 fd 的集合。
    writefds:输入输出线参数,监控“可写”事件的 fd 集合。
    exceptfds:输入输出线参数,监控“异常”事件的 fd 集合。
    timeout:超时事件结构体,传入 NULL 时,select 永久阻塞,直到有 fd 就绪;
    传入 (struct timeval) {0, 0}:非阻塞模式,select 无论有无 fd 准备就绪立即返回;
    传入超时时间:在超时时间内阻塞等待 fd 就绪,若没 fd 就绪,使 select 返回 0,
    若有 fd 就绪则返回就绪的 fd。

返回值:
    >0:表示具体有多少个就绪的 fd。
    =0:表示在超时时间内没有 fd 准备就绪。
    <0:select 错误。
功能:
    让一个进程 / 线程同时监控多个文件描述符(File Descriptor,FD),
    等待其中任意一个或多个 FD 变为 “就绪状态”(可读、可写或发生异常),
    从而避免为每个 FD 单独创建线程 / 进程,提升 I/O 效率。

        fd_set 集合select 用于描述 “待监控 FD 集合” 的专用数据结构,本质是一个位图,每一位对应一个 FD(位为 1 表示监控该 FD)。

// timeval 结构体
struct timeval {
    long tv_sec;  // 秒
    long tv_usec; // 微秒(1 秒 = 1e6 微秒)
};

2. 使用 select 实现 TCP 回显服务器

2.1 前置代码

2.1.1 互斥锁模块封装

// Mutex.hpp

#pragma once 
#include <pthread.h>

// 将互斥量接口封装成面向对象的形式
namespace MutexModule
{
    class Mutex
    {
    public:
        Mutex()
        {
            int n = pthread_mutex_init(&_mutex, nullptr);
            (void)n;
        }
        ~Mutex()
        {
            int n = pthread_mutex_destroy(&_mutex);
            (void)n;
        }

        void Lock()
        {
            int n = pthread_mutex_lock(&_mutex);
            (void)n;
        }

        void Unlock()
        {
            int n = pthread_mutex_unlock(&_mutex);
            (void)n;
        }

        pthread_mutex_t* Get()  //  获取原生互斥量的指针
        {
            return &_mutex;
        }
    private:
        pthread_mutex_t _mutex;
    };
    
    // 采用RAII风格进行锁管理,当局部临界区代码运行完的时候,局部LockGuard类型的对象自动进行释放,调用析构函数释放锁
    class LockGuard
    {
    public:
        LockGuard(Mutex &mutex)
        : _mutex(mutex)
        {
            _mutex.Lock();
        }

        ~LockGuard()
        {
            _mutex.Unlock();
        }
    private:
        Mutex& _mutex;
    };
}

2.1.2 日志类封装

// Log.hpp

#ifndef __LOG_HPP__
#define __LOG_HPP__

#include <iostream>
#include <cstdio>
#include <string>
#include <filesystem> //C++17
#include <sstream>
#include <fstream>
#include <ctime>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"

namespace LogModule
{
    using namespace MutexModule;

    const std::string gsep = "\r\n";
    // 策略模式 -- 利用C++的多态特性
    // 1. 刷新策略 a: 向显示器打印 b: 向文件中写入
    // 刷新策略基类
    class LogStrategy
    {
    public:
        virtual ~LogStrategy() = default;
        virtual void SyncLog(const std::string &message) = 0;
    };

    // 显示器打印日志的策略
    class ConsoleLogStrategy : public LogStrategy
    {
    public:
        ConsoleLogStrategy()
        {
        }

        void SyncLog(const std::string &message) override
        {
            // 加锁使多线程原子性的访问显示器
            LockGuard lockGuard(_mutex);
            std::cout << message << gsep;
        }

        ~ConsoleLogStrategy()
        {
        }

    private:
        Mutex _mutex;
    };

    // 文件打印日志策略
    // 默认的日志文件路径和日志文件名
    const std::string defaultPath = "./log";
    const std::string defaultFile = "my.log";
    class FileLogStrategy : public LogStrategy
    {
    public:
        FileLogStrategy(const std::string &path = defaultPath, const std::string &file = defaultFile)
            : _path(path),
              _file(file)
        {
            // 加锁使多线程原子性的访问文件
            LockGuard lockGuard(_mutex);
            // 判断目录是否存在
            if (std::filesystem::exists(_path)) // 检测文件系统对象(文件,目录,符号链接等)是否存在
            {
                return;
            }
            try
            {
                // 如果目录不存在,递归创建目录
                std::filesystem::create_directories(_path);
            }
            catch (const std::filesystem::filesystem_error &e) // 如果创建失败则打印异常信息
            {
                std::cerr << e.what() << '\n';
            }
        }

        void SyncLog(const std::string &message) override
        {
            LockGuard lockGuard(_mutex);
            // 追加方式向文件中写入
            std::string fileName = _path + (_path.back() == '/' ? "" : "/") + _file;
            // std::ofstream是C++标准库中用于输出到文件的流类,主要用于将数据写入文件
            std::ofstream out(fileName, std::ios::app);
            if (!out.is_open())
            {
                return;
            }
            out << message << gsep;
            out.close();
        }

        ~FileLogStrategy()
        {
        }

    private:
        std::string _path; // 日志文件所在路径
        std::string _file; // 日志文件本身
        Mutex _mutex;
    };

    // 2. 形成完整日志并刷新到指定位置
    // 2.1 日志等级
    enum class LogLevel
    {
        DEBUG,
        INFO,
        WARNING,
        ERROR,
        FATAL
    };

    // 2.2 枚举类型的日志等级转换为字符串类型
    std::string Level2Str(LogLevel level)
    {
        switch (level)
        {
        case LogLevel::DEBUG:
            return "DEBUG";
        case LogLevel::INFO:
            return "INFO";
        case LogLevel::WARNING:
            return "WARNING";
        case LogLevel::ERROR:
            return "ERROR";
        case LogLevel::FATAL:
            return "FATAL";
        default:
            return "UNKNOWN";
        }
    }

    // 2.3 获取当前时间的函数
    std::string GetCurTime()
    {
        // time 函数参数为一个time_t类型的指针,若该指针不为NULL,会把获取到的当前时间值存储在指针指向的对象中
        // 若传入为NULL,则仅返回当前时间,返回从1970年1月1日0点到目前的秒数
        time_t cur = time(nullptr);
        struct tm curTm;
        // localtime_r是localtime的可重入版本,主要用于将time_t类型表示的时间转换为本地时间,存储在struct tm 结构体中
        localtime_r(&cur, &curTm);
        char timeBuffer[128];
        snprintf(timeBuffer, sizeof(timeBuffer), "%4d-%02d-%02d %02d:%02d:%02d",
                 curTm.tm_year + 1900,
                 curTm.tm_mon + 1,
                 curTm.tm_mday,
                 curTm.tm_hour,
                 curTm.tm_min,
                 curTm.tm_sec);
        return timeBuffer;
    }

    // 2.4 日志形成并刷新
    class Logger
    {
    public:
        // 默认刷新到显示器上
        Logger()
        {
            EnableConsoleLogStrategy();
        }

        void EnableConsoleLogStrategy()
        {
            // std::make_unique用于创建并返回一个std::unique_ptr对象
            _fflushStrategy = std::make_unique<ConsoleLogStrategy>();
        }

        void EnableFileLogStrategy()
        {
            _fflushStrategy = std::make_unique<FileLogStrategy>();
        }
        //  内部类默认是外部类的友元类,可以访问外部类的私有成员变量
        //  内部类LogMessage,表示一条日志信息的类
        class LogMessage
        {
        public:
            LogMessage(LogLevel &level, std::string &srcName, int lineNum, Logger &logger)
                : _curTime(GetCurTime()),
                  _level(level),
                  _pid(getpid()),
                  _srcName(srcName),
                  _lineNum(lineNum),
                  _logger(logger)
            {
                // 日志的基本信息合并起来
                // std::stringstream用于在内存中进行字符串的输入输出操作, 提供一种方便的方式处理字符串
                // 将不同类型的数据转换为字符串,也可以将字符串解析为不同类型的数据
                std::stringstream ss;
                ss << "[" << _curTime << "] "
                   << "[" << Level2Str(_level) << "] "
                   << "[" << _pid << "] "
                   << "[" << _srcName << "] "
                   << "[" << _lineNum << "] "
                   << "- ";

                _logInfo = ss.str();
            }

            //  使用模板重载运算符<< -- 支持不同数据类型的输出运算符重载
            template <typename T>
            LogMessage &operator<<(const T &info)
            {
                std::stringstream ss;
                ss << info;
                _logInfo += ss.str();
                return *this;
            }

            ~LogMessage()
            {
                if (_logger._fflushStrategy)
                {
                    _logger._fflushStrategy->SyncLog(_logInfo);
                }
            }

        private:
            std::string _curTime;   // 日志时间
            LogLevel _level;    // 日志等级
            pid_t _pid; // 进程pid
            std::string _srcName;   // 输出日志的文件名
            int _lineNum;   //输出日志的行号
            std::string _logInfo;   //完整日志内容
            Logger &_logger;    // 方便使用策略进行刷新
        };

        // 使用宏进行替换之后调用的形式如下
        // logger(level, __FILE__, __LINE__) << "hello world" << 3.14;
        // 这里使用仿函数的形式,调用LogMessage的构造函数,构造一个匿名的LogMessage对象
        // 返回的LogMessage对象是一个临时对象,它的生命周期从创建开始到包含它的完整表达式结束(可以简单理解为包含
        // 这个对象的该行代码)
        // 代码调用结束的时候,如果没有LogMessage对象进行临时对象的接收,则会调用析构函数,
        // 如果有LogMessage对象进行临时对象的接收,会调用拷贝构造或者移动构造构造一个对象,并析构临时对象
        // 所以通过临时变量调用析构函数进行日志的打印
        LogMessage operator()(LogLevel level, std::string name, int line)
        {
            return LogMessage(level, name, line, *this);
        }

        ~Logger()
        {
        }

    private:
        std::unique_ptr<LogStrategy> _fflushStrategy;
    };

    //  定义一个全局的Logger对象
    Logger logger;

    // 使用宏定义,简化用户操作并且获取文件名和行号
    #define LOG(level) logger(level, __FILE__, __LINE__) // 使用仿函数的方式进行调用
    #define Enable_Console_Log_Strategy() logger.EnableConsoleLogStrategy()
    #define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}

#endif

2.1.3 Common 文件

        提供各种错误的错误码以及一个没有拷贝构造和赋值重载的基类

// Common.hpp

#pragma once

#include <iostream>
#include <memory>
#include <functional>
#include <unistd.h>
#include <string>
#include <cstring>
#include <signal.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "Log.hpp"

using namespace LogModule;

// 强转 struct sockaddr_in * 为 struct sockaddr * 的宏
#define CONV(addr) ((struct sockaddr*)&addr)

// 将各种错误的错误码用一个枚举类型表示
enum EixtCode
{
    OK,
    USAGE_ERR,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    FORK_ERR
};

// 没有拷贝构造和赋值重载的基类
class NoCopy
{
public:
    NoCopy(){}
    ~NoCopy(){}
    NoCopy(const NoCopy &) = delete;
    const NoCopy &operator=(const NoCopy&) = delete;
};

2.1.4 InetAddr 类

        用于描述一个套接字的网络地址和主机地址,并提供主机地址和网络地址相互转换方法。

// InetAddr.hpp

#pragma once

#include "Common.hpp"

class InetAddr
{
public:
    InetAddr() {};
    // 使用套接字创建对象的构造函数
    InetAddr(struct sockaddr_in &addr)
    {
        SetAddr(addr);
    }

    // 使用主机序列创建的构造函数
    InetAddr(std::string &ip, uint16_t port) : _ip(ip), _port(port)
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);
    }

    // 仅使用端口号创建,ip 设为 INADDR_ANY
    InetAddr(uint16_t port) : _port(port), _ip()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        _addr.sin_addr.s_addr = INADDR_ANY;
    }

    void SetAddr(struct sockaddr_in &addr)
    {
        _addr = addr;
        _port = ntohs(_addr.sin_port);
        char ipbuffer[64];
        inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));
        _ip = ipbuffer;
    }
    uint16_t Port() { return _port; }
    std::string Ip() { return _ip; }
    const struct sockaddr_in &NetAddr() { return _addr; }
    const struct sockaddr *NetAddrPtr() { return CONV(_addr); }
    socklen_t NetAddrLen() { return sizeof(_addr); }
    bool operator==(const InetAddr &addr) { return addr._ip == _ip && addr._port == _port; }
    std::string StringAddr() { return _ip + ":" + std::to_string(_port); }
    ~InetAddr() {}

private:
    struct sockaddr_in _addr;
    std::string _ip;
    uint16_t _port;
};

2.1.5 Socket 类封装

// Socket.hpp

#pragma once

#include "Common.hpp"
#include "InetAddr.hpp"

namespace SocketModule
{
    const static int gbacklog = 16;

    // 模板方法模式
    // 套接字基类
    class Socket
    {
    public:
        virtual ~Socket() {}
        virtual void SocketOrDie() {}
        virtual void BindOrDie(uint16_t port) = 0;
        virtual void ListenOrDie(int backlog) = 0;
        // virtual std::shared_ptr<Socket> Accept(InetAddr *client) = 0;
        virtual int Accept(InetAddr *client) = 0;
        virtual void Close() = 0;
        virtual int Recv(std::string* out) = 0;
        virtual int Send(const std::string &message) = 0;
        virtual int Connect(std::string &server_ip, uint16_t server_port) = 0;
        virtual int Fd() = 0;

        // 使用该函数进行tcp套接字的创建和初始化
        void BuildTcpSocketMethod(uint16_t port, int backlog = gbacklog)
        {
            SocketOrDie();
            BindOrDie(port);
            ListenOrDie(backlog);
        }

        void BuildTcpClientSocketMethod()
        {
            SocketOrDie();
        }
    };

    const static int defaultfd = -1;

    // tcp 子类套接字
    class TcpSocket : public Socket
    {
    public:
        TcpSocket(int fd = defaultfd) : _sockfd(fd)
        {
        }

        ~TcpSocket() {}

        void SocketOrDie() override
        {
            _sockfd = ::socket(AF_INET, SOCK_STREAM, 0);
            if (_sockfd < 0)
            {
                LOG(LogLevel::FATAL) << "socket error";
                exit(SOCKET_ERR);
            }
            LOG(LogLevel::INFO) << "socket success";
        }

        void BindOrDie(uint16_t port) override
        {
            InetAddr localAddr(port);
            int n = ::bind(_sockfd, localAddr.NetAddrPtr(), localAddr.NetAddrLen());
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "bind error";
                exit(BIND_ERR);
            }
            LOG(LogLevel::INFO) << "bind success";
        }

        void ListenOrDie(int backlog) override
        {
            int n = ::listen(_sockfd, backlog);
            if (n < 0)
            {
                LOG(LogLevel::FATAL) << "listen error";
                exit(LISTEN_ERR);
            }
            LOG(LogLevel::INFO) << "listen success";
        }

        // std::shared_ptr<Socket> Accept(InetAddr *client) override   //返回一个服务套接字,并带出客户端地址
        int Accept(InetAddr *client) override 
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);

            int fd = ::accept(_sockfd, CONV(peer), &len);
            if (fd < 0)
            {
                LOG(LogLevel::WARNING) << "accept warning...";
                return -1;
            }
            client->SetAddr(peer);
            LOG(LogLevel::INFO) << "accept success, " << client->StringAddr() << " sockfd: " << fd;

            return fd;
        }

        void Close() override
        {
            if (_sockfd >= 0)
                ::close(_sockfd);
        }

        int Recv(std::string* out) override
        {
            char buffer[1024];
            ssize_t n = ::recv(_sockfd, buffer, sizeof(buffer) - 1, 0);
            if (n > 0)
            {
                buffer[n] = 0;
                *out += buffer;
            }

            return n;
        }

        int Send(const std::string &message) override
        {
            return send(_sockfd, message.c_str(), message.size(), 0);
        }

        int Connect(std::string &server_ip, uint16_t server_port) override
        {
            InetAddr server(server_ip, server_port);
            return ::connect(_sockfd, server.NetAddrPtr(), server.NetAddrLen());
        }

        int Fd() {
            return _sockfd;
        }
    private:
        int _sockfd;
    };
}

2.2 select 版 TCP 回显服务器

2.2.1 服务器处理流程

2.2.2 代码实现

// SelectServer.hpp -- 服务器类

#pragma once

#include <iostream>
#include <memory>
#include <unistd.h>
#include "Socket.hpp"

using namespace SocketModule;
using namespace LogModule;

class SelectServer
{
    const static int size = sizeof(fd_set) * 8;
    const static int defaultfd = -1;

public:
    SelectServer(int port) : _listensock(std::make_unique<TcpSocket>()), _isrunning(false)
    {
        // 1. 创建套接字并绑定端口号并开始进行监听
        _listensock->BuildTcpSocketMethod(port);

        // 2. 将 fd 辅助数组全部默认设置为 -1
        for (int i = 0; i < size; ++i)
            _fd_array[i] = defaultfd;

        // 3. 将 fd 辅助数组中的首元素设置为监听套接字的 fd
        _fd_array[0] = _listensock->Fd();
    }

    void Start()
    {
        _isrunning = true;

        while (_isrunning)
        {
            fd_set rfds;    // 定义需要监听读事件的 fd 集合
            FD_ZERO(&rfds); // 清空 fds
            int maxfd = defaultfd;
            for (int i = 0; i < size; ++i)
            {
                // 循环找到每个有效的 fd
                if (_fd_array[i] == defaultfd)
                    continue;
                // 1. 每次 select 前对 rfds 进行重置(因为 select 执行之后会改变 rfds)
                FD_SET(_fd_array[i], &rfds);
                // 2. 每次需要关心的 fd 集合都有变换,更新最大的 fd
                if (maxfd < _fd_array[i])
                    maxfd = _fd_array[i];
            }
            PrintFd();

            int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
            switch (n)
            {
            case -1:
                // select 错误
                LOG(LogLevel::ERROR) << "select error";
                break;
            case 0:
                // 2. 阻塞等待超时
                LOG(LogLevel::INFO) << "time out...";
                break;
            default:
                // 3. 读事件就绪(listen 套接字也是读事件就绪)
                LOG(LogLevel::DEBUG) << "读事件就绪..., n: " << n;
                Dispatcher(rfds);   // 进行事件派发
                break;
            }
        }

        _isrunning = false;
    }

    void Stop() {
        _isrunning = false;
    }

    ~SelectServer() {}

    // 事件派发器
    void Dispatcher(fd_set& rfds) {
        for (int i = 0; i < size; ++i) {
            // 1. 排除不合法的 fd
            if (_fd_array[i] == defaultfd)
                continue;
            if (FD_ISSET(_fd_array[i], &rfds)) {
                if (_fd_array[i] == _listensock->Fd())
                    Accepter();
                else
                    Recver(i);
            }
        }
    }

    // 连接管理器
    void Accepter() {
        InetAddr client;
        int sockfd = _listensock->Accept(&client);  // 此时的 accept 不会被阻塞
        if (sockfd >= 0) {
            LOG(LogLevel::INFO) << "get a new link, sockfd: " << sockfd << ", client is: " << client.StringAddr();
            // 将新连接托管给 select
            int pos = 0;
            for (; pos < size; ++pos)
                if (_fd_array[pos] == defaultfd)
                    break;

            if (pos == size) {
                LOG(LogLevel::WARNING) << "select server full";
                close(sockfd);
            } else
                _fd_array[pos] = sockfd;
        }
    }

    // IO处理器
    void Recver(int pos) {
        char buffer[1024];
        ssize_t n = recv(_fd_array[pos], buffer, sizeof(buffer) - 1, 0);    // 这里读的时候会有 bug,不能保证一次读取全部的数据
        if (n > 0) {
            // 1. 读取到客户端传入的数据
            buffer[n] = 0;
            std::cout << "sockfd: " << _fd_array[pos] << ", client say@ " << buffer << std::endl;
        } else if (n == 0) {
            // 2. 客户端退出 -- 将该客户端的 fd 移除辅助数组并关闭连接
            LOG(LogLevel::INFO) << "sockfd: " << _fd_array[pos] << ", client quit...";
            _fd_array[pos] = defaultfd;
            close(_fd_array[pos]);
        } else {
            // 3. 读取出错 -- select 不再关心该 fd,并关闭连接
            LOG(LogLevel::ERROR) << "recv error";
            _fd_array[pos] = defaultfd;
            close(_fd_array[pos]);
        }
    }

    void PrintFd()
    {
        std::cout << "_fd_array[]: ";
        for (int i = 0; i < size; ++i)
            if (_fd_array[i] != defaultfd)
                std::cout << _fd_array[i] << " ";
        std::cout << std::endl;
    }

private:
    std::unique_ptr<Socket> _listensock;
    bool _isrunning;
    int _fd_array[size]; // 栈上定义数组时(包括函数内,类成员函数内以及类中),数组大小必须是编译期确定的常量,只使用 const 修饰的变量是在运行时定义的
};
// Main.cc -- 主函数

#include "SelectServer.hpp"

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cout << "Usage: " << argv[0] << " port" << std::endl;
        exit(USAGE_ERR);
    }

    Enable_Console_Log_Strategy();
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<SelectServer> svr = std::make_unique<SelectServer>(port);
    svr->Start();
    return 0;
}

3. select 的优缺点

        优点:

        1. 跨平台兼容性极强select 是 POSIX 标准定义的 I/O 多路复用接口,支持所有主流操作系统。

        2. 支持多类型 FD,通用性强select 不局限于网络 I/O,可同时监控多种类型的文件描述符。如:网络socket,本地文件,管道等。

        3. API 简单易懂,入门成本低。

        缺点:

        1. FD 数量存在硬限制:单个 select 调用最多只能监控 1024 个 FD,若需要监控更多 FD,则需要重新编译内核。

        2. 线性扫描效率低,FD 越多越慢select 返回后,无法直接获取 “就绪的 FD 列表”,只能通过 遍历 0~max_fd 所有 FD(用 FD_ISSET 检查)来筛选就绪事件。

        3. fd_set 集合需重复初始化,代码冗余select 会破坏传入的 fd_set 集合:返回时,集合中仅保留 “就绪的 FD”,未就绪的 FD 会被清空。因此需要一个辅助数组来存储需要监控的 FD 集合,在每次 select 前进行初始化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值