多路转接(上)——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,就能退出了。

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

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

相关文章

【mmrotate】*** is not in the task util registry

问题&#xff1a; 使用mmrotate-1.x 自定义类时&#xff0c;明明已经注册&#xff0c;并添加到__init__.py中&#xff0c;但提示没有注册 from mmdet.registry import MODELSMODELS.register_module() class RotatedATSSAssigner(BaseAssigner): 分析&#xff1a; 具体看提…

在linux安装单机版hadoop-3.3.6

一、下载hadoop https://mirrors.tuna.tsinghua.edu.cn/apache/hadoop/core/hadoop-3.3.6/ 二、配置环境变量 1、配置java环境变量 2、配置hadoop环境变量 export HADOOP_HOME/usr/local/bigdata/hadoop-3.3.6 export HBASE_HOME/usr/local/bigdata/hbase-2.5.6 export JA…

Python进行数据可视化,探索和发现数据中的模式和趋势。

文章目录 前言第一步&#xff1a;导入必要的库第二步&#xff1a;加载数据第三步&#xff1a;创建基本图表第四步&#xff1a;添加更多细节第五步&#xff1a;使用Seaborn库创建更复杂的图表关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Pyth…

Vue 将响应式数据转为普通对象

toRaw&#xff1a;将一个 reactive 生成的响应式数据转为普通对象。 toRaw 适用于&#xff1a;获取响应式数据对应的普通对象&#xff0c;对这个普通对象所有的操作&#xff0c;都不会引起页面的更新。 markRaw&#xff1a;标记一个对象&#xff0c;使其永远不会再成为响应式…

设置区块链节点输出等级为警告级,并把日志存储阈值位100MB并验证;

题目 获取指定区块链节点输出等级为警告级&#xff0c;并设置日志存储阈值位100MB并验证&#xff1b; 操作步骤 1.切换目录 cd nodes/127.0.0.1/node0 2.打开配置文件并修改 vim config.ini warn&#xff1a;警告

NOIP2023模拟15联测36 均分财产

题目大意 有 n n n个数&#xff0c;你希望能删除其中不超过 k k k个数&#xff0c;然后将剩下的数划分为两个子集&#xff08;可以有重复的数字&#xff09;&#xff0c;满足这两个子集的数的和是相等的。 为了降低出题和做题的难度&#xff0c;可以认为这 n n n个数在 1 1 1…

初识微服务技术栈

认识微服务 随着互联网行业的发展&#xff0c;对服务的要求也越来越高&#xff0c;服务架构也从单体架构逐渐演变为现在流行的微服务架构&#xff0c;这些架构之间有怎样的差别呢&#xff1f; 导学&#xff1a; 了解微服务的优缺点&#xff1b;了解微服务架构的演变过程&am…

智能井盖传感器功能,万宾科技产品介绍

在国家治理方面&#xff0c;对社会的治理是一个重要的领域&#xff0c;一定要在推进社会治理现代化过程中提高市政府的管理和工作能力&#xff0c;推动社会拥有稳定有序的发展。在管理过程中对全市井盖进行统一化管理&#xff0c;可能是市政府比较头疼的难题&#xff0c;如果想…

Mybatis(一)

1. Mybatis简介 MyBatis下载地址 1.1 MyBatis历史 MyBatis最初是Apache的一个开源项目iBatis, 2010年6月这个项目由Apache Software Foundation迁移到了Google Code。随着开发团队转投Google Code旗下&#xff0c;iBatis3.x正式更名为MyBatis。代码于2013年11月迁移到Github…

Flink(三)【运行时架构】

前言 今天学习 Flink 的一些原理性的东西&#xff0c;比较偏概念&#xff0c;但是十分重要。有人觉得上来框框敲代码才能学到东西&#xff0c;那是狗屁不通的道理&#xff08;虽然我以前也这么认为&#xff09;。个人认为&#xff0c;学习 JavaEE那些框架&#xff0c;你上来就敲…

React 中的 Virtual DOM 是什么

在 React 中&#xff0c;Virtual DOM&#xff08;虚拟 DOM&#xff09;是一种用于提高性能和优化渲染的技术。它是 React 的核心概念之一。 Virtual DOM 是一个轻量级的内存数据结构&#xff0c;它是对真实 DOM 的抽象表示。在 React 中&#xff0c;每个组件都有对应的 Virtua…

毅速丨为什么不锈钢材料在金属3D打印中应用广泛

不锈钢材料作为一种常见材料&#xff0c;在金属3D打印中应用广泛&#xff0c;可以说是目前使用率最高的材料&#xff0c;为什么不锈钢大受欢迎&#xff0c;主要由几点原因。 第一、工艺适合性 金属3D打印的工艺&#xff0c;如直接金属激光烧结&#xff08;DMLS&#xff09;或选…

人工智能模型转ONNX 连接摄像头使用ONNX格式的模型进行推理

部署之后模型的运算基本上能快5倍。本地部署之后&#xff0c;联网都不需要&#xff0c;数据和隐私不像在网上那样容易泄露了。 模型部署的通用流程 各大厂商都有自己的推理工具。 训练的归训练&#xff0c;部署的归部署&#xff0c;人工智能也分训练端和部署端&#xff0c;每一…

Docker从入门到上天系列第四篇:docker平台入门图解与平台架构图解

大神推荐:作者有幸结识技术大神孙哥为好友获益匪浅,现在把孙哥作为朋友分享给大家。 孙哥链接:孙哥个人主页 作者简介:一个颜值99分,只比孙哥差一点的程序员。 本专栏简介:话不多说,让我们一起干翻Docker 本文章简介:话不多说,让我们讲清楚Docker的平台入门图解和平台…

达梦主备部署

达梦主备部署 一.概括1&#xff09;环境软件下载2&#xff09;集群规划 二.安装1&#xff09;安装前2&#xff09;安装数据库 三.主备机器部署1)初始化数据库&#xff08;1&#xff09;主库配置&#xff08;2&#xff09;备库配置 2)脱机备份&#xff08;1&#xff09;主服务器…

云端生成式 AI – 基于 Amazon EKS 的 Stable Diffusion 图像生成方案

Stable Diffusion 是当下生成式 AI 领域最受欢迎的开源多模态语言-图像模型&#xff0c;由于其易用的接口和良好的使用体验&#xff0c;受到了开源社区和广大设计行业从业者的追捧。Stable Diffusion 模型版本正在快速迭代&#xff0c;并带动了各行各业的生产力变革。目前市场上…

十、K8S之ConfigMap

ConfigMap 一、概念 在K8S中&#xff0c;ConfigMap是一种用于存储配置数据的API对象&#xff0c;一般用于存储Pod中应用所需的一些配置信息&#xff0c;或者环境变量。将配置于 Pod 分开&#xff0c;避免应为修改配置导致还需要重新构建 镜像与容器。 二、创建 可以使用 ku…

java制作游戏,如何使用libgdx,入门级别教学

第一步&#xff0c;进入libgdx的官网。点击get started 进入这个页面&#xff0c;点击setup a project 进入这个页面直接点击&#xff0c;Generate a project. 点击下载&#xff0c;下载创建工具 它会让你下载一个jar包&#xff0c;有java环境的人可以双击直接打开。 把android…

selenium 对当前已经打开的窗口进行调试

要求selenium版本4.11.2 使用cmd进入chrome浏览器的路径执行如下命令&#xff0c;创建一个端口为9522的窗口 chrome.exe --remote-debugging-port9522 --user-data-dir"D:\selenium\AutomationProfile"代码里面创建实例&#xff0c;调用driver即可 from selenium …