Main.cc
#include "SelectServer.hpp"
#include <memory>
int main()
{
// std::cout <<"fd_set bits num : " << sizeof(fd_set) * 8 << std::endl;
std::unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
Socket.hpp
#include <iostream>
#include <string>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
enum
{
SocketErr = 2,
BindErr,
ListenErr,
};
// TODO
const int backlog = 10;
class Sock
{
public:
Sock()
{
}
~Sock()
{
}
public:
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
perror("socker error");
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
}
void Bind(uint16_t port)
{
struct sockaddr_in local;
memset(&local, 0, sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
perror("bind error");
exit(BindErr);
}
}
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
perror("listen error");
exit(ListenErr);
}
}
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
perror("accept error");
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
void Close()
{
close(sockfd_);
}
int Fd()
{
return sockfd_;
}
private:
int sockfd_;
};
SelectServer.hpp
#pragma once
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8888;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1;
class SelectServer
{
public:
SelectServer(uint16_t port = defaultport) : _port(port)
{
for (int i = 0; i < fd_num_max; i++)
{
fd_array[i] = defaultfd;
// std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
}
}
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter()
{
// 我们的连接事件就绪了
std::string clientip;
uint16_t clientport = 0;
int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会
if (sock < 0) return;
// 替换lg(Info,…)为cout,保留原有的日志信息格式
cout << "[Info] accept success, " << clientip << ": " << clientport << ", sock fd: " << sock << endl;
// sock -> fd_array[]
int pos = 1;
for (; pos < fd_num_max; pos++) // 第二个循环
{
if (fd_array[pos] != defaultfd)
continue;
else
break;
}
if (pos == fd_num_max)
{
// 替换lg(Warning,…)为cerr(警告信息用标准错误流输出)
cerr << "[Warning] server is full, close " << sock << " now!" << endl;
close(sock);
}
else
{
fd_array[pos] = sock;
PrintFd();
// TODO
}
}
void Recver(int fd, int pos)
{
// demo
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) – 1); // bug?
if (n > 0)
{
buffer[n] = 0;
cout << "get a messge: " << buffer << endl;
}
else if (n == 0)
{
// 替换lg(Info,…)为cout
cout << "[Info] client quit, me too, close fd is : " << fd << endl;
close(fd);
fd_array[pos] = defaultfd; // 这里本质是从select中移除
}
else
{
// 替换lg(Warning,…)为cerr
cerr << "[Warning] recv error: fd is : " << fd << endl;
close(fd);
fd_array[pos] = defaultfd; // 这里本质是从select中移除
}
}
void Dispatcher(fd_set &rfds)
{
for (int i = 0; i < fd_num_max; i++) // 这是第三个循环
{
int fd = fd_array[i];
if (fd == defaultfd)
continue;
if (FD_ISSET(fd, &rfds))
{
if (fd == _listensock.Fd())
{
Accepter(); // 连接管理器
}
else // non listenfd
{
Recver(fd, i);
}
}
}
}
void Start()
{
int listensock = _listensock.Fd();
fd_array[0] = listensock;
for (;;)
{
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = fd_array[0];
for (int i = 0; i < fd_num_max; i++) // 第一次循环
{
if (fd_array[i] == defaultfd)
continue;
FD_SET(fd_array[i], &rfds);
if (maxfd < fd_array[i])
{
maxfd = fd_array[i];
// 替换lg(Info,…)为cout
cout << "[Info] max fd update, max fd is: " << maxfd << endl;
}
}
// accept?不能直接accept!检测并获取listensock上面的事件,新连接到来,等价于读事件就绪
// struct timeval timeout = {1, 0}; // 输入输出,可能要进行周期的重复设置
struct timeval timeout = {0, 0}; // 输入输出,可能要进行周期的重复设置
// 如果事件就绪,上层不处理,select会一直通知你!
// select告诉你就绪了,接下来的一次读取,我们读取fd的时候,不会被阻塞
// rfds: 输入输出型参数。 1111 1111 -> 0000 0000
int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
switch (n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cerr << "select error" << endl;
break;
default:
// 有事件就绪了,TODO
cout << "get a new link!!!!!" << endl;
Dispatcher(rfds); // 就绪的事件和fd你怎么知道只有一个呢???
break;
}
}
}
void PrintFd()
{
cout << "online fd list: ";
for (int i = 0; i < fd_num_max; i++)
{
if (fd_array[i] == defaultfd)
continue;
cout << fd_array[i] << " ";
}
cout << endl;
}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
int fd_array[fd_num_max]; // 数组, 用户维护的!
// int wfd_array[fd_num_max];
};
makefile
select_server:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f select_server
select函数
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
2. 逐个参数解析
| nfds | int | 监听的 FD 范围上限:值为最大FD + 1(因为 FD 从 0 开始)⚠️ 必须传对,否则 select 监听不完整 | 代码中先遍历fd_array找到maxfd,然后传maxfd + 1,确保所有有效 FD 都被监听 |
| readfds | fd_set* | 读事件监听集合(输入输出型参数):✅ 输入:你要监听哪些 FD 的读事件✅ 输出:哪些 FD 的读事件已就绪传nullptr表示不监听 | 代码中只监听读事件(新连接 / 客户端发数据 / 断开),所以只传&rfds,其他事件集合传nullptr |
| writefds | fd_set* | 写事件监听集合(输入输出型参数):监听 FD 是否可写(如发送数据不阻塞)传nullptr表示不监听 | 代码中不需要主动发送数据,所以传nullptr |
| exceptfds | fd_set* | 异常事件监听集合(输入输出型参数):监听 FD 的异常事件(如带外数据)传nullptr表示不监听 | 代码中无需处理异常事件,传nullptr |
| timeout | struct timeval* | 超时时间(输入输出型参数):控制 select 的阻塞行为,结构定义:struct timeval { long tv_sec; // 秒 long tv_usec; // 微秒 }; | 代码中注释了超时设置,最终传nullptr(永久阻塞);若传&timeout需每次循环重新赋值 |
关键补充:fd_set 的操作函数
fd_set是内核维护的 FD 集合,不能直接操作,必须用以下宏:
- FD_ZERO(&rfds):清空集合(你的代码中每次循环都要调用,因为 select 会修改集合)
- FD_SET(fd, &rfds):将 FD 加入监听集合(你的代码中遍历fd_array添加有效 FD)
- FD_ISSET(fd, &rfds):判断 FD 是否在 “就绪集合” 中(你的代码中Dispatcher函数用它判断哪个 FD 就绪)
- FD_CLR(fd, &rfds):将 FD 从集合中移除(你的代码中通过fd_array[pos] = -1间接实现)
timeout 的三种取值场景
| nullptr | 永久阻塞,直到有 FD 事件就绪 | 代码中最终选择此方式,保证及时响应 |
| tv_sec=0, tv_usec=0 | 非阻塞:立即返回,不管有没有事件就绪 | 代码中注释了此方式(测试用) |
| tv_sec>0 或 tv_usec>0 | 限时阻塞:超时前有事件则返回,超时也返回 | 适合需要定期执行其他逻辑的场景 |
返回值解析
select的返回值是整数,不同值对应不同场景,你的代码中用switch处理了返回值,具体含义如下:
| > 0 | 成功:返回 “就绪的 FD 总数”(读 / 写 / 异常事件的 FD 数量之和) | 调用Dispatcher分发事件,处理就绪的 FD(监听 FD / 客户端 FD) |
| 0 | 超时:没有 FD 就绪,且等待时间达到timeout设置的值 | 打印超时提示(你的代码中因传nullptr,不会走到此分支) |
| -1 | 失败:调用出错(如参数错误、被信号中断),同时设置errno | 打印select error,但未处理errno(生产环境建议打印errno定位问题) |
常见错误原因(返回 – 1 时)
- nfds传错(如小于等于最大 FD);
- fd_set未初始化(忘记FD_ZERO);
- 被信号中断(如SIGINT),可结合errno == EINTR重试;
- FD 已被关闭但仍加入监听集合。
输入输出型参数的坑:你的代码中每次循环都重新FD_ZERO(&rfds) + FD_SET,这是正确的!因为select返回后,readfds会被修改为仅包含就绪的 FD,未就绪的 FD 会被清空,所以下一次循环必须重新构建集合。
maxfd 的计算:你的代码中遍历fd_array找maxfd,然后传maxfd + 1,这是select的核心要求 —— 如果nfds传小了,大于等于nfds的 FD 不会被监听,会导致客户端连接无响应。
返回值 > 0 的注意点:你的代码中打印get a new link!!!!!",但实际返回值 > 0 表示 “就绪的 FD 总数”,可能是 1 个(新连接),也可能是多个(多个客户端同时发数据),Dispatcher遍历判断每个 FD 是否就绪是正确的处理方式。
select 接口的核心缺点
1. fd 数量有硬限制(核心痛点)
-
受内核宏 FD_SETSIZE 限制(默认 1024),最多监听 1024 个文件描述符(fd);
-
虽可改内核宏,但移植性极差且增加内存开销,无法适配高并发场景;
-
对比:poll 无此硬限制,仅受系统资源(ulimit -n)约束。
2. 接口设计冗余,易出错
-
fd_set 是输入 / 输出复用结构体,内核会修改它标记事件,因此每次调用 select 前必须重置(FD_ZERO 清空 + FD_SET 重新添加 fd);
-
代码繁琐,易因忘记重置导致 bug,且增加不必要的初始化开销;
-
对比:poll 的 events(监听事件)和 revents(实际事件)分离,无需重置。
3. 遍历效率极低
-
select 仅返回 “有事件的 fd 数量”,不告知具体 fd;需遍历 0~FD_SETSIZE 所有 fd,用 FD_ISSET 检查是否有事件;
-
哪怕仅 1 个 fd 有事件,也要遍历 1024 次,效率随 fd 数量线性下降;
-
对比:poll 仅需遍历数组中有效 fd(fd≠-1),无需全量遍历。
4. 数据拷贝开销大
-
每次调用需拷贝 3 个 fd_set(读 / 写 / 异常)到内核,返回时再拷贝回用户态;
-
fd_set 越大(如改 FD_SETSIZE 为 8192),拷贝数据量和性能损耗越明显;
-
对比:poll 仅拷贝 pollfd 数组,无效 fd(fd=-1)无需拷贝,开销更小。
5. 事件类型单一,不灵活
-
仅支持 “读、写、异常” 三类事件,无法精准监听 HUP(客户端断开)、ERR(fd 出错)等细粒度事件;
-
客户端断开需通过 “读操作返回 0” 间接判断,逻辑不直观;
-
对比:poll 可直接通过 POLLHUP/POLLERR 识别这类事件。
总结
select 最核心的问题是 fd 数量硬限制 和 全量遍历 / 拷贝,高并发场景性能极差;
接口设计冗余(需重置 fd_set)、事件类型单一,易用性远低于 poll;
poll 解决了 select 的核心缺陷,是更适配多 fd 场景的替代方案。



