点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
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 once
enum
{
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