当前位置: 首页 > news >正文

Select 服务器实战教学:从 Socket 封装到多客户端并发

文章目录

  • 引言
  • 一、框架设计思路​
  • 二、核心代码解析
    • 1. Socket 虚基类(Socket.hpp)​
    • 2. TcpSocket 派生类(Socket.hpp)​
    • 3. SelectServer 封装(SelectServer.hpp)​
      • 3.1 初始化流程
      • 3.2 事件循环核心
      • 3.3 客户端事件处理
    • 4. 客户端实现(TcpClient.hpp)​
  • 三、Select 使用核心注意事项​
  • 四、测试效果展示
    • 1. Makefile
    • 2. 启动服务器
    • 3. 客户端连接测试(多客户端并发)​
    • 4. 服务器日志输出
    • 5. 异常测试​
  • 总结与扩展

引言

Select 是 Linux 系统中经典的 I/O 多路复用模型,通过单个进程 / 线程管理多个文件描述符(FD),实现并发处理多个客户端连接。本文将基于 “虚基类抽象 + 派生类实现” 的设计思想,封装 Socket 接口并完成 SelectServer 开发,同时详解 select 使用的坑点与解决方案。

一、框架设计思路​

核心设计理念​

采用 “接口抽象 + 具体实现” 的分层架构,优势如下:​

  • 解耦性Socket 虚基类定义统一接口,派生类(如 TcpSocket)负责具体协议实现;​
  • 扩展性:后续可快速添加 UdpSocket 等派生类,无需修改服务器核心逻辑;​
  • 可维护性:业务逻辑与网络操作分离,SelectServer 专注于事件管理。

架构分层

┌─────────────────┐
│   SelectServer  │  服务器核心:事件循环、连接管理、回调分发
└────────┬────────┘│
┌────────▼────────┐
│    TcpSocket    │  派生类:TCP协议的Socket实现
└────────┬────────┘│
┌────────▼────────┐
│     Socket      │  虚基类:定义Socket统一接口
└─────────────────┘

二、核心代码解析

1. Socket 虚基类(Socket.hpp)​

定义 Socket 操作的统一接口,所有派生类需实现纯虚函数:

class Socket {
public:virtual ~Socket() {}virtual void SocketOrDie() = 0;    // 创建套接字(失败则退出)virtual void BindOrDie(uint16_t port) = 0; // 绑定端口virtual void ListenOrDie(int backlog) = 0; // 监听连接virtual int Accept() = 0;          // 接受新连接virtual void Close() = 0;          // 关闭套接字virtual int Recv(std::string *out) = 0; // 接收数据virtual int Send(const std::string& message) = 0; // 发送数据virtual int Connect(const std::string &server_ip, uint16_t port) = 0; // 客户端连接virtual int Fd() = 0;              // 获取文件描述符
};

2. TcpSocket 派生类(Socket.hpp)​

实现 TCP 协议的 Socket 操作,重点处理错误场景:​

  • 关键细节:​
    • SocketOrDie():创建套接字失败直接退出,保证后续操作的有效性;​
    • Accept():非致命错误(如信号中断)仅打印日志,不退出进程;​
    • Recv/Send:使用 MSG_NOSIGNAL 标志,避免客户端断开时触发 SIGPIPE 信号导致服务器崩溃;​
    • 所有系统调用(如bind、listen)均做错误校验,确保稳定性。​

3. SelectServer 封装(SelectServer.hpp)​

核心职责:管理监听套接字与客户端连接,通过 select 实现事件驱动。

3.1 初始化流程

bool Init() {signal(SIGPIPE, SIG_IGN); // 忽略SIGPIPE信号(关键!)_listen_socket = std::make_unique<TcpSocket>();_listen_socket->SocketOrDie();_listen_socket->BindOrDie(_port);_listen_socket->ListenOrDie(_backlog);_max_fd = _listen_socket->Fd(); // 初始化最大FD_is_running = true;return true;
}

3.2 事件循环核心

void Start() {fd_set read_fds;while (_is_running) {FD_ZERO(&read_fds); // 重置FD集合(select会修改集合,必须每次重置)FD_SET(_listen_socket->Fd(), &read_fds);// 添加所有客户端FD到集合,并更新最大FDfor (const auto& [fd, client] : _clients) {FD_SET(fd, &read_fds);_max_fd = std::max(_max_fd, fd);}// 阻塞等待事件(无超时)int ready = select(_max_fd + 1, &read_fds, nullptr, nullptr, nullptr);if (ready == -1) {std::cerr << "select失败(非致命)" << std::endl;continue;}// 处理新连接和客户端数据if (FD_ISSET(_listen_socket->Fd(), &read_fds)) HandleNewConnection();HandleClientEvent(read_fds);}
}

3.3 客户端事件处理

void HandleClientEvent(fd_set& read_fds) {for (auto it = _clients.begin(); it != _clients.end();) {int client_fd = it->first;Socket* client = it->second.get();if (FD_ISSET(client_fd, &read_fds)) {std::string data;int n = client->Recv(&data);if (n <= 0) { // n=0:客户端正常断开;n<0:接收错误client->Close();_client_ips.erase(client_fd);it = _clients.erase(it); // 安全删除,避免迭代器失效continue;}_handler(client, _client_ips[client_fd], data); // 回调业务逻辑}++it;}
}

4. 客户端实现(TcpClient.hpp)​

提供简单的 TCP 客户端,用于测试服务器功能:​

  • 支持连接服务器、发送数据、接收回显;​
  • 捕获 SIGINT 信号(Ctrl+C),优雅断开连接。

三、Select 使用核心注意事项​

  1. 必须重置 FD 集合(关键坑点!)​
    • 原因select 会修改传入的 fd_set,将未就绪的 FD 从集合中清除;​
    • 解决方案:每次循环调用 FD_ZERO 重置集合,重新添加所有需要监听的 FD(监听 FD + 客户端 FD)。​
  2. 正确维护max_fd​
    • 原因select 的第一个参数是「最大 FD+1」,若值过小会导致高 FD 的事件漏检;若值过大则浪费资源;​
    • 解决方案:每次添加客户端 FD 时更新 _max_fd,确保其始终是当前最大的 FD。​
  3. 忽略SIGPIPE信号​
    • 场景:客户端断开连接后,服务器继续调用 send 会触发 SIGPIPE 信号,导致服务器崩溃;​
    • 解决方案:初始化时调用 signal(SIGPIPE, SIG_IGN) 忽略该信号。​
  4. 处理accept的非致命错误​
    • 场景accept 可能被信号中断(如 SIGINT),属于非致命错误;​
    • 解决方案:仅打印日志,不退出进程,继续循环等待下一次连接。​
  5. 客户端 FD 的安全删除​
    • 问题:遍历客户端集合时删除 FD,会导致迭代器失效;​
    • 解决方案:使用 for 循环 + 迭代器,删除后通过 it = _clients.erase(it) 更新迭代器。​
  6. 正确处理 recv 的返回值​
    • n > 0:正常接收数据,触发业务逻辑;​
    • n == 0:客户端正常断开连接,关闭 FD 并删除;​
    • n < 0:接收错误,关闭 FD 并删除。​
  7. send 添加 MSG_NOSIGNAL 标志​
    • 作用:与忽略 SIGPIPE 配合,避免客户端断开时 send 触发信号。

四、测试效果展示

1. Makefile

因为代码中涉及到了结构化绑定的语法:for (const auto& [fd, client] : _clients),这是C++17 的特性,所以需要指定 -std=c++17

.PHONY:all
all:select_server tcpclientselect_server:SelectServer.ccg++ -o $@ $^ -std=c++17 # -lpthread 
tcpclient:TcpClient.ccg++ -o $@ $^ -std=c++17.PHONY:clean
clean:rm -f select_server tcpclient

2. 启动服务器

./SelectServer 8888
# 输出:
# SelectServer 初始化成功,监听端口:8888,listen_fd:3
# 服务器启动成功,监听端口 8888 ...

3. 客户端连接测试(多客户端并发)​

客户端 1:

./TcpClient 127.0.0.1 8888
# 输出:
# 成功连接到服务器[127.0.0.1:8888],客户端fd:4
# 请输入要发送的数据(输入exit退出):Hello Select!
# 已发送:Hello Select!(字节数:13)
# 收到服务器回显:Server Echo: Hello Select!(字节数:24)

客户端 2:

./TcpClient 127.0.0.1 8888
# 输出:
# 成功连接到服务器[127.0.0.1:8888],客户端fd:5
# 请输入要发送的数据(输入exit退出):多客户端测试
# 已发送:多客户端测试(字节数:8)
# 收到服务器回显:Server Echo: 多客户端测试(字节数:19)

4. 服务器日志输出

新客户端连接,fd:4,地址:[127.0.0.1:43210]
新客户端连接,fd:5,地址:[127.0.0.1:43211]
[127.0.0.1] 发送数据: Hello Select!
[127.0.0.1] 发送数据: 多客户端测试

5. 异常测试​

  • 客户端断开:关闭客户端 1,服务器日志:

    连接已关闭(fd=4)
    客户端[127.0.0.1]断开连接,fd: 4
    
  • 服务器优雅停止:Ctrl+C 触发 SIGINT,服务器日志:

    收到中断信号,正在断开连接...
    关闭客户端连接,fd:5
    关闭监听Socket,fd:3
    SelectServer 已停止
    

总结与扩展

  1. Select 模型优缺点​
    • 优点:跨平台(支持 Linux/Windows)、实现简单、无需多线程;​
    • 缺点:FD 数量上限(默认 1024)、轮询效率低(遍历所有 FD)。​
  2. 后续优化方向​
    1. 增加超时机制:select的第 5 个参数设置超时时间,避免永久阻塞;​
    2. 支持 FD 扩容:修改系统参数 /proc/sys/fs/file-max 提升 FD 上限;​
    3. 业务逻辑异步化:将耗时操作(如数据库查询)放入线程池,避免阻塞事件循环;​
    4. 升级到 epoll:高并发场景下替换为 epoll 模型,解决 select 的性能瓶颈。
http://www.dtcms.com/a/578154.html

相关文章:

  • Linux----文件系统
  • 国家允许哪几个网站做顺风车嘉兴网站建设外包公司
  • 新乡建网站个体工商户做网站
  • C# 分部类实现计算器功能
  • 怎样建设个人网站广告赚钱彩票投资理财平台网站建设
  • 代码编辑器
  • C# 中,0.1 在什么情况下不等于 0.1 ?
  • 哪块行业需要网站建设揭阳企业建站系统
  • 目前主流网站开发所用软件建筑工程公司起名
  • 【stm32协议外设篇】- NEO-6M GPS 模块
  • 内网网站开发费用泰安网签查询2023
  • 微算法科技(NASDAQ MLGO)采用动态层次管理和位置聚类技术,修改pBFT算法以提高私有区块链网络运行效率
  • 潍坊网络建站模板wordpress 指定页面nofollow
  • 从Hive on YARN到Hive on Spark
  • 创作写作-李劭卓
  • 论文分享 |Spark-TTS:用解耦语音令牌实现高效可控的语音合成
  • Spark 文本分类实战经验总结
  • 英伟达体系内关于 DGX Spark 的讨论观点整理
  • 模版型网站a站为什么会凉
  • 强软弱虚四种引用
  • [Esterel大师课] Gérard Berry:使用Esterel v7进行同步多时钟电路设计(2013)
  • 有什么学做木工的网站吗WordPress添加下载弹窗
  • 目标检测模型SSD详解与实现
  • 网站弹窗广告代码企业官方网站的作用
  • 网站建设排行山西省确诊病例最新情况
  • 线程池浅谈
  • KubeSphere在线安装单节点K8S集群
  • 北京安慧桥网站建设口碑好的家装前十强
  • 著名建筑网站正规的教育机构有哪些
  • Linux - Vault