网站设置专栏,网站建设客户案例,金山网站建设费用,无障碍 网站 怎么做碌碌无为#xff0c;则余生太长#xff1b; 欲有所为#xff0c;则人生苦短。 --- 中岛敦 《山月记》--- 从零开始认识五种IO模型 1 前言2 认识多路转接select3 多路转接select等待连接4 完善代码5 总结 1 前言
上一篇文章我们讲解了五种IO模型的基本概念#xff0c;并… 碌碌无为则余生太长 欲有所为则人生苦短。 --- 中岛敦 《山月记》--- 从零开始认识五种IO模型 1 前言2 认识多路转接select3 多路转接select等待连接4 完善代码5 总结 1 前言
上一篇文章我们讲解了五种IO模型的基本概念并通过系统调用使用了非阻塞IO。 一般的服务器不会使用非阻塞IO因为非阻塞IO非常耗费CPU资源导致CPU发热效率下降非阻塞IO只有在特定情况下才比较好用
今天我们来学习多路转接select。
我们知道IO 等 拷贝。拷贝的前提是底层有数据没有数据的时候就需要进行等待。为了提高效率可以等待多个文件描述符。多路转接就是等待文件描述符上的新事件等到就可以通知程序员事件已经就绪可以进行拷贝
这个事件可以是
读事件就绪OS底层有数据了写事件就绪OS底层有空间了
今天我们要学习的就是多路转接select
2 认识多路转接select
我们先来看其作用与定位
select的定位是只在IO中只负责等待不进行拷贝 并且select可以等待多个文件描述符有新事件就进行通知。
来看select系统调用
SELECT(2) Linux Programmers Manual SELECT(2)NAMEselect, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexingSYNOPSIS#include sys/select.hint select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);void FD_CLR(int fd, fd_set *set);int FD_ISSET(int fd, fd_set *set);void FD_SET(int fd, fd_set *set);void FD_ZERO(fd_set *set);int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);Feature Test Macro Requirements for glibc (see feature_test_macros(7)):pselect(): _POSIX_C_SOURCE 200112L
select函数中有5个参数都是用来干什么的呢
int nfds输入性参数 表示等待的多个文件描述符最大值 加 1。比如等待1 2 5 6 99 这几个文件描述符那么就要传入100。注意不是文件描述符的个数struct timeval *timeout输入输出性参数 这是一个结构体表示微秒级别的时间戳其中有两个参数分别表示秒和微秒。这个参数告诉select在这个时间戳内进行阻塞式select超出时间就进行一次返回。如果时间以内等到了新事件就返回并把剩余时间返回。传入{00}就是非阻塞轮询了。传入nullptr表示一直阻塞等待事件。
那么现在我们知道了两个参数我们探索一下返回值
大于0有几个就绪了等于0超时返回了小于0select出错了
那么其他三个参数呢首先fd_set代表文件描述符集是用位图进行维护的位图下标表示文件描述符该比特位的内容表示对应信息一共1024比特位可以表示1024个文件描述符下面我们就来了解一下这三个参数 这三个参数都是fd_set是输入输出参数分别对应读事件写事件异常事件。通过这三个位图的设置我们就可以对一个文件描述符的操作指明清楚。今天我们以读事件为例进行讲解
输入时传入一个读事件文件描述符就是告诉OS要帮我们关心fd_set集合中的所有fd的读事件。这里比特位的位置表示文件描述符的编号比特位的内容表示是否关心fd的读事件!输出时OS会返回一个读事件文件描述符表示你让我关心的文件描述符集中哪些已经就绪了这里比特位的位置表示文件描述符的编号比特位的内容表示事件是否发生
OK现在我们了解了select的基本参数下面我们就开始使用select进行编程
3 多路转接select等待连接
我们首先把之前的套接字基础的类拷贝过来
class Socket:实现套接字的创建工作并进入监听模式。class InetAddr:网络套接字基本信息类用于进行网络套接字传参工作。class Log:进行日志信息的打印便于调试
然后我们就来设计Selectsever类
成员变量需要端口号TcpSocket套接字类构造函数中进行端口号的初始化并创建套接字设置为监听模式循环函数中不能直接进行accept获取连接因为底层不一定有数据直接进行会阻塞式等待。所以我们可以把accept看做IO函数将等的任务交给select函数。select函数需要对监听套接字进行等待
#pragma once#include Socket.hpp
#include sys/select.h
#include Log.hppusing namespace socket_ns;
using namespace log_ns;class SelectServer
{
public:SelectServer(uint16_t port) : _port(port),_listensock(std::make_uniqueTcpSocket()){// 建立监听套接字_listensock-BuildListenSocket(_port);}~SelectServer(){};void Initserver(){}void Loop(){//进入服务while(true){//不能直接进行accept 因为底层不一定建立了连接,所以需要等待底层就绪//等待过程交给select//_listensock-Accepter();//创建fd_setfd_set rfds ;FD_ZERO(rfds);//加入监听套接字文件描述符FD_SET( _listensock-GetSockfd() , rfds);//创建timeoutstruct timeval timeout {3 , 0};//进行selectint n ::select(_listensock-GetSockfd() 1 , rfds , nullptr , nullptr , timeout);switch (n){case 0://超时LOG(DEBUG , timeout : %d.%d\n , timeout.tv_sec , timeout.tv_usec);break;case -1://出错了LOG(ERROR, select error\n);break;default://正常LOG(INFO, have event ready: n %d\n , n);//执行任务HandlerEvent(rfds);break;}}}private:uint16_t _port;std::unique_ptrSocket _listensock;
};
我们运行程序来看等待效果 可以正常的进行等待当我们进行连接时 select函数就能告诉我们有哪些文件描述符就绪可以进行拷贝。这里可以得到一个现象
如果事件就绪但是不处理select就会一直通知我们直到我们处理这个事件。
当我们知道底层就绪时我们就可以进行拷贝了
void HandlerEvent(fd_set rfds){//判断是否是套接字就绪if(FD_ISSET(_listensock-GetSockfd() , rfds)){//连接事件就绪//那么这里我们可以进行accept吗InetAddr addr;int sockfd _listensock-Accepter(addr);//已经就绪 ,不会阻塞//这时会得到一个新连接if(sockfd 0){LOG(DEBUG ,get a new link , client info %s:%d\n , addr.Ip().c_str() ,addr.Port());//TODO}else{return ;}}}但是有几个问题
在上面的handler函数中我们已经获取到了连接那么下面敢不敢直接进行读取呢 当然不能因为建立连接并不代表会有请求传过来所以还需要等待请求那么怎么知道底层有没有就绪呢 还是通过select进行等待想办法将新的fd添加给select进行统一管理那么这样select等待的fd不就越来越多这要怎么进行维护呢 通过辅助数据结构进行维护由于select接口的参数是输入输出性无法保存文件描述符所以必然需要额外的数据结构进行维护文件描述符
4 完善代码
针对上面的三个问题我们首先要做的就是想办法通过一个数据结构维护需要进行select的文件描述符。每次进入循环进行select时就要通过这个数据结构初始化rfds然后在通过对返回值的rfds与辅助数据结构中的文件描述符进行比对对有新事件的文件描述符进行处理
对于这个数据结构我们选择最简单的一维C风格数组即可进行初始化时都设置为默认值-1 const static int gnum sizeof(fd_set) * 8;const static int gdefault -1;//...void Initserver(){// 对数组进行初始化for (int i 0; i gnum; i){fd_array[i] gdefault;}// 加入监听套接字fd_array[0] _listensock-GetSockfd();}//...// 辅助数组int fd_array[gnum];通过这个数组当我们进行循环时每次就都需要通过这个数组进行初始化rfds。 void Loop(){// 进入服务while (true){// 创建fd_setfd_set rfds;FD_ZERO(rfds);int max_fd 0;// 首先根据fd_array将合法fd加入到rfdsfor (int i 0; i gnum; i){if (fd_array[i] gdefault)continue;// 加入合法的文件描述符FD_SET(fd_array[i], rfds);// 维护一个文件描述符最大值if (fd_array[i] max_fd)max_fd fd_array[i];}// 创建timeoutstruct timeval timeout {30, 0};// 进行selectint n ::select(max_fd 1, rfds, nullptr, nullptr, timeout);switch (n){case 0:// 超时LOG(DEBUG, timeout : %d.%d\n, timeout.tv_sec, timeout.tv_usec);break;case -1:// 出错了LOG(ERROR, select error\n);break;default:// 正常LOG(INFO, have event ready: n %d\n, n);// 处理事件HandlerEvent(rfds);PrintDebug();break;}}}接下来我们来看handlerevent函数进行select之后如果有事件就绪程序就会进入handlerevent函数。那么我们要如何判断是哪一个文件操作符的事件就绪了呢
直接遍历数组进行FD_ISSET通过对每一个合法fd进行判断我们就能够知道是哪一个文件操作符有事件就绪如果是listenfd就绪说明有新连接需要进行accepter获取新连接的fd将其存入到文件描述符数组中如果是普通fd就绪我们进行读写操作即可如果有连接退出了要及时更新数组。 void Accepter(){// 连接事件就绪InetAddr addr;int sockfd _listensock-Accepter(addr); // 已经就绪 ,不会阻塞// 这时会得到一个新连接if (sockfd 0){LOG(DEBUG, get a new link , client info %s:%d\n, addr.Ip().c_str(), addr.Port());// 将新获取的fd加入到数组中LOG(INFO, get new fd :%d\n, sockfd);bool flag false;for (int i 0; i gnum; i){if (fd_array[i] gdefault){flag true;fd_array[i] sockfd;break;}elsecontinue;}if (flag false){LOG(WARNING, fd_array have fill!\n);}}}void HandlerIO(int fd){char buffer[1024];int n ::recv(fd, buffer, sizeof(buffer) - 1, 0);if (n 0){// 读取到了数据buffer[n] 0;std::string echo_str [client say]#;echo_str buffer;std::cout echo_str std::endl;// 返回一个报文std::string content htmlbodyh1hello bite/h1/body/html;std::string ret_str HTTP/1.0 200 OK\r\n;ret_str Content-Type: text/html\r\n;ret_str Content-Length: std::to_string(content.size()) \r\n\r\n;ret_str content;// echo_str buffer;::send(fd, ret_str.c_str(), ret_str.size(), 0); // 临时方案}else if (n 0){// 此时fd退出了LOG(INFO, fd:%d quit!\n, fd);::close(fd);fd gdefault;}else{LOG(ERROR, recv error! errno:%d\n, errno);::close(fd);fd gdefault;}}void HandlerEvent(fd_set rfds){// 遍历fd_array判断是否有就绪的新事件for (int i 0; i gnum; i){if (fd_array[i] gdefault)continue;// 如果有新事件if (FD_ISSET(fd_array[i], rfds)){// 进行判断是scokfd 还是普通fdif (fd_array[i] _listensock-GetSockfd()){Accepter();}// 普通fd 进行正常读写else{HandlerIO(fd_array[i]);}}}}这样就使用select完成了对连接的获取读取工作来看效果 可以看到我们的数组中的有效fd随着客户端连接与中断会动态变化
5 总结
根据上面的代码我们可以总结出select的一些优缺点
每次调用 select都需要手动设置 fd 集合 从接口使用角度来说也非常不便.每次调用 select 都需要把 fd 集合从用户态拷贝到内核态 这个开销在 fd 很多时会很大。这个是多路转接IO无法避免的问题同时每次调用 select 都需要在内核遍历传递进来的所有 fd这个开销在 fd 很多时很大。select 支持的文件描述符数量太小虽然操作系统中文件描述符也有限制但是这是操作系统的缺陷。同样select也是缺点
这里不断的要进行循环遍历数组造成的性能开销是比较大的所以就有了其他两种多路转接方案poll与epoll