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 多路复用的结构体,主要在调用 select
、FD_SET
、FD_CLR
、FD_ISSET
等宏时使用,尤其是在使用 select
函数时。它的主要作用是用来表示一组文件描述符(存储文件描述符的集合),这些文件描述符可以是用于网络套接字、文件或其他 I/O 资源。在 <sys/select.h>
头文件中定义。
fd_set
可以概括为一个可以容纳多个文件描述符的位图(bit vector)。每一个 bit 代表一个文件描述符(通常从 0 开始),如果某个 bit 被设置,表示对应的文件描述符在集合中。
3.2、宏和函数
与 fd_set
一起使用的常见宏和操作包括:
FD_ZERO(fd_set *set)
: 清空fd_set
集合。FD_SET(int fd, fd_set *set)
: 将指定的文件描述符fd
添加到fd_set
集合中(即将对应比特位设置为 1)。FD_CLR(int fd, fd_set *set)
: 将指定的文件描述符fd
从fd_set
集合中移除。FD_ISSET(int fd, fd_set *set)
: 检查指定的文件描述符fd
是否在fd_set
集合中。
FD_SET(int fd, fd_set *set)
将对应比特位设置以后,通过select
函数带到内核里面去,然后看它有没有事件发生。
四、select 实现 TCP 服务器
-
使用
FD_ZERO
和FD_SET
宏来初始化和设置文件描述符集合。 -
传入相关参数,循环调用
select
函数。 -
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 的可读事件、可写事件。明白了多个客户端访问一个服务器应该怎么做。
相关文章:
C++网络编程:select IO多路复用及TCP服务器开发
C网络编程:使用select实现IO多路复用 一、什么是 IO 多路复用?二、IO多路复用器 select三、相关接口3.1、fd_set 结构体3.2、宏和函数 四、select 实现 TCP 服务器五、总结 一、什么是 IO 多路复用? 在网络编程中,最容易想到的并…...
部署 L2JMobius 天堂2芙蕾雅版本
首先下载所需要的服务器端 “L2J_Mobius.zip” 和芙蕾雅客户端(三个压缩文件), 我的网盘下载:https://pan.baidu.com/s/1XdlcCFPvXnzfwFoVK7Sn7Q?pwdavd4 所有文件都在“芙蕾雅”目录下,也可以加入企鹅交流裙 87470…...
C#开发合集
用C#轻松搞定m3u8视频下载与合并 嘿,程序员们!今天咱们来聊聊如何用C#写个小程序,轻松下载和合并m3u8视频文件。没错,就是那种分段的流媒体视频。准备好了吗?让我们开始吧! 准备工作 在动手之前…...
鸿蒙面试 --- 性能优化
性能优化可以从三个方面入手 感知流畅、渲染性能、运行性能 感知流畅 在应用开发中,动画可以为用户界面增添生动、流畅的交互效果,提升用户对应用的好感度。然而,滥用动画也会导致应用性能下降,消耗过多的系统资源,…...
React的基础知识:Context
1. Context 在 React 中,Context 提供了一种通过组件树传递数据的方式,无需手动在每个层级传递 props。这在处理一些全局应用状态时非常有用,比如用户认证、主题、语言偏好等。 如何使用 Context 创建 Context:首先,…...
微知-lspci访问到指定的PCIe设备的几种方式?(lspci -s bus;lspci -d devices)
通过bdf号查看 -s (bus) lspci -s 03:00.0通过vendor id或者device id等设备查看 -d (device) lspci -d 15b3: #这里是vendor号,所以在前面 lspci -d :1021 #这里是设备号,所以要:在前vendorid和deviceid…...
【Kubernetes 集群核心概念:Pod】pod生命周期介绍【五】
5.1 Pod生命周期 Pod的生命周期指的是从Pod创建到终止的整个过程。它分为以下两种常见情况: 长期运行Pod: 例如运行HTTP服务的Pod,它在正常情况下会一直运行,但可以手动删除或终止。短期运行Pod: 例如执行计算任务的…...
c++的虚继承说明、案例、代码
虚继承的基本概念 在 C 中,虚继承主要用于解决多继承时可能出现的菱形继承问题。菱形继承是指一个类有两个(或更多)子类,而这两个子类又同时继承自一个共同的基类,当这些子类又被另一个类继承时,就形成了菱…...
小米PC电脑手机互联互通,小米妙享,小米电脑管家,老款小米笔记本怎么使用,其他品牌笔记本怎么使用,一分钟教会你
说在前面 之前我们体验过妙享中心,里面就有互联互通的全部能力,现在有了小米电脑管家,老款的笔记本竟然用不了,也可以理解,毕竟老款笔记本做系统研发的时候没有预留适配的文件补丁,至于其他品牌的winPC小米…...
介绍SSD硬盘
SSD硬盘(固态硬盘,Solid State Drive)是一种利用闪存技术存储数据的存储设备,与传统的机械硬盘(HDD)不同,SSD没有任何活动部件,因此其性能和耐用性较为优越。以下是SSD硬盘的一些主要…...
CMAKE常用命令详解
NDK List基本用法 Get–获取列表中指定索引的元素 list(Get list_name index output_var)解释 list_name: 要操作集合的名称index: 要取得的元素下标output_var: 保存从集合中取得元素的结果 栗子 list(GET mylist 0 first_element) # 获取第一个元素APPEND–在列表末尾…...
Vue3的通灵之术Teleport
前言 近期Vue3更新了一些新的内容,我都还没有一个一个仔细去看,但是还是有必要去解读一下新内容的。就先从Teleport 开始吧。 官方对 Teleport 的解释是:<Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传…...
ue5第三人称闯关游戏学习(一)
视频资料38 - Compilers and Editors_哔哩哔哩_bilibili 上一个第一人称射击项目做完 接下来要更深入学习。 引入资产与C来创建第三人称闯关游戏 这次要引入的资产有两个分别是 Unreal Learning Kit:Game和stylized character kit: casual 01 不过有个比较麻…...
IIC 随机写+多次写 可以控制写几次
verilog module icc_tx#(parameter SIZE 2 , //用来控制写多少次 比如地址是0000 一个地址只能存放8bit数据 超出指针就会到下一个地址0001parameter CLK_DIV 50_000_000 ,parameter SPEED 100_000 ,parameter LED 50 )( input wire c…...
controller中的参数注解@Param @RequestParam和@RequestBody的不同
现在controller中有个方法:(LoginUserRequest是一个用户类对象) PostMapping("/test/phone")public Result validPhone(LoginUserRequest loginUserRequest) {return Result.success(loginUserRequest);}现在讨论Param("login…...
手搓人工智能-最优化算法(1)最速梯度下降法,及推导过程
“Men pass away, but their deeds abide.” 人终有一死,但是他们的业绩将永存。 ——奥古斯坦-路易柯西 目录 前言 简单函数求极值 复杂函数梯度法求极值 泰勒展开 梯度,Nabla算子 Cauchy-Schwarz不等式 梯度下降算法 算法流程 梯度下降法…...
多目标优化算法——多目标粒子群优化算法(MOPSO)
Handling Multiple Objectives With Particle Swarm Optimization(多目标粒子群优化算法) 一、摘要: 本文提出了一种将帕累托优势引入粒子群优化算法的方法,使该算法能够处理具有多个目标函数的问题。与目前其他将粒子群算法扩展…...
Swift——自动引用计数ARC
ARC ARC是swift使用的一种管理应用程序内存的机制,对于C语言我们知道,当我们申请一块空间,通常需要手动释放,不然会造成空间浪费,而有了ARC机制,你无需考虑内存的管理,因为ARC会在类的实例不再…...
【Quarkus】基于CDI和拦截器实现AOP功能(进阶版)
Quarkus 基于CDI和拦截器实现AOP功能(进阶版) 拦截器的属性成员拦截器的重复使用基于属性成员和重复使用的拦截器的发消息案例 本节来了解一下拦截器高级特性(拦截器的重复使用和属性成员),官网说明:https:…...
【踩坑日记】【教程】如何在ubuntu服务器上配置公钥登录以及bug解决
前言 在日常开发和运维中,为了提高服务器登录的安全性,我们通常会选择使用 SSH 密钥认证 来替代传统的密码登录。然而,在配置 SSH 公钥登录的过程中,可能会遇到各种坑和 Bug。本文将从零开始,手把手教你如何在 Ubuntu…...
insmod一个ko提供基础函数供后insmod的ko使用的方法
一、背景 在内核模块开发时,多个不同的内核模块,有时候可能需要都共用一些公共的函数,比如申请一些平台性的公共资源。但是,这些公共的函数又不方便去加入到内核镜像里,这时候就需要把这些各个内核模块需要用到的一些…...
七、传统循环神经网络(RNN)
传统循环神经网络 RNN 前言一、RNN 是什么?1.1 RNN 的结构1.2 结构举例 二、RNN 模型的分类2.1 按照 输入跟输出 的结构分类2.2 按照 内部结构 分类 三、传统 RNN 模型3.1 RNN内部结构图3.2 内部计算公式3.3 其中 tanh 激活函数的作用3.4 传统RNN优缺点 四、代码演示…...
LeetCode:19.删除链表倒数第N个节点
跟着carl学算法,本系列博客仅做个人记录,建议大家都去看carl本人的博客,写的真的很好的! 代码随想录 LeetCode:19.删除链表倒数第N个节点 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表…...
【RISC-V CPU debug 专栏 2 -- Debug Module (DM), non-ISA】
文章目录 调试模块(DM)功能必须支持的功能可选支持的功能兼容性要求规模限制Debug Module Interface (DMI)总线类型地址与操作地址空间控制机制Debug Module Interface Signals请求信号响应信号信号流程Reset Control复位控制方法全局复位 (`ndmreset`)Hart 复位 (`hartreset…...
单片机学习笔记 11. 外部中断
更多单片机学习笔记:单片机学习笔记 1. 点亮一个LED灯单片机学习笔记 2. LED灯闪烁单片机学习笔记 3. LED灯流水灯单片机学习笔记 4. 蜂鸣器滴~滴~滴~单片机学习笔记 5. 数码管静态显示单片机学习笔记 6. 数码管动态显示单片机学习笔记 7. 独立键盘单片机学习笔记 8…...
基于stm32的智能教室管理系统/智能家居系统
基于stm32的智能教室管理系统/智能家居系统 持续更新,欢迎关注!!! ** 基于stm32的智能教室管理系统/智能家居系统 ** 目前,物联网已广泛应用在我们的生活中。智慧校园是将校园中的生活、学习、工作等相关的资源联系在一起,实现管理的智能化…...
基于 Qt 和 GStreamer 的环境中构建播放器
一、功能与需求分析 功能描述 播放本地视频文件(如 MP4、MKV)。 支持基本控制功能(播放、暂停、停止、跳转)。 提供音量调节功能。 在 Windows 环境下使用 Visual Studio 2022 编译。 技术选型 Qt:用于构建用户界面。 GStreamer:负责视频解码和播放。 Visual Studio 202…...
windows docker 入门
这个教程将指导你如何安装Docker、运行第一个容器以及理解一些基本概念。 第一步:安装Docker Desktop for Windows 系统要求: Windows 10 64位版本(专业版、企业版或教育版)。启用Hyper-V和Windows Subsystem for Linux (WSL 2)。…...
baomidou Mabatis plus引入异常
1 主要异常信息 Error creating bean with name dataSource 但是有个重要提示 dynamic-datasource Please check the setting of primary 解决方法: <dependency><groupId>com.baomidou</groupId><artifactId>dynamic-datasource-spring…...
深度学习中的正则化模型是什么意思?
一、定义 在深度学习中,正则化是一种用于防止过拟合的技术。过拟合是指模型在训练数据上表现非常好,但在新的、未见过的数据(测试数据)上表现很差的情况。正则化模型就是通过在损失函数中添加额外的项来约束模型的复杂度…...
有做学业水平测试的网站/网站怎么营销推广
Ubuntu server下搭建Maven私服Nexus Maven私服Nexus的作用,主要是为了节省资源,在内部作为maven开发资源共享服务器来使用。 1、下载 通过root用户进去Ubuntu server $ cd /opt $ wget http://download.sonatype.com/nexus/oss/nexus-2.10.0-02-bu…...
做网站哪个最好/互联网推广是做什么的
《程序员》第10期发表了我写的“Fortran 2003:完美还是虚幻?”一文。我把该文的节选贴在这里吧,要阅读全文的话,可以找《程序员》杂志来看。 Fortran 2003:完美还是虚幻?(节选)王咏刚…...
wordpress get term/原创文章代写平台
(1)注册登录:注册普通账号登录;登录后可以修改用户的基本信息,也可以退出。 (2)浏览资讯:浏览网站管理发布的资讯,可以评论,评论后需要管理员审核和查看。也可以收藏资讯。 (3)关于我们:浏览网…...
2018淘宝客网站开发/网络推广策划书
GAT是MTK在DDMS基础上进行二次开发封装的一个集多种debug功能为一体的工具,除 了包含原有DDMS的功能以外还支持kernel抓取,获取native进程列表,backtrace decode,提取手机端debug信息等功能;当MTKLogger出现异常时&am…...
网站建设与维护服务/爱客crm
1117 聪明的木匠 1 秒 131,072 KB 20 分 3 级题 思路: 第一次切割Li, 花费体力:M 第二次切割Lj,花费体力:M-Li 第三次切割Ls,花费体力:M-Li-Lj N次切割后花费体力为:M-Li-Lj-……-Lk 那么&#…...
网站开发和安卓开发/天津放心站内优化seo
附件:Android-kali.zip:http://pan.baidu.com/s/1gdh7YjL 里面有linux Deploy,VNC,SSH以及最重要的linux.img。如果有不可以用的,自己再下新的就行了。 1.安装linux Deploy与配置 这里要注意的是发行版和架构的设置,如…...