目录
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
此篇完,感谢收看!

916

被折叠的 条评论
为什么被折叠?



