多路转接(上)——select

目录
一、select接口
1.认识select系统调用
2.对各个参数的认识
二、编写select服务器
1.两个工具类
2.网络套接字封装
3.服务器类编写
4.源文件编写
5.运行
一、select接口
1.认识select系统调用
int select(int nfds, fd_set readfds, fd_set writefds, fd_set exceptfds, struct timeval* timeout);
头文件:sys/time.h、sys/types.h、unistd.h
功能:select负责IO中多个描述符等的那部分,该函数会在描述符的读写异常事件就绪时,提醒进程进行处理。
参数:后面讲。
返回值:返回值大于0表示有相应个文件描述符就绪,返回值等于0表示没有文件描述符就绪,超时返回,返回值小于0表示select调用失败。
2.对各个参数的认识
(1)struct timeval* timeout
它是一个struct timeval类型的类指针,定义如下:
struct timeval
{time_t tv_sec; /* Seconds. */suseconds_t tv_usec; /* Microseconds. */
};
内部有两个成员,第一个是秒,第二个是微秒。
这个timeout如果传参nullptr,默认select阻塞等待,只有当一个或者多个文件描述符就绪时才会通知上层进程去读取数据。
参数如果传入struct timeval timeout = {0, 0},秒和微秒时间都设置成0,此时select就使用非阻塞等待,需要程序员编写轮询检测代码。
参数如果设置了具体值,如struct timeval timeout = {5, 0},时间设置成5秒,select就会在5秒内阻塞等待,如果在5秒内有文件描述符就绪,则通知上层;如果没有则超时返回。
(2)int nfds
表示要等待的所有文件描述符中的最大值加一。
假设要等待3个文件描述符,分别为3、4和7,则传参时就需要传7+1=8给nfds。
(3)fd_set
fd_set是一个等待读取就绪文件描述符的位图。
它的每一个比特位代表一个文件描述符,比特位的状0和1表示该比特位是否被select监听。
下面就是fd_set位图的示意图,表示偏移量为1、3、5的三个文件描述符需要被select监视。

fd_set类型的大小为128字节,每个字节有8个比特位,所以fd_set类型能包含1024个比特位,也表明select最多能监视1024个文件描述符。
虽然我们知道fd_set属于位图结构,但是我们并不清楚其内部实现。
所以在对位图数据进行增删 查改时一定要使用系统提供的增删查改接口。
- FD_ZERO(fd_set *fdset);——将fd_set清零,集合中不含任何文件描述符,可用于初始化
- FD_SET(int fd, fd_set *fdset);——将fd加入fd_set集合
- FD_CLR(int fd, fd_set *fdset);——将fd从fd_set集合中移除
- FD_ISSET(int fd, fd_set *fdset);——检测fd是否在fd_set集合中,不在则返回0
(4)fd_set* reads
fd_set* reads、fd_set* writefds、fd_set* exceptfds中间的这三个参数属于输出型参数。
在这里我以fd_set* reads为例进行讲解。
fd_set* reads表示读位图,传递的参数表示需要被监视的文件描述符,而且select只关心是这些文件描述符内否有数据需要被读取。
假如说,我们定义了一个fd_set变量使用FD_SET将文件描述符1、3、5填入变量,最后将该变量的指针传入函数。

在select正常返回或超时返回时,它会更改这个变量。
比方说,select调用完成后将位图改为下面的样式,表明文件描述符1、3准备好了,可以由系统调用去读取。由于两个文件描述符就绪,所以返回值为2。

在下次进行select调用时,我们还能再次修改该位图,增加或减少需要监听的文件描述符。
select再次返回时,该位图依旧会被修改,从而指示在这一次调用后哪些文件描述符已经准备就绪。
也就是说,传参时这个位图代表需要监听的描述符,调用返回时这个位图代表已就绪的文件描述符。
fd_set* reads与fd_set* writefds、fd_set* exceptfds在使用上是一样的,只不过fd_set* writefds只关心进程向文件描述符中写数据的操作,而fd_set* exceptfds只关心该文件描述符是否出现了错误。
它们也会以同样的方式修改自己对应的fd_set变量,从而达到通知进程的目的。
二、编写select服务器
1.两个工具类
代码需要使用两个工具类,err.hpp储存所有的错误码,原来打印日志的log.hpp也继续使用。
err.hpp
#pragma once#include<iostream>enum errorcode
{USAGE_ERROR = 1,SOCKET_ERROR,BIND_ERROR,LISTEN_ERROR
};
log.hpp
#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<time.h>
#include<stdarg.h>//一个文件用于保存正常运行的日志,一个保存错误日志
#define LOG_FILE "./log.txt"
#define ERROR_FILE "./error.txt"//按照当前程序运行的状态,定义五个宏
//NORMAL表示正常,WARNING表示有问题但程序也可运行,ERROR表示普通错误,FATAL表示严重错误
#define DEBUG 0
#define NORMAL 1
#define WARNING 2
#define ERROR 3
#define FATAL 4//将运行等级转换为字符串
const char* to_string(int level)
{switch(level){case(DEBUG):return "DEBUG";case(NORMAL):return "NORMAL";case(WARNING):return "WARNING";case(ERROR):return "ERROR";case(FATAL):return "FATAL";default:return nullptr;}
}//将固定格式的日志输出到屏幕和文件中
//第一个参数是等级,第二个参数是需要输出的字符串
void logmessage(int level, const char* format, ...)
{//输出到屏幕char logprefix[1024];snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid:%d]", to_string(level), time(nullptr), getpid());//按一定格式将错误放入字符串char logcontent[1024];va_list arg;//可变参数列表va_start(arg, format);vsnprintf(logcontent, sizeof(logcontent), format, arg);std::cout << logprefix << logcontent << std::endl;//输出到文件中//打开两个文件FILE* log = fopen(LOG_FILE, "a");FILE* err = fopen(ERROR_FILE, "a");if(log != nullptr && err != nullptr){FILE* cur = nullptr;if(level == DEBUG || level == NORMAL || level == WARNING)cur = log;if(level == ERROR || level == FATAL)cur = err;if(cur)fprintf(cur, "%s%s\n", logprefix, logcontent);fclose(log);fclose(err);}
}
2.网络套接字封装
将之前写的socket、bind、accept等函数封装到一个Sock类中。
#pragma once
#include<iostream>
#include<string>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"log.hpp"
#include"err.hpp"class Sock
{
private:static const int backlog = 32;//队列长度为32
public:static int Socket(){int listensock = socket(AF_INET, SOCK_STREAM, 0);//创建套接字if(listensock < 0)//创建套接字失败打印错误原因{logmessage(FATAL, "create socket error");//socket失败属于最严重的错误exit(SOCKET_ERROR);//退出}logmessage(NORMAL, "create socket success:%d", listensock);//创建套接字成功,打印让用户观察到//打开端口复用保证程序退出后可以立即正常启动int opt = 1;setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));return listensock;}static void Bind(int listensock, int port){struct sockaddr_in local;//储存本地网络信息local.sin_family = AF_INET;//通信方式为网络通信local.sin_port = htons(port);//将网络字节序的端口号填入local.sin_addr.s_addr = INADDR_ANY;//INADDR_ANY就是ip地址0.0.0.0的宏if(bind(listensock, (struct sockaddr*)&local, sizeof(local)) < 0)//绑定IP,不成功打印信息{logmessage(FATAL, "bind socket error");//bind失败也属于最严重的错误exit(BIND_ERROR);//退出}logmessage(NORMAL, "bind socket success");//绑定IP成功,打印让用户观察到}static void Listen(int listensock){//listen设置socket为监听模式if(listen(listensock, backlog) < 0) // 第二个参数backlog后面在填这个坑{logmessage(FATAL, "listen socket error");exit(LISTEN_ERROR);}logmessage(NORMAL, "listen socket success");}static int Accept(int listensock, std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;//储存本地网络信息socklen_t len = sizeof(peer);int sock = accept(listensock, (struct sockaddr*)&peer, &len);if(sock < 0){logmessage(ERROR, "accept fail");//接收新文件描述符失败}else{logmessage(NORMAL, "accept a new link");//接收新文件描述符成功*clientip = inet_ntoa(peer.sin_addr);*clientport = ntohs(peer.sin_port);}return sock;}
};
3.服务器类编写
服务器类的相关函数都定义在selectserver.hpp内,而且我们实现的select服务器只关心读事件。
我大致说一下运行流程:在构造对象后,initserver创建套接字并初始化成员变量,start函数循环调用select函数,然后我们筛选出有效的有读事件就绪的描述符放入_fdarray数组,然后使用handler_read函数处理事件。
最后,在handler_read函数内判断描述符是普通描述符还是监听描述符。
普通描述符读事件就绪表示需要读取数据,我们实现一个Receiver进行处理;监听描述符读事件就绪表示有链接需要接收,我们实现一个Accepter函数进行处理。
#pragma once
#include<iostream>
#include<sys/time.h>
#include<sys/types.h>
#include<unistd.h>
#include<functional>
#include"sock.hpp"namespace select_func
{static const int default_port = 8080;//默认端口号为8080static const int fdnum = sizeof(fd_set) * 8;//最大端口号为1024static const int default_fd = -1;//将所有需要管理的文件描述符放入一个数组,-1是数组中的无效元素using func_t = std::function<std::string (const std::string&)>;class SelectServer{public:SelectServer(func_t func, int port = default_port):_listensock(-1),_port(default_port),_fdarray(nullptr),_func(func){}~SelectServer(){if(_listensock > 0)close(_listensock);//关闭监听文件描述符if(_fdarray)delete []_fdarray;//释放存储文件描述符的数组}void initserver(){//创建listen套接字,绑定端口号,设为监听状态_listensock = Sock::Socket();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);//构建一个储存所有需要管理的文件描述符的数组,并把数组所有元素置为-1_fdarray = new int[fdnum];for(int i = 0; i<fdnum; ++i){_fdarray[i] = default_fd;}//将listen套接字放在第一个,在程序运行的全过程中都不会被修改_fdarray[0] = _listensock;}void start(){while(1){//填写位图fd_set fds;FD_ZERO(&fds);int maxfd = _fdarray[0];//最初,012三个描述符默认打开,3为监听描述符,所以描述符的最大值为3for(int i = 0; i<fdnum; ++i){if(_fdarray[i] != default_fd)//筛选出有效的文件描述符{FD_SET(_fdarray[i], &fds);//将该文件描述符加入位图if(_fdarray[i] > maxfd)//fdarray储存有新增加的有效文件描述符maxfd = _fdarray[i];//maxfd需要根据元素增大}}//logmessage(NORMAL, "maxfd:%d", maxfd);//调用select//struct timeval timeout = {1, 0};int n = select(maxfd+1, &fds, nullptr, nullptr, nullptr);//非阻塞调用switch(n){case 0://没有描述符就绪logmessage(NORMAL, "time out.");break;case -1://select出错了logmessage(ERROR, "select error, error code:%d %s", errno, strerror(errno));break;default://有描述符就绪(获取链接就属于读就绪)//logmessage(NORMAL, "server get new tasks.");handler_read(fds);//处理数据break;}}}void Accepter(){//走到这里说明等的过程select已经完成了std::string clientip;uint16_t clientport = 0;//select只负责等,接收链接还是需要accept,但是这次调用不会阻塞了int sock = Sock::Accept(_listensock, &clientip, &clientport);if (sock < 0)//接收出错不执行return;logmessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);//链接已经被建立,新的描述符通信产生//这个描述符我们也要再次插入数组int i = 0;for(i = 0; i<fdnum; ++i){if(_fdarray[i] == default_fd) break;}if(i == fdnum)//数组满了{logmessage(WARNING, "server if full, please wait");close(sock);//关闭该链接}else_fdarray[i] = sock;//将数据插入数组print_list();//打印数组的内容}void Receiver(int sock, int pos){//接收客户端发来的数据char buffer[1024];ssize_t n = recv(sock, buffer, sizeof(buffer)-1, 0);if (n > 0){buffer[n] = 0;//在末尾加上/0logmessage(NORMAL, "client# %s", buffer);}else if (n == 0){close(sock);_fdarray[pos] = default_fd;logmessage(NORMAL, "client quit");return;}else{close(sock);_fdarray[pos] = default_fd;logmessage(ERROR, "client quit: %s", strerror(errno));return;}//使用回调函数处理数据std::string response = _func(buffer);//发回响应write(sock, response.c_str(), response.size());}void handler_read(fd_set& fds){//我们将读取数据的处理分为两种://第一种是获取到了新链接//第二种是有数据需要被读取for(int i = 0; i<fdnum; ++i){//筛选出有效的文件描述符if(_fdarray[i] != default_fd){//listensock就绪表示进程获取到了新链接if(FD_ISSET(_fdarray[i], &fds) && _fdarray[i] == _listensock)Accepter();//建立链接//其他普通文件描述符就绪else if(FD_ISSET(_fdarray[i], &fds))Receiver(_fdarray[i], i);//接收数据}}}void print_list(){std::cout << "fd list:" << std::endl;for(int i = 0; i<fdnum; ++i){if(_fdarray[i] != default_fd)std::cout << _fdarray[i] << " ";}std::cout << std::endl;}private:int _listensock;int _port;int* _fdarray;func_t _func;};
}
4.源文件编写
还是用老方法,initserver初始化,start开始运行,unique_ptr管理对象。
#include"selectserver.hpp"
#include"err.hpp"
#include<memory>using namespace std;
using namespace select_func;static void Usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}string transaction(const string& str)
{return str;
}int main(int argc, char *argv[])
{unique_ptr<SelectServer> p(new SelectServer(transaction));p->initserver();p->start();return 0;
}
5.运行
为了省事,我们直接使用telnet作为客户端即可。
下面我们对服务器进行连接,发送数据,查看其运行。
注意,telnet发送数据需要按Ctrl+],然后出现telnet>后,按Enter键后才能输入数据并按Enter发送。最后,如果向退出telnet,同样按Ctrl+],然后输入q或者quit,就能退出了。

相关文章:
多路转接(上)——select
目录 一、select接口 1.认识select系统调用 2.对各个参数的认识 二、编写select服务器 1.两个工具类 2.网络套接字封装 3.服务器类编写 4.源文件编写 5.运行 一、select接口 1.认识select系统调用 int select(int nfds, fd_set readfds, fd_set writefds, fd_set ex…...
基于SSM的图书管理借阅系统设计与实现
末尾获取源码 开发语言:Java Java开发工具:JDK1.8 后端框架:SSM 前端:采用JSP技术开发 数据库:MySQL5.7和Navicat管理工具结合 服务器:Tomcat8.5 开发软件:IDEA / Eclipse 是否Maven项目&#x…...
Python的内存优化
在Python中,内存管理和优化是一个复杂的话题,因为它涉及到Python解释器的内部机制,特别是Python的垃圾收集和内存分配策略。Python通过自动垃圾收集机制管理内存,主要包括引用计数和标记-清除算法。 Python内存管理机制ÿ…...
蓝桥杯-回文日期[Java]
目录: 学习目标: 学习内容: 学习时间: 题目: 题目描述: 输入描述: 输出描述: 输入输出样例: 示例 1: 运行限制: 题解: 思路: 学习目标: 刷蓝桥杯题库日记 学习内容: 编号498题目回文日期难度…...
acwing算法基础之搜索与图论--树与图的遍历
目录 1 基础知识2 模板3 工程化 1 基础知识 树和图的存储:邻接矩阵、邻接表。 树和图的遍历:dfs、bfs。 2 模板 树是一种特殊的图(即,无环连通图),与图的存储方式相同。 对于无向图中的边ab,…...
前端uniapp请求真是案例(带源码)
目录 案例一案例二最后 案例一 <template><view class"box"><!-- <view class"title-back" click"backPrivious"><</view> --><!-- <view class"title-back" click"backPrivious"…...
MySQL -- mysql connect
MySQL – mysql connect 文章目录 MySQL -- mysql connect一、Connector/C 使用1.环境安装2.尝试链接mysql client 二、MySQL接口1.初始化2.链接数据库3.下发mysql命令4.获取执行结果5.关闭mysql链接6.在C语言中连接MySQL 三、MySQL图形化界面推荐 使用C接口库来进行连接 一、…...
如何用AI帮你下载安卓源码
以Android 11源码下载流程图如下所示: 1. 安装Git和Repo工具 2. 创建一个工作目录 3. 初始化仓库并下载源码 4. 切换到指定的分支 5. 编译源码 具体步骤如下: 安装Git和Repo工具:在Linux或Mac上,可以通过终端运行以下命令安装Gi…...
第三章:人工智能深度学习教程-基础神经网络(第三节-Tensorflow 中的多层感知器学习)
在本文中,我们将了解多层感知器的概念及其使用 TensorFlow 库在 Python 中的实现。 多层感知器 多层感知也称为MLP。它是完全连接的密集层,可将任何输入维度转换为所需的维度。多层感知是具有多个层的神经网络。为了创建神经网络,我们将神…...
Python的版本如何查询?
要查询Python的版本,可以使用以下方法之一: 1.在命令行中使用python --version命令。这会显示安装在计算机上的Python解释器的版本号。 # Author : 小红牛 # 微信公众号:wdPython2.在Python脚本中使用import sys语句,然后打印sy…...
Git的高效使用 git的基础 高级用法
Git的高效使用 git的基础 高级用法 前言 什么是Git 在日常的软件开发过程中,软件版本的管理都离不开使用Git,Git是一个开源的分布式版本控制系统,可以有效、高速地处理从很小到非常大的项目版本管理。 也是Linus Torvalds为了帮助管理Linu…...
关于主表和子表数据的保存
业务需求: 投注站信息保存在表A里,投注站下的设备信息保存在表B里, 一个投注站会有多个设备,要在一个表单里进行投注站和设备信息的填写,保存,回填,修改。 思路: 1)将…...
如何在后台执行 SwiftData 操作
文章目录 前言Core Data 私有队列上下文SwiftData 并发支持使用 ModelActor合并上下文更改的问题通过标识符访问模型总结 前言 SwiftData 是一个用于处理数据操作的框架,特别是在 Swift 语言中进行并发操作。本文介绍了如何在后台执行 SwiftData 操作以及与 Core D…...
TCP和UPD协议
一)应用层协议简介:根据需求明确要传输的信息,明确要传输的数据格式; 应用层协议:这个协议,实际上是和程序员打交道最多的协议了 1)其它四层都是操作系统,驱动,硬件实现好了的,咱们是不需要管 2)应用层:当我…...
MySQL:锁机制
目录 概述三种层级的锁锁相关的 SQLMyISAM引擎下的锁InnoDB引擎下的锁InnoDB下的表锁和行锁InnoDB下的共享锁和排他锁InnoDB下的意向锁InnoDB下的记录锁,间隙锁,临键锁记录锁(Record Locks)间隙锁(Gap Locks࿰…...
软考-高级-系统架构设计师教程(清华第2版)【第1章-绪论-思维导图】
软考-高级-系统架构设计师教程(清华第2版)【第1章-绪论-思维导图】 课本里章节里所有蓝色字体的思维导图...
【Git】安装和常用命令的使用与讲解及项目搭建和团队开发的出现的问题并且给予解决
目录 Git的简介 介绍 Git的特点及概念 Git与SVN的区别 图解 编辑 命令使用 安装 使用前准备 搭建项目环境 编辑 团队开发 Git的简介 介绍 Git 是一种分布式版本控制系统,是由 Linux 之父 Linus Torvalds 于2005年创建的。Git 的设计目标是为了更好地管…...
Python进行数据可视化,探索和发现数据中的模式和趋势。
文章目录 前言第一步:导入必要的库第二步:加载数据第三步:创建基本图表第四步:添加更多细节第五步:使用Seaborn库创建更复杂的图表关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Pyth…...
2023年中国自然语言处理行业研究报告
第一章 行业概况 1.1 定义 自然语言处理(Natural Language Processing,简称NLP)是一门交叉学科,它结合了计算机科学、人工智能和语言学的知识,旨在使计算机能够理解、解释和生成人类语言。NLP的核心是构建能够理解和…...
RISC-V与RISC Zero zkVM的关系
1. 引言 本文基本结构为: 编程语言背景介绍RISC-V虚拟机作为zkVM电路为何选择RISC-V? 2. 编程语言背景介绍 高级编程语言不专门针对某个架构,其便于人类编写。高级编程语言代码,经编译器编译后,会生成针对专门某架…...
Cesium1.95中高性能加载1500个点
一、基本方式: 图标使用.png比.svg性能要好 <template><div id"cesiumContainer"></div><div class"toolbar"><button id"resetButton">重新生成点</button><span id"countDisplay&qu…...
vscode(仍待补充)
写于2025 6.9 主包将加入vscode这个更权威的圈子 vscode的基本使用 侧边栏 vscode还能连接ssh? debug时使用的launch文件 1.task.json {"tasks": [{"type": "cppbuild","label": "C/C: gcc.exe 生成活动文件"…...
SAP学习笔记 - 开发26 - 前端Fiori开发 OData V2 和 V4 的差异 (Deepseek整理)
上一章用到了V2 的概念,其实 Fiori当中还有 V4,咱们这一章来总结一下 V2 和 V4。 SAP学习笔记 - 开发25 - 前端Fiori开发 Remote OData Service(使用远端Odata服务),代理中间件(ui5-middleware-simpleproxy)-CSDN博客…...
视觉slam十四讲实践部分记录——ch2、ch3
ch2 一、使用g++编译.cpp为可执行文件并运行(P30) g++ helloSLAM.cpp ./a.out运行 二、使用cmake编译 mkdir build cd build cmake .. makeCMakeCache.txt 文件仍然指向旧的目录。这表明在源代码目录中可能还存在旧的 CMakeCache.txt 文件,或者在构建过程中仍然引用了旧的路…...
解读《网络安全法》最新修订,把握网络安全新趋势
《网络安全法》自2017年施行以来,在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂,网络攻击、数据泄露等事件频发,现行法律已难以完全适应新的风险挑战。 2025年3月28日,国家网信办会同相关部门起草了《网络安全…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...
Vue ③-生命周期 || 脚手架
生命周期 思考:什么时候可以发送初始化渲染请求?(越早越好) 什么时候可以开始操作dom?(至少dom得渲染出来) Vue生命周期: 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...
CVPR2025重磅突破:AnomalyAny框架实现单样本生成逼真异常数据,破解视觉检测瓶颈!
本文介绍了一种名为AnomalyAny的创新框架,该方法利用Stable Diffusion的强大生成能力,仅需单个正常样本和文本描述,即可生成逼真且多样化的异常样本,有效解决了视觉异常检测中异常样本稀缺的难题,为工业质检、医疗影像…...
uniapp 实现腾讯云IM群文件上传下载功能
UniApp 集成腾讯云IM实现群文件上传下载功能全攻略 一、功能背景与技术选型 在团队协作场景中,群文件共享是核心需求之一。本文将介绍如何基于腾讯云IMCOS,在uniapp中实现: 群内文件上传/下载文件元数据管理下载进度追踪跨平台文件预览 二…...
stm32wle5 lpuart DMA数据不接收
配置波特率9600时,需要使用外部低速晶振...
