多路转接select、poll

相关函数

fcntl

在Linux网络编程中,fcntl 函数是一个功能强大的系统调用,它允许程序在运行时对已打开的文件描述符(包括网络套接字)进行多种控制操作。它的核心价值在于能够动态地修改描述符的行为和属性,而无需关闭并重新打开它

复制代码
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );
  • fd: 要操作的文件描述符,可以是打开的文件、管道或网络套接字。
  • cmd: 要执行的命令,决定了函数的具体行为。
  • ...: 可变参数,是否使用以及它的类型取决于 cmd

在网络编程中,主要利用以下几个 cmd

  • 设置非阻塞 I/O :通过 F_SETFL 命令设置 O_NONBLOCK 标志。这使得对套接字的读写操作(如 read, write, accept, connect)不会阻塞进程。如果操作不能立即完成,函数会返回 -1,并设置 errnoEAGAINEWOULDBLOCK。这通常与 selectpollepoll 等I/O多路复用机制结合使用,以实现单线程或单进程处理大量并发连接。
  • 管理异步 I/O 所有权 :通过 F_SETOWN 命令设置套接字的所有者(进程ID或进程组ID)。当套接字上发生I/O事件(如数据到达)时,内核会向所有者发送 SIGIO 信号。这是一种较老的非阻塞处理方式,但在现代高性能网络中已较少使用,取而代之的是 epoll
  • 复制文件描述符F_DUPFD 命令可以复制一个文件描述符,其功能类似于 dupdup2 系统调用。这在需要重定向标准输入/输出或进行进程间通信时可能会用到

select

select 是 Linux/Unix 系统中用于 I/O 多路复用的系统调用之一。它允许程序监视多个文件描述符(包括 socket、管道、普通文件等),等待其中一个或多个文件描述符变为"就绪"状态(即可读、可写或发生异常),从而避免为每个描述符阻塞在一个单独的操作上。它在网络编程中尤其常用,用于实现单线程同时处理多个客户端连接。

复制代码
#include <sys/select.h>
#include <sys/time.h>

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

参数详解:

nfds

  • 含义:监视的文件描述符中最大描述符值 + 1
  • 原因:select 内部使用位图来标记描述符,需要知道遍历的范围。例如,如果监视的描述符集合中包含 3、5、7,那么 nfds 应该设为 8(最大值 7 + 1)。
  • 注意:这个参数可以简单设置为 FD_SETSIZE(通常为 1024),但为了效率,应尽量精确。

readfds

  • 类型:fd_set *
  • 作用:指向可读文件描述符集合的指针。我们通过 FD_SET 宏将需要监视可读事件的描述符加入此集合。
  • select 返回时,该集合会被修改,只留下那些真正可读的描述符。如果不关心可读事件,可设为 NULL

writefds

  • 类型:fd_set *
  • 作用:指向可写文件描述符集合的指针。用法与 readfds 类似。

exceptfds

  • 类型:fd_set *
  • 作用:指向异常条件文件描述符集合的指针。异常条件通常指带外数据(out-of-band data)或某些错误状态。TCP 的紧急数据(带外数据)会使描述符处于异常就绪状态。

timeout

  • 类型:struct timeval *

  • 作用:设置 select 等待的时间上限。

    struct timeval {
    long tv_sec; // 秒
    long tv_usec; // 微秒
    };

  • 三种情况:

    • timeout == NULL:阻塞等待,直到至少一个描述符就绪。
    • timeout->tv_sec == 0 && timeout->tv_usec == 0:非阻塞轮询,立即返回。
    • 其他:等待指定的时间,超时后返回 0(无论是否有描述符就绪)。

返回值

  • 成功:返回就绪的文件描述符总数(即三个集合中置位的总数)。如果超时则返回 0。
  • 失败 :返回 -1,并设置 errno 以指示错误。常见的错误有 EBADF(无效描述符)、EINTR(被信号中断)等。

操作 fd_set 的宏

fd_set 是一个固定大小的位图,通常由系统定义其最大容量 FD_SETSIZE(通常为 1024)。使用以下宏来操作集合:

  • FD_ZERO(fd_set *set):清空集合。
  • FD_SET(int fd, fd_set *set):将描述符 fd 加入集合。
  • FD_CLR(int fd, fd_set *set):将描述符 fd 从集合中移除。
  • FD_ISSET(int fd, fd_set *set):测试 fd 是否在集合中(即 select 返回后,检查 fd 是否就绪)。

select 的优缺点

优点:

  • 跨平台 :几乎所有操作系统都支持 select,包括 Windows(Winsock 也有类似实现)。
  • 简单易用:API 直观,适合小型应用或原型开发。
  • 超时控制:可以精确设置等待时间。

缺点:

  • 文件描述符数量限制fd_set 的大小由 FD_SETSIZE 决定,通常为 1024。虽然可以修改内核宏重新编译,但缺乏灵活性。
  • 效率问题
    • 每次调用 select 都需要将整个 fd_set 从用户态拷贝到内核态,开销随描述符数量增加而增加。
    • 返回后需要遍历所有描述符(从 0 到 max_fd)来检查哪些就绪,时间复杂度 O(n)。
  • 描述符集合被修改:每次调用后都需要重新设置集合,增加了编程复杂性。
  • 不支持边缘触发select 只支持水平触发(只要文件描述符可读/写,每次调用都会返回),而 epoll 支持边缘触发,可提高性能。

注意事项

  • 每次调用前重新设置集合 :因为 select 会修改传入的集合,只保留就绪的描述符。所以通常需要维护一个"主集合",每次循环复制一份。
  • 避免描述符值超出范围 :确保 nfds 是最大描述符 +1,且所有描述符都小于 FD_SETSIZE
  • 超时参数在返回后可能被修改 :某些系统(如 Linux)可能会修改 timeval 结构,剩余的时间会被写回。如果不希望被修改,可以每次重新设置。
  • 处理信号中断select 被信号中断时返回 -1,errnoEINTR。通常在循环中重新调用或忽略。
  • 非阻塞 I/O 的配合 :与 select 配合使用的 socket 通常应设置为非阻塞模式,以防止某个 socket 就绪后再次阻塞(如 read 可能只读取部分数据)。
  • 对监听 socket 的处理 :当监听 socket 可读时,应尽快 accept,以免积压连接。

替代方案

  • poll :使用数组传递描述符和事件,没有最大描述符限制(但仍需遍历),API 比 select 稍简单。
  • epoll(Linux 特有):使用事件驱动机制,支持大量描述符且效率高(O(1) 复杂度),是现代 Linux 高并发网络编程的首选。
  • kqueue (BSD 系统)、IOCP(Windows)等。

总结

select 是 I/O 多路复用的基础工具,虽然在高并发场景下有性能瓶颈和限制,但理解它对于学习更高级的多路复用机制非常有帮助。在编写小型网络应用或需要跨平台时,select 仍然是一个可行的选择。使用时注意其特点,遵循正确的编程模式,就能实现高效的并发处理。

poll

poll 是 Linux 系统中的一个系统调用,主要用于 I/O 多路复用。它允许一个程序同时监视多个文件描述符(如套接字、管道、文件等),阻塞等待,直到其中一个或多个文件描述符准备好执行 I/O 操作(例如,有数据可读、可以写入数据或发生错误)。这使得程序可以高效地处理多个并发连接,而无需为每个连接创建单独的进程或线程。

复制代码
#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

参数说明

  • fds:这是一个指向 struct pollfd 结构体数组的指针。数组中的每个元素都描述了一个需要监视的文件描述符以及我们关心的事件。
  • nfdsfds 数组中元素的数量。nfds_t 实际上是一个无符号整型。
  • timeout:指定 poll 在返回前等待多长时间,单位是毫秒
    • -1:永远阻塞,直到某个被监视的文件描述符上有事件发生。
    • 0:立即返回,即使没有任何事件发生(非阻塞轮询)。
    • > 0:等待指定的毫秒数。如果超时,则返回 0。

核心数据结构: struct pollfd

这是 poll 的核心,它将"需要监视的文件描述符"、"用户关心的事件"和"内核返回的事件"清晰地分离开来。

复制代码
struct pollfd {
    int   fd;         // 需要监视的文件描述符。如果设置为负数,则此描述符被忽略。
    short events;     // 用户关心的事件掩码(告诉内核,我对这个fd的哪些事件感兴趣)。
    short revents;    // 内核返回的事件掩码(内核告诉我,这个fd实际发生了哪些事件)。
};
  • events 由用户在调用 poll 之前设置。
  • revents 由内核在 poll 返回时设置。如果某个文件描述符上没有任何事件发生,其 revents 会被内核清空为 0

eventsrevents 是位掩码,可以通过按位或 (|) 组合使用。以下是常用的事件标志

优点

  1. 无文件描述符数量限制 :不像 selectFD_SETSIZE 限制,poll 可以处理任意数量的文件描述符,仅受系统内存限制。
  2. 接口更友好 :使用 pollfd 数组分离了输入(events)和输出(revents),不需要像 select 那样每次调用前都重新构建整个监视集合。
  3. 效率相对 select 略高 :在处理中等数量的连接时,比 select 更方便,且避免了 select 中位图操作的一些不便。

缺点

  1. 仍然存在"线性遍历"性能问题
    • 用户态到内核态的数据拷贝 :每次调用 poll,内核都需要将整个 pollfd 数组从用户态拷贝到内核态。当监视的描述符数量巨大(例如上万)时,这个拷贝开销会非常大。
    • 内核态的遍历 :内核需要线性遍历整个传入的 pollfd 数组,来检查每个文件描述符的状态。即使只有少数几个描述符活跃,这个"扫描"所有描述符的操作也是必不可少的,导致时间复杂度为 O(n)。
  1. 性能随连接数增加而下降 :由于上述遍历机制,当管理的并发连接数非常多时,poll 的性能会急剧下降。这使得它不适合需要处理成千上万连接的高并发 场景(此时 epoll 是更好的选择)

总结

总的来说,pollselect 的一个很好的改进版本,它解决了 select 的文件描述符数量限制和接口不便的问题。对于中等规模的并发连接(例如几百到一两千),poll 是一个简单、高效且可移植的选择。然而,在现代 Linux 高性能网络编程中,对于需要处理数万甚至更多并发连接的场景,epoll 由于其事件驱动的 O(1) 复杂度,已经成为事实上的标准。poll 可以被看作是从 selectepoll 演进过程中一个重要的中间步骤

代码

相同部分

Logger.hpp

复制代码
#pragma once

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

// 规定出场景的日志等级
enum class LogLevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

// 日志转换成为字符串
std::string Level2String(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";
    }
}

// 根据时间戳,获取可读性较强的时间信息
// 20XX-08-04 12:27:03
std::string GetCurrentTime()
{
    // 1. 获取时间戳
    time_t currtime = time(nullptr);

    // 2. 如何把时间戳转换成为20XX-08-04 12:27:03
    struct tm currtm;
    localtime_r(&currtime, &currtm);

    // 3. 转换成为字符串
    char timebuffer[64];
    snprintf(timebuffer, sizeof(timebuffer), "%4d-%02d-%02d %02d:%02d:%02d",
             currtm.tm_year + 1900,
             currtm.tm_mon + 1,
             currtm.tm_mday,
             currtm.tm_hour,
             currtm.tm_min,
             currtm.tm_sec);

    return timebuffer;
}

// 策略模式,策略接口
// 1. 刷新的问题 -- 假设我们已经有了一条完整的日志,string->设备(显示器,文件)
// 基类方法
class LogStrategy
{
public:
    // 不同模式核心是刷新方式的不同
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &logmessage) = 0;
};

// 控制台日志策略,就是日志只向显示器打印,方便我们debug
// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
    ~ConsoleLogStrategy()
    {
    }
    void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::cout << logmessage << std::endl;
        }
    }

private:
    // 显示器也是临界资源,保证输出线程安全
    Mutex _lock;
};

// 默认路径和日志名称
const std::string logdefaultdir = "log";
const static std::string logfilename = "test.log";

// 文件日志策略
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
    // 构造函数,建立出来指定的目录结构和文件结构
    FileLogStrategy(const std::string &dir = logdefaultdir,
                    const std::string filename = logfilename)
        : _dir_path_name(dir), _filename(filename)
    {
        LockGuard lockguard(&_lock);
        if (std::filesystem::exists(_dir_path_name))
        {
            return;
        }
        try
        {
            std::filesystem::create_directories(_dir_path_name);
        }
        catch (const std::filesystem::filesystem_error &e)
        {
            std::cerr << e.what() << "\r\n";
        }
    }
    // 将一条日志信息写入到文件中
    void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::string target = _dir_path_name;
            target += "/";
            target += _filename;
            // 追加方式
            std::ofstream out(target.c_str(), std::ios::app); // append
            if (!out.is_open())
            {
                return;
            }
            out << logmessage << "\n"; // out.write
            out.close();
        }
    }

    ~FileLogStrategy()
    {
    }

private:
    std::string _dir_path_name; // log
    std::string _filename;      // hello.log => log/hello.log
    Mutex _lock;
};

// 具体的日志类
// 1. 定制刷新策略
// 2. 构建完整的日志
class Logger
{
public:
    Logger()
    {
    }
    void EnableConsoleLogStrategy()
    {
        _strategy = std::make_unique<ConsoleLogStrategy>();
    }
    void EnableFileLogStrategy()
    {
        _strategy = std::make_unique<FileLogStrategy>();
    }

    // 内部类,实现RAII风格的日志格式化和刷新
    // 这个LogMessage,表示一条完整的日志对象
    class LogMessage
    {
    public:
        // RAII风格,构造的时候构建好日志头部信息
        LogMessage(LogLevel level, std::string &filename, int line, Logger &logger)
            : _curr_time(GetCurrentTime()),
              _level(level),
              _pid(getpid()),
              _filename(filename),
              _line(line),
              _logger(logger)
        {
            // stringstream不允许拷贝,所以这里就当做格式化功能使用
            std::stringstream ss;
            ss << "[" << _curr_time << "] "
               << "[" << Level2String(_level) << "] "
               << "[" << _pid << "] "
               << "[" << _filename << "] "
               << "[" << _line << "]"
               << " - ";
            _loginfo = ss.str();
        }
        // 重载 << 支持C++风格的日志输入,使用模版,表示支持任意类型
        template <typename T>
        LogMessage &operator<<(const T &info)
        {
            std::stringstream ss;
            ss << info;
            _loginfo += ss.str();
            return *this;
        }
        // RAII风格,析构的时候进行日志持久化,采用指定的策略
        ~LogMessage()
        {
            if (_logger._strategy)
            {
                _logger._strategy->SyncLog(_loginfo);
            }
        }

    private:
        std::string _curr_time; // 日志时间
        LogLevel _level;        // 日志等级
        pid_t _pid;             // 进程pid
        std::string _filename;
        int _line;

        std::string _loginfo; // 一条合并完成的,完整的日志信息
        Logger &_logger;      // 引用外部logger类, 方便使用策略进行刷新
    };
    // 故意拷贝,形成LogMessage临时对象,后续在被<<时,会被持续引用,
    // 直到完成输入,才会自动析构临时LogMessage,至此也完成了日志的显示或者刷新
    // 同时,形成的临时对象内包含独立日志数据
    // 未来采用宏替换,进行文件名和代码行数的获取
    LogMessage operator()(LogLevel level, std::string filename, int line)
    {
        return LogMessage(level, filename, line, *this);
    }
    ~Logger()
    {
    }

private:
    // 写入日志的策略
    std::unique_ptr<LogStrategy> _strategy;
};

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

// 使用宏,可以进行代码插入,方便随时获取文件名和行号
#define LOG(level) logger(level, __FILE__, __LINE__)

// 提供选择使用何种日志策略的方法
#define EnableConsoleLogStrategy() logger.EnableConsoleLogStrategy()
#define EnableFileLogStrategy() logger.EnableFileLogStrategy()

Mutex.hpp

复制代码
#pragma once
#include <iostream>
#include <mutex>
#include <pthread.h>

class Mutex
{
public:
    Mutex()
    {
        pthread_mutex_init(&_lock, nullptr);
    }
    void Lock()
    {
        pthread_mutex_lock(&_lock);
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    pthread_mutex_t *Get()
    {
        return &_lock;
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }
private:
    pthread_mutex_t _lock;
};

class LockGuard
{
public:
    LockGuard(Mutex *_mutex):_mutexp(_mutex)
    {
        _mutexp->Lock();
    }
    ~LockGuard()
    {
        _mutexp->Unlock();
    }
private:
    Mutex *_mutexp;
};

InetAddr.hpp

复制代码
#pragma once
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <string>
#include "Logger.hpp"

using namespace std;

#define Conv(addr) ((struct sockaddr *)&addr)

class InetAddr
{
private:
    void Net2Host()
    {
        _port = ntohs(_addr.sin_port);
        // _ip = inet_ntoa(_addr.sin_addr);
        char ipbuffer[64];
        inet_ntop(AF_INET, &(_addr.sin_addr.s_addr), ipbuffer, sizeof(ipbuffer));
        _ip = ipbuffer;
    }
    void Host2Net()
    {
        memset(&_addr, 0, sizeof(_addr));
        _addr.sin_family = AF_INET;
        _addr.sin_port = htons(_port);
        // _addr.sin_addr.s_addr = inet_addr(_ip.c_str());
        inet_pton(AF_INET, _ip.c_str(), &(_addr.sin_addr.s_addr));
    }

public:
    InetAddr(){}
    // 默认ip为INADDR_ANY(0.0.0.0)
    InetAddr(uint16_t port, const string ip = "0.0.0.0")
        : _ip(ip),
          _port(port)
    {
        Host2Net();
    }

    InetAddr(struct sockaddr_in &addr)
    {
        _addr = addr;
        Net2Host();
    }

    void Init(const struct sockaddr_in peer)
    {
        _addr = peer;
        Net2Host();
    }

    struct sockaddr *Addr()
    {
        return Conv(_addr);
    }

    string IP()
    {
        return _ip;
    }

    uint16_t Port()
    {
        return _port;
    }

    bool operator==(const InetAddr &addr)
    {
        return _ip == addr._ip && _port == addr._port;
    }

    socklen_t Length()
    {
        return sizeof(_addr);
    }

    string ToString()
    {
        return _ip + "-" + to_string(_port);
    }

    ~InetAddr()
    {
    }

private:
    // 网络风格地址
    struct sockaddr_in _addr;

    // 主机风格地址
    string _ip;
    uint16_t _port;
};

Socket.hpp

复制代码
#pragma once
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <memory>
#include <functional>
#include "Logger.hpp"
#include "InetAddr.hpp"
using namespace std;

static int gbacklog = 16; // 设置默认的backlog
static const int gsockfd = -1;

// 设置错误码
enum
{
    OK,
    CREATE_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR
};

// 定义一个抽象类,父类中定义算法的骨架,将某些步骤的具体实现延迟到子类中
class Socket
{
public:
    // 编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,
    // 所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写
    // 基类必须有虚析构函数
    virtual ~Socket() {}
    // 纯虚函数强制派生类重写虚函数
    virtual void CreatSocket() = 0;
    virtual void BindSocket(int port) = 0;
    virtual void ListenSocket() = 0;
    //virtual shared_ptr<Socket> Accept(InetAddr *clientaddr) = 0;
     virtual int Accept(InetAddr *clientaddr) = 0;
    virtual bool Connect(InetAddr &peer) = 0;
    virtual int SockFd() = 0;
    virtual void Close() = 0;
    virtual ssize_t Recv(std::string *out) = 0;
    virtual ssize_t Send(const std::string &in) = 0;

public:
    void BuildListenSocketMethod(int port)
    {
        CreatSocket();
        BindSocket(port);
        ListenSocket();
    }

    void BuildClientSocketMethod()
    {
        CreatSocket();
    }
};

class TcpSocket : public Socket
{
public:
    TcpSocket() : _sockfd(gsockfd)
    {
    }
    TcpSocket(int sockfd) : _sockfd(sockfd)
    {
    }
    // override帮助用户检测是否重写
    void CreatSocket() override
    {
        _sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_sockfd < 0)
        {
            LOG(LogLevel::FATAL) << "creat socket error";
            exit(CREATE_ERR);
        }
        LOG(LogLevel::DEBUG) << "creat socket success";
    }
    void BindSocket(int port) override
    {
        InetAddr local(port);
        if (bind(_sockfd, local.Addr(), local.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "bind socket error";
            exit(BIND_ERR);
        }
        LOG(LogLevel::DEBUG) << "bind socket success";
    }
    void ListenSocket() override
    {
        if (listen(_sockfd, gbacklog) != 0)
        {
            LOG(LogLevel::FATAL) << "listen socket error";
            exit(LISTEN_ERR);
        }
        LOG(LogLevel::DEBUG) << "listen socket success";
    }
    int Accept(InetAddr *clientaddr) override
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int clientfd = accept(_sockfd, (sockaddr *)&peer, &len);
        if (clientfd < 0)
        {
            LOG(LogLevel::FATAL) << "accept error";
            return -1;
        }
        clientaddr->Init(peer);
        return clientfd;
    }
    bool Connect(InetAddr &peer) override
    {
        if (connect(_sockfd, peer.Addr(), peer.Length()) != 0)
        {
            LOG(LogLevel::FATAL) << "connect error";
            return false;
        }
        LOG(LogLevel::DEBUG) << "connect " << peer.ToString() << " success";
        return true;
    }
    int SockFd() override
    {
        return _sockfd;
    }
    void Close() override
    {
        if (_sockfd >= 0)
            close(_sockfd);
    }
    // 今天重点放在读,通过读,理解序列和反序列和自定义协议的过程
    ssize_t 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;
    }
    ssize_t Send(const std::string &in)
    {
        // send 用于 已建立连接 的套接字(典型如 TCP),数据将自动发送给连接的对端。
        // sendto 主要用于 无连接 的套接字(典型如 UDP),每次发送都需要明确指定目标地址。
        return send(_sockfd, in.c_str(), in.size(), 0);
    }

    ~TcpSocket() {}

private:
    int _sockfd;
};

不同部分

SelectEchoServer

SelectEchoServer.hpp
复制代码
#pragma once
#include "Socket.hpp"
#include <sys/select.h>
#include <sys/time.h>

const static int gsize = sizeof(fd_set) * 8;
const static int gdefaultfd = -1;

class SelectServer
{
public:
    SelectServer(uint16_t port) : _listensocket(make_unique<TcpSocket>())
    {
        _listensocket->BuildListenSocketMethod(port);
        for (int i = 0; i < gsize; i++)
            fd_arr[i] = gdefaultfd;
        // 添加_listensocket到fd_arr
        fd_arr[0] = _listensocket->SockFd();
    }
    // 1. 新连接到来 && 连接断开也属于新事件到来
    // 2. 给server发送数据,让指定的fd就绪,也是新事件到来!
    void Accepter()
    {
        // 获取新连接了
        InetAddr clientadddr;
        // "拷贝"
        // 会不会被阻塞呢?不会!!,因为select已经告诉我了,已经就绪了!
        int sockfd = _listensocket->Accept(&clientadddr);
        if (sockfd > 0)
        {
            LOG(LogLevel::INFO) << "get new sockfd: " << sockfd << ", client addr: " << clientadddr.ToString();

            // 可以直接调用recv(sockfd ...) 吗?不能!
            // 那应该怎么办?sockfd 托管给select!!
            // 如何把新的sockfd托管给select? 只要把新的sockfd放入辅助数组中即可!
            int pos = 0;
            for (; pos < gsize; pos++)
            {
                if (fd_arr[pos] == gdefaultfd)
                {
                    fd_arr[pos] = sockfd;
                    break;
                }
            }
            if (pos == gsize)
            {
                LOG(LogLevel::WARNING) << "server is full!";
                close(sockfd);
            }
        }
    }
    void Recver(int index)
    {
        int sockfd = fd_arr[index];
        char buffer[1024];
        // 这里读取,会不会阻塞?不会!
        // 这里的读取有问题!
        ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say@ " << buffer << std::endl;
            std::string echo_string = "server echo# ";
            echo_string += buffer;
            send(sockfd, echo_string.c_str(), echo_string.size(), 0); // ?
        }
        else if (n == 0)
        {
            LOG(LogLevel::INFO) << "client quit, me too: " << fd_arr[index];
            fd_arr[index] = gdefaultfd;
            close(sockfd);
        }
        else
        {
            LOG(LogLevel::WARNING) << "recv error: " << fd_arr[index];
            fd_arr[index] = gdefaultfd;
            close(sockfd);
        }
    }
    void DispatchEvent(fd_set &rfds)
    {
        LOG(LogLevel::DEBUG) << "fd就绪,有事件到来";
        // select执行后将就绪的fd在rfds中置1
        for (int i = 0; i < gsize; i++)
        {
            if (fd_arr[i] == gdefaultfd)
                continue;
            // 查找就绪的事件
            if (FD_ISSET(fd_arr[i], &rfds))
            {
                // 将就绪的事件派发到不同的处理模块
                if (fd_arr[i] == _listensocket->SockFd())
                {
                    Accepter(); // 连接管理器
                }
                else
                {
                    Recver(i); // IO处理器
                }
            }
        }
    }
    void Run()
    {
        // 这里能直接accept获取连接吗?不对
        // 获取新连接,本质也是IO!!,叫做关心读事件!
        // accept(),本质不就是"阻塞读"吗?
        // 应该把listensock,fd添加交给select!!
        while (true)
        {
            // 关心读事件fd集合
            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd = gdefaultfd;
            struct timeval timeout = {0, 0};
            for (int i = 0; i < gsize; i++)
            {
                // 查找,将fd_arr中存在的事件全部托管给select
                if (fd_arr[i] == gdefaultfd)
                    continue;
                // 若该事件是读事件,添加fd到rfds
                FD_SET(fd_arr[i], &rfds);
                if (maxfd < fd_arr[i])
                    maxfd = fd_arr[i];
                LOG(LogLevel::DEBUG) << "添加fd:" << fd_arr[i];
            }
            // 重新看待select!
            // 就绪事件通知机制,让select替我们等待,有事件就绪了通知我们
            int n = select(maxfd+1, &rfds, nullptr, nullptr, nullptr);
            if (n == -1)
            {
                LOG(LogLevel::ERROR) << "select error";
                break;
            }
            else if (n == 0)
            {
                LOG(LogLevel::DEBUG) << "超时,timeout:" << timeout.tv_sec << ":" << timeout.tv_usec;
                sleep(1);
                break;
            }
            else
            {
                // 有事件就绪了,将就绪事件派发出去
                DispatchEvent(rfds);
            }
        }
    }
    ~SelectServer() {}

private:
    unique_ptr<Socket> _listensocket;
    // fd_arr负责管理进程中需要等待的fd
    int fd_arr[gsize];
};
Main.cc
复制代码
#include "SelectEchoServer.hpp"
#include<string>
#include<iostream>
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localport" << std::endl;
}


int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    // 网络服务
    uint16_t serverport = std::stoi(argv[1]);

    EnableConsoleLogStrategy();
    std::unique_ptr<SelectServer> selectsvr = std::make_unique<SelectServer>(serverport);
    selectsvr->Run();

    return 0;
}
Makefile
复制代码
SelectEchoServer:Main.cc
    g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
    rm -f 2.SelectEchoServer

PollEchoServer

PollEchoServer.hpp
复制代码
#pragma once
#include "Socket.hpp"
#include <sys/select.h>
#include <sys/time.h>
#include <poll.h>
const static int gsize = sizeof(fd_set) * 8;
const static int gdefaultfd = -1;

class PollServer
{
public:
    PollServer(uint16_t port) : _listensocket(make_unique<TcpSocket>())
    {
        _listensocket->BuildListenSocketMethod(port);
        for (int i = 0; i < gsize; i++)
        {
            // 初始化
            fd_arr[i].fd = gdefaultfd;
            fd_arr[i].events = fd_arr[i].revents = 0;
        }
        // events 由用户在调用 poll 之前设置
        fd_arr[0].fd = _listensocket->SockFd();
        fd_arr[0].events = POLLIN;
    }
    // 1. 新连接到来 && 连接断开也属于新事件到来
    // 2. 给server发送数据,让指定的fd就绪,也是新事件到来!
    void Accepter()
    {
        // 获取新连接了
        InetAddr clientadddr;
        // "拷贝"
        // 会不会被阻塞呢?不会!!,因为select已经告诉我了,已经就绪了!
        int sockfd = _listensocket->Accept(&clientadddr);
        if (sockfd > 0)
        {
            LOG(LogLevel::INFO) << "get new sockfd: " << sockfd << ", client addr: " << clientadddr.ToString();

            // 可以直接调用recv(sockfd ...) 吗?不能!
            // 那应该怎么办?sockfd 托管给select!!
            // 如何把新的sockfd托管给select? 只要把新的sockfd放入辅助数组中即可!
            int pos = 0;
            for (; pos < gsize; pos++)
            {
                if (fd_arr[pos].fd == gdefaultfd)
                {
                    fd_arr[pos].fd = sockfd;
                    fd_arr[pos].events = POLLIN;
                    break;
                }
            }
            if (pos == gsize)
            {
                LOG(LogLevel::WARNING) << "server is full!";
                close(sockfd);
            }
        }
    }
    void Recver(int index)
    {
        int sockfd = fd_arr[index].fd;
        char buffer[1024];
        // 这里读取,会不会阻塞?不会!
        // 这里的读取有问题!
        ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "client say@ " << buffer << std::endl;
            std::string echo_string = "server echo# ";
            echo_string += buffer;
            send(sockfd, echo_string.c_str(), echo_string.size(), 0); // ?
        }
        else if (n == 0)
        {
            LOG(LogLevel::INFO) << "client quit, me too: " << fd_arr[index].fd;
            fd_arr[index].fd = gdefaultfd;
            fd_arr[index].events = fd_arr[index].revents = 0;
            close(sockfd);
        }
        else
        {
            LOG(LogLevel::WARNING) << "recv error: " << fd_arr[index].fd;
            fd_arr[index].fd = gdefaultfd;
            fd_arr[index].events = fd_arr[index].revents = 0;
            close(sockfd);
        }
    }
    void EventDispatcher()
    {
        LOG(LogLevel::INFO) << "fd就绪,有新事件到来";

        for (int i = 0; i < gsize; i++)
        {
            if (fd_arr[i].fd == gdefaultfd)
            {
                continue;
            }

            // 读事件就绪
            if (fd_arr[i].revents & POLLIN)
            {
                if (fd_arr[i].fd == _listensocket->SockFd())
                {
                    Accepter(); // 连接管理器
                }
                else
                {
                    Recver(i); // IO处理器
                }
            }
        }
    }
    void Run()
    {
        while (true)
        {
            int timeout = -1;
            int n = poll(fd_arr, gsize, timeout);
            switch (n)
            {
            case 0:
                LOG(LogLevel::DEBUG) << "timeout...: ";
                break;
            case -1:
                LOG(LogLevel::ERROR) << "poll error";
                break;
            default:
                // 一定有事件就绪了, 就绪事件,派发到不同的处理模块
                EventDispatcher();
                break;
            }
        }
    }
    ~PollServer() {}

private:
    unique_ptr<Socket> _listensocket;
    // fd_arr负责管理进程中需要等待的fd
    struct pollfd fd_arr[gsize];
};
Main.cc
复制代码
#include "PollEchoServer.hpp"
#include<string>
#include<iostream>
void Usage(std::string proc)
{
    std::cerr << "Usage: " << proc << " localport" << std::endl;
}


int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    // 网络服务
    uint16_t serverport = std::stoi(argv[1]);

    EnableConsoleLogStrategy();
    std::unique_ptr<PollServer> selectsvr = std::make_unique<PollServer>(serverport);
    selectsvr->Run();

    return 0;
}
Makefile
复制代码
PoolEchoServer:Main.cc
    g++ -o $@ $^ -std=c++17
.PHONY:clean
clean:
    rm -f PoolEchoServer
相关推荐
沐知全栈开发1 小时前
C# 预处理器指令
开发语言
m0_730115111 小时前
C++中的命令模式实战
开发语言·c++·算法
xuansec1 小时前
PHP 反序列化漏洞学习笔记(CTF向总结)
笔记·学习·php
我命由我123452 小时前
Element Plus 2.2.27 的单选框 Radio 组件,选中一个选项后,全部选项都变为选中状态
开发语言·前端·javascript·html·ecmascript·html5·js
Albert Edison2 小时前
【C++11】可变参数模板
java·开发语言·c++
liuxin_07252 小时前
Composer 安装
php·composer
sg_knight2 小时前
设计模式实战:策略模式(Strategy)
java·开发语言·python·设计模式·重构·架构·策略模式
麦麦鸡腿堡2 小时前
JavaWeb_SpringBootWeb,HTTP协议,Tomcat快速入门
java·开发语言