目录
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 前进行初始化。
1020

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



