C++网络编程:select IO多路复用及TCP服务器开发

C++网络编程:使用select实现IO多路复用

  • 一、什么是 IO 多路复用?
  • 二、IO多路复用器 select
  • 三、相关接口
    • 3.1、fd_set 结构体
    • 3.2、宏和函数
  • 四、select 实现 TCP 服务器
  • 五、总结

一、什么是 IO 多路复用?

在网络编程中,最容易想到的并发模型就是“一请求一线程”模型,逻辑非常容易理解。但是,“一请求一线程”模型对资源消耗非常高,高并发下很容易就达到了瓶颈;那么单线程可不可以实现高并发连接呢?当然是可行的,那就是IO多路复用。

IO多路复用 (I/O Multiplexing) 是一种允许多个 I/O 流共享同一个线程的技术。它通过一个机制,让单线程可以同时监听多个文件描述符(比如网络连接、文件、管道等),一旦某个描述符就绪(例如可以读写数据),系统就通知该线程,从而避免了线程阻塞在单个 I/O 操作上。

想象一下一个电话接线员,他可以同时接听多个电话线,当某个电话响了,他就去接听那个电话。这就是 IO 多路复用的精髓。 不像传统的阻塞式 I/O,每个电话线都需要一个单独的接线员去监听,IO 多路复用只用一个“接线员”就能高效地处理多个“电话”。

关键概念:

  • 单线程处理多个连接, 这是 IO 多路复用的核心优势。
  • 事件驱动: IO 多路复用依赖于操作系统提供的事件通知机制。当某个文件描述符就绪时,操作系统会通知应用程序,应用程序再根据事件处理相应的 I/O 操作。
  • 非阻塞 I/O: 通常与非阻塞 I/O 结合使用。非阻塞 I/O 调用不会阻塞线程,而是立即返回,即使 I/O 操作未完成。
  • 负责监听多个文件描述符的事件。

常用实现方式: select、poll、epoll (Linux)、kqueue (BSD)

优势:

  • 高效率: 单线程处理多个连接,减少了上下文切换开销。
  • 高并发: 可以处理大量的并发连接。
  • 资源消耗低: 相比多线程/多进程模型,资源消耗更低。

劣势:

  • 相比传统的阻塞式 I/O,编程复杂度略高。
  • 不同的操作系统有不同的 IO 多路复用实现方式。

二、IO多路复用器 select

本文先跟大家介绍一种最常见的IO多路复用机制:select。用于监视多个文件描述符,以便在某个或某些文件描述符就绪时进行相应的处理。

在这里插入图片描述

select 函数的基本原型如下:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

参数说明:

  • nfds: 监视的文件描述符数量(最大值 + 1)。
  • readfds: 监视可读的文件描述符集合(读事件)。
  • writefds: 监视可写的文件描述符集合(写事件)。
  • exceptfds: 监视异常条件的文件描述符集合。
  • timeout: 指定等待事件发生的最大时间。如果设置为 NULL,则 select 会无限期等待。

select的特点:

  • select 是一个 POSIX 标准函数,多数 UNIX/Linux 系统和 Windows 系统都支持 select
  • select 有一个文件描述符数目的限制,通常是 1024(取决于实现和系统配置)。
  • 虽然 select 可以处理多个文件描述符,但其性能在文件描述符数量较多时会降低,因为它需要线性扫描所有文件描述符集合来检测哪个文件描述符处于就绪状态。

select的第一个参数我们可以看得出,select内部实现是通过循环遍历所有的fd来确定每个fd的状态。所有,设置select的第一个参数时一定要比实际使用的fd还要大。

三、相关接口

在介绍接口之前,我们可以想一个问题:如何去标识一个 IO 的事件?事件一般只有两个状态:有与没有。IO 在Linux中是一个int类型的值。针对事件的两个状态,可以用一个bit标识,比如char类型有8bit,就可以用来标识8个 IO 的一个事件状态,比如一个 IO 的可读事件有还是没有。这就是接下来要讲的fd_set结构体。

3.1、fd_set 结构体

fd_set 是用于处理 I/O 多路复用的结构体,主要在调用 selectFD_SETFD_CLRFD_ISSET 等宏时使用,尤其是在使用 select 函数时。它的主要作用是用来表示一组文件描述符(存储文件描述符的集合),这些文件描述符可以是用于网络套接字、文件或其他 I/O 资源。在 <sys/select.h> 头文件中定义。

fd_set 可以概括为一个可以容纳多个文件描述符的位图(bit vector)。每一个 bit 代表一个文件描述符(通常从 0 开始),如果某个 bit 被设置,表示对应的文件描述符在集合中。

3.2、宏和函数

fd_set 一起使用的常见宏和操作包括:

  1. FD_ZERO(fd_set *set): 清空 fd_set 集合。
  2. FD_SET(int fd, fd_set *set): 将指定的文件描述符 fd 添加到 fd_set 集合中(即将对应比特位设置为 1)。
  3. FD_CLR(int fd, fd_set *set): 将指定的文件描述符 fdfd_set 集合中移除。
  4. FD_ISSET(int fd, fd_set *set): 检查指定的文件描述符 fd 是否在 fd_set 集合中。

FD_SET(int fd, fd_set *set)将对应比特位设置以后,通过select函数带到内核里面去,然后看它有没有事件发生。

四、select 实现 TCP 服务器

  1. 使用 FD_ZEROFD_SET 宏来初始化和设置文件描述符集合

  2. 传入相关参数,循环调用 select 函数。

  3. select 返回后,可以通过循环检查文件描述符集合来确定哪些文件描述符就绪,并进行相应的处理。

我们这里把select封装到一个类里面,方便调用。

定义一个类(server.h):

#include <sys/socket.h>
#include <netinet/in.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>#include <iostream>
#include <cstring>
#include <string>#define BUFFER_LEN      4096class TcpServerSelect {enum IoMode { Blocking, NonBlocking };
public:TcpServerSelect() : _port(8080), _listenBlock(20), _listenfd(-1), _ioMode(Blocking), _finished(false) {}int     initializer();void    run();void    setMode(int mode) { _ioMode = mode; }void    setBlock(int num) { _listenBlock = num; }void    setPort(short port) { _port = port; }void    setFinished(bool finished) { _finished = finished; }protected:bool    setIoMode(int fd, int mode);void    acceptConnect(int& maxfd, fd_set& rfds);void    recvData(int clientfd, fd_set& wfds, fd_set& rfds);void    sendData(int clientfd, fd_set& wfds, fd_set& rfds);protected:short       _port;int         _listenBlock;int         _listenfd;int         _ioMode;bool        _finished;std::string _strData;
};

功能实现(server.cpp):

int 
TcpServerSelect::initializer()
{if (_listenfd > 0) {std::cout << "The listening port already exists. listen fd " << _listenfd << std::endl;return 0;}// 1. Create socket_listenfd = socket(AF_INET, SOCK_STREAM, 0);if (_listenfd == -1) {std::cout << "socket return " << errno << ", " << strerror(errno) << std::endl;return -1;}// 2. Set the port and bind it.sockaddr_in serverAddr;memset(&serverAddr, 0, sizeof(sockaddr_in));serverAddr.sin_family = AF_INET;serverAddr.sin_addr.s_addr = htons(INADDR_ANY); // bind ip address.serverAddr.sin_port = htons(_port);  // bind port.if (bind(_listenfd, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {std::cout << "bind return " << errno << ", " << strerror(errno) << std::endl;return -2;}if (_ioMode == NonBlocking) {// set nonblock mode.setIoMode(_listenfd, O_NONBLOCK);}// 3. listening port.if (listen(_listenfd, _listenBlock) == -1) {std::cout << "listen return " << errno << ", " << strerror(errno) << std::endl;return -3;}std::cout << "server listening port " << _port << std::endl;return 0;
}void    
TcpServerSelect::run()
{if (_listenfd < 0) {std::cout << "Initialization not completed." << std::endl;return;}fd_set wfds, rfds, exceptfds;FD_ZERO(&wfds);FD_ZERO(&exceptfds);FD_ZERO(&rfds);FD_SET(_listenfd, &rfds);struct timeval timeout;// 设置超时时间timeout.tv_sec = 5; // 5秒timeout.tv_usec = 0;// 小技巧:通过中间变量将判断位和修改位分开。fd_set wset, rset;// 遍历多少个fdint maxfd = _listenfd;while (!_finished) {// 4. selectwset = wfds;rset = rfds;// 注意,这里的参数是使用的rset和wset,将判断位和修改位分开。int ret = select(maxfd + 1, &rset, &wset, &exceptfds, &timeout);if (ret == 0)continue;if (ret < 0) {std::cout << "select return error: " << errno << ". " << strerror(errno) << std::endl;continue;}if (FD_ISSET(_listenfd, &rset))acceptConnect(maxfd, rfds);for (int i = _listenfd + 1; i <= maxfd; ++i) {if (FD_ISSET(i, &rset))recvData(i, wfds, rfds);else if (FD_ISSET(i, &wset))sendData(i, wfds, rfds);}}close(_listenfd);_listenfd = -1;
}bool 
TcpServerSelect::setIoMode(int fd, int mode)
{int flag = fcntl(fd, F_GETFL, 0);if (flag == -1) {std::cout << "fcntl get flags return " << errno << ", " << strerror(errno) << std::endl;return false;}flag |= mode;if (fcntl(fd, F_SETFL, flag) == -1) {std::cout << "fcntl set flags return " << errno << ", " << strerror(errno) << std::endl;return false;}return true;
}void    
TcpServerSelect::acceptConnect(int& maxfd, fd_set& rfds)
{// 4. accept connect.sockaddr_in clientAddr;memset(&clientAddr, 0, sizeof(clientAddr));socklen_t clienLen = sizeof(clientAddr);int clientfd = accept(_listenfd, (sockaddr *)&clientAddr, &clienLen);if (clientfd == -1) {std::cout << "accept return " << errno << ", " << strerror(errno) << std::endl;return;}std::cout << "client fd " << clientfd << std::endl;FD_SET(clientfd, &rfds);if (clientfd > maxfd)maxfd = clientfd;
}void    
TcpServerSelect::recvData(int clientfd, fd_set& wfds, fd_set& rfds)
{// 6. recv messagechar buffer[BUFFER_LEN];int ret = recv(clientfd, buffer, BUFFER_LEN, 0);if (ret == 0) {std::cout << "client " << clientfd << " connection dropped" << std::endl;close(clientfd);// clear read ready.FD_CLR(clientfd, &rfds);return;} else if (ret == -1) {std::cout << "recv buffer return " << errno << ", " << strerror(errno) << std::endl;return;}std::cout << "recv buffer from client "<< clientfd << ": " << buffer << std::endl;_strData = buffer;FD_CLR(clientfd, &rfds);// set fd send data ready.FD_SET(clientfd, &wfds);
}void    
TcpServerSelect::sendData(int clientfd, fd_set& wfds, fd_set& rfds)
{if (_strData.empty())_strData = "Hello, Client!";// 5. send message.if (send(clientfd, _strData.c_str(), _strData.size(), 0) == -1) {std::cout << "send buffer return " << errno << ", " << strerror(errno) << std::endl;return;}// clear write fdFD_CLR(clientfd, &wfds);// set fd send data ready.FD_SET(clientfd, &rfds);}

代码中使用了这样一段赋值操作:

fd_set wset, rset;
// ......
wset = wfds;
rset = rfds;
// ......

这样做的目的是为了将判断位和修改位分开,避免直接对同一个变量做操作造成问题;但是,一定要捋清楚判断位和修改位的使用,不要混淆。

另外,要注意到,对于listenfd来说,调用accept后缓冲区就清空了,状态就自动转换为非就绪。

使用:

int main(int argc, char**argv)
{TcpServerSelect server;if (server.initializer() != 0)return -1;server.run();
}

五、总结

IO多路复用的作用是用来检测 IO是否有事件;这里所谓的“事件”就是“可读”、“可写”。IO 多路复用器select可以管理所有的IO,select 在处理大规模文件描述符时存在性能瓶颈(例如,文件描述符数量有限制)。

通过select我们理解了 IO 的可读事件、可写事件。明白了多个客户端访问一个服务器应该怎么做。

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/479855.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HDU Go Running(最小点覆盖 + 网络流优化)

题目大意&#xff1a;有一条无限长跑道&#xff0c;每个人可以规定自己跑步的方向&#xff0c;起点&#xff0c;跑步起止时间。每个人跑步的速度都是1m/s。最后从监控人员哪里得到了n个报告&#xff0c;每个报告给出了某人在某一时候所在的位置&#xff0c;问跑步的最少可能人数…

28.UE5实现对话系统

目录 1.对话结构的设计&#xff08;重点&#xff09; 2.NPC对话接口的实现 2.1创建类型为pawn的蓝图 2.2创建对话接口 3.对话组件的创建 4.对话的UI设计 4.1UI_对话内容 4.2UI_对话选项 4.3UI_对话选项框 5.对话组件的逻辑实现 通过组件蓝图&#xff0c;也就是下图中的…

Reachy 2,专为AI与机器人实验室打造的卓越开源双臂移动操作平台!

近期&#xff0c;花粉机器人&#xff08;POLLEN ROBOTICS&#xff09;隆重推出Reachy 2仿生机器人——下一代开源操作平台&#xff0c;为AI与机器人实验室带来理想的双臂移动操作科研平台&#xff01; Reachy 2的仿生性&#xff1a; 》拥有两个基于Maxon无刷电机的仿生7自由度…

python的openpyxl库设置表格样式:字体/边框/对齐/颜色等

学习目录 1. 安装和使用openpyxl库设置表格样式 2 设置字体font 3 设置边框 4 设置对齐方式 5 设置单元格数据格式 6 设置行高和列宽 7 填充单元格颜色 附录-关于颜色说明 本章节主要介绍如何使用openpyxl库设置表格中的一些样式&#xff0c;比如字体&#xff0c;边框…

Git旧文件覆盖引发思考

一天&#xff0c;我的同事过来找到我&#xff0c;和我讲&#xff1a;张叫兽&#xff0c;大事不好&#xff0c;我的文件被人覆盖了。git是真的不好用啊 git不好用&#xff1f;文件被覆盖&#xff1b;瞬间我似乎知道了什么&#xff0c;让我想到了某位男明星的语法&#xff1a;他…

QSqlTableModel的使用

实例功能 这边使用一个实例显示数据库 demodb 中 employee 数据表的内容&#xff0c;实现编辑、插入、删除的操作&#xff0c;实现数据的排序和记录过滤&#xff0c;还实现 BLOB 类型字段 Photo 中存储照片的显示、导入等操作&#xff0c;运行界面如下图&#xff1a; 在上图中…

什么是代理,nodenginx前端代理详解

一. 什么是代理&#xff1f; 代理就是通过一个特殊的网络服务去访问另一网络服务的一种间接访问方式。像我们不能直接访问国外的网站&#xff0c;只能使用VPN&#xff0c;就是使用了代理 二. 前端为什么要用代理&#xff1f; 首先明确以下两个概念 &#xff08;1&#xff09…

java脚手架系列16-AI大模型集成

之所以想写这一系列&#xff0c;是因为之前工作过程中有几次项目是从零开始搭建的&#xff0c;而且项目涉及的内容还不少。在这过程中&#xff0c;遇到了很多棘手的非业务问题&#xff0c;在不断实践过程中慢慢积累出一些基本的实践经验&#xff0c;认为这些与业务无关的基本的…

网络安全中的数据科学如何重新定义安全实践?

组织每天处理大量数据&#xff0c;这些数据由各个团队和部门管理。这使得全面了解潜在威胁变得非常困难&#xff0c;常常导致疏忽。以前&#xff0c;公司依靠 FUD 方法&#xff08;恐惧、不确定性和怀疑&#xff09;来识别潜在攻击。然而&#xff0c;将数据科学集成到网络安全中…

【算法day1】数组:双指针算法

题目引用 这里以 1、LeetCode704.二分查找 2、LeetCode27.移除元素 3、LeetCode977.有序数组的平方 这三道题举例来说明数组中双指针的妙用。 1、二分查找 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜…

open-instruct框架使用记录:只使用huggingface数据集的小部分进行训练,如何修改dataset_info.json文件

open-instruct框架 这篇笔记主要记录以下问题&#xff1a;只使用huggingface下载的数据集中的一小部分数据进行数据训练。而且我不想修改open-instruct的加载数据集的代码&#xff0c;以及脚本中的--dataset_mixer_list参数的指定等。下面是我的思路历程。 if args.dataset_na…

Jenkins升级到最新版本后无法启动

1. 场景还原 最近在web界面将jenkins升级到最新版本后&#xff0c;后台无法启动jenkins服务&#xff0c;服务状态如下&#xff1a; 运行jenkins命令提示invalid Java version jenkins --version jenkins: invalid Java version: java version "1.8.0_202" Java(TM)…

DRM(数字权限管理技术)防截屏录屏----ffmpeg安装

提示&#xff1a;ffmpeg安装 文章目录 [TOC](文章目录) 前言一、下载二、配置环境变量三、运行ffmpeg四、文档总结 前言 FFmpeg是一套可以用来记录、转换数字音频、视频&#xff0c;并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的…

Unity版本使用情况统计(更新至2024年11月)

UWA发布&#xff5c;本期UWA发布的内容是第十五期Unity版本使用统计&#xff0c;统计周期为2024年5月至2024年11月&#xff0c;数据来源于UWA网站&#xff08;www.uwa4d.com&#xff09;性能诊断提测的项目。希望给Unity开发者提供相关的行业趋势作为参考。 2024年5月 - 2024年…

Spring Aop 中对JoinPoint的理解

以下是源码中对 JoinPoint 的描述 A runtime joinpoint is an event that occurs on a static joinpoint (i.e. a location in a program). For instance, an invocation is the runtime joinpoint on a method (static joinpoint). The static part of a given joinpoint can…

C中指针在64位操作系统下为什么是4而不是8

好久没写C了&#xff0c;今天用VScode想写个Demo&#xff0c;翻了下指针资料&#xff0c;想打印下指针大小&#xff0c;发现是4&#xff0c;但是理论上64位系统不应该是8么&#xff1f; 结论就是我编的是32位程序&#xff0c;编译器按照32位编译的。 用vscode的C 插件编译运行…

使用 pycharm 新建不使用 python 虚拟环境( venv、conda )的工程

有时候我们发现一个好玩的 demo&#xff0c;想赶快在电脑上 pip install 一下跑起来&#xff0c;发现因为 python 的 venv、conda 环境还挺费劲的&#xff0c;因为随着时间的发展&#xff0c;之前记得很清楚的 venv、conda 的用法&#xff0c;不经常使用&#xff0c;半天跑不起…

鸿蒙主流路由详解

鸿蒙主流路由详解 Navigation Navigation更适合于一次开发,多端部署,也是官方主流推荐的一种路由控制方式,但是,使用起来入侵耦合度高,所以,一般会使用HMRouter,这也是官方主流推荐的路由 Navigation官网地址 个人源码地址 路由跳转 第一步-定义路由栈 Provide(PageInfo) pag…

Flink Sink的使用

经过一系列Transformation转换操作后&#xff0c;最后一定要调用Sink操作&#xff0c;才会形成一个完整的DataFlow拓扑。只有调用了Sink操作&#xff0c;才会产生最终的计算结果&#xff0c;这些数据可以写入到的文件、输出到指定的网络端口、消息中间件、外部的文件系统或者是…

鸿蒙本地模拟器 模拟TCP服务端的过程

鸿蒙模拟器模拟TCP服务端的过程涉及几个关键步骤&#xff0c;主要包括创建TCPSocketServer实例、绑定IP地址和端口、监听连接请求、接收和发送数据以及处理连接事件。以下是详细的模拟过程&#xff1a; **1.创建TCPSocketServer实例&#xff1a;**首先&#xff0c;需要导入鸿蒙…