目录
1、五种IO模型:
2、非阻塞IO
3、多路转接之select
3.1、明白select的执行过程
1、应用层read&write的时候,本质把数据从用户写给OS ---- 本质就是拷贝
2、IO = 等待 + 拷贝(大部分时间在等待,比及有数据了才举行拷贝)
要举行拷贝,必须先判断条件成立。 这个条件就是读写事件 读事件停当,就是读缓冲区有足够的数据可以读 写事件停当就是发送缓冲区中要有足够的空间
什么叫做高效IO呢? 单元时间内,IO过程中,等的比重越小,IO服从就越高!
实在险些所有提高IO服从的计谋,本质就是让等的比重变小。
1、五种IO模型:
五种IO模型:
1、张三:钓鱼的时候,专心钓鱼,不做任何其他的事,反面任何人说话。这叫阻塞式IO。
2、李四:钓鱼的时候,在等鱼咬勾的时候还会一边看书。这是非阻塞轮询IO。
3、王五:等鱼咬钩的时候,在鱼竿上面放一个铃铛,当鱼咬钩的时候,就会给他提醒。这是信号驱动式IO。
4、赵六:他有100个鱼竿,全都放在水里,自己在岸边巡查,有鱼咬钩立马去检察。这是多路复用、多路转接式IO。
5、田七:他是老板,他把钓鱼这件事委托给他的助理小王,田七只是钓鱼行为的发起者,他要的只是鱼。这是异步IO。
阻塞IO vs 非阻塞IO 它们的IO服从实在是雷同的,因为IO = 等待 + 拷贝 (等待的时间实在是一样的,只是等的方式差别)
同步IO vs 异步IO 同步IO实在是当前这个人有没有参与IO,参与了就是同步,没参与就是异步。异步IO不参与IO只是发起IO,最后拿效果就行。 同步IO和线程同步实在没有任何关系。
因为多路复用式IO服从最高 我们后面重点阐明多路复用式IO和非阻塞IO
2、非阻塞IO
文件描述符本质就是一个数组下标,每一个数组下标指向的都是一个内核中的文件对象,文件对象中有文件的flags,用fcntl设置一个文件描述符的属性,实在就是设置这个文件在底层struct file中的标志位。
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd,... /* arg */ );
将文件描述符设置为非阻塞,以后在利用read、write、recv、send...都是利用非阻塞的方式举行IO的。
- void SetNoBlock(int fd) {
- int fl = fcntl(fd, F_GETFL); //获得指定文件描述符的标记位
- if (fl < 0) //小于0,获取失败
- {
- perror("fcntl");
- return;
- }
- fcntl(fd, F_SETFL, fl | O_NONBLOCK);
- //为指定的文件描述符设置标记位 在老的标记位f1的基础上添加 O_NONBLOCK(非阻塞)
- }
复制代码 1、将文件描述符设置成为非阻塞,假如底层fd数据没有停当,recv、read、write、send返回值会以出错的形式返回(因为返回值大于0就是读到了,等于0就是关闭了)
2、有两种情况: 我真的出错了、底层没有停当
3、我要怎么区分这两种情况??? 通过errno错误码区分(11是底层数据没有停当)
- else if(errno == EWOULDBLOCK)
- {
- cout << "0 fd data not ready, try again!" << endl;
- }
复制代码- #include <fcntl.h>
- #include <unistd.h>
- #include<errno.h>
- #include <cstdlib>
- #include <iostream>
- using namespace std;
- //对指定的fd设置非阻塞
- void SetNonBlock(int fd) {
- int fl = fcntl(fd, F_GETFL);
- if (fl < 0) {
- cerr << "fcntl error" << endl;
- exit(1);
- }
- fcntl(fd, F_SETFL, fl | O_NONBLOCK);
- }
- int main() {
- SetNonBlock(0);
- while (1) {
- char buffer[1024];
- ssize_t s = read(0, buffer, sizeof(buffer) - 1);
- if (s > 0) {
- buffer[s] = 0;
- cout << buffer << endl;
- } else if (s == 0) {
- cout << "读到文件结尾了" << endl;
- break;
- }
- else
- {
- //1. 数据没用准备好 2. 真的出错了. 都以-1的返回值返回
- // 数据没有准备好,不算出错. 需要区分这两种情况
- if(errno == EWOULDBLOCK || errno == EAGAIN)
- {
- cout<<"os底层数据还没就绪"<<endl;
- cout<<errno<<endl;
- }
- //被信号中断, 也不算read出错
- else if(errno == EINTR)
- {
- cout<<"IO interrupted by signal"<<endl;
- }
- else
- {
- cout<<"read error"<<endl;
- break;
- }
- }
- sleep(1);
- }
- }
复制代码 3、多路转接之select
IO = 等 + 拷贝
select:只负责举行等待。一次可以等待多个fd
停当事件通常分为可读事件,可写事件和非常事件
#inclde <sys/select>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
第一个参数:要等的最大的文件描述符+1
第二个:关心读 fd_set是内核提供的一种数据类型,它是位图 输入输出型参数
输入时:用户告诉内核,我给你一个或多个fd,你要帮我关心fd上面的读事件哦,假如读事件停当了,你要告诉我。
输出时:内核告诉用户,用户你要我关心的多个fd,有哪些停当了,用户你赶紧读取吧。
输入时:比特位的位置(从右向左数),表示文件描述符编号,比特位的内容,0/1,表示是否需要内核关心。
输出时:比特位为0/为1,表示哪些用户关心的fd,上面的读事件已经停当了。
fd_set是一张位图,让用户和内核传递fd是否停当的信息的。
第三个: 关心写
这就涉及到许多对位图修改的动作,系统提供了接口。
fd_set类型参数, 输入你想要关心的fd聚集, 输出时, 此结构中存放, 已经事件停当的fd聚集. 比如你想要关心0~10号文件描述符的读事件, 函数返回时, 此聚集中可能只有1.3.5号fd被返回了, 也就是说只有1.3.5号fd的事件停当了
第五个参数:
struct timeval:结构体 时间结构体:
{5,0}:每隔5s,timeout一次。5s阻塞式等待,这5s没有文件描述符停当,就返回,再重新进入,重复(我们需要重复设置)。假如等待5s期间有文件描述符停当了,就会立刻返回
timeval类型参数: 假如你设置阻塞时间为5秒, 但是等待了三秒后就有事件停当, 函数就返回了, 那么timeval类型参数的值会被设置成为2秒.
他是输入输出参数,可能要举行周期的重复设置
{0,0}:非阻塞等待。立马返回
NULL:阻塞等待
这个参数是输入输出型参数
select返回值和错误码:
大于0,有n个fd停当了
等于0,超时,没有出错,但是也没有fd停当
小于0,等待出错
3.1、明白select的执行过程
假如事件停当,上层不处置惩罚,select会一直关照你!
select的缺点:
1、等待的fd是有上限的
2、输入输出型参数比较多,数据拷贝的频率比较高(用户到内核,内核到用户)
3、输入输出型参数比较多,每次都要对关心的fd举行事件重置
4、用户层,利用第三方数组管理用户的fd,用户层需要许多次遍历,内核中检测fd事件停当也要遍历
- #pragma once
- #include <iostream>
- #include <sys/select.h>
- #include <sys/time.h>
- #include "Socket.hpp"
- using namespace std;
- static const uint16_t defaultport = 8080;
- 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", clientip.c_str(), clientport);
- // 以前走到这里可以直接读取数据,可是,现在这里不可以
- // 以前是多线程、多进程, 文件描述符其实是托管给执行流的 他阻塞是不影响的
- // 我们现在是单进程,不能建立完连接,立马就进行读 如果不发,当前的进程就阻塞了
- // select里面只有一个listen套接字 要将sock设置进select里 这样读文件描述符集里的文件描述符就会变得越来越多
- // accept获取新连接, 不能直接读,因为不清楚是否就绪,selcet清楚是否就绪,所以要将新获取的连接 添加到辅助数组里
- // select把数据处理完之后,下次循环时会重新再进行把文件描述符添加到rfds里,再交给select由他来监听
- int pos = 1;
- for (; pos < fd_num_max; pos++)
- {
- if (fd_array[pos] != default)
- continue; // 被占用
- else
- break;
- }
- if (pos == fd_num_max)
- {
- // 全部被占用
- lg(Warning, "server is full, close %d now!", sock);
- close(sock);
- }
- else // 提前break说明由-1位置(即没有被占用)
- {
- fd_array[pos] = sock; // 新获取的连接往数组中添加
- }
- }
- void Recver(int fd, int pos)
- {
- char buffer[1024];
- ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
- if (n > 0)
- {
- buffer[n] = 0; // 把它当字符串
- cout << "get a message: " << buffer << endl;
- }
- else if (n == 0) // 读失败
- {
- lg(Info, " client quit,me too, close fd is : ", fd);
- close(fd);
- fd_array[pos] = defaultfd; // 这里本质是从select中移除
- }
- }
- void Dispatchar(fd_set &rfds) // 读事件就绪 //事件派发器
- {
- for (int i = 0; i < fd_num_max; i++)
- {
- int fd = fd_arry[i];
- if (fd == dafaultfd)
- conitnue;
- if (FD_ISSET(fd, &rfds)) // 判断listen套接字是否在集合里,即是否就绪
- {
- if(fd == _listensock.Fd())
- {
- Accepter(); //连接管理器
- }
- else //不是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_arry[i] == default)
- continue; //没有被设置过的
- Fd_SET(fd_array[i], &rfds); // 将文件描述符添加到集合里
- if(maxfd < fd_array[i])
- {
- maxfd = fd_array[i];
- }
- }
- // accept 的本质是检测listensocket上面有没有连接事件 即底层有三次握手 他一次只能等一个文件描述符 所以要交给select
- // 新连接到来,对于select来讲就是读事件就绪
- // 读文件描述符集,他是一个位图
-
-
- struct timeval timeout = {5, 0}; //需要被重复设置 因为如果不重复设置就 会剩余的时间替换
- //输入输出参数,每次从内核返回的时候值可能就被改过了 ,所以需要重复设置
- int n = select(maxfd + 1, &rfds, nullptr, nullptr, &timeout); // 告诉OS关心这个文件描述符上的读事件
- //select 是如果事件就绪,上层不处理,select会一直通知你
- //select告诉你就绪了,接下来的一次读取,fd不会阻塞
- switch(n)
- {
- case 0:
- //等待超时,在等待期间任何事情都没有发生
- break;
- case -1:
- //异常
- break;
- default:
- //有事件就绪
- HandlerEvent(rfds);//就绪的事件在rfds里
- break;
- }
- }
- }
- ~SelectServer()
- {
- _listensock.Close();
- }
- private:
- Sock _listensock;
- uint16_t _port;
- int fd_array[fd_num_max]{defaultfd};//设置辅助数组
- };
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |