提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
前言
在以往模拟实现的tcp服务器,服务器的监听套接字会为每一个客户端创建一个新的套接字,用于与制定客户端通信进行通信,但因为不知道哪个描述符什么时候有数据到来
,因此流程中固定先获取新建连接,然后接受数据,但是这样有可能会造成程序的阻塞~~
1.在没有新建连接时调用accept获取新建连接;
2.在制定客户端没有数据是调用recv接收数据;
以前的解决办法:多执行流(多进程&多线程),给每一个客户端通信都创建一个执行流负责通信,这样一个执行流不会影响其他客户端;
但是这样方式,有一个缺陷就是,占用资源较多;此时就需要引入多路复用模型了·!!!
1、多路复用模型初识
多路复用模型,又名多路转接模型;
功能: 针对大量描述符进行IO事件监控,让进程可以只针对就绪描述符进行IO操作,提高IO效率,避免针对未就绪描述符操作而导致效率降低或阻塞;
针对前言中的情况,使用多路复用模型,首先其对大量IO事件进行监控,让进程可以知道哪个描述符有数据到来,也就是哪个就绪就去处理哪个!!!同时此时也就不用创建进程或线程,也就自然不会出现上述的阻塞现象!但是,并不是提升了效率,对于多执行流性质,其是充分利用了CPU资源,但多路复用,则是类似于一种单执行流形式~~~
(当然,多路转接模型也可以搭配线程池使用,哪个描述符就绪就把哪个描述符抛入线程池中进行处理~~)
多路转接模型的应用场景:
- 1.有大量描述符需要进行IO就绪事件监控,但同一时间只有少量活跃的场景
;(或者搭配多执行流处理)因为同一时间活跃的非常多,那么排在后面的描述符,半天都得不到处理~
- 2.针对当描述符有收发数据的超时控制场景
;
而多转接模型的实现,有三种模式,分别是select模型,poll模型,epoll模型
;
2、select模型
select模型:就是进行大量的描述符事件监控;
2.1、操作流程
- 针对不同的IO事件(可读事件,可写事件,异常事件),定义不同的描述符集合,如果要对哪个描述符监控哪个事件,就把这个描述符添加到对应集合中;
- 调用监控接口,将集合拷贝到内核中,进行轮询遍历进行监控,当有描述符就绪,或者监控超时都没有就绪,则监控返回;
而监控呢,就是在内核中遍历一遍,没有就绪则把对应进程加载至内核的IO事件队列中,然后进程会陷入等待直至被唤醒,再次遍历集合判断哪个描述符是否就绪了指定事件;
注:监控调用返回之前,会将集合中所有未就绪的描述符,从集合中移除(监控调用返回后,集合中只有就绪的描述符!)
- 调用返回,这时候判断哪个描述符还在哪个事件的描述符集合中,就表示哪个描述符就绪了对应事件,进而进行操作;
2.2、接口介绍
int select(int nfds,fd_set* readfds,fd_set* writefds,fd_set* exceptfds,struct timeval* timeout);
参数:
nfds:几种事件的描述符集合最大的描述符+1;
readfds:可读事件描述符集合; writefds:可写事件描述符集合; exceptfds:异常事件描述符集合;
timeout: struct timeval{uint32_t ntv_usec; uint32_t tv_sec}; 监控超时等待时间
返回值:成功返回就绪事件个数,出错返回-1;若没有描述符就绪则返回0;
此外还几个接口:
void FD_CLR(int fd, fd_set *set); //从set集合一处fd描述符
int FD_ISSET(int fd, fd_set *set); //判断fd描述符是否在set集合中
void FD_SET(int fd, fd_set *set); //向set集合中添加fd描述符
void FD_ZERO(fd_set *set); //初始化清空set集合
2.3、select模型的模拟实现类
#include "tcp_socket.hpp"
#include <vector>
#include <time.h>
#include <sys/select.h>
class Select{
private:
fd_set _rfds;//可读事件的描述符集合的备份,每次监控都是从这个集合复制拷贝一份出来监控(因为select存在对集合的修改)
int _max_fd;
public:
//初始化成员变量
Select()
:_max_fd(-1)
{
FD_ZERO(&_rfds);
}
//将sock中的描述符fd,添加到rfds可读事件的描述符集合中
bool Add(TcpSocket &sock)
{
int fd=sock.GetFd();
FD_SET(fd,&_rfds);//将描述符添加至可读事件描述符集合中
_max_fd=_max_fd>fd?_max_fd:fd;//重新设置最大的描述符
return true;
}
//将描述符fd,从rfd集合中移除
bool Del( TcpSocket& sock)
{
int fd=sock.GetFd();
FD_CLR(fd,&_rfds);//从当前集合中移除最大的描述符,
for(int i=_max_fd;i>=0;i--)
{
if(FD_ISSET(i,&_rfds))
{
_max_fd=i;
break;
}
}
return false;
}
//监控,返回就绪的描述符数组
bool Wait(std::vector<TcpSocket> *array,int timeout=3000)
{
fd_set tmp=_rfds;//使用临时集合进行监控,因为select会修改集合中的描述符
struct timeval tv;
tv.tv_usec = (timeout%1000)*1000;
tv.tv_sec = timeout/1000;
int ret = select(_max_fd+1,&tmp,NULL,NULL,&tv);
if(ret < 0)
{
perror("select error");
return false;
}
else if(ret == 0)
{
printf("select timeout!\n");
return false;
}
for(int i=0;i<=_max_fd;i++)
{
if(FD_ISSET(i,&tmp))
{
TcpSocket sock;
sock.SetFd(i);
array->push_back(sock);
}
}
return true;
}
};
2.4、select模型优缺点
优点:
遵循posix标注,跨平台移植性较好(在其他平台下也阔以使用select实现多路转接);
缺点:
1.能够监控的描述符数量有上限限制;
select的监控集合,是一个数组,当做位图使用,因此能监控多少描述符,就决定有多少比特位,取决于这个宏 _FD_SETSIZE=1024;
2.select监控原理多次进行轮询遍历集合,会导致监控的描述符越多,效率就会越低(性能会随着描述符增多而降低
);
3.select由于会修改集合,因此每次监控都需要重新添加描述符到集合中;
4.select返回的是一个就绪的描述符集合,而并不是直接给与就绪的描述符,因而需要用户遍历所有描述符查看哪个描述符还在集合中;
3、poll模型
poll模型,也是针对大量描述符进行监控,但是poll监控是为了每个描述符设置一个事件结构体;
3.1、操作流程
- 定义要监控的描述符事件结构体数组,向数组中添加需要监控的描述符以及事件信息;
例:struct pollfd pfds[10]; pfds[0].fd=0; pfds[0].events=POLLIN; - 调用监控接口开始监控,将需要监控的数据拷贝到内核中进行监控(监控原理也是多次轮询遍历);
监控调用返回前,会将描述符实际就绪的事件,填充至revents成员中,若无就绪,则被置为0; - 监控接口调用完毕后,遍历事件结构体数组,通过每个元素的revents成员,确定描述符就绪了什么事件,进行对应操作;
3.2、接口介绍
int poll(struct pollfd*fds , nfds_t nfds , int timeout);
参数:
fds:事件结构体数组的首地址; nfds:数组中有效元素的个数; timeout:监控超时时间(毫秒为单位);
返回值:
返回值为实际就绪的事件个数; 返回0时,表示监控超时; 返回值小于0时,则表示出错;
注:
struct pollfd{
int fd; 这是要监控的描述符;
short events; 针对这个描述符要监控的事件; 常用:POLLIN-可读; POLLINT-可写(其都是些比特位)
short revents; 监控调用返回后这个描述符实际就绪的事件;
};
3.3、接口的简单实用
#include <stdio.h>
#include <sys/poll.h>
#include <unistd.h>
#include <string.h>
int main()
{
struct pollfd pfds[10];
int poll_count=0;
pfds[poll_count].fd=0;//表示监控的描述符是标准输入
pfds[poll_count].events=POLLIN;//针对标准输入要监控的是可读事件
poll_count++;
while(1)
{
int ret=poll(pfds,poll_count,3000);
if(ret<0)
{
perror("poll error");
usleep(1000);
continue;
}
else if(ret==0)
{
printf("poll timeout!\n");
usleep(1000);
continue;
}
for(int i=0;i<poll_count;i++)
{
if(pfds[i].revents & POLLIN)//pfds[i]就绪了可读事件
{
char buf[1024];
read(i,buf,1023);
printf("%d 描述符就绪,读取数据:%s\n",pfds[i].fd,buf);
}
else if(pfds[i].revents & POLLOUT)
{
//pfds[i]就绪了可写事件
}
}
}
return 0;
}
3.4、poll模型的优缺点
优点:
1.相对于select操作流程更为简单;
2.所能监控的描述符数量没有上限(描述符有多少对应结构体数组就可以定义多大);
缺点:
1.无法跨平台;
2.监控原理涉及多次轮询遍历,因此效率也会随着监控的描述符数量增多而降低;
4、epoll模型
epoll模型,针对大量描述符进行事件监控(被认为是linux2.6以后最好用的多路转接模型)
4.1、接口认识
创建句柄
int epoll_create(int size); //在内核创建了struct eventpoll结构体,里面有两个重要成员---一棵红黑树&一个双向链表
参数:size ---- 所能监控的最大描述符,但在linux2.6.8之后被忽略,但是必须大于0,也就是没有了准确上限
返回值:成功返回epoll句柄 — 描述符;(也就是可通过该描述符找到该内核的结构体)
添加监控
int epol_ctl(int epfd, int cmd, int fd, struct epoll_event* ev);
参数:
epfd: epoll_create 返回的操作句柄;
cmd: 要进行的操作;
- EPOLL_CTL_ADD(添加监控)
- EPOLL_CTL_DEL (移除监控)
- EPOLL_CTL_MOD (修改监控)
fd : 要操作的描述符;
ev: 要操作的描述符所对应的事件结构体;
返回值:成功返回0,失败返回-1;
其中参数中涉及的结构体
struct epoll_event{
uint32_t events;//要监控的事件以及监控返回后保存实际就绪的事件
//EPOLLIN - 可读事件 EPOLLOUT - 可写
epoll_data_t data;//用户数据变量
};
typedef union epoll_data{
void* ptr;
int fd;
}epoll_data_t;
大致的思路就是描述符就绪了对应events事件,之后会返回一个联合体的data数据,进而完成响应操作;
开始监控
int epoll_wait(int epollfd, struct epoll_events * evs,int maxevents, int timeout);
参数:
epfd:epoll操作句柄
evs:事件结构体的首地址,用于存放就绪的事件信息;
maxevents:数组的大小或者元素个数(实则是设置一个上限,避免就绪事件过多数组空间不够而导致的错误);
timeout:监控的超时时间(毫秒为单位);
返回值:
成功返回就绪事件个数,出错返回-1,超时则返回0;
4.2、操作流程
1.首先通过epoll_create创建epoll句柄;
2. 向epoll添加监控----实则就是给内核的epoll结构体中的红黑树添加制定描述符的对应事件信息
;
3.epoll_wait 开始监控;
-
epoll的监控默认是异步阻塞监控;
-
epoll开始监控,实则是告诉
系统
,可以开始监控了,监控过程由系统完成; -
系统在监控的时候为每一个描述符都定义了一个回调函数,回调函数的功能实则就是一旦描述符就绪了指定事件,则把描述符对应的事件结构添加到rdlist双向链表中;
-
此时
进程只需要看rdllist双向链表是否为空,就可以确定是否有描述符就绪
,而无需像select,poll那样轮询遍历链表;
- epoll_wait传入的数组在调用返回后,
里面保存的都是就绪的描述符对应事件的结构体
,返回值就是元素的个数; - 用户可针对事件结构体的evs[i].data.fd进行events事件的操作;
- epoll_wait传入的数组在调用返回后,
4.3、epoll类的模拟实现
#include <sys/epoll.h>
#include <time.h>
#include <vector>
#include <cstdlib>
#include "tcp_socket.hpp"
#define EPOLL_MAX 100
class Epoll{
private:
int _epfd;//epoll操作句柄
public:
Epoll()
:_epfd(-1)
{
_epfd=epoll_create(1);
if(_epfd<0)
{
perror("epoll error");
exit(0);
}
}
//向epoll添加监控
bool Add(TcpSocket sock)
{
struct epoll_event ev;
ev.data.fd = sock.GetFd();
ev.events = EPOLLIN;
int ret = epoll_ctl( _epfd , EPOLL_CTL_ADD , sock.GetFd() , &ev);
if(ret < 0){
perror("epoll_ctl error");
return false;
}
return true;
}
//解除对sock描述符的监控
bool Del(TcpSocket &sock)
{
int ret=epoll_ctl(_epfd,EPOLL_CTL_DEL, sock.GetFd(),NULL);
if(ret<0)
{
perror("epoll_ctl error");
return false;
}
return true;
}
bool Wait(std::vector<TcpSocket>* arry,int timeout=3000)
{
//int epoll_wait(int epollfd, struct epoll_events * evs,int maxevents, int timeout)
struct epoll_event evs[EPOLL_MAX];
int ret=epoll_wait(_epfd,evs, EPOLL_MAX, timeout);
if(ret<0)
{
perror("epoll_wait error");
return false;
}
else if(ret==0)
{
printf("epoll timeout!\n");
return false;
}
//epoll_wait返回值ret就是就绪事件的个数
for(int i=0;i<ret;i++)
{
if(evs[i].events & EPOLLIN)//就绪事件的判断(可读)
{
TcpSocket sock;
sock.SetFd(evs[i].data.fd);
arry->push_back(sock);
}
}
return true;
}
};
4.4、epoll的触发方式(面试会考哟~)
在添加介绍添加监控的epoll_ctl函数中的struct epoll_events事件结构体中的events中,存在一EPOLLET标志;
其表示设置epoll事件边缘触发方式,而epoll事件默认为水平触发;
而epoll的事件触发方式:
- 水平触发(默认的触发方式)
针对可读 : 接收缓冲区的达到低水位标记是就会触发事件,而低水位标记默认为1, 即缓冲区中有数据就会触发事件;
针对可写:即发送缓冲区中有剩余空间就会触发事件; - 边缘触发(EPOLLET)
针对可读:只有新数据到来才会触发事件,无论缓冲区中是否有数据;
针对可写;发送缓冲区剩余空间大小从无到有才回触发事件;
边缘触发方式的思想是为了让程序员一次性把数据处理完毕,减少处理次数,以此提高效率(实际上提高并不明显);
而那么怎么才能一次性将数据处理完呢?
解决初思想:通常是循环读取数据,直至将缓冲区中的数据处理完;
那么等缓冲区中数据处理完毕后,recv会陷入阻塞,怎么解决呢?
因此边缘触发方式下,通常需要将描述符设置为非阻塞,让描述符的操作都成为非阻塞操作(若没有数据就报错返回);
而设置非阻塞,可通过fcntl接口实现~~;
int fcntl(int fd, int cmd, ... /* arg */ );
1.获取描述符属性;
- int flag=fctl( fd, F_GETFL, 0);
- 在原有属性的基础上新增非阻塞属性;
- fcntl(fd,F_SETFD,flag | O_NONBLOCK);
- O_NONBLOCK非阻塞属性,需要原有基础属性上进行修改;
边缘触发方式使用场景:
1. 想要尽量一次性处理所有请求,减少事件触发次数提高效率;
2. 避免水平触发模式下,因为半条数据没有处理而不断触发事件的情况;
缓冲区中数据不完整,因此想要等到新数据来了,再去取出完整数据进行处理;
4.5、epoll优缺点分析
- 缺点:
跨平移植性差; - 优点:
1.所能监控的描述符数量没有上限制;
2.监控原理是异步阻塞,让系统进行监控,系统为事件做了回调函数,将就绪事件添加到就绪链表,进程仅仅需用检验链表是否为空,就可判断是否有描述符就绪,而非轮询遍历操作,因此性能不会随着描述符增多而降低;(适用于超大量描述符的事件监控)
3.直接返回就绪的事件信息,可以直接对就绪描述符进行指定事件的操作,不需要空遍历来判断哪个描述符就绪;
而select和poll相较epoll的优势,针对单个描述符的超时控制上,select和epoll更加简单轻便;
总结
以上就是博主关于多路转接模型以及实现的讲解;
希望能助铁子们,面试一臂之力~~~