Linux网络编程:多路转接(多路复用)select

目录

I/O多路转接的诞生背景

I/O多路转接之select

何谓select

select函数原型

关于 fd_set 的结构:

关于timeval结构:

select执行过程

1. 定义并初始化描述符集合

2. 调用 select 函数

3. 检查 select 返回后的结果

关于编写代码上的一些特点

select特点

select缺点

了解socket就绪条件

读就绪

写就绪

异常就绪(了解)

实现select回声服务器

Mutex.hpp

logger.hpp

InetAddr.hpp

Socket.hpp

SelectServer.hpp

Makefile

Main.cc

I/O多路转接之poll

poll函数接口

poll是如何解决文件描述符数量限制的?

将select代码修改为poll

PollServer.hpp


I/O多路转接的诞生背景

上一篇博客我们介绍了非阻塞I/O,我们了解到非阻塞IO模式下会通过轮询的方式检查I/O操作是否就绪。这种方式相比于传统的阻塞I/O,不仅提高了效率,还避免了创建大量线程所带来的内存和上下文切换开销。

但是轮询本身是昂贵的。如果大多数时候轮询的结果都是“未就绪”,那么程序就会陷入“空转”,消耗大量 CPU 时间在无用的系统调用上,导致 CPU 占用率 100% 但实际工作量很少。这种轮询被称为 busy-waiting

正因为纯非阻塞 I/O 的轮询方式效率太低,实践中我们很少直接使用它。取而代之的是它的黄金搭档——I/O 多路复用。或者叫I/O 多路转接。

I/O 多路复用(如 select, poll, epoll)解决了纯非阻塞轮询的核心痛点。

  • 工作原理:应用程序将所有需要监视的 I/O 描述符注册到一个多路复用器(如 epoll)上。然后,应用程序阻塞在一个特殊的系统调用上(如 epoll_wait)。这个调用会帮应用程序监视所有注册的描述符。当其中任何一个或多个 I/O 就绪(数据可读或可写)时,epoll_wait才会返回,并告诉应用程序哪些描述符就绪了。

  • 回到快递类比:I/O 多路复用就像一个智能门铃系统

    • 你在家工作。

    • 当快递员(数据)到达时,门铃会响(epoll_wait返回)

    • 你不需要每隔5分钟就跑去看一眼。

    • 门铃一响,你就知道有包裹到了,直接去取即可。

这样,I/O 多路复用结合非阻塞 I/O,既保持了非阻塞的高效(线程不阻塞在单个 I/O 上),又避免了盲目轮询对 CPU 的浪费。​ 应用程序只在有“真正工作”可做时才被唤醒。

下面我们先来介绍多路转接select

I/O多路转接之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;

rdset,wrset,exset分别对应于需要检测的可读⽂件描述符的集合,可写⽂件描述符的集合及异常

⽂件描述符的集合;

参数timeout为结构timeval,⽤来设置select()的等待时间

参数timeout的取值:

NULL:则表⽰select()没有timeout,select将⼀直被阻塞,直到某个⽂件描述符上发⽣了事

件;

0:仅检测描述符集合的状态,然后⽴即返回,并不等待外部事件的发⽣。

特定的时间值:如果在指定的时间段⾥没有事件发⽣,select将超时返回。

返回值介绍:

执⾏成功则返回⽂件描述词状态已改变的个数

如果返回0代表在描述词状态改变前已超过timeout时间

当有错误发⽣时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和

timeout的值变成不可预测。

错误值可能为:

EBADF ⽂件描述词为⽆效的或该⽂件已关闭

EINTR 此调⽤被信号所中断

EINVAL 参数n 为负值。

ENOMEM 核⼼内存不⾜

关于 fd_set 的结构:

其实这个结构就是⼀个整数数组, 更严格的说, 是⼀个 "位图". 使⽤位图中对应的位来表⽰要监视的⽂件描述符.如果某位被设置为1,表示对应的文件描述符需要被监控。

提供了⼀组操作fd_set的接⼝, 来⽐较⽅便的操作位图

void FD_CLR(int fd, fd_set *set); // ⽤来清除描述词组set中相关fd 的位
int 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结构:

timeval结构⽤于描述⼀段时间⻓度,如果在这个时间内,需要监视的描述符没有事件发⽣则函数返回,返回值为0。

select执行过程

理解select模型的关键在于理解fd_set,为说明⽅便,取fd_set⻓度为1字节,fd_set中的每⼀bit可以对应⼀个⽂件描述符fd。则1字节⻓的fd_set最⼤可以对应8个fd.

(1)执⾏fd_set set; FD_ZERO(&set);则set⽤位表⽰是0000,0000。

(2)若fd=5,执⾏FD_SET(fd,&set);后set变为0001,0000(第5位置为1)

(3)若再加⼊fd=2,fd=1,则set变为0001,0011

(4)执⾏select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都发⽣可读事件,则select返回,此时set变为0000,0011。注意:没有事件

发⽣的fd=5被清空。

执行过程可以总结为以下三步:

1. 定义并初始化描述符集合

fd_set是一个位图结构,每一位代表一个文件描述符。

#include <sys/select.h>

fd_set read_fds;    // 监视"可读"事件的描述符集合
fd_set write_fds;   // 监视"可写"事件的描述符集合  
fd_set except_fds;  // 监视"异常"事件的描述符集合

// 清空集合
FD_ZERO(&read_fds);
// 将我们关心的描述符(比如监听套接字 listen_fd)加入集合
FD_SET(listen_fd, &read_fds);

int max_fd = listen_fd; // 记录当前最大的文件描述符,这是 select 需要的参数
2. 调用 select 函数
int select(int nfds, 
           fd_set *restrict readfds, 
           fd_set *restrict writefds,
           fd_set *restrict exceptfds, 
           struct timeval *restrict timeout);
  • nfds: 要监视的最大文件描述符值 +1。例如,你要监视描述符 3, 5, 8,那么 nfds应该是 9。这是为了限制内核检查的范围,提高效率。

  • readfds: 传入时,指向我们关心的"可读"描述符集合。返回时,内核会修改这个集合,只保留那些真正可读的描述符。

  • writefds: 同 readfds,但用于"可写"事件。

  • exceptfds: 同 readfds,但用于"异常"事件(如带外数据)。

  • timeout: 超时时间。如果设为 NULL,则永远阻塞;如果设为 0,则立即返回(轮询);如果指定时间,则超时后返回。

  • 返回值

    • > 0: 就绪的文件描述符总数。

    • = 0: 超时,没有描述符就绪。

    • -1: 发生错误。

3. 检查 select 返回后的结果

select返回后,我们需要遍历所有原先设置的描述符集合,使用 FD_ISSET宏来判断具体是哪个描述符就绪了。

// 调用 select,阻塞等待
int retval = select(max_fd + 1, &read_fds, NULL, NULL, NULL);

if (retval == -1) {
    perror("select()");
    exit(EXIT_FAILURE);
} else if (retval > 0) {
    // 有描述符就绪了!遍历查找是哪个。
    for (int fd = 0; fd <= max_fd; fd++) {
        if (FD_ISSET(fd, &read_fds)) {
            // 这个 fd 肯定有数据可读,不会阻塞
            if (fd == listen_fd) {
                // 监听套接字可读,表示有新连接到达
                accept_new_connection(listen_fd);
            } else {
                // 已连接套接字可读,可以读取数据
                handle_client_data(fd);
            }
        }
    }
}

关于编写代码上的一些特点

select特点

可监控的⽂件描述符个数取决于sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表

⽰⼀个⽂件描述符(位图),则我服务器上⽀持的最⼤⽂件描述符是512*8=4096.

将fd加⼊select监控集的同时,还要再使⽤⼀个数据结构array保存放到select监控集中的fd,

        ◦ ⼀是⽤于再select 返回后,array作为源数据和fd_set进⾏FD_ISSET判断。

        ◦ ⼆是select返回后会把以前加⼊的但并⽆事件发⽣的fd清空,则每次开始select前都要重新从

        array取得fd逐⼀加⼊(FD_ZERO最先),扫描array的同时取得fd最⼤值maxfd,⽤于select的第

        ⼀个参数。

备注:

fd_set的⼤⼩可以调整,可能涉及到重新编译内核。

select缺点

• 每次调⽤select, 都需要⼿动设置fd集合, 从接⼝使⽤⻆度来说也⾮常不便.(1)

• 每次调⽤select,都需要把fd集合从⽤⼾态拷⻉到内核态,这个开销在fd很多时会很⼤

• 同时每次调⽤select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很⼤(3)

• select⽀持的⽂件描述符数量太⼩.(2)

了解到这些特点,所以我们需要新的转接方案:poll(改良版select只解决了上边标识的(1))(2)缺点),下边介绍。

了解socket就绪条件

看半天没看懂这就绪条件是在讲啥,于是问下ai

读就绪

socket内核中, 接收缓冲区中的字节数, ⼤于等于低⽔位标记SO_RCVLOWAT. 此时可以⽆阻塞的读该⽂件描述符, 并且返回值⼤于0;

ps:SO_RCVLOWAT:接收低水位标记,默认通常是 1 字节,只要接收缓冲区中有数据可读(哪怕只有1字节),就触发读就绪。

socket TCP通信中, 对端关闭连接, 此时对该socket读, 立即返回0;

  • 情况:对方调用 close()shutdown(SHUT_WR)

  • 表现:此时 read()/recv()返回 0,表示连接已关闭

  • 多路复用器行为:会标记该socket为读就绪,但读取时得到EOF

监听的socket上有新的连接请求;(获取新连接accept本质是阻塞读)

  • 专门针对:处于 LISTEN状态的服务器socket

  • 触发条件:有新的客户端完成三次握手,连接进入已完成队列

  • 处理:此时调用 accept()会立即返回新连接,不会阻塞

socket上有未处理的错误;

  • 常见错误:连接超时、连接被重置(RST)、网络不可达等

  • 检测方式getsockopt(fd, SOL_SOCKET, SO_ERROR, ...)

  • 多路复用器行为:同时标记为读就绪和写就绪

写就绪

socket内核中, 发送缓冲区中的可⽤字节数(发送缓冲区的空闲位置⼤⼩), ⼤于等于低⽔位标记

SO_SNDLOWAT, 此时可以⽆阻塞的写, 并且返回值⼤于0;

  • SO_SNDLOWAT:发送低水位标记,默认值系统相关(通常较大)

  • 现实意义发送缓冲区有足够空间容纳你要写入的数据

  • 重要提醒:写就绪不代表你可以无限写入!一次 write()可能仍然只部分成功。

socket的写操作被关闭(close或者shutdown). 对⼀个写操作被关闭的socket进⾏写操作, 会触发

SIGPIPE信号;

  • 触发方式close(fd)shutdown(fd, SHUT_WR)

  • 后果:继续写入会触发 SIGPIPE信号(默认终止进程)

  • 防护:忽略 SIGPIPE信号,并检查 write()返回值

socket使⽤⾮阻塞connect连接成功或失败之后;

socket上有未读取的错误;

错误socket会同时标写为读写就绪。

错误socket同时标记为读/写就绪是为了确保应用程序无论如何都能发现错误。​ 无论程序只监视读事件、只监视写事件,还是两者都监视,这种"双重保险"机制都能保证错误被及时检测到,避免程序因遗漏错误检查而一直阻塞或行为异常。这相当于内核在说:"快检查这个socket,它出问题了,别管你是想读还是想写!"

异常就绪(了解)

非常少见的就绪条件

1.接收到带外数据(Out-of-Band Data, OOB)

这是异常就绪最经典、最主要的用途。

  • 什么是带外数据?

    • 也叫紧急数据。可以理解为网络通信中的“加急通道”。

    • 当一方有非常紧急的消息(比如:立刻终止传输、服务器要重启了)需要优先通知对方时,会发送OOB数据。

    • TCP中通过设置URG标志位来实现,只有一个字节的紧急数据。

  • 为什么需要异常就绪来处理OOB?

    • 如果没有异常就绪,带外数据会混在正常的接收缓冲区里,你无法区分普通数据和紧急数据。

    • 当你监听异常事件时,一旦有OOB数据到达,socket会立即被标记为异常就绪。这时程序可以优先去处理这个紧急消息,而不是等处理完所有普通数据再说。

  • 如何处理?

    • select等函数报告某个socket异常就绪时,程序应该调用 recv并设置 MSG_OOB标志来读取这个紧急数据。

2. 其他网络异常

虽然OOB是主要用途,但一些系统实现中,异常就绪也可能报告其他协议错误,例如:

  • 接收到非法的TCP数据包。

  • 某些类型的ICMP错误消息(比如“目标端口不可达”)。

但需要注意的是,很多常见的错误(如连接超时、对端强制断开)通常是通过读就绪写就绪来报告的(当你进行读写操作时,函数返回错误值-1)。

实现select回声服务器

从代码中我们具体体会select的特点与缺点

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);
    }
    pthread_mutex_t *Get()
    {
        return &_lock;
    }
    void Unlock()
    {
        pthread_mutex_unlock(&_lock);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_lock);
    }

private:
    pthread_mutex_t _lock;
};

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

private:
    Mutex *_mutexp;
};

logger.hpp

#pragma once
#include <iostream>
#include <filesystem>
#include <fstream>
#include <string>
#include <sstream>
#include <memory>
#include <unistd.h>
#include "Mutex.hpp"

enum class LoggerLevel
{
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};

std::string LoggerLevelToString(LoggerLevel level)
{
    switch (level)
    {
    case LoggerLevel::DEBUG:
        return "Debug";
    case LoggerLevel::INFO:
        return "Info";
    case LoggerLevel::WARNING:
        return "Warning";
    case LoggerLevel::ERROR:
        return "Error";
    case LoggerLevel::FATAL:
        return "Fatal";
    default:
        return "Unknown";
    }
}

std::string GetCurrentTime()
{
    // 获取时间戳
    time_t timep = time(nullptr);
    // 把时间戳转化为时间格式
    struct tm currtm;
    localtime_r(&timep, &currtm);

    // 转化为字符串
    char buffer[64];
    snprintf(buffer, sizeof(buffer), "%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 buffer;
}

class LogStrategy
{
public:
    virtual ~LogStrategy() = default;
    virtual void SyncLog(const std::string &logmessage) = 0;
};

// 显示器刷新
class ConsoleLogStrategy : public LogStrategy
{
public:
    ~ConsoleLogStrategy()
    {
    }
    virtual void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lockguard(&_lock);
            std::cout << logmessage << std::endl;
        }
    }

private:
    Mutex _lock;
};

const std::string default_dir_path_name = "/var/log/";
const std::string default_filename = "test.log";
// 文件刷新
class FileLogStrategy : public LogStrategy
{
public:
    FileLogStrategy(const std::string dir_path_name = default_dir_path_name,
                    const std::string filename = default_filename)
        : _dir_path_name(dir_path_name), _filename(filename)
    {
        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";
        }
    }
    ~FileLogStrategy()
    {
    }
    virtual void SyncLog(const std::string &logmessage) override
    {
        {
            LockGuard lock(&_lock);
            std::string target = _dir_path_name;
            target += '/';
            target += _filename;
            std::ofstream out(target.c_str(), std::ios::app);
            if (!out.is_open())
            {
                return;
            }
            out << logmessage << "\n";
            out.close();
        }
    }

private:
    std::string _dir_path_name;
    std::string _filename;
    Mutex _lock;
};

class Logger
{
public:
    Logger()
    {
    }
    void EnableConsoleStrategy()
    {
        _strategy = std::make_unique<ConsoleLogStrategy>();
    }

    void EnableFileStrategy()
    {
        _strategy = std::make_unique<FileLogStrategy>();
    }

    class LogMessage
    {
    public:
        LogMessage(LoggerLevel level, std::string filename, int line, Logger& logger)
            : _curr_time(GetCurrentTime()), _level(level), _pid(getpid())
            , _filename(filename), _line(line), _logger(logger)
        {
            std::stringstream ss;
            ss << "[" << _curr_time << "] "
               << "[" << LoggerLevelToString(_level) << "] "
               << "[" << _pid << "] "
               << "[" << _filename << "] "
               << "[" << _line << "]"
               << " - ";
               _loginfo = ss.str();
        }

        template <typename T>
        LogMessage &operator<<(const T &info)
        {
            std::stringstream ss;
            ss << info;
            _loginfo += ss.str();
            return *this;
        }

        ~LogMessage()
        {
            if (_logger._strategy)
            {
                _logger._strategy->SyncLog(_loginfo);
            }
        }

    private:
        std::string _curr_time; // 时间戳
        LoggerLevel _level;     // 日志等级
        pid_t _pid;             // 进程pid
        std::string _filename;  // 文件名
        int _line;              // 行号
        std::string _loginfo;   // 一条合并完成的,完整的日志信息
        Logger &_logger;        // 提供刷新策略的具体做法
    };
    LogMessage operator()(LoggerLevel level, std::string filename, int line)
    {
        return LogMessage(level, filename, line, *this);
    }
    ~Logger()
    {
    }

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

Logger logger;

#define LOG(level) logger(level, __FILE__, __LINE__)
#define EnableConsoleStrategy() logger.EnableConsoleStrategy()
#define EnableFileStrategy() logger.EnableFileStrategy()

InetAddr.hpp

#pragma once

// 该类 用于描述客户端套接字信息
// 方便后续用来管理客户端
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>

#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),ipbuffer,strlen(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));
    }

public:
    InetAddr()
    {

    }
    // 网络风格地址构造
    InetAddr(const struct sockaddr_in &addr)
        : _addr(addr)
    {
        Net2Host();
    }
    // 主机地址风格构造
    InetAddr(uint16_t port, const std::string &ip = "0.0.0.0")
        : _port(port), _ip(ip)
    {
        Host2Net();
    }
    void Init(const struct sockaddr_in &addr)
    {
        _addr=addr;
        Net2Host();
    }
    std::string Ip()
    {
        return _ip;
    }
    uint16_t Port()
    {
        return _port;
    }
    struct sockaddr* Addr()
    {
        return Conv(_addr);
    }
    socklen_t Length()
    {
        return sizeof(_addr);
    }
    std::string ToString()
    {
        return _ip+"-"+std::to_string(_port);
    }
    bool operator==(const InetAddr&addr)
    {
        return _ip==addr._ip&&_port==addr._port;
        //return _ip==addr._ip;
    }
    ~InetAddr()
    {
    }

private:
    struct sockaddr_in _addr; // 网络风格地址
    // 主机风格地址
    std::string _ip;
    uint16_t _port;
};

Socket.hpp

#ifndef __SOCKET_HPP__
#define __SOCKET_HPP__

#include<iostream>
#include<string>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<memory>
#include"logger.hpp"
#include"InetAddr.hpp"




enum 
{
    OK,
    CREATE_ERR,
    BIND_ERR,
    LISTEN_ERR,
    ACCEPT_ERR,
};




static int gbacklog =16;
static const int gsockfd=-1;


//设计模式:模块方法模式
//基类定义骨架流程
//子类实现具体步骤
class Socket
{
public:
    //Socket *sock=new TcpSocket();
    //基类虚构设置为虚函数,目的是保证子类资源能析构,然后再析构基类资源,如果不是虚函数子类析构会被跳过
    virtual ~Socket(){}
    //定义子类必须实现的抽象接口,do what?(相当于是函数声明,函数定义在子类完成)
    //virtual ...=0纯虚函数
    //目的:
    //1:强制子类实现这个函数(如果该类不需要这个函数就做个空函数)否则编译报错;
    //2.使基类变成抽象类,基类不能创建对象,只能通过子类创建
    virtual void CreateSocketOrDie()=0;
    virtual void BindSocketOrDie(int port)=0;
    virtual void ListenSocketOrDie(int backlog)=0;
    // virtual std::shared_ptr<Socket> Accept(InetAddr *clientaddr)=0;
    virtual int Accept(InetAddr *clientaddr)=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;
    virtual bool Connect(InetAddr &peer)=0;

    //other...
public:
    //基于抽象接口构建的通用逻辑,How to do?
    //方便复用通用流程
    void BuildListenSocketMethod(int _port)
    {
        CreateSocketOrDie();
        BindSocketOrDie(_port);
        ListenSocketOrDie(gbacklog);
    }
    void BuildClientSocketMethod()
    {
        CreateSocketOrDie();
    }
    // void BuildUdpSocketMethod()
    // {
    //     CreateSocketOrDie();
    //     BindSocketOrDie();
    // }
    // void BuildTcpSocketMethod()
    // {
    //     CreateSocketOrDie();
    //     BindSocketOrDie();
    //     ListenSocketOrDie();
    // }
};


class TcpSocket:public Socket
{
public:
    TcpSocket()
    :_sockfd(gsockfd)
    {

    }
    TcpSocket(int sockfd):_sockfd(sockfd)
    {

    }
    void CreateSocketOrDie() override
    {
        _sockfd=socket(AF_INET,SOCK_STREAM,0);
        if(_sockfd<0)
        {
            LOG(LoggerLevel::FATAL)<<"create socker error!";
            exit(CREATE_ERR);
        }
        LOG(LoggerLevel::INFO)<<"create socket success!!!";
    }
    void BindSocketOrDie(int port) override
    {
        InetAddr local(port);
        if(bind(_sockfd,local.Addr(),local.Length())!=0)
        {
            LOG(LoggerLevel::FATAL)<<"bind socker error!";
            exit(BIND_ERR);
        }
        LOG(LoggerLevel::INFO)<<"bind socket success!!!";
    }
    void ListenSocketOrDie(int backlog) override
    {
        if(listen(_sockfd,backlog)!=0)
        {
            LOG(LoggerLevel::FATAL)<<"listen socker error!";
            exit(LISTEN_ERR);
        }
        LOG(LoggerLevel::INFO)<<"listen socket success!!!";
    }
    // std::shared_ptr<Socket> Accept(InetAddr *clientaddr) override
    int Accept(InetAddr *clientaddr) override
    {
        struct sockaddr_in peer;
        socklen_t len=sizeof(peer);
        int fd=accept(_sockfd,(struct sockaddr*)&peer,&len);
        if(fd<0)
        {
            LOG(LoggerLevel::WARNING)<<"accept socker error!";
            return -1;
        }
        LOG(LoggerLevel::INFO)<<"accept socket success!!!";

        clientaddr->Init(peer);
        return fd;
        // return std::make_shared<TcpSocket>(fd);
    }
    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) override
    {
        return send(_sockfd,in.c_str(),in.size(),0);
    }
    bool Connect(InetAddr &peer) override
    {
        int n=connect(_sockfd,peer.Addr(),peer.Length());
        if(n>=0)return true;
        return false;
    }

    ~TcpSocket() 
    {

    }

private:
    int _sockfd;
};

// class UdpSocket:public Socket
// {

// };

#endif

SelectServer.hpp

#pragma once

#include <iostream>
#include<string>
#include <memory>
#include <sys/select.h>
#include "logger.hpp"
#include "Socket.hpp"

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

// TCP server
class SelectServer
{
public:
    SelectServer(uint16_t port) : _listensock(std::make_unique<TcpSocket>())
    {
        _listensock->BuildListenSocketMethod(port);
        for (int i = 0; i < gsize; ++i)
        {
            fd_array[i] = gdefaultfd;
        }
        fd_array[0] = _listensock->SockFd();
    }
    void Accepter()
    {
        // 获取新连接
        InetAddr clientaddr;
        // 这里不会阻塞,因为select那已经阻塞过了,现在已经就绪
        int sockfd = _listensock->Accept(&clientaddr);
        if (sockfd > 0)
        {
            LOG(LoggerLevel::INFO) << "get new sockfd: " << sockfd << ",client addr: " << clientaddr.ToString();
            int pos = 0;
            for (; pos < gsize; ++pos)
            {
                if (fd_array[pos] == gdefaultfd)
                {
                    fd_array[pos] == sockfd;
                    break;
                }
            }
            if (pos == gsize)
            {
                LOG(LoggerLevel::WARNING) << "server is full!";
                close(sockfd);
            }
        }
    }
    void Recver(int index)
    {
        int sockfd=fd_array[index];
        char buffer[1024];
        //这里recv也不会堵塞
        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(LoggerLevel::INFO)<<"client quit,me too: "<<fd_array[index];
            fd_array[index]=gdefaultfd;
            close(sockfd);
        }
        else{
            LOG(LoggerLevel::WARNING)<<"recv error: "<<fd_array[index];
            fd_array[index]=gdefaultfd;
            close(sockfd);
        }
    }
    void EventsDispatcher(fd_set &rfds)
    {
        LOG(LoggerLevel::INFO) << "fd就绪,有新事件到来";

        for (int i = 0; i < gsize; ++i)
        {
            if (fd_array[i] == gdefaultfd)
                continue;

            // 读就绪,内层进一步判断是哪个文件描述符准备好了
            if (FD_ISSET(fd_array[i], &rfds))
            {
                if (fd_array[i] == _listensock->SockFd())
                {
                    //如果是监听套接字,说明有新的客户端要建立连接
                    Accepter();     //连接管理器
                }
                else{
                    //如果是普通套接字,意味着有数据可读或连接关闭
                    Recver(i);      //IO处理器
                }
            }
        }
    }
    void Run()
    {
        while (true)
        {
            struct timeval timeout = {0, 0};
            fd_set rfds;
            FD_ZERO(&rfds);
            // 找出你通过 FD_SET设置到读、写、异常三个描述符集合中的所有文件描述符。
            // 找出这些fd中数值最大的那一个
            int maxfd = gdefaultfd;
            for (int i = 0; i < gsize; ++i)
            {
                if (fd_array[i] == gdefaultfd)
                    continue;

                FD_SET(fd_array[i], &rfds);

                if (maxfd < fd_array[i])
                    maxfd = fd_array[i];

                LOG(LoggerLevel::DEBUG) << "添加fd: " << fd_array[i];
            }

            //多路转接
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout);
            switch (n)
            {
            case 0:
                LOG(LoggerLevel::DEBUG) << "timeout... : " << timeout.tv_sec << " : " << timeout.tv_usec;
                break;
            case -1:
                LOG(LoggerLevel::ERROR) << "select error";
                break;
            default:
                //有新事件就绪,函数内部进一步分模块处理
                EventsDispatcher(rfds);
                break;
            }
        }
    }
    ~SelectServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    int fd_array[gsize];
};

Makefile

select_server:Main.cc
	g++  -o $@ $^ -std=c++17
.PHONY:clean
clean:
	rm -f select_server

Main.cc

#include"SelectServer.hpp"
#include<memory>
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]);
    EnableConsoleStrategy();
    std::unique_ptr<SelectServer> selectsvr=std::make_unique<SelectServer>(serverport);
    selectsvr->Run();

    return 0;
}

I/O多路转接之poll

poll函数接口

#include <poll.h>

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

// pollfd结构
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events */
    short revents; /* returned events */
};

参数说明

• fds是⼀个poll函数监听的结构列表. 每⼀个元素中, 包含了三部分内容: ⽂件描述符, 监听的事件集

合, 返回的事件集合.

• nfds表⽰fds数组的⻓度.

• timeout表⽰poll函数的超时时间, 单位是毫秒(ms).

events和revents(这俩本质也是位图)的取值(这些取值本质就是只有一个比特位为1的宏值):

POLLIN:设置成这个,相当于select的可读文件描述符

POLLOUT:设置成这个,相当于select的可写文件描述符

poll是如何解决文件描述符数量限制的?

select的核心问题是使用固定大小的位图(fd_set),而 poll采用了完全不同的数据结构:一个由 struct pollfd组成的数组

1. 关键数据结构:struct pollfd

#include <poll.h>

struct pollfd {
    int   fd;         /* 要监视的文件描述符 */
    short events;     /* 要监视的事件(读、写等) */
    short revents;    /* 实际发生的事件(由内核填充) */
};

3. 工作原理举例

假设要监视文件描述符 3, 5, 1000:

select的笨拙方式

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(3, &readfds);
FD_SET(5, &readfds); 
FD_SET(1000, &readfds); // 可以设置,但...

// 必须传 1000+1 作为第一个参数,内核要扫描0-1000的所有位!
select(1001, &readfds, NULL, NULL, NULL);

poll的聪明方式:

struct pollfd fds[3]; // 只需要为实际监视的fd分配空间

fds[0].fd = 3;      fds[0].events = POLLIN;
fds[1].fd = 5;      fds[1].events = POLLIN; 
fds[2].fd = 1000;   fds[2].events = POLLIN;

// 直接传递这个数组,内核只检查这3个fd!
poll(fds, 3, -1);

核心优势poll只关心你真正要监视的那些文件描述符,不管它们的数值有多大、多分散。你监视100个fd,就分配100个struct pollfd,轻松支持数万甚至更多的连接。

将select代码修改为poll

PollServer.hpp

#pragma once

#include <iostream>
#include<string>
#include <memory>
#include <poll.h>
#include "logger.hpp"
#include "Socket.hpp"

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

// TCP server
class PollServer
{
public:
    PollServer(uint16_t port) : _listensock(std::make_unique<TcpSocket>())
    {
        _listensock->BuildListenSocketMethod(port);
        for (int i = 0; i < gsize; ++i)
        {
            fd_array[i].fd = gdefaultfd;
            fd_array[i].events=fd_array[i].revents=0;
        }
        fd_array[0].fd = _listensock->SockFd();
        fd_array[0].events=POLLIN;
    }
    void Accepter()
    {
        // 获取新连接
        InetAddr clientaddr;
        // 这里不会阻塞,因为select那已经阻塞过了,现在已经就绪
        int sockfd = _listensock->Accept(&clientaddr);
        if (sockfd > 0)
        {
            LOG(LoggerLevel::INFO) << "get new sockfd: " << sockfd << ",client addr: " << clientaddr.ToString();
            int pos = 0;
            for (; pos < gsize; ++pos)
            {
                if (fd_array[pos].fd == gdefaultfd)
                {
                    fd_array[pos].fd == sockfd;
                    fd_array[pos].events=POLLIN;    // | POLLOUT
                    break;
                }
            }
            if (pos == gsize)
            {
                LOG(LoggerLevel::WARNING) << "server is full!";
                close(sockfd);
            }
        }
    }
    void Recver(int index)
    {
        int sockfd=fd_array[index].fd;
        char buffer[1024];
        //这里recv也不会堵塞
        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(LoggerLevel::INFO)<<"client quit,me too: "<<fd_array[index].fd;
            fd_array[index].fd=gdefaultfd;
            fd_array[index].events=fd_array[index].revents=0;
            close(sockfd);
        }
        else{
            LOG(LoggerLevel::WARNING)<<"recv error: "<<fd_array[index].fd;
            fd_array[index].fd=gdefaultfd;
            fd_array[index].events=fd_array[index].revents=0;
            close(sockfd);
        }
    }
    void EventsDispatcher()
    {
        LOG(LoggerLevel::INFO) << "fd就绪,有新事件到来";

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

            // 读就绪,内层进一步判断是哪个文件描述符准备好了
            if (fd_array[i].revents&POLLIN)
            {
                if (fd_array[i].fd == _listensock->SockFd())
                {
                    //如果是监听套接字,说明有新的客户端要建立连接
                    Accepter();     //连接管理器
                }
                else{
                    //如果是普通套接字,意味着有数据可读或连接关闭
                    Recver(i);      //IO处理器
                }
            }
        }
    }
    void Run()
    {
        while (true)
        {
            //多路转接
            int timeout=1000;
            int n = poll(fd_array,gsize,timeout);
            switch (n)
            {
            case 0:
                LOG(LoggerLevel::DEBUG) << "timeout... ";
                break;
            case -1:
                LOG(LoggerLevel::ERROR) << "poll error";
                break;
            default:
                //有新事件就绪,函数内部进一步分模块处理
                EventsDispatcher();
                break;
            }
        }
    }
    ~PollServer()
    {
    }

private:
    std::unique_ptr<Socket> _listensock;
    struct pollfd fd_array[gsize]; //或者指针类型
    
    // int fd_array[gsize];
};

下一篇介绍epoll

此篇完,感谢收看!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

_dindong

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

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

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

打赏作者

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

抵扣说明:

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

余额充值