【Linux网络编程】I/O多路转接之select

select

  • 1.初识select
  • 2.了解select基本概念和接口介绍
  • 3.select服务器
  • 4.select特点及优缺点总结

在这里插入图片描述

点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃

1.初识select

我们曾经说过 IO = 等 +数据拷贝

select是多路转接的一种,它只负责等待,可以一次等待多次fd,更为重要的是select本身没有数据拷贝的能力,拷贝要read、write来完成。

所以select在IO环节中只负责等,一旦哪一个文件描述符就绪了,那select要有方式来告知上层哪一个文件描述符好了。然后上层来读取。

系统提供select函数来实现多路复用输入/输出模型.

  • select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
  • 程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;

2.了解select基本概念和接口介绍

在这里插入图片描述

nfds:因为select可以一次等待多个文件描述符,而每一个文件描述符它的本质是数组下标,所以多个文件描述符它的数字大小肯定不一样,同时多个文件描述符也是不同整数构成的,它一定有最大一定有最小,而其中第一次参数表示,select要监视的多个fd中值最大的fd+1

如当前监视的是3、4、5、6,那个这个nfd就是6+1。

除了第一个参数,剩下的四个参数有一个共同特点,全都是输入输出型参数,也就是说未来是由我们传给select,传过去之后OS也要对传入的值做修改,然后输出给我们。

timeout:select一次等待多个fd,最后一个参数决定了,当select在等多个fd时它具体的等待方式是什么

timeout设置为nullptr:阻塞式。也就是select一次等待多个df,但没有任何一个fd就绪时,select只能在底层阻塞,这个调用就不返回,直到有任何一个就绪了。

struct timeval结构内第一个变量表示的是秒,第二个变量表示的是微秒。

在这里插入图片描述

当用户调用select的时候,如果定义了一个struct timeval timeout={0,0},传给最后一个参数,表示非阻塞

也就是select一次等待多个df,但没有任何一个fd就绪时,select立马返回。

如果定义了一个struct timeval timeout={5,0},传给最后一个参数。表示的是select的调用,5s以内阻塞式,超过5s非阻塞返回一次假设5s内有任何一个fd就绪了select都可以立马返回。 然后这个参数会被设置成剩下的秒数。

返回值
ret > 0 表示有ret个fd就绪了
ret == 0 表示超时返回。假设设置时间是5s,5s内阻塞式不返回,超过5s没有一个就绪就是超时了。
ret < 0 表示select调用失败了。比如你今天服务器打开3、4、5这三个描述符,你现在只有这三个fd是合法的,可是你非要把10或20也管理起来,10和20在进程根本没有被打开你还要交给select,那select当然就调用失败了。

失败返回-1,erron被设置。

在这里插入图片描述

其实select中间三个参数是最重要的!下面介绍一下

在这里插入图片描述

select在等什么呢?它在等文件描述符上的事件就绪!
那是文件描述符上的什么事件就绪呢?
通常一般分三类:
读事件就绪:表示这个文件描述符缓冲区有数据了,可以读了。
写事件就绪:表示缓冲区内有空间了,可以写了。

读写事件就绪我们统称为IO事件就绪

异常事件就绪:在进行读写时可能会发生各种意外,比如正在给对方写入对方把文件描述符关了,此时我正在向一个已经关闭的客户端写入,这个时候在写入时可能出现异常。

select未来关心的事情,只有三类:读,写,异常 —> 对于任何一个fd,都是这三种

所以这三个参数就分别对应就是让select关心的读,写,异常事件。
在这里插入图片描述

可是select不是可以同时管理多个fd的读、写、异常事件吗?
可是现在select中除了第一个参数给我多个fd的感受,我们好像没有见到有多个fd。

我们可以看到这三个参数的类型是fd_set
它其实是一个位图结构,用来表示文件描述符集合

在这里插入图片描述

在信号的时候,有三种表pending表,block表,还有handler表,其中pending表,block表也就是位图结构。每个比特位表示不同的信号。

文件描述符是0、1、2等这样的数组下标,一:决定了大家都不同 ,二:大家会连续。所以我们采用位图结构表征各个文件描述符。位图结构一般实现都是采用结构体里面套数组完成。你想有多大位图自己设置就可以。

下面以读事件为例,写和异常完全一模一样!

在这里插入图片描述

如果想让select关心写,在定义一个位图结构,把文件描述符设置进写集合里。关心异常也是同样做法。

在这里插入图片描述

因为后面参数都是输入输出型参数,所以操作系统直接在你传的位图中做修改

在这里插入图片描述

所以对同一个参数做修改,本质就是让用户和内核之间互相沟通,互相知晓对方要的或者关心关心的

因此读、写、异常这里操作都是一模一样的, 如果你想让select既关心一个文件描述符的读又关心写,那就定义两种位图,把在这个文件描述符分别添加到读文件描述符集,写文件描述符集。那OS就帮我同时关心该文件描述符的读和写了。

所以读、写、异常三个参数位置的不同表示用户和内核分别交互的不同事件。

参数细节现在就说完了。还有一个问题fd_set是一个位图,能之间对fd_set这个位图做任何修改吗?
不可以,不建议! 操作系统为了更好支持我们向位图里进行设置,查看位图等。系统给我们配了对应的位图操作接口。

   void FD_CLR(int fd, fd_set *set);  //把一个fd从集合中清除int  FD_ISSET(int fd, fd_set *set); //判断一个fd是否在集合里void FD_SET(int fd, fd_set *set); //把一个fd设置到集合里void FD_ZERO(fd_set *set); //把证文件描述符集清空

3.select服务器

接下来我们写一个select服务器,这里我们先只处理读取,只获取数据。写入等到epoll哪里在处理,边写边介绍select 服务器的更多细节。

先准备一下要用东西,下面有些代码是我们以前写tcp服务器已经写过了,这里就不在重复说,直接用了。

错误码封装

#pragma onceenum
{USAGG_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};

日志函数封装

#pragma once#include<iostream>
#include<string>
#include<stdio.h>
#include <cstdarg>
#include<ctime>
#include<sys/types.h>
#include<unistd.h>
#include<fstream>#define DUGNUM  0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4#define LOG_NORMAL "log.txt"
#define LOG_ERR "log.error"const char* level_to_string(int level)
{switch(level){case DUGNUM: return "DUGNUM";case NORMAL: return "NORMAL";case WARNING: return "WARNING";case ERROR: return "ERROR";case FATAL: return "FATAL";}
}//时间戳变成时间
char* timeChange()
{time_t now=time(nullptr);struct tm* local_time;local_time=localtime(&now);static char time_str[1024];snprintf(time_str,sizeof time_str,"%d-%d-%d %d-%d-%d",local_time->tm_year + 1900,\local_time->tm_mon + 1, local_time->tm_mday,local_time->tm_hour, \local_time->tm_min, local_time->tm_sec);return time_str;
}void logMessage(int level,const char* format,...)
{//[日志等级] [时间戳/时间] [pid] [message]//[WARNING] [2024-3-21 10-46-03] [123] [创建sock失败]
#define NUM 1024//获取时间char* nowtime=timeChange();char logprefix[NUM];snprintf(logprefix,sizeof logprefix,"[%s][%s][pid: %d]",level_to_string(level),nowtime,getpid());//char logconten[NUM];va_list arg;va_start(arg,format);vsnprintf(logconten,sizeof logconten,format,arg);std::cout<<logprefix<<logconten<<std::endl; 
};

创建套接字封装
这里为了方便我们全部写成静态成员函数了。后面我们epoll这里在设计一下

#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"using namespace std;class Sock
{const static int backlog = 32;public:static int sock(){// 1. 创建socket文件套接字对象int sock = socket(AF_INET, SOCK_STREAM, 0);if (sock < 0){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success: %d", sock);//当服务器挂了,可以重启int opt = 1;setsockopt(sock, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));return sock;}static void Bind(int sock,int port){// 2. bind绑定自己的网络信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");}static void Listen(int sock){// 3. 设置socket 为监听状态if (listen(sock, backlog) < 0) {logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}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 error, next");else{logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?*clientip = inet_ntoa(peer.sin_addr);*clientport = ntohs(peer.sin_port);}return sock;}
};

调用

#include "selectServer.hpp"
#include "err.hpp"
#include <memory>static void usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " port" << "\n\n";
}int main(int argc,char* argv[])
{if(argc != 2){usage(argv[0]);exit(USAGG_ERR);}unique_ptr<SelectServer> usl(new SelectServer(atoi(argv[1])));usl->initServer();usl->start();return 0;
}

服务器

#include <iostream>
#include "sock.hpp"using namespace std;class SelectServer
{static const int defaultport = 8080;public:SelectServer(int port = defaultport) : _port(port), _listensock(-1){}void initServer(){// 1.创建套接字_listensock = Sock::sock();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);}void start(){for (;;){string clientip;uint16_t clientport;int sock = Sock::Accept(_listensock, &clientip, &clientport); // accept = 等 + 获取if (sock < 0)continue;// 开始进行服务器的处理逻辑}}~SelectServer(){if (_listensock < 0)close(_listensock);}private:int _listensock;int _port;
};

上面服务器的代码都是我们曾经写过的,今天写select服务器当然不是这么简单了。

下面我要在网络服务器中引入select接口。

select它是一个只做监听的只做IO中等待的系统调用接口,一旦有事件就绪了它会通知我,它一次可以等待多个文件描述符,可是目前我们面临的第一个尴尬问题是,你刚开始的服务器根本没有多个文件描述符,刚开始只有一个啊,而且还是_listensock套接字。可是我们也知道_listensock也是套接字,而后序所有多出来的套接字,本质上都是从_listensock上来的,所以select要监管多个套接字的话,首先要把_listensock监管起来!

因此_listensock首先要交给select,那我们要想清楚了,select有读事件,写事件,异常事件,并没有任何所谓的监听事件啊!但是没问题,能交给!_listensock的连接就绪事件 == 读事件就绪,因为本质就是对方发的连接请求触发的三次握手,也属于客户端向服务器发信息,所以认为是读事件就绪!

这里也不应该_listensock套接字创建好了直接循环获取accept,因为accept函数自己通过_listensock套接字获取连接时,没有连接时accpet也在阻塞等。有连接了才能获取连接然后返回。所以accpet = 等 + 获取。这种写法是阻塞式写法,我们想用的是多路转接。

在这里插入图片描述

我们的想法是当底层连接就绪了,你来通知我,这个时候我在调用accept。 此时相当于让select帮我负责等,而accpet只负责获取连接不会被阻塞。

现在要做的就是先调用select,因为我们现在就一个_listensock,因此select目前写法是不正确的。我们接下来慢慢改。

void start()
{// 目前有些地方写的有问题fd_set rfds;FD_ZERO(&rfds);             // 对读文件描述符集初始化FD_SET(_listensock, &rfds); // 将_listensock添加到读文件描述符集合中struct timeval timeout={3,0};for (;;){//我告诉select关心读文件描述符集中的_listensock事件,就绪了之后告诉我int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout);switch (n){case 0:logMessage(NORMAL, "timeout...");break;case -1:logMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));break;default://说明有事件就绪了,目前只有一个监听事件就绪了break;}// string clientip;// uint16_t clientport;// int sock = Sock::Accept(_listensock, &clientip, &clientport);  accept = 等 + 获取// if (sock < 0)//     continue;// // 开始进行服务器的处理逻辑}
}

我们先验证这个timeout,目前是3s内阻塞,超过3s非阻塞返回一次。截图效果可能不明显。
在这里插入图片描述

这里简单描述一下,刚开始等待3秒,3秒后返回打印timeout… ,然后就不阻塞了,一直非阻塞打印timeout… 为什么?

原因在于timeout传进来,它是一个输入输出型参数。当你输入时是3s,3内没有任何时间就绪,那么这个timeout时间就在select内部慢慢见到了0s,然后回过头在去select的时候,这个timeout已经被改过了,就变成全0了。所以此时只能是第一次阻塞等待了,剩下都是非阻塞了。

在这里插入图片描述

所以正确写法是把timeout放入循环内部,保证每一次都对它重新设定。
这样就是没有任何事件就绪时,每隔3秒非阻塞返回一次。同样也可以把timeout改成0,此时就是非阻塞了。并且可以不要timeout直接把最后一个参数设为nullptr,此时就是阻塞了。

void start(){fd_set rfds;FD_ZERO(&rfds);             // 对读文件描述符集初始化FD_SET(_listensock, &rfds); // 将_listensock添加到读文件描述符集合中for (;;){struct timeval timeout={3,0};//特定时间阻塞,超过特定时间非阻塞返回一次//struct timeval timeout={0,0};非阻塞式//我告诉select关心读文件描述符集中的_listensock事件,就绪了之后告诉我//int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout);//阻塞式int n = select(_listensock + 1, &rfds, nullptr, nullptr, nullptr);switch (n){case 0:logMessage(NORMAL, "timeout...");break;case -1:logMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));break;default://说明有事件就绪了,目前只有一个监听事件就绪了break;}}}

下面改一点代码,然后用telnet连接一下这个服务器,看一下。

void start()
{for (;;){fd_set rfds;FD_ZERO(&rfds);             // 对读文件描述符集初始化FD_SET(_listensock, &rfds); // 将_listensock添加到读文件描述符集合中//struct timeval timeout = {3, 0};// 我告诉select关心读文件描述符集中的_listensock事件,就绪了之后告诉我// int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout);int n = select(_listensock + 1, &rfds, nullptr, nullptr, nullptr);switch (n){case 0:logMessage(NORMAL, "timeout...");break;case -1:logMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));break;default:// 说明有事件就绪了,目前只有一个监听事件就绪了logMessage(NORMAL, "get a new link...");break;}}
}

我们看到一直在打印get a new link… 日志在一直打说明我们当前循环的时候select一直在循环。不是只有一个连接吗?怎么会给我这么多就绪消息。

在这里插入图片描述

原因在于我们并没有把底层的连接取走,所以每一次调用select我们对应的_listensock套接字上面的事件都是就绪的,所以每一次都添加关心的都是_listensock套接字,所以select每一次都帮我们检测连接_listensock套接字有没有就绪。

在这里插入图片描述

为什么客户端关闭了,服务器还在疯狂打印呢?

因为连接建立成功后,断开连接是双方的事。服务器连连接都没拿上去也就没有办法断开因为没有调用close。所以依旧告诉连接就绪。

在这里插入图片描述

所以我们发现select果然是有把连接就绪事件告诉我们的能力了,然后我们也通过select监听到有连接事件到来了,因此我们还要获取对应连接。连接就绪事件是被放在我们传给select的rfds里。输入时是用户告诉内核你要帮我关心该文件描述符集中那些fd的读事件,输出时内核告诉用户该文件描述符集中那些fd读就绪了。

void HandlerEvent(fd_set &rfds)
{// 这里目前一定是_listensock,只有这一个// 但未来可能有很多fd就绪了,如何判断是_listensock就绪了呢?// 因此这里判断一下_listensock是否在这个集合里if (FD_ISSET(_listensock, &rfds)){//走到这里就是, select 告送我,_listensock就绪了,然后才能执行下面代码string clientip;uint16_t clientport;int sock = Sock::Accept(_listensock, &clientip, &clientport);  accept = 等 + 获取if (sock < 0) return;logMessage(NORMAL,"accept success [%s:%d]",clientip.c_str(),clientport);}
}void start()
{for (;;){fd_set rfds;FD_ZERO(&rfds);             // 对读文件描述符集初始化FD_SET(_listensock, &rfds); // 将_listensock添加到读文件描述符集合中// struct timeval timeout = {3, 0};//  我告诉select关心读文件描述符集中的_listensock事件,就绪了之后告诉我//  int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout);int n = select(_listensock + 1, &rfds, nullptr, nullptr, nullptr);switch (n){case 0:logMessage(NORMAL, "timeout...");break;case -1:logMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));break;default:// 说明有事件就绪了,目前只有一个监听事件就绪了logMessage(NORMAL, "get a new link...");HandlerEvent(rfds);break;}}
}

在这里插入图片描述
这一次我们把连接拿上来,所以在select就没有新连接了。也就不会疯狂打印get a new link…

不过还有问题,走到这里accpet函数,会不会被阻塞?
不会!因为走到这里,_listensock已经是就绪的了。accpet直接读取绝对是有返回的。

在这里插入图片描述

得到一个sock套接字后,然后我们可以直接进行read/recv吗?
显然不能!你直接调用read/recv你就能保证底层有数据吗?不能!建立好连接就是不给你发数据,因为我们是单进程你直接调用read/recv就直接阻塞挂起了。根据原因就是你根本不清楚对应sock读事件是否就绪。 一调可能就阻塞,你不清楚那谁清楚?整个代码只有select有资格检测事件是否就绪

所以接下来并不是立马读取,而是将新的sock 托管给select! 让select帮我关心这个sock有没有事件就绪。

在这里插入图片描述

现在问题是,你怎么把这个sock托管给select?

一般而言编写select服务器,要使用select,需要程序员自己维护一个保存合法fd的数组!

首先套接字会越来越多,你怎么知道这么多fd那些fd是合法的那些是非法的。其次rfds这个参数是输入输出型的,你可能曾经设置过5个fd,可能循环一次这些fd全都被清空了,那你怎么知道历史上还有那些fd,最后这么多fd你怎么保证更新出来的fd最大值是谁!

所以我们得自己维护一个合法fd数组

在这里插入图片描述

因为增加一个类成员变量_fdarray

在这里插入图片描述

然后在初始服务时,new一个数组,但是这个数组给多大呢?
在这里插入图片描述

我们未来保存所有文件描述符的类型是fd_set,这是Linux内核给我们提供的自定义类型,既然是一种类型,它必有大小,而且大小是固定的!

在这里插入图片描述
所以,我们能够添加的fd的个数一定是有上限的!
在这里插入图片描述
大小是128在这里插入图片描述

不过这里sizeof求得是字节,但fd_set是一个位图结构,因此还有乘8才是真实大小。

1024,也就是说select服务器能够处理得文件描述符上限是1024个

在这里插入图片描述

所以new数组大小就是1024,

在这里插入图片描述

然后对数组做一下初始化

在这里插入图片描述
然后在服务器启动之前,未来所有合法fd都在这个数组里面,未来要重新设置要关心的读文件描述符集,更新最大值都在这个数组找,这就决定了,在刚开始的时候首先最开始只有一个_listensock套接字先设置进数组。

void initServer()
{// 1.创建套接字_listensock = Sock::sock();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);_fdarray = new int[fdnum];for (int i = 0; i < fdnum; ++i)_fdarray[i] = defaultfd;_fdarray[0] = _listensock;//这个位置后面就不变了
}

然后启动服务器,每一次select之前都要在数组内找到合法fd最大值是多少,并且将数组中所有合法fd重新设置到读文件描述符集

void start()
{for (;;){fd_set rfds;FD_ZERO(&rfds); // 对读文件描述符集初始化int maxfd = _fdarray[0];for (int i = 0; i < fdnum; ++i){if (_fdarray[i] == defaultfd)//非法,没有被设置的continue;//因为rfds是输入输出型参数,因此每次都要将合法fd,重新添加到读文件描述符集FD_SET(_fdarray[i], &rfds);if (maxfd < _fdarray[i])maxfd = _fdarray[i];//更新合法fd中最大fd}// FD_SET(_listensock, &rfds); // 将_listensock添加到读文件描述符集合中//  struct timeval timeout = {3, 0};//   我告诉select关心读文件描述符集中的_listensock事件,就绪了之后告诉我//   int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout);int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);switch (n){case 0:logMessage(NORMAL, "timeout...");break;case -1:logMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));break;default:// 说明有事件就绪了,目前只有一个监听事件就绪了logMessage(NORMAL, "get a new link...");HandlerEvent(rfds);break;}}
}

接下来继续之前未完的事情,accpet获取新的sock,将新的sock,托管给select!
将新的sock,托管给select的本质,其实就是将sock,添加到fdarray数组里!

void HandlerEvent(fd_set &rfds)
{// 这里目前一定是_listensock,只有这一个// 但未来可能有很多fd就绪了,如何判断是_listensock就绪了呢?// 因此这里判断一下_listensock是否在这个集合里if (FD_ISSET(_listensock, &rfds)){// 走到这里,accept 函数,会不会被阻塞?// 走到这里就是, select 告送我,_listensock就绪了,然后才能执行下面代码string clientip;uint16_t clientport;int sock = Sock::Accept(_listensock, &clientip, &clientport);  accept = 等 + 获取if (sock < 0)return;logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);// 得到一个sock套接字后,然后我们可以直接进行read/recv吗? 不能,整个代码只有select有资格检测事件是否就绪// 将新的sock 托管给select!// 将新的sock,托管给select的本质,其实就是将sock,添加到fdarray数组里!int i = 0;for (; i < fdnum; ++i){//找放合法fd的位置if (_fdarray[i] != defaultfd)continue;elsebreak;}if (i == fdnum)//处理数组满的情况{logMessage(WARNING, "server if full, please wait");close(sock);}else//添加到_fdarray数组{_fdarray[i] = sock;}}
}

然后这里会从将sock从fdarray数组中交给select

在这里插入图片描述

为了看一看托管的文件描述符是不是越来越多了,写一个打印函数,当把sock添加fdarray数组之后调用一下这个函数,看一下

void print()
{for (int i = 0; i < fdnum; ++i){if (_fdarray[i] != defaultfd)cout << _fdarray[i] << " ";}cout << endl;
}

在这里插入图片描述

所以随着连接增多,对应的合法fd全部都会添加到_fdarray数组中,然后处理完就绪事件之后,每次在调用select之前都会把_fdarray中合法fd添加到rfds,找到合法fd最大值,一起交给select。

在这里插入图片描述

可是随着数组中合法fd越来越多,select帮我们监管的fd也越来越多了,那么事件的总类也变得越来越多了,不过我们目前只处理读事件。并且我们处理读事件函数中也只写了一个_listensock套接字获取accpet获取连接。这是不够的,我们还需要考虑处理正常的IO。

下面把处理_listensock套接字单独拿出来做一个封装

void Accepter(int listensock)
{logMessage(DEBUG, "Accepter in");// 走到这里,accept 函数,会不会被阻塞?// 走到这里就是, select 告送我,_listensock就绪了,然后才能执行下面代码string clientip;uint16_t clientport;int sock = Sock::Accept(listensock, &clientip, &clientport);  accept = 等 + 获取if (sock < 0)return;logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);// 得到一个sock套接字后,然后我们可以直接进行read/recv吗? 不能,整个代码只有select有资格检测事件是否就绪// 将新的sock 托管给select!// 将新的sock,托管给select的本质,其实就是将sock,添加到fdarray数组里!int i = 0;for (; i < fdnum; ++i){if (_fdarray[i] != defaultfd)continue;elsebreak;}if (i == fdnum){logMessage(WARNING, "server if full, please wait");close(sock);}else{_fdarray[i] = sock;}print();logMessage(DEBUG, "Accepter out");
}// 1.handler event rfds 中,不仅仅是有一个fd是就绪的,可能存在多个
// 2.我们的select目前只处理了read事件
void HandlerEvent(fd_set &rfds)
{// 你怎么知道那些fd就绪了呢? 我不知道,我只能遍历for (int i = 0; i < fdnum; ++i){// 不合法fdif (_fdarray[i] == defaultfd)continue;// 合法fd,但不一定就绪,要先判断if (_fdarray[i] == _listensock && FD_ISSET(_listensock, &rfds))Accepter(_listensock);//处理_listensockelse if (FD_ISSET(_fdarray[i], &rfds))Recver(_fdarray[i], i);//处理其他sock}
}

接下来处理正常IO

void Recver(int sock, int pos)
{logMessage(DEBUG, "in Recver");// 1. 读取request// 这样读取是有问题的!char buffer[1024];// 这里在进行读取的时候,会不会被阻塞?ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); //...
}

不会被阻塞!走到这里一定是sock读事件已经就绪了。其次这里不仅仅处理一个sock,可能多个sock都会进来,所以就一个栈上的缓存区去读取绝对是有问题的,其次你怎么保证数据一次就读完了呢?没有读完是不是要循环读取,但是你怎么保证在读的时候不会被阻塞呢?并且读完了就是一个完整的请求了吗?然后反序列化等等,这些问题我们都在epoll哪里处理!

void Recver(int sock, int pos)
{logMessage(DEBUG, "in Recver");// 1. 读取request// 这样读取是有问题的!char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 这里在进行读取的时候,会不会被阻塞?if (s > 0)//读取成功{buffer[s] = 0;logMessage(NORMAL, "client# %s", buffer);}else if (s == 0) //对方关闭了文件描述符{close(sock);//我也关_fdarray[pos] = defaultfd;//不让select关心该sock了logMessage(NORMAL, "client quit");return;}else//读取失败{close(sock);_fdarray[pos] = defaultfd;logMessage(ERROR, "client quit: %s", strerror(errno));return;}// 2.处理request,
}

接下来处理request,这里我们在类里在加入一个回调函数,然后简单的直接返回就行了

在这里插入图片描述

    void Recver(int sock, int pos){logMessage(DEBUG, "in Recver");// 1. 读取request// 这样读取是有问题的!char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 这里在进行读取的时候,会不会被阻塞?if (s > 0)//读取成功{buffer[s] = 0;logMessage(NORMAL, "client# %s", buffer);}else if (s == 0) //对方关闭了文件描述符{close(sock);//我也关_fdarray[pos] = defaultfd;//不让select关心该sock了logMessage(NORMAL, "client quit");return;}else//读取失败{close(sock);_fdarray[pos] = defaultfd;logMessage(ERROR, "client quit: %s", strerror(errno));return;}// 2. 处理requeststd::string response = _cbs(buffer);// 3. 返回response}

接下来把处理结果给给用户返回,但是你怎么保证写事件就绪了呢?所以这里还要在创建一个fd_set写事件,然后把sock添加到这个写文件描述符集,在添加到select写这里,等到对应sock读事件就绪了,才能给用户返回去。不过那样代码太复杂了,所以今天不考虑这么多,直接先写。

在这里插入图片描述

void Recver(int sock, int pos)
{logMessage(DEBUG, "in Recver");// 1. 读取request// 这样读取是有问题的!char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 这里在进行读取的时候,会不会被阻塞?if (s > 0)//读取成功{buffer[s] = 0;logMessage(NORMAL, "client# %s", buffer);}else if (s == 0) //对方关闭了文件描述符{close(sock);//我也关_fdarray[pos] = defaultfd;//不让select关心该sock了logMessage(NORMAL, "client quit");return;}else//读取失败{close(sock);_fdarray[pos] = defaultfd;logMessage(ERROR, "client quit: %s", strerror(errno));return;}// 2. 处理requeststd::string response = _cbs(buffer);// 3. 返回response// write bugwrite(sock, response.c_str(), response.size());logMessage(DEBUG, "out Recver");
}

在这里插入图片描述

自此简单的select服务器写完了,下面是服务器完整代码

#pragma once#include <iostream>
#include<functional>
#include "sock.hpp"using namespace std;class SelectServer
{static const int defaultport = 8080;static const int fdnum = sizeof(fd_set) * 8;static const int defaultfd = -1;using func_t=function<string(string)>;public:SelectServer(func_t f,int port = defaultport) : _cbs(f),_port(port), _listensock(-1), _fdarray(nullptr){}void initServer(){// 1.创建套接字_listensock = Sock::sock();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);_fdarray = new int[fdnum];for (int i = 0; i < fdnum; ++i)_fdarray[i] = defaultfd;_fdarray[0] = _listensock; // 这个位置后面就不变了}void print(){for (int i = 0; i < fdnum; ++i){if (_fdarray[i] != defaultfd)cout << _fdarray[i] << " ";}cout << endl;}void Accepter(int listensock){logMessage(DEBUG, "Accepter in");// 走到这里,accept 函数,会不会被阻塞?// 走到这里就是, select 告送我,_listensock就绪了,然后才能执行下面代码string clientip;uint16_t clientport;int sock = Sock::Accept(listensock, &clientip, &clientport);  accept = 等 + 获取if (sock < 0)return;logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);// 得到一个sock套接字后,然后我们可以直接进行read/recv吗? 不能,整个代码只有select有资格检测事件是否就绪// 将新的sock 托管给select!// 将新的sock,托管给select的本质,其实就是将sock,添加到fdarray数组里!int i = 0;for (; i < fdnum; ++i){if (_fdarray[i] != defaultfd)continue;elsebreak;}if (i == fdnum){logMessage(WARNING, "server if full, please wait");close(sock);}else{_fdarray[i] = sock;}print();logMessage(DEBUG, "Accepter out");}void Recver(int sock, int pos){logMessage(DEBUG, "in Recver");// 1. 读取request// 这样读取是有问题的!char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0); // 这里在进行读取的时候,会不会被阻塞?if (s > 0)//读取成功{buffer[s] = 0;logMessage(NORMAL, "client# %s", buffer);}else if (s == 0) //对方关闭了文件描述符{close(sock);//我也关_fdarray[pos] = defaultfd;//不让select关心该sock了logMessage(NORMAL, "client quit");return;}else//读取失败{close(sock);_fdarray[pos] = defaultfd;logMessage(ERROR, "client quit: %s", strerror(errno));return;}// 2. 处理requeststd::string response = _cbs(buffer);// 3. 返回response// write bugwrite(sock, response.c_str(), response.size());logMessage(DEBUG, "out Recver");}// 1.handler event rfds 中,不仅仅是有一个fd是就绪的,可能存在多个// 2.我们的select目前只处理了read事件void HandlerEvent(fd_set &rfds){// 你怎么知道那些fd就绪了呢? 我不知道,我只能遍历for (int i = 0; i < fdnum; ++i){// 不合法fdif (_fdarray[i] == defaultfd)continue;// 合法fd,但不一定就绪,要先判断if (_fdarray[i] == _listensock && FD_ISSET(_listensock, &rfds))Accepter(_listensock);else if (FD_ISSET(_fdarray[i], &rfds))Recver(_fdarray[i], i);}}void start(){for (;;){fd_set rfds;FD_ZERO(&rfds); // 对读文件描述符集初始化int maxfd = _fdarray[0];for (int i = 0; i < fdnum; ++i){if (_fdarray[i] == defaultfd) // 非法,没有被设置的continue;// 因为rfds是输入输出型参数,因此每次都要将合法fd,重新添加到读文件描述符集FD_SET(_fdarray[i], &rfds);if (maxfd < _fdarray[i])maxfd = _fdarray[i];}// FD_SET(_listensock, &rfds); // 将_listensock添加到读文件描述符集合中//  struct timeval timeout = {3, 0};//   我告诉select关心读文件描述符集中的_listensock事件,就绪了之后告诉我//   int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout);int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);switch (n){case 0:logMessage(NORMAL, "timeout...");break;case -1:logMessage(WARNING, "select error, code: %d, err string: %s", errno, strerror(errno));break;default:// 说明有事件就绪了,目前只有一个监听事件就绪了logMessage(NORMAL, "have event ready!");HandlerEvent(rfds);break;}}}~SelectServer(){if (_listensock < 0)close(_listensock);if (_fdarray)delete[] _fdarray;}private:int _listensock;int _port;int *_fdarray;func_t _cbs;};

4.select特点及优缺点总结

  1. select能同时等待的文件fd是有上限的,除非重新改内核,否则无法解决
  2. 必须借助第三方数组,来维护合法的fd
  3. select的大部分参数都是输入输出型的,调用select前,要重新设置所有的fd,调用之后,我们还要更新所有的fd,这带来的就是遍历的成本 — (用户层面)
  4. select为什么第一个参数是最大fd+1呢?
    因为select要等待多个文件描述符, 它怎么知道要等那些文件描述符那些事件呢?怎么知道给你返回那个文件描述符的事件就绪了?所以它要去查,它既然要查,就要去限定它去查的范围,因为文件描述符就是一个个数组下标,我有多个文件描述符表,要遍历到哪里呢?所以就有了最大值fd+1! 确定遍历范围 — (内核层面)
  5. select 采用位图,用户->内核,内核->用户,来回的进行数据拷贝,有拷贝成本的问题

因为select有如此之多的问题,select接口使用不方便,每次还要重新手动设置等等。。所以我们要有一种新的解决方案,这种方案就是多种转接之epoll。不过在此之前先了解多种转接之poll

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

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

相关文章

系统架构设计师 - 计算机组成与体系结构(1)

计算机组成与体系结构 计算机组成与体系结构计算机结构 ★CPU 组成结构运算器组成控制器组成 计算机体系结构冯诺依曼结构哈弗结构 嵌入式芯片&#xff08;了解&#xff09; 存储系统 ★★★★概述Cache主存编址磁盘管理磁盘基本结构与存取过程磁盘优化分布存储磁盘管理 大家好…

数据挖掘(二)数据预处理

前言 基于国防科技大学 丁兆云老师的《数据挖掘》 数据挖掘 数据挖掘&#xff08;一&#xff09;数据类型与统计 2、数据预处理 2.1数据清理 缺失值处理&#xff1a; from sklearn.impute import SimpleImputer# 创建一个SimpleImputer对象&#xff0c;指定缺失值的处理策略…

Spring框架学习笔记(二):Spring IOC容器配置 Bean,分别基于XML配置bean 和 基于注解配置 bean

1 Spring 配置/管理 bean 介绍 Bean 管理包括两方面 &#xff1a;创建 bean 对象&#xff1b;给 bean 注入属性 Bean 配置方式&#xff1a;基于 xml 文件配置方式&#xff1b;基于注解方式 2 基于 XML 配置 bean 2.1 通过类型来获取 bean 方法&#xff1a;给getBean传入一…

无人机+三角翼:小摩托无人机技术详解

无人机与三角翼的结合&#xff0c;为航空领域带来了一种新型且独特的飞行器——“小摩托”无人机。这种无人机结合了无人机的灵活性和三角翼的飞行稳定性&#xff0c;成为了航空运动领域中的一款热门产品。以下是对“小摩托”无人机技术的详解&#xff1a; 1. 定义与特点&#…

MFC中关于CMutex类的学习

MFC中关于CMutex类的学习 最近在项目中要实现两个线程之间的同步&#xff0c;MFC中提供了4个类&#xff0c;分别是CMutex(互斥量)、CCriticalSection(临界区)、CEvent(事件对象)、CSemaphore(信号量)。有关这4个类的说明&#xff0c;大家可以参考微软官方文档&#xff1a; CM…

七、Redis三种高级数据结构-HyperLogLog

Redis HyperLogLog是用来做基数统计的算法&#xff0c;HyperLogLog在优点是&#xff0c;在输入的元素的数量或者体积非常大时&#xff0c;计算基数占用的空间总是固定的、并且非常小。在Redis里每个HyperLogLog键只需花费12KB内存&#xff0c;就可以计算接近 264 个元素的基数。…

#04 构建您的第一个神经网络:PyTorch入门指南

文章目录 前言理论基础神经网络层的组成前向传播与反向传播 神经网络设计步骤1&#xff1a;准备数据集步骤2&#xff1a;构建模型步骤3&#xff1a;定义损失函数和优化器步骤4&#xff1a;训练模型步骤5&#xff1a;评估模型结论 前言 在过去的几天里&#xff0c;我们深入了解了…

头歌实践教学平台:CG1-v2.0-直线绘制

第1关&#xff1a;直线光栅化-DDA画线算法 一.任务描述 1.本关任务 (1)根据直线DDA算法补全line函数&#xff0c;其中直线斜率0<k<1&#xff1b; (2)当直线方程恰好经过P(x,y)和T(x,y1)的中点M时&#xff0c;统一选取直线上方的T点为显示的像素点。 2.输入 (1)直线两…

使用com.google.common.collect依赖包中的Lists.transform()方法转换集合对象之后,修改集合中的对象属性,发现不生效

目录 1.1、错误描述 &#xff08;1&#xff09;引入依赖 &#xff08;2&#xff09;模拟代码 &#xff08;3&#xff09;运行结果 1.2、解决方案 1.1、错误描述 最近在开发过程中&#xff0c;使用到了com.google.common.collect依赖包&#xff0c;通过这个依赖包中提供的…

Vue踩坑,less与less-loader安装,版本不一致

无脑通过npm i less -D安装less之后&#xff0c;继续无脑通过npm i less-loader -D安装less-loader出现如下错误&#xff1a; 解决方法&#xff1a; 1) npm uninstall less与 npm uninstall less-loader 2) 直接对其版本&#xff1a; npm i less3.0.4 -D npm i less-loader…

es关闭开启除了系统索引以外的所有索引

1、es 开启 “删除或关闭时索引名称支持通配符” 功能 2、kibanan平台执行 POST *,-.*/_close 关闭索引POST *,-.*/_open 打开索引3、其他命令 DELETE index_* // 按通配符删除以index_开头的索引 DELETE _all // 删除全部索引 DELETE *,-.* 删除全…

鸿蒙OpenHarmony开发板解析:【系统能力配置规则】

如何按需配置部件的系统能力 SysCap&#xff08;SystemCapability&#xff0c;系统能力&#xff09;是部件向开发者提供的接口的集合。 开发前请熟悉鸿蒙开发指导文档&#xff1a;gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md点击或者复制转到。 部件配置系统…

Java入门——类和对象(上)

经读者反映与笔者考虑&#xff0c;近期以及往后内容更新将主要以java为主&#xff0c;望读者周知、见谅。 类与对象是什么&#xff1f; C语言是面向过程的&#xff0c;关注的是过程&#xff0c;分析出求解问题的步骤&#xff0c;通过函数调用逐步解决问题。 JAVA是基于面向对…

DDOS攻击实战演示,一次DDOS的成本有多低?

DDoS攻击成本概览 分布式拒绝服务&#xff08;DDoS&#xff09;攻击以其低廉的启动成本和惊人的破坏力著称。攻击者通过黑市轻松获取服务&#xff0c;成本从几十元人民币的小额支出到针对大型目标的数千乃至数万元不等。为了具体理解这一成本结构&#xff0c;我们将通过一个简…

每日两题 / 226. 翻转二叉树 98. 验证二叉搜索树(LeetCode热题100)

226. 翻转二叉树 - 力扣&#xff08;LeetCode&#xff09; 以后续遍历的方式交换当前节点的左右指针 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), ri…

机器学习-12-sklearn案例03-flask对外提供服务

整体思路 训练一个模型&#xff0c;把模型保存 写一个基于flask的web服务&#xff0c;在web运行时加载模型&#xff0c;并在对应的接口调用模型进行预测并返回 使用curl进行测试&#xff0c;测试通过 再创建一个html页面&#xff0c;接受参数输入&#xff0c;并返回。 目录结…

CSS悬浮动画

<button class"btn">悬浮动画</button>.btn {position: absolute;top: 50%;left: 50%;transform: translate(-50%, -50%);padding: 10px 20px;width: 200px;height: 50px;background-color: transparent;border-radius: 5px;border: 2px solid powderblu…

R2S+ZeroTier+Trilium

软路由使用ZeroTier搭建远程笔记 软路由使用ZeroTier搭建远程笔记 环境部署 安装ZeroTier安装trilium 环境 软路由硬件&#xff1a;友善 Nanopo R2S软路由系统&#xff1a;OpenWrt&#xff0c;使用第三方固件nanopi-openwrt。内网穿透&#xff1a;ZeroTier。远程笔记&…

银河麒麟操作系统 v10 离线安装 Docker v20.10.9

1查看系统版本 [rootweb-0001 ~]# cat /etc/os-release NAME"Kylin Linux Advanced Server" VERSION"V10 (Tercel)" ID"kylin" VERSION_ID"V10" PRETTY_NAME"Kylin Linux Advanced Server V10 (Tercel)" ANSI_COLOR"…

pyqt动画效果放大与缩小

pyqt动画效果放大与缩小 QPropertyAnimation介绍放大与缩小效果代码 QPropertyAnimation介绍 QPropertyAnimation 是 PyQt中的一个类&#xff0c;它用于对 Qt 对象的属性进行动画处理。通过使用 QPropertyAnimation&#xff0c;你可以平滑地改变一个对象的属性值&#xff0c;例…