多路转接(上)——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. 编程语言背景介绍 高级编程语言不专门针对某个架构,其便于人类编写。高级编程语言代码,经编译器编译后,会生成针对专门某架…...
OpenLayers 可视化之热力图
注:当前使用的是 ol 5.3.0 版本,天地图使用的key请到天地图官网申请,并替换为自己的key 热力图(Heatmap)又叫热点图,是一种通过特殊高亮显示事物密度分布、变化趋势的数据可视化技术。采用颜色的深浅来显示…...
从深圳崛起的“机器之眼”:赴港乐动机器人的万亿赛道赶考路
进入2025年以来,尽管围绕人形机器人、具身智能等机器人赛道的质疑声不断,但全球市场热度依然高涨,入局者持续增加。 以国内市场为例,天眼查专业版数据显示,截至5月底,我国现存在业、存续状态的机器人相关企…...
pam_env.so模块配置解析
在PAM(Pluggable Authentication Modules)配置中, /etc/pam.d/su 文件相关配置含义如下: 配置解析 auth required pam_env.so1. 字段分解 字段值说明模块类型auth认证类模块,负责验证用户身份&am…...
django filter 统计数量 按属性去重
在Django中,如果你想要根据某个属性对查询集进行去重并统计数量,你可以使用values()方法配合annotate()方法来实现。这里有两种常见的方法来完成这个需求: 方法1:使用annotate()和Count 假设你有一个模型Item,并且你想…...
【Zephyr 系列 10】实战项目:打造一个蓝牙传感器终端 + 网关系统(完整架构与全栈实现)
🧠关键词:Zephyr、BLE、终端、网关、广播、连接、传感器、数据采集、低功耗、系统集成 📌目标读者:希望基于 Zephyr 构建 BLE 系统架构、实现终端与网关协作、具备产品交付能力的开发者 📊篇幅字数:约 5200 字 ✨ 项目总览 在物联网实际项目中,**“终端 + 网关”**是…...
智能仓储的未来:自动化、AI与数据分析如何重塑物流中心
当仓库学会“思考”,物流的终极形态正在诞生 想象这样的场景: 凌晨3点,某物流中心灯火通明却空无一人。AGV机器人集群根据实时订单动态规划路径;AI视觉系统在0.1秒内扫描包裹信息;数字孪生平台正模拟次日峰值流量压力…...
网络编程(UDP编程)
思维导图 UDP基础编程(单播) 1.流程图 服务器:短信的接收方 创建套接字 (socket)-----------------------------------------》有手机指定网络信息-----------------------------------------------》有号码绑定套接字 (bind)--------------…...
优选算法第十二讲:队列 + 宽搜 优先级队列
优选算法第十二讲:队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...
Linux --进程控制
本文从以下五个方面来初步认识进程控制: 目录 进程创建 进程终止 进程等待 进程替换 模拟实现一个微型shell 进程创建 在Linux系统中我们可以在一个进程使用系统调用fork()来创建子进程,创建出来的进程就是子进程,原来的进程为父进程。…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...
