跳到主要内容Linux 高级 IO:I/O 多路转接 select 接口原理与 TCP 服务器实现 | 极客日志C++
Linux 高级 IO:I/O 多路转接 select 接口原理与 TCP 服务器实现
综述由AI生成Linux 高级 IO 中的 I/O 多路转接技术 select 详解。文章介绍了 select 接口的五个参数及返回值含义,fd_set 位图操作函数 FD_SET/FD_CLR/FD_ISSET/FD_ZERO 的使用,以及 timeout 参数的三种等待模式。通过逐步完善代码,实现了基于 select 的 TCP 服务器,包括监听连接、管理客户端文件描述符数组、处理读写事件及断连逻辑。最后分析了 select 的缺点,如文件描述符数量限制、用户态内核态拷贝开销大、遍历效率低等,引出后续 poll 和 epoll 的学习方向。
PentesterX25 浏览 Linux 高级 IO:I/O 多路转接 select 接口和原理讲解
前言
本文基于 Linux 高级 IO 模型系列,重点讲解 I/O 多路转接中的 select 机制。主要涵盖 select 接口参数、返回值含义、fd_set 位图操作以及 timeout 设置,并通过逐步完善代码实现一个 select 版本的 TCP 服务器。
一、接口和原理讲解
1. select 参数与返回值
select 接口共有 5 个参数,后四个为输入输出型参数。
- nfds: 等待多个文件描述符中最大的那个值加 1。
- readfds: 读事件位图(fd_set*)。
- writefds: 写事件位图(fd_set*)。
- exceptfds: 异常事件位图(fd_set*)。
- timeout: 超时时间结构体(struct timeval*)。
返回值 n 的含义:
- n > 0: 表示有 n 个文件描述符就绪。
- n = 0: 表示等待超时,无错误,无 fd 就绪。
- n < 0: 表示等待出错。
2. fd_set 位图操作
fd_set 是内核提供的位图类型,用于传递用户关心的文件描述符集合。系统提供了四个宏进行操作:
FD_ZERO(&fdset): 初始化位图,全部置 0。
FD_SET(fd, &fdset): 设置指定位置为 1,表示关心该 fd。
FD_CLR(fd, &fdset): 移除指定位置,置为 0。
FD_ISSET(fd, &fdset): 判断指定位置是否为 1,即是否就绪。
输入输出含义:
- 输入: 用户告诉内核需要关心哪些 fd 的事件。
- 输出: 内核返回哪些 fd 的事件已经就绪。
3. timeout 参数
{5, 0}: 阻塞等待 5 秒,若期间无事件则返回,剩余时间更新。
{0, 0}: 非阻塞立即返回。
NULL: 阻塞等待直到有事件就绪。
二、select 版本的 TCP 服务器
1. 项目结构
- Main.cc: 主函数入口。
- SelectServer.hpp: 服务器封装类。
- Socket.hpp: 套接字封装。
- Log.hpp: 日志工具。
- makefile: 编译脚本。
2. 代码实现步骤
Main.cc
#include "SelectServer.hpp"
#include <memory>
int main {
;
svr->();
svr->();
;
}
()
unique_ptr<SelectServer> svr(new SelectServer())
Init
Start
return
0
SelectServer.hpp (基础版)
定义默认端口 8080,包含监听套接字和端口号成员。
#include <iostream>
#include <sys/select.h>
#include "Log.hpp"
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
class SelectServer {
public:
SelectServer(uint16_t port = defaultport) : _port(port) {}
bool Init() {
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Start() {}
~SelectServer() { _listensock.Close(); }
private:
Sock _listensock;
uint16_t _port;
};
完善一:启动循环与 select 调用
在 Start 函数中添加死循环,使用 select 等待 listensock 的读事件。
void Start() {
int listensock = _listensock.Fd();
for (;;) {
fd_set rfds;
FD_ZERO(&rfds);
FD_SET(listensock, &rfds);
struct timeval timeout = {2, 0};
int n = select(listensock + 1, &rfds, nullptr, nullptr, &timeout);
switch (n) {
case 0: cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl; break;
case -1: cout << "select error" << endl; break;
default: cout << "get a new link" << endl; break;
}
}
}
注意:每次调用 select 前需重置 rfds 和 timeout,因为它们是输入输出参数。
完善二:非阻塞模式
将 timeout 设置为 {0, 0},实现非阻塞 IO 效果。
完善三:阻塞模式
将 timeout 设置为 nullptr,实现阻塞等待。
完善四:管理客户端连接
引入 fd_array 数组存储客户端连接的 sock 文件描述符,限制最大数量为 sizeof(fd_set)*8 (通常 1024)。
static const int fd_num_max = sizeof(fd_set) * 8;
int defaultfd = -1;
for(int i=0; i<fd_num_max; i++) fd_array[i] = defaultfd;
当 accept 获取新连接时,查找数组中值为 -1 的位置存入;若已满则关闭连接并打印警告。
完善五:遍历数组设置 select 参数
在 Start 循环中遍历 fd_array,将所有有效 fd 通过 FD_SET 加入 rfds,并计算 maxfd 更新 nfds 参数。
int maxfd = fd_array[0];
for(int i=0; i<fd_num_max; i++) {
if(fd_array[i] != defaultfd) {
FD_SET(fd_array[i], &rfds);
if(maxfd < fd_array[i]) maxfd = fd_array[i];
}
}
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
完善六:处理读写事件
在 HandlerEvent 中遍历 rfds,区分 listensock 和 client_sock。
listensock 就绪:调用 Accept 获取连接。
client_sock 就绪:调用 read 读取数据。
- n > 0: 打印数据。
- n == 0: 客户端断开,close 并清理数组。
- n < 0: 出错,close 并清理数组。
完善七:代码重构
将逻辑拆分为 Accepter (连接管理器), Recver (数据读取器), Dispatcher (分发器),提高可读性。
三、select 的缺点
- 数量限制: fd_set 位图大小固定(通常 1024),不支持扩容。
- 性能开销: 每次调用 select 都需要在用户态和内核态之间拷贝 fd_set 数据。
- 遍历效率: 无论有多少 fd 就绪,内核都需要遍历所有传入的 fd 来检查状态;用户层也需要遍历数组来找出就绪的 fd。
- 重置频繁: 每次调用 select 前必须重新设置 fd_set 和 timeout。
随着并发连接数增加,select 的效率会显著下降,因此后续可学习 poll 和 epoll 模型。
四、源代码
Main.cc
#include "SelectServer.hpp"
#include <memory>
int main() {
unique_ptr<SelectServer> svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
makefile
selectserver:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f selectserver
SelectServer.hpp
#include <iostream>
#include <sys/select.h>
#include <unistd.h>
#include "Log.hpp"
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8080;
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;
}
bool Init() {
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter() {
string clientip;
uint16_t clientport;
int sock = _listensock.Accept(&clientip, &clientport);
if(sock < 0) return;
lg(Info, "accept success, %s:%d, sock fd: %d", clientip.c_str(), clientport, sock);
int pos = 1;
for(; pos < fd_num_max; pos++) {
if(fd_array[pos] == defaultfd) break;
}
if(pos == fd_num_max) {
lg(Warning, "server is full, close %d now", sock);
close(sock);
} else {
fd_array[pos] = sock;
PrintFd();
}
}
void Recver(int fd, int pos) {
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer)-1);
if(n > 0) {
buffer[n-1] = 0;
cout << "get a message: " << buffer << endl;
} else if(n == 0) {
lg(Info, "client quit, me too, close fd: %d", fd);
close(fd);
fd_array[pos] = defaultfd;
} else {
lg(Warning, "recv error, fd: %d", fd);
close(fd);
fd_array[pos] = defaultfd;
}
}
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 {
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) {
FD_SET(fd_array[i], &rfds);
if(maxfd < fd_array[i]) maxfd = fd_array[i];
lg(Info, "max fd update, max fd is: %d", maxfd);
}
}
struct timeval timeout = {0, 0};
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch(n) {
case 0: cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl; break;
case -1: cout << "select error" << endl; break;
default: cout << "get a new link" << endl; Dispatcher(rfds); break;
}
}
}
void PrintFd() {
cout << "online fd list: ";
for(int i = 0; i < fd_num_max; i++) {
if(fd_array[i] != defaultfd) cout << fd_array[i] << ' ';
}
cout << endl;
}
~SelectServer() { _listensock.Close(); }
private:
Sock _listensock;
uint16_t _port;
int fd_array[fd_num_max];
};
Log.hpp
#pragma once
#include <iostream>
#include <string>
#include <ctime>
#include <cstdio>
#include <cstdarg>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log {
public:
Log() { printMethod = Screen; path = "./log/"; }
void Enable(int method) { printMethod = method; }
~Log() {}
std::string levelToString(int level) {
switch(level) {
case Info: return "Info";
case Debug: return "Debug";
case Warning: return "Warning";
case Error: return "Error";
case Fatal: return "Fatal";
default: return "";
}
}
void operator()(int level, const char* format, ...) {
time_t t = time(nullptr);
struct tm* ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]",
levelToString(level).c_str(), ctime->tm_year + 1900, ctime->tm_mon + 1,
ctime->tm_mday, ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
char logtxt[2 * SIZE];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
printLog(level, logtxt);
}
void printLog(int level, const std::string& logtxt) {
switch(printMethod) {
case Screen: std::cout << logtxt << std::endl; break;
case Onefile: printOneFile(LogFile, logtxt); break;
case Classfile: printClassFile(level, logtxt); break;
default: break;
}
}
void printOneFile(const std::string& logname, const std::string& logtxt) {
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);
if(fd < 0) return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string& logtxt) {
std::string filename = LogFile;
filename += ".";
filename += levelToString(level);
printOneFile(filename, logtxt);
}
private:
int printMethod;
std::string path;
};
Log lg;
Socket.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
const int backlog = 10;
enum { SocketErr = 1, BindErr, ListenErr };
class Sock {
public:
Sock() {}
void Socket() {
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if(sockfd_ < 0) {
lg(Fatal, "socket error, %s : %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEPORT, &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;
socklen_t len = sizeof(local);
if(bind(sockfd_, (struct sockaddr*)&local, len) < 0) {
lg(Fatal, "bind error, %s : %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen() {
if(listen(sockfd_, backlog) < 0) {
lg(Fatal, "listen error, %s : %d", strerror(errno), errno);
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) {
lg(Warning, "accept error, %s : %d", strerror(errno), errno);
return -1;
}
char ipstr[128];
inet_ntop(AF_INET, &(peer.sin_addr), ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string& serverip, uint16_t serverport) {
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(serverport);
inet_pton(AF_INET, serverip.c_str(), &(peer.sin_addr));
socklen_t len = sizeof(peer);
int n = connect(sockfd_, (struct sockaddr*)&peer, len);
if(n == -1) {
std::cerr << "connect to " << serverip << ':' << serverport << "error" << std::endl;
return false;
}
return true;
}
void Close() {
if(sockfd_ > 0) close(sockfd_);
}
int Fd() { return sockfd_; }
~Sock() {}
private:
int sockfd_;
};
总结
本文详细讲解了 Linux 高级 IO 中 select 接口的原理与用法,并通过逐步完善代码实现了支持多客户端连接的 TCP 服务器。文章涵盖了 select 的参数设置、fd_set 位图操作、超时控制、连接管理与事件分发等核心逻辑,最后分析了 select 模型的局限性,为后续学习 poll 和 epoll 打下基础。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online