企业网站托管公司,域名注册价格,环艺毕业设计代做网站,用jquery做的书籍网站文章目录 一、五种IO模型二、非阻塞IO1.fcntl2.实现函数SetNoBlock3.轮询方式读取标准输入 三、I/O多路转接之select1.初识select2.select函数原型3.socket就绪条件4.select的特点5.select缺点6.select使用案例--只读取数据的server服务器1.err.hpp2.log.hpp3.sock.hpp4.select… 文章目录 一、五种IO模型二、非阻塞IO1.fcntl2.实现函数SetNoBlock3.轮询方式读取标准输入 三、I/O多路转接之select1.初识select2.select函数原型3.socket就绪条件4.select的特点5.select缺点6.select使用案例--只读取数据的server服务器1.err.hpp2.log.hpp3.sock.hpp4.select_server.hpp5.main.cc 一、五种IO模型
什么是IO什么是高效的IO
我们知道我们调用read/recv的时候如果缓冲区中没有数据那么就会阻塞住有数据的时候read/recv会进行拷贝完成之后返回。所以 IO 等 数据拷贝
那么如何让IO更加的高效呢只要减少等待的比重即可。
我们以几个人钓鱼为例来说明五种IO模型 张三钓鱼的时候一直盯着鱼竿不做任何其他事情 李四钓鱼的时候偶尔看看书看看手机 王五在鱼竿上方一个铃铛铃铛响了就说明有鱼在铃铛没有的时间内王五也看看书看看手机 赵六同时投放了多个鱼竿来回进行检测看哪个鱼竿上有鱼 田七和小王田七是一个老板想吃鱼就让小王去钓钓满一桶之后小王联系田七田七来取鱼 对于上面的几种钓鱼方式鱼就是数据河为内核空间鱼漂鱼就绪数据就绪事件鱼竿文件描述符
钓鱼的动作read/recv调用
张三是阻塞式IO李四是非阻塞式IO王五是信号驱动式IO赵六是多路转接/多路复用田七是异步IO
张三李四王五在效率上没有差别但是李四和王五可以做其他的事情张三李四王五赵六每个人都等了每个人都参与了IO的过程称为同步IO。田七没有参与IO的两个阶段的任何一个阶段称为异步IO
在我们看来赵六的钓鱼效率是最高的因为他等的比重比较低单位时间内钓鱼的效率就高
为什么多路转接/多路复用是高效的代名词IO 等(减少等的比重) 拷贝
阻塞IO是最常见的IO模型 非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用 信号驱动IO: 内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作 IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态. 异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据). 任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少
同步通信vs异步通信(synchronous communication/ asynchronouscommunication)
同步和异步关注的是消息通信机制 所谓同步就是在发出一个调用时在没有得到结果之前该调用就不返回. 但是一旦调用返回就得到返回值了; 换句话说就是由调用者主动等待这个调用的结果; 异步则是相反调用在发出之后这个调用就直接返回了所以没有返回结果; 换句话说当一个异步过程调用发出后调用者不会立刻得到结果; 而是在调用发出后被调用者通过状态、通知来通知调用者或通过回调函数处理这个调用. 另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不想干的概念 进程/线程同步也是进程/线程之间直接的制约关系 是为完成某种任务而建立的两个或多个线程这个线程需要在某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系. 尤其是在访问临界资源的时候 阻塞 vs 非阻塞
阻塞和非阻塞关注的是程序在等待调用结果消息返回值时的状态 阻塞调用是指调用结果返回之前当前线程会被挂起. 调用线程只有在得到结果之后才会返回. 非阻塞调用指在不能立刻得到结果之前该调用不会阻塞当前线程 其他高级IO
非阻塞IO纪录锁系统V流机制I/O多路转接也叫I/O多路复用,readv和writev函数以及存储映射IOmmap这些统称为高级IO
二、非阻塞IO
1.fcntl
一个文件描述符, 默认都是阻塞IO.
函数原型如下
#include unistd.h
#include fcntl.h
int fcntl(int fd, int cmd, ... /* arg */ );传入的cmd的值不同, 后面追加的参数也不相同.
fcntl函数有5种功能: 复制一个现有的描述符cmdF_DUPFD. 获得/设置文件描述符标记(cmdF_GETFD或F_SETFD). 获得/设置文件状态标记(cmdF_GETFL或F_SETFL). 获得/设置异步I/O所有权(cmdF_GETOWN或F_SETOWN). 获得/设置记录锁(cmdF_GETLK,F_SETLK或F_SETLKW) 我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞
2.实现函数SetNoBlock
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞
void SetNoBlock(int fd)
{ int fl fcntl(fd, F_GETFL); if (fl 0){ perror(fcntl);return; }fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图).
然后再使用F_SETFL将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK参数.
3.轮询方式读取标准输入
#include iostream
#include unistd.h
#include fcntl.hvoid SetNonBlock(int fd)
{int fl fcntl(fd, F_GETFL);if (fl 0){perror(fcntl);return;}fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}int main()
{SetNonBlock(0);while (true){char buffer[1024];ssize_t s read(0, buffer, sizeof(buffer) - 1);if (s 0){std::cout buffer std::endl;}else{perror(read);sleep(1);continue;}}return 0;
}三、I/O多路转接之select
1.初识select
系统提供select函数来实现多路复用输入/输出模型. select系统调用是用来让我们的程序监视多个文件描述符的状态变化的; 程序会停在select这里等待直到被监视的文件描述符有一个或多个发生了状态改变 select : IO 等 拷贝
select 只负责等待可以一次等待多个fdselect本身没有数据拷贝的能力拷贝要readwrite来完成
2.select函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);参数解释: 参数nfds是需要监视的最大的文件描述符值1 readfds,writefds,exceptfds分别对应于需要检测的可读文件描述符的集合可写文件描述符的集 合及异常文件描述符的集合; 参数timeout为结构timeval用来设置select()的等待时间 fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout都是输入输出型参数
参数timeout取值: NULL则表示select没有timeoutselect将一直被阻塞直到某个文件描述符上发生了事件; 0仅检测描述符集合的状态然后立即返回并不等待外部事件的发生。 特定的时间值如果在指定的时间段里没有事件发生select将超时返回。 关于fd_set结构 其实这个结构就是一个整数数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符.
提供了一组操作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的全部位我们以读事件为例来说明readfds,writefds,exceptfds
作为输入时表示用户告诉内核你要帮我关心一下我给你的集合中的所有的fd的读事件–哪些fd上的读事件内核你要关心比特位的位置表示fd的数值比特位的内容表示是否关心
作为输出时内核告诉用户你所关心的多个fd中有哪些已经就绪了。比特位的位置表示fd的数值比特位的内容表示哪些fd上面的读事件已经就绪了
这样让用户和内核之间相互沟通互相知晓对方要的或者关心的。
fd_set是一种类型既然是一种类型必定有大小而且是固定的所以能够添加的fd的个数也是有上限的
关于timeval结构
timeval结构用于描述一段时间长度如果在这个时间内需要监视的描述符没有事件发生则函数返回返回值为0。 struct timaval
{time_t tv_sec; /* seconds */suseconds_t tv_usec; /* microseconds */
};传输参数
nullptr : 阻塞式
struct timeval timeout {0,0}; 非阻塞
struct timeval timeout {5,0};
5s以内阻塞式超过5s非阻塞返回一次函数返回值 执行成功则返回文件描述词状态已改变的个数 如果返回0代表在描述词状态改变前已超过timeout时间没有返回 当有错误发生时则返回-1错误原因存于errno此时参数readfdswritefds, exceptfds和timeout的值变成不可预测。 ret 0 : 有几个fd就绪了 ret 0 : 超时返回了 ret 0 : select 调用失败了 错误值可能为 EBADF 文件描述词为无效的或该文件已关闭 EINTR 此调用被信号所中断 EINVAL 参数n 为负值。 ENOMEM 核心内存不足 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协议头中, 有一个紧急指针的字段), 4.select的特点
1.可监控的文件描述符个数取决与sizeof(fd_set)的值. 我这边服务器上sizeof(fd_set)512每bit表示一个文件描述符则我服务器上支持的最大文件描述符是512*84096。即select能同时等待的文件fd是有上限的除非重新改变内核否则无法解决。
2.将fd加入select监控集的同时还要再使用一个数据结构array保存放到select监控集中的fd
2.1一是用于再select 返回后array作为源数据和fd_set进行FD_ISSET判断。即必须借助第三方数组来维护合法的fd
2.2二是select返回后会把以前加入的但并无事件发生的fd清空则每次开始select前都要重新从array取得fd逐一加入(FD_ZERO最先)扫描array的同时取得fd最大值maxfd用于select的第一个参数。select第一个参数为什么是最大fd1呢确定遍历范围 – 内核层面
3.select的大部分参数是输入输出型的调用select前要重新设置所有的fd,调用之后我们还要检查更新所有的fd这就带来遍历成本
4.select 采用位图用户 - 内核内核 - 用户来回的进行数据拷贝拷贝成本问题
备注: fd_set的大小可以调整可能涉及到重新编译内核
5.select缺点
每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
每次调用select都需要把fd集合从用户态拷贝到内核态这个开销在fd很多时会很大
同时每次调用select都需要在内核遍历传递进来的所有fd这个开销在fd很多时也很大
select支持的文件描述符数量太小有上限
6.select使用案例–只读取数据的server服务器
1.err.hpp
#pragma onceenum
{USAGE_ERR 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};2.log.hpp
#pragma once#include iostream
#include unistd.h
#include sys/types.h
#include stdarg.h#define NORMAL 0
#define DEBUG 1
#define WARNING 2
#define ERROR 3
#define FATAL 4#define LOG_NORMAL ./log.txt
#define LOG_ERR ./err.txt#define NUM 1024const char *to_levelstr(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, ...)
{// [日志等级] [时间戳/时间] [pid] [messge]char logprofix[NUM];snprintf(logprofix, sizeof logprofix, [%s][%ld][pid:%d], to_levelstr(level), (long int)time(nullptr), getpid());char logcontent[NUM];va_list arg;va_start(arg, format);vsnprintf(logcontent, sizeof logcontent, format, arg);std::cout logprofix logcontent std::endl;FILE *log fopen(LOG_NORMAL, a);FILE *error fopen(LOG_ERR, a);if (log error){FILE *cur nullptr;if (level DEBUG || level NORMAL || level WARNING)cur log;if (level ERROR || level FATAL)cur error;if (cur)fprintf(cur, %s%s\n, logprofix, logcontent);fclose(log);fclose(error);}
}3.sock.hpp
#pragma once#include iostream
#include cstring
#include string
#include unistd.h
#include sys/types.h
#include sys/socket.h
#include netinet/in.h
#include arpa/inet.h#include log.hpp
#include err.hppclass Sock
{static const int backlog 32;public:// 1. 创建socket文件套接字对象static int 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;}// 2.bind自己的网络信息static void Bind(int sock, const 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;int n bind(sock, (struct sockaddr *)local, sizeof local);if (n 0){LogMessage(FATAL, socket bind error);exit(BIND_ERR);}LogMessage(NORMAL, socket bind success);}// 3. 设置socket 为监听状态static void Listen(int sock){int n listen(sock, backlog);if (n 0){LogMessage(FATAL, socket listen error);exit(LISTEN_ERR);}LogMessage(NORMAL, socket listen success);}// 4.获取连接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, socket 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;}
};4.select_server.hpp
#pragma once#include iostream
#include cstring
#include cstdlib
#include cerrno
#include string
#include functional
#include unistd.h
#include sys/socket.h
#include sys/types.h
#include sys/select.h#include sock.hpp
#include log.hpp
#include err.hnamespace select_ns
{const static int defaultport 8080;const static int fdnum sizeof(fd_set) * 8;const static int defaultfd -1;typedef std::functionstd::string(std::string) func_t;class selectServer{public:selectServer(const func_t func, const uint16_t port defaultport): _port(port), _func(func), _listensock(-1), _fdarray(nullptr){}void initServer(){_listensock Sock::Socket();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(){std::cout fd list: ;for (int i 0; i fdnum; i){if (_fdarray[i] ! defaultfd){std::cout _fdarray[i] ;}}}void Accepter(int listensock){std::string clientip;uint16_t clientport;int sock Sock::Accept(listensock, clientip, clientport);if (sock 0)return;LogMessage(NORMAL, accept success[%s:%d], clientip.c_str(), clientport);// sock我们能直接recv/read 吗不能整个代码只有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 is full,please wait);close(sock);}else_fdarray[i] sock;Print();}void Recver(int sock, int pos){// 1. 读取requestchar 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;LogMessage(NORMAL, client quit);return;}else{close(sock);_fdarray[pos] defaultfd;LogMessage(ERROR, client error:%s, strerror(errno));return;}// 2. 处理requeststd::string response _func(buffer);// 3. 返回responsewrite(sock, response.c_str(), response.size());}void HandlerEvent(fd_set rfds){for (int i 0; i fdnum; i){if (_fdarray[i] defaultfd)continue;if (FD_ISSET(_fdarray[i], rfds) _fdarray[i] _listensock)Accepter(_listensock);else if (FD_ISSET(_fdarray[i], rfds))Recver(_fdarray[i], i);else{}}}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;FD_SET(_fdarray[i], rfds); // 合法 fd 全部添加到读文件描述符集中if (maxfd _fdarray[i])maxfd _fdarray[i]; // 更新所有fd中最大的fd}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 _port;int _listensock;int *_fdarray;func_t _func;};
}5.main.cc
#include selectServer.hpp
#include err.hpp
#include memoryusing namespace std;
using namespace select_ns;static void Usage(const string proc)
{std::cerr Usage:\n\t proc port\n\n;
}string transaction(const string request)
{return request;
}int main(int argc, char *argv[])
{if (argc ! 2){Usage(argv[0]);exit(USAGE_ERR);}// std::unique_ptrselectServer svr(new selectServer(transaction,atoi(argv[0])));std::unique_ptrselectServer svr(new selectServer(transaction));svr-initServer();svr-start();return 0;
}