Linux:五种IO模型
一、五种IO模型
1.1 高效IO的初步理解
IO其实就是“input”和“output” 尤其在网络部分,IO的特性非常明显!!
如果是在本地文件,本质上就是将数据写到内核文件缓冲区,具体什么时候刷到磁盘上,是由OS决定的!!而在网络中,本质上也是将数据写到发送缓冲区,但是具体什么时候发送,也是由OS决定的!!
所以应用层进行read或write的时候,本质上是把数据从用户层写给OS!这也是IO的本质!read和write函数的本质其实就是拷贝函数!!
但是拷贝并不是一定能立马执行的!比如说read的时候,如果我的接收缓冲区没有数据,我得阻塞,而write的时候,我的发送缓冲区满了,那么我也得阻塞!!
所以要进行拷贝!必须要先判断读写事件是否就绪!!
IO=等+拷贝
问题1:什么是读写事件呢??
——>你想读就得等读事件就绪,就是接收缓冲区有数据,想写就得等写事件就绪,就是等发送缓冲区要有足够多的空间,想读就是得读事件就绪,以上统称读写事件就绪!
问题2:什么是高效的IO呢??
——>任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往 往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是单位时间内等待时间的比重减少!
问题3:怎么理解等的比重减少呢?
——>比如说你当前是单进程,如果读写时间没有就绪就会阻塞住,只会等一个文件描述符,而如果是多线程,他可以等待多个文件描述符,此时的IO等待时间不是串型的而是并行的!!
1.2 用“钓鱼”理解五种IO模型
接下来我们就要介绍五种IO模型,什么叫模型呢??其实就是规律,未来不管是读文件还是写文件都离不开其中一种!!
钓鱼=等+钓(可以比喻IO)
1、张三(新手) 拿着自己的鱼漂(用来主动检测读写事件是否就绪) 鱼竿(相当于文件描述符) 鱼钩坐在椅子上,然后一下钩就死死盯着鱼漂, 鱼漂不动张三也不动,谁找他喊他他都不回应 直到鱼上钩 ----这是阻塞式IO(策略是在内核将数据准备好之前, 系统调用会一直等待所有的套接字, 默认都是阻塞方式.)
2、李四(有两三年钓鱼经验,坐不住)喊张三,张三不理他 他也就坐在那钓鱼了 但是他比较坐不住,他会每隔一段时间检查一下鱼漂,不会一直死死盯着,其他时间他会把视线转移到自己的手机上刷抖音,所以他检测的时候如果检测不到就会立刻做自己的事情 不会一直死盯 检测条件就绪了才钓鱼 ——这是非阻塞等待IO (策略是如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.)
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一 般只有特定场景下才使用.
3、王五 (有五年钓鱼经验) 他看张三和李四一个一直动,一个一动不动,觉得他们是菜鸟,他也跟着钓鱼了,然后他在鱼竿上绑了一个铃铛 然后他就把鱼竿插起来不管了 直接躺在旁边玩手机 基本不关注鱼竿,直接等铃铛响 他才会去把鱼钓上来。 我们会发现张三和李四是主动去检测的 而王五的方式就是我不会主动检测,就是鱼上钩了会自己通知我 ——信号驱动式IO (策略是内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作. )
4、赵六(富豪、好胜) 所以他拉了一卡车的鱼竿 把所有的鱼竿都插起来 然后他会来回走动检测(周期性遍历)哪边有鱼上钩 ——这就是多路转接(策略最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态)
5、田七(世界首富 但是不是很专业) 司机开车带着他经过河边的时候,他发现河边有4个非常奇怪的人 钓鱼的姿势形态各异 于是他就很好奇 也想去钓鱼 然后突然公司打电话要开紧急会议 可是他又想吃鱼 于是他就把司机小王叫了过来 说我要去开会 你帮我钓鱼 等你钓满一桶了打电话给我 我再让人来接你
田七并不是喜欢钓鱼 他是钓鱼行为的发起者 他要的是鱼(数据) 田七这种方式叫做——异步IO (由内核在数据拷贝完成时, 通知应用程序)
因为小王在钓鱼的时候 他正在开会 此时的小王就相当于是OS 桶就相当于是一段缓冲区,电话就相当于是一种通知方式 他将IO工作交给了OS 由OS自动去检测然后将数据放在缓冲区里 等缓冲区满了就通知你来取 田七在应用层用就可以了,田七并不参与具体的IO过程 而前四种方式就叫做同步IO
问题1:为什么赵六效率最高呢??拿到鱼竿多效率及高么??
——>假设你是一条鱼 你看到旁边这么多鱼竿 你会咬哪一个呢??显然赵六钓到鱼的机会最大,因为多个鱼竿可以让我们每一个等待的过程在时间上是并行重叠的!!所以整体上等的比重就减少了!!!
问题2: 阻塞IOvs非阻塞IO
——>阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.
在效率方面没有任何区别(因为IO=等+拷贝 大家的区别只是等的方式不同),我们一般说非阻塞效率会高一点不是IO效率高 而是他在非阻塞轮询的时候可以做其他的事情
问题3:王五有等吗??
——>王五也算一种等!!要不然他为什么不直接回家呢??就算我们说他没等,鱼咬钩的时候他也要参与钓鱼的过程(IO) 只要有参与IO,就一定有同步的过程,所以也是同步IO
问题4:同步IOVS 异步IO
——>同步IO就是有参与O的过程,而异步IO就只是发起IO,但是并不参与IO的过程,OS完成IO后会通知上层拿结果,然后直接用就行了!!
问题5:同步通信vs异步通信
——> 同步和异步关注的是消息通信机制.
所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
问题6:同步IOVS 线程同步
——>他俩就是老婆和老婆饼的关系(毫无关联!),同步IO是IO层面的概念,而线程同步是两个线程谁先谁后的问题!!所以以后在看到 "同步" 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步 与互斥的同步.
问题7:异步IO效率不高呢??为什么实际场景多路转接用的多?
——>田七再厉害也只有一套装备 而且异步IO写出来的服务逻辑比较混乱 所以现在已经有很多方法(比如协程)在逐步取代异步IO了 所以这里最值得我们学习的是多路转接和非阻塞!!
问题8:异步IOvs信号驱动
——>异步IO是由OS完成拷贝的过程然后通知上层,而信号驱动是告诉上层可以进行拷贝了!
问题9:其他高级IO
——>非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO.
二、非阻塞轮询
我们会发现以上接口有一个flag参数,我们可以通过设置来让该事件以非阻塞轮询的方式来访问套接字,但是这种方法太麻烦了!!
因为我们读写本质就是读写文件描述符指向的文件缓冲区,而文件描述符本质上是下标,所以更通用的做法就是把文件描述符属性设置成非阻塞(其实就是他指向的文件对象struct file里面的一个标志位)告诉内核这个文件描述符我们要以非阻塞的方式来操作!
2.1 fcntl
一个文件描述符, 默认都是阻塞IO.
int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同. fcntl函数有5种功能:
复制一个现有的描述符(cmd=F_DUPFD).
获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.
2.2 实现函数SetNoBlock
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞.
void SetNonBlock(int fd)
{
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
cout << " set " << fd << " nonblock done" << endl;
}
使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.
2.3 轮询方式读取标准输入
int main()
{
char buffer[1024];
SetNonBlock(0);
sleep(1);
while (true)
{
// printf("Please Enter# ");
// fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n - 1] = 0;
cout << "echo : " << buffer << endl;
}
else if (n == 0)
{
cout << "read done" << endl;
break;
}
else
{
// 1. 设置成为非阻塞,如果底层fd数据没有就绪,recv/read/write/send, 返回值会以出错的形式返回
// 2. a. 真的出错 b. 底层没有就绪
// 3. 我怎么区分呢?通过errno区分!!!
if (errno == EWOULDBLOCK)
{
cout << "0 fd data not ready, try again!" << endl;
// do_other_thing();
sleep(1);
}
else
{
cerr << "read error, n = " << n << "errno code: "
<< errno << ", error str: " << strerror(errno) << endl;
}
// TODO 信号中断IO?
}
}
return 0;
}
问题:如果将文件描述符设置为非阻塞了,如果底层fd数据没有就绪,recv/read/write/send,返回值会以出错(-1)的返回,为什么呢??
——>因为他实在没办法了!!>0表示成功,=0表示关闭,那么只能是<0了
所以此时<0有两种情况(1)真的出错了 (2)底层读写事件没有就绪
那我怎么区分呢??所以规定在返回-1的时候会设置错误码,我们可以通过错误码去判断!
因此一旦被设置为非阻塞了,那么返回-1情况在分类讨论的时候还需要根据错误码加一层判断,不能直接break。
当然我们也可以写一个函数让他在轮询的期间去做点别的事情!
三、select-多路转接
以前我们学到的大多数接口是既等又IO,而现在我们可以用一个select专门用来等,并且他一次可以等待多个文件描述符,从而在等的时间上实现并行!!
3.1 select介绍
系统提供select函数来实现多路复用输入/输出模型
select系统调用是用来让我们的程序监视多个文件描述符的状态变化的;
程序会停在select这里等待,直到被监视的文件描述符有一个或多个发生了状态改变;
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
3.1.1 参数解释
nfds:是需要监视的最大的文件描述符值+1;
因为文件描述符是下标,所以可以理解为监听的文件描述符的范围。
rdset,wrset,exset:分别对应于需要检测的 可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
输入输出型参数! 设置时表示要监听的文件描述符,返回时由内核设置,表示已经就绪的文件描述符
timeout:结构timeval,用来设置select()的等待时间
输入输出型参数! 比如等待时间是5s,如果2秒就有文件描述符就绪了,那么就会返回3秒
3.1.2 关于timeval结构体
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
关于取值:
NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。(第一个为单位s,第二个单位为ms)
3.1.3 关于fd_set结构体
这个结构是由内核提供的一种数据类型,其实就是一个整数数组, 更严格的说, 是一个 "位图". 使用位图中对应的位来表示要监视的文件描述符. (用来给用户和内核做沟通)
他是一个输入输出型参数!!
输入时,由用户告诉内核:我给你的一个或者多个fd,你要帮我关心上面的事件哦!如果就绪了你一定要告诉我哈!!
输出时,由内核告诉用户:你让我关心的多个fd中,有一些已经就绪了哦,用户你赶紧读取吧
所以使用select注定一定有大量位图操作!
用户:这个位图由我自己来操作吗??
OS说:你还是别直接操作了吧,你连他的结构都没搞清楚,还是让我来给你提供一批操作位图的接口吧!!所以提供了一组操作fd_set的接口, 来比较方便的操作位图,
void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位
3.1.4 函数返回值
执行成功则返回文件描述词状态已改变的个数
如果返回0代表在描述词状态改变前已超过timeout时间,没有返回
当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds, exceptfds和timeout的 值变成不可预测。
错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足
3.2 理解select执行过程
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd.
(1)执行fd_set set; FD_ZERO(&set);则set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);
后set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。注意:没有事件发生的fd=5被清空。
3.3 socket就绪条件
读就绪
socket内核中, 接收缓冲区中的字节数, 大于等于低水位标记SO_RCVLOWAT. 此时可以无阻塞的读该文件描述符, 并且返回值大于0;
socket TCP通信中, 对端关闭连接, 此时对该socket读, 则返回0;
监听的socket上有新的连接请求;
socket上有未处理的错误;
写就绪
socket内核中, 发送缓冲区中的可用字节数(发送缓冲区的空闲位置大小), 大于等于低水位标记SO_SNDLOWAT, 此时可以无阻塞的写, 并且返回值大于0;
socket的写操作被关闭(close或者shutdown). 对一个写操作被关闭的socket进行写操作, 会触发SIGPIPE信号;
socket使用非阻塞connect连接成功或失败之后;
socket上有未读取的错误;
异常就绪
socket上收到带外数据. 关于带外数据, 和TCP紧急模式相关(回忆TCP协议头中, 有一个紧急指针的字段),
3.4 通过编码深入理解
Socket.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "Log.hpp"
enum
{
SocketErr = 2,
BindErr,
ListenErr,
};
// TODO
const int backlog = 10;
class Sock
{
public:
Sock()
{
}
~Sock()
{
}
public:
void Socket()
{
sockfd_ = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd_ < 0)
{
lg(Fatal, "socker error, %s: %d", strerror(errno), errno);
exit(SocketErr);
}
int opt = 1;
setsockopt(sockfd_, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));
}
void Bind(uint16_t port)
{
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(sockfd_, (struct sockaddr *)&local, sizeof(local)) < 0)
{
lg(Fatal, "bind error, %s: %d", strerror(errno), errno);
exit(BindErr);
}
}
void Listen()
{
if (listen(sockfd_, backlog) < 0)
{
lg(Fatal, "listen error, %s: %d", strerror(errno), errno);
exit(ListenErr);
}
}
int Accept(std::string *clientip, uint16_t *clientport)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int newfd = accept(sockfd_, (struct sockaddr*)&peer, &len);
if(newfd < 0)
{
lg(Warning, "accept error, %s: %d", strerror(errno), errno);
return -1;
}
char ipstr[64];
inet_ntop(AF_INET, &peer.sin_addr, ipstr, sizeof(ipstr));
*clientip = ipstr;
*clientport = ntohs(peer.sin_port);
return newfd;
}
bool Connect(const std::string &ip, const uint16_t &port)
{
struct sockaddr_in peer;
memset(&peer, 0, sizeof(peer));
peer.sin_family = AF_INET;
peer.sin_port = htons(port);
inet_pton(AF_INET, ip.c_str(), &(peer.sin_addr));
int n = connect(sockfd_, (struct sockaddr*)&peer, sizeof(peer));
if(n == -1)
{
std::cerr << "connect to " << ip << ":" << port << " error" << std::endl;
return false;
}
return true;
}
void Close()
{
close(sockfd_);
}
int Fd()
{
return sockfd_;
}
private:
int sockfd_;
};
log.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#define SIZE 1024
#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4
#define Screen 1
#define Onefile 2
#define Classfile 3
#define LogFile "log.txt"
class Log
{
public:
Log()
{
printMethod = Screen;
path = "./log/";
}
void Enable(int method)
{
printMethod = method;
}
std::string levelToString(int level)
{
switch (level)
{
case Info:
return "Info";
case Debug:
return "Debug";
case Warning:
return "Warning";
case Error:
return "Error";
case Fatal:
return "Fatal";
default:
return "None";
}
}
// void logmessage(int level, const char *format, ...)
// {
// time_t t = time(nullptr);
// struct tm *ctime = localtime(&t);
// char leftbuffer[SIZE];
// snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
// ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
// ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
// // va_list s;
// // va_start(s, format);
// char rightbuffer[SIZE];
// vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
// // va_end(s);
// // 格式:默认部分+自定义部分
// char logtxt[SIZE * 2];
// snprintf(logtxt, sizeof(logtxt), "%s %s
", leftbuffer, rightbuffer);
// // printf("%s", logtxt); // 暂时打印
// printLog(level, logtxt);
// }
void printLog(int level, const std::string &logtxt)
{
switch (printMethod)
{
case Screen:
std::cout << logtxt << std::endl;
break;
case Onefile:
printOneFile(LogFile, logtxt);
break;
case Classfile:
printClassFile(level, logtxt);
break;
default:
break;
}
}
void printOneFile(const std::string &logname, const std::string &logtxt)
{
std::string _logname = path + logname;
int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
if (fd < 0)
return;
write(fd, logtxt.c_str(), logtxt.size());
close(fd);
}
void printClassFile(int level, const std::string &logtxt)
{
std::string filename = LogFile;
filename += ".";
filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
printOneFile(filename, logtxt);
}
~Log()
{
}
void operator()(int level, const char *format, ...)
{
time_t t = time(nullptr);
struct tm *ctime = localtime(&t);
char leftbuffer[SIZE];
snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
ctime->tm_hour, ctime->tm_min, ctime->tm_sec);
va_list s;
va_start(s, format);
char rightbuffer[SIZE];
vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
va_end(s);
// 格式:默认部分+自定义部分
char logtxt[SIZE * 2];
snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);
// printf("%s", logtxt); // 暂时打印
printLog(level, logtxt);
}
private:
int printMethod;
std::string path;
};
Log lg;
// int sum(int n, ...)
// {
// va_list s; // char*
// va_start(s, n);
// int sum = 0;
// while(n)
// {
// sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
// n--;
// }
// va_end(s); //s = NULL
// return sum;
// }
Makefile:
select_server:Main.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f select_server
SelectServer.hpp:
#pragma once
#include
#include
#include
#include "Socket.hpp"
using namespace std;
static const uint16_t defaultport = 8888;
static const int fd_num_max = (sizeof(fd_set) * 8);
int defaultfd = -1;
class SelectServer
{
public:
SelectServer(uint16_t port = defaultport) : _port(port)
{
for (int i = 0; i < fd_num_max; i++)
{
fd_array[i] = defaultfd;
// std::cout << "fd_array[" << i << "]" << " : " << fd_array[i] << std::endl;
}
}
bool Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
return true;
}
void Accepter()
{
// 我们的连接事件就绪了
std::string clientip;
uint16_t clientport = 0;
int sock = _listensock.Accept(&clientip, &clientport); // 会不会阻塞在这里?不会
if (sock < 0) return;
lg(Info, "accept success, %s: %d, sock fd: %d", clientip.c_str(), clientport, sock);
// sock -> fd_array[]
int pos = 1;
for (; pos < fd_num_max; pos++) // 第二个循环
{
if (fd_array[pos] != defaultfd)
continue;
else
break;
}
if (pos == fd_num_max)
{
lg(Warning, "server is full, close %d now!", sock);
close(sock);
}
else
{
fd_array[pos] = sock;
PrintFd();
// TODO
}
}
void Recver(int fd, int pos)
{
// demo
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?
if (n > 0)
{
buffer[n] = 0;
cout << "get a messge: " << buffer << endl;
}
else if (n == 0)
{
lg(Info, "client quit, me too, close fd is : %d", fd);
close(fd);
fd_array[pos] = defaultfd; // 这里本质是从select中移除
}
else
{
lg(Warning, "recv error: fd is : %d", fd);
close(fd);
fd_array[pos] = defaultfd; // 这里本质是从select中移除
}
}
void Dispatcher(fd_set &rfds)
{
for (int i = 0; i < fd_num_max; i++) // 这是第三个循环
{
int fd = fd_array[i];
if (fd == defaultfd)
continue;
if (FD_ISSET(fd, &rfds))
{
if (fd == _listensock.Fd())
{
Accepter(); // 连接管理器
}
else // non listenfd
{
Recver(fd, i);
}
}
}
}
void Start()
{
int listensock = _listensock.Fd();
fd_array[0] = listensock;
for (;;)
{
fd_set rfds;
FD_ZERO(&rfds);
int maxfd = fd_array[0];
for (int i = 0; i < fd_num_max; i++) // 第一次循环
{
if (fd_array[i] == defaultfd)
continue;
FD_SET(fd_array[i], &rfds);
if (maxfd < fd_array[i])
{
maxfd = fd_array[i];
lg(Info, "max fd update, max fd is: %d", maxfd);
}
}
// accept?不能直接accept!检测并获取listensock上面的事件,新连接到来,等价于读事件就绪
// struct timeval timeout = {1, 0}; // 输入输出,可能要进行周期的重复设置
struct timeval timeout = {0, 0}; // 输入输出,可能要进行周期的重复设置
// 如果事件就绪,上层不处理,select会一直通知你!
// select告诉你就绪了,接下来的一次读取,我们读取fd的时候,不会被阻塞
// rfds: 输入输出型参数。 1111 1111 -> 0000 0000
int n = select(maxfd + 1, &rfds, nullptr, nullptr, /*&timeout*/ nullptr);
switch (n)
{
case 0:
cout << "time out, timeout: " << timeout.tv_sec << "." << timeout.tv_usec << endl;
break;
case -1:
cerr << "select error" << endl;
break;
default:
// 有事件就绪了,TODO
cout << "get a new link!!!!!" << endl;
Dispatcher(rfds); // 就绪的事件和fd你怎么知道只有一个呢???
break;
}
}
}
void PrintFd()
{
cout << "online fd list: ";
for (int i = 0; i < fd_num_max; i++)
{
if (fd_array[i] == defaultfd)
continue;
cout << fd_array[i] << " ";
}
cout << endl;
}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
int fd_array[fd_num_max]; // 数组, 用户维护的!
// int wfd_array[fd_num_max];
};
Main.cc
#include "SelectServer.hpp"
#include
int main()
{
// std::cout <<"fd_set bits num : " << sizeof(fd_set) * 8 << std::endl;
std::unique_ptr svr(new SelectServer());
svr->Init();
svr->Start();
return 0;
}
注意事项:
1、不能直接aceept,因为他大部分时间都在等,一次只能等一个文件描述符!!(listensock上面的时间是新链接到来,就是三次握手完成,链接投递到全连接队列里,然后你再通过accept把链接从底层拿上来),所以新链接来了相当于是读事件就绪!!
2、 定义fd_set类型变量 如果是在栈上定义,可能会出现乱码,所以在使用前要记得先清空!!
3、因为timeout是输入输出型参数!!所以返回之后可能已经修改过了!!所以为了维持他的效果我们就必须周期性重复设置!!
4、因为(1)rfds是一个输入输出型参数,每次都会被重新设置,且随着不断获取新链接,套接字的数量会越来越多!不能写死,应是动态计算 (2)select不仅仅要等lisentsock,也要等读的sock
因此需要有一个辅助数组arrry来监控select中的fd,他不仅可以方便我们(1)将文件描述符信息在不同函数之间的传递(2)用于在select 返回后,array作为源数据和fd_set进行FD_ISSET判断。。(3)select返回后会把以前加入的但并无事件发生的fd清空,则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。(4)让左侧是监听套接字,右侧是读套接字
5、辅助数组里有链接就绪和读就绪,我们怎么区分呢??——>确认就绪之后,再加一层判断。证明自己是否是监听套接字。
6、关于Dispatcher(事件派发器),就是收到了多个就绪的文件描述符,然后跟array进行判断并派发,如果是连接就绪就交给连接事件处理,如果是读就绪就交给读事件处理。
因为就绪的时间不一定只有一个,所以必须要循环去遍历!
7、关于recver,读的时候不能直接读,因为读的时候内容可能不完整,这就涉及到了协议的内容!
3.5 select缺点
1、等待的fd是有上限的!!
可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)=512,每bit表示一个文件 描述符,则我服务器上支持的最大文件描述符是512*8=4096.
备注: fd_set的大小可以调整,可能涉及到重新编译内核
2、输入输出型参数比较多,数据拷贝的频率很高,且每次都需要对关心的fd进行重置
3、用户层是,使用第三方数组管理用户的fd,用户层需要多次遍历,内核中检测fd时间就绪也要遍历。