IO多路转接之select
poll与select的区别:
fds:需要管理的文件描述符。
nfds:需要管理文件描述符的个数。
timeout_ts:设置超时时间。
返回值:文件描述符就绪的数目。
小于0代表失败。
等于0代表超时。
poolfd结构体如上图:
event:用户告诉内核需要关心的文件描述符。
revent:内核告诉用户,那些文件描述符就绪。
关心的事件如上图,这个宏都是一段二进制序列。需要重复添加使用 | 即可,判断是否就绪用&对应的事件不为0就代表这个事件就绪
重点只关注两个POLLIN与POLLOUT这两个宏。
封装套接字:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
namespace NetWork_Socket{
class Sock{
public:
static int Socket(){
//创建监听套接字
int listenSock=socket(AF_INET,SOCK_STREAM,0);
if(listenSock<0){
std::cout<<"socket error"<<std::endl;
exit(-1);
}
int opt=1;
//设置套接字属性
setsockopt(opt,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
return listenSock;
}
static bool Bind(int listenSock,int port){
//绑定,IP=INADDR_ANY
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(listenSock,(struct sockaddr*)&local,sizeof(local))<0){
std::cout<<"bind error"<<std::endl;
exit(-2);
}
return true;
}
static int Listen(int listenSock,int Len){//全连接队列长度
//监听
if(listen(listenSock,Len)<0){
std::cout<<"listen error"<<std::endl;
exit(-3);
}
return true;
}
};
}
poll多路转接服务器
#include"sock.h"
#include"poll.h"
namespace IO_POLL{
class POll_Sever{
private:
int listen_sock;
int port;
public:
POll_Sever(int _port):port(_port){
listen_sock=NetWork_Sorket::Sork::Socket();
}
void InitSever(){
NetWork_Sorket::Sork::Bind(listen_sock,port);
NetWork_Sorket::Sork::Listen(listen_sock,5);
}
void Start(){
struct pollfd rfds[64];
for(int i=0;i<64;i++)//初始化结构体
{
rfds[i].fd=-1;
rfds[i].events=0;
rfds[i].revents=0;//->
}
rfds[0].fd=listen_sock;
rfds[0].events |=POLLIN;
rfds[0].revents=0;
while(true){
switch(poll(rfds,64,-1)){
case 0:
std::cout<<"time out"<<std::endl;
break;
case -1:
std::cerr<<"poll error"<<std::endl;
break;
default:
for(int i=0;i<64;i++){
if(rfds[i].fd==-1){
continue;
}
if(rfds[i].revents&POLLIN){
if(rfds[i].fd==listen_sock){
//获取链接->
//accept();
std::cout<<"get a link!"<<std::endl;
//将accept获取的连接添加到数组中
}
else{
//recv
//将文件描述符从数组中删除
}
}
}
break;
}
}
}
~POll_Sever(){}
};
}
poll可以监视套接字数量与pollfd数组大小有关,解决了select监视文件描述符数量有限的问题。
优点:
缺点:
epoll与select和poll思路相同,作用都是等待资源就绪。
使用epoll需要三个接口。
返回值是文件描述符。失败返回-1。
参数size:一般填写128于256。随意填写,现在标准弃用了这个参数,为了兼容之前版本。
select与epolll来讲,数据流有两个方向。
①用户–>内核 和②内核–>用户。
epoll解决1问题时是用epoll_ctl()函数。
epoll_ctl函数解决了用户向内核添加,删除某些文件描述符以及对应文件描述符所关心的事件
参数解析在epoll工作原理哪里。
所以:进程可以创建多个epoll模型。
events和maxevents参数是解决②内核–>用户问题的,用户通过这两个参数那到内核通知用户那些事件已经就绪。
timeout参数以毫秒为单位。
函数返回0代表超时,-1代表失败。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目
综上:epoll_ctl函数本质是修改红黑树。
epfd:操作的epoll模型
op:表示操作动作,用三个宏表示。
结构体中的events,需要关注文件描述符的那些时间。
events的宏值有:每个宏只占1比特位,可以通过位运算传多个参数
结构体中的date:
将事件就绪的套接字设置到date的fd项中,为例区分监听套接字还是普通套接字,详情见实例。
同时:
注意:用户区定义epoll_event结构体数组,内核最后将返回的数据拷贝到数组中,一定要涉及拷贝过程。不存在系统通过内存映射机制将task_struct与内核数据建立映射,避免系统拷贝数据的说法。操作系统不可能让用户直接访问内核的就绪队列。
epoll服务器流程如下:
封装套接字
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
namespace NetWork_Sorket{
class Sock{
public:
static int Socket(){
//创建监听套接字
int listenSock=socket(AF_INET,SOCK_STREAM,0);
if(listenSock<0){
std::cout<<"socket error"<<std::endl;
exit(-1);
}
int opt=1;
//设置套接字属性
setsockopt(opt,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
return listenSock;
}
static bool Bind(int listenSock,int port){
//绑定,IP=INADDR_ANY
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(listenSock,(struct sockaddr*)&local,sizeof(local))<0){
std::cout<<"bind error"<<std::endl;
exit(-2);
}
return true;
}
static int Listen(int listenSock,int Len){//全连接队列长度
//监听
if(listen(listenSock,Len)<0){
std::cout<<"listen error"<<std::endl;
exit(-3);
}
return true;
}
};
}
epoll服务器
#pragma once
#include"sock.h"
#include
namespace EpollSever{
#define MAXSIZE 64
const int Back_Log=5;
class epoll_sever{
private:
int listen_sock;
int epfd;
int port;
public:
epoll_sever(int _port):port(_port){
listen_sock=NetWork_Sorket::Sock::Socket();
NetWork_Socket::Sock::Bind(listen_sock,port);
NetWork_Socket::Sock::Listen(listen_sock,Back_Log);
epfd=epoll_create(256);
if(epfd<0){//256无意义->
std::cerr<<"epoll create error"<<std::endl;
exit(4);
}
}
void Start(){
//将listen_sock添加到红黑树中,关心读
AddEvent(listen_sock,EPOLLIN);
int timeout=1000;
struct epoll_event fd_events[MAXSIZE];//MAXSIZE不能额大于创建epoll时的size及256
while(true){
int num=epoll_wait(epfd,fd_events,MAXSIZE,timeout);
//内核会将就绪事件依次放入数组中,不会做重复遍历
if(num>0){
std::cout<<"epoll wait succeed!"<<std::endl;
for(int i=0;i<num;i++){
int sock=fd_events[i].data.fd;
if(fd_events[i].events&EPOLLIN){
//读事件就绪
if(sock==listen_sock){
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=0;
if((sock=accept(listen_sock,(struct sockaddr*)&peer,&len))<0){
std::cerr<<"accept error"<<std::endl;
continue;
}
std::cout<<"get a new link"<<std::endl<<"ip: "<<inet_ntoa(peer.sin_addr)<<" port"<<ntohs(peer.sin_port)<<std::endl;
AddEvent(sock,EPOLLIN);//先监测读事件就绪情况,只有主动写时才设置EPOLLOUT
}
else{
char buff[1024];
ssize_t size=recv(sock,buff,sizeof(buff)-1,0);//存在数据读不完或粘包问题
if(size>0){
buff[size]=0;
std::cout<<buff<<std::endl;
}
else{
std::cout<<"client close !"<<std::endl;
close(sock);
DelEvent(sock);
}
}
}
else if(fd_events[i].events&EPOLLOUT){
//写事件就绪
//......
}
}
}
else if(num==0){
std::cout<<"time out"<<std::endl;
}
else{
std::cerr<<"epoll wait error"<<std::endl;
}
}
}
~epoll_sever(){
if(listen_sock>=0){
close(listen_sock);
}
if(epfd>=0){
close(epfd);
}
}
private:
void AddEvent(int sock,uint32_t event){
struct epoll_event ev;
memset(&ev,0,sizeof(ev));
ev.events|=event;
ev.data.fd=sock;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev)<0){
std::cerr<<"epoll ctl error:"<<sock<<std::endl;
}
}
void DelEvent(int sock){
if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr)<0){
std::cout<<"epoll ctl del error"<<std::endl;
}
}
};
}
#include"epoll_sever.h"
#include
void UsrHelp(const char*name){
std::cout<<"UsrHelp# "<<name<<" +port"<<std::endl;
}
int main(int argc,char*argv[]){
if(argc!=2){
UsrHelp(argv[0]);
exit(5);
}
int port=atoi(argv[1]);
EpollSever::epoll_sever*sever=new EpollSever::epoll_sever(port);
sever->Start();
return 0;
}
epoll有两种工作方式,水平触发(LT)与边缘触发(ET)
水平触发(LT):只要底层有数据没有被取走,会一直通知上层,需要上层读取数据。
边缘触发(ET):只有当底层数据从无到有,或者数据变化时,会通知上层一次,需要上层读取数据。
epoll告知上层数据就绪的方式有上述两种方式。
边缘触发Edge Triggered工作模式:
只有当底层数据从无到有,或者数据变化时,会通知上层一次,需要上层一次将数据全部读走。
所以:ET模式会倒逼应用层立即将就绪的数据全部读取完毕。
为了保证recv函数能够将发来的数据全部读取完毕,需要循环调用recv函数,直到读取大小 小于要读取的值时,说明数据已经被读取完毕。最后将读取的数据拼接起来即可。
注意:ET模式下,recv/write函数必须设置成非阻塞读取模式
假设ET模式下recv函数是阻塞读取的话:
如果数据共有300字节,每次recv读取100字节,不满足上面说的读取结束要求,会进行第四次循环读取,第四次循环recv时因为无数据会阻塞等待,这个线程会被挂起。无法再响应任何外部事件。
水平触发Level Triggered 工作模式:
只要底层有数据没有被取走,会一直通知上层,需要上层读取数据。
LT模式不要求一次将数据读取完毕,但是如果设置成要一次读取完毕,会面临和ET模式一样的问题。需要将recv/write函数设置成非阻塞形式。
因为ET模式不需要重读通知上层数据就绪,所以ET模式比LT模式工作效率高。
设置epoll ET工作模式只需要将event事件|EPOLLET宏即可
声明:为例防止粘包问题,这里选择将每个数据包之间用 | 来进行区分(制定协议)。
Github
Gitee
封装套接字sock.h
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
namespace NetWork_Socket{
class Sock{
public:
static int Socket(){
//创建监听套接字
int listenSock=socket(AF_INET,SOCK_STREAM,0);
if(listenSock<0){
std::cout<<"socket error"<<std::endl;
exit(-1);
}
int opt=1;
//设置套接字属性
setsockopt(opt,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
return listenSock;
}
static bool Bind(int listenSock,int port){
//绑定,IP=INADDR_ANY
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(listenSock,(struct sockaddr*)&local,sizeof(local))<0){
std::cout<<"bind error"<<std::endl;
exit(-2);
}
return true;
}
static int Listen(int listenSock,int Len){//全连接队列长度
//监听
if(listen(listenSock,Len)<0){
std::cout<<"listen error"<<std::endl;
exit(-3);
}
return true;
}
};
}
Reactor设计模式,实现epoll任务派发功能。当任务就绪时采用回调机制执行对应的回调方法
epoller.h
#pragma once
#include"sock.h"
#include
#include
#include
namespace EpollSever{
#define MAXSIZE 64
class epoller;
class EventItem;
typedef int(*call_back)(EventItem*);//函数指针回调函数 const& 输入 *(指针)输出 &输入输出
class EventItem{
public:
int sock=0;
//回指epoll模型
epoller* Epoller=nullptr;
//回调函数,用来进行数据处理
call_back recv_hander=nullptr;
call_back send_hander=nullptr;
call_back error_hander=nullptr;
//为了保证数据读取完毕,需要定义缓冲区
std::string inbuff;//输入缓冲区
std::string outbuff;//发送缓冲区
void RegisterCallBack(call_back _recv,call_back _send,call_back _error){//管理回调函数,删除回调函数,只需要将参数设置为nullpt
recv_hander=_recv;
send_hander=_send;
error_hander=_error;
}
};
class epoller{
private:
int epfd;
std::unordered_map<int,EventItem> event_item;//sock映射到EventItem,每个sock都有自己独立的缓冲区和处理方法
public:
epoller(){
epfd=epoll_create(256);
if(epfd<0){//256无意义->
std::cerr<<"epoll create error"<<std::endl;
exit(4);
}
std::cout<<"creat epoll fd="<<epfd<<std::endl;
}
void DisPatch(int timeout){// 如果底层特定事件就绪,就把特定事件分派给回调函数统一处理,称为事件分派器
struct epoll_event fd_events[MAXSIZE];//MAXSIZE不能额大于创建epoll时的size及256
int num=epoll_wait(epfd,fd_events,MAXSIZE,timeout);
//内核会将就绪事件依次放入数组中,不会做重复遍历
//std::cout<<"就绪事件数:"<
for(int i=0;i<num;i++){
int sock=fd_events[i].data.fd;
if((fd_events[i].events&EPOLLERR)||(fd_events[i].events&EPOLLHUP)){
//对端异常或者连接断开->
if(event_item[sock].error_hander!=nullptr){
//将事件设置为读写关心
fd_events[i].events=EPOLLIN|EPOLLOUT;
event_item[sock].error_hander(&event_item[sock]);
}
}
if(fd_events[i].events&EPOLLIN){
//读事件就绪
if(event_item[sock].recv_hander!=nullptr){//读回调函数存在
event_item[sock].recv_hander(&event_item[sock]);//这个套接字独有的缓存区。
}
}
if(fd_events[i].events&EPOLLOUT){
//写事件就绪
if(event_item[sock].send_hander!=nullptr){
event_item[sock].send_hander(&event_item[sock]);
}
}
}
}
~epoller(){
if(epfd>=0){
close(epfd);
}
}
void AddEvent(int sock,uint32_t event,EventItem&item){
struct epoll_event ev;
memset(&ev,0,sizeof(ev));
ev.events|=event;
ev.data.fd=sock;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&ev)<0){
std::cerr<<"epoll ctl error:"<<sock<<std::endl;
}
else{
// std::cout<<"add sock "<
event_item.insert(std::make_pair(sock,item));
}
}
void DelEvent(int sock){
if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr)<0){
std::cout<<"epoll ctl del error"<<std::endl;
}
std::cout<<"remove sock "<<sock<<std::endl;
event_item.erase(sock);
}
void EnableReadWrite(int sock,bool read,bool write){//之前没有关心过套接字的写,需要套接字写时调用这个接口修改成写即可
//修改让epoll关注这个套接字的读写->
struct epoll_event event;
event.data.fd=sock;
event.events=(read==true?EPOLLIN:0)|(write==true?EPOLLOUT:0)|EPOLLET;//ET工作模式选择读写
if(epoll_ctl(epfd,EPOLL_CTL_MOD,sock,&event)<0){
std::cerr<<"epoll ctl mod error ,sock:"<<sock<<std::endl;
}
}
};
}
实现服务器自定义报文解析和设置非阻塞套接字等基本功能的头文件
Util.h
#pragma once
#include
#include
#include
#include
#include
//设置文件描述符为非阻塞
namespace Util{
void SetNoBlock(int sock){//将套接字设置非阻塞
int fd=fcntl(sock,F_GETFL);
fcntl(sock,F_SETFL,fd|O_NONBLOCK);
}
void SplitStr(std::string&in,std::vector<std::string>&buff,std::string gist){
//asdc|derc|代表两个完整的报文,序列化报文
while(true){
size_t pos=in.find(gist);
if(pos==std::string::npos){
break;
}
std::string ms=in.substr(0,pos);
buff.push_back(ms);
in.erase(0,pos+gist.size());//从头开始截取pos+gist.size()个
}
}
void Deserialize(std::string& in,int&x,int&y){
//in 认为字符串风格为1+1,不考虑错误情况
int pos=in.find("+");
x=atoi(in.substr(0,pos).c_str());
y=atoi(in.substr(pos+1).c_str());//跳过加号
}
}
epoller事件就绪时的各种回调函数头文件
app_interface.h
#pragma once
#include
#include"epoller.h"
#include"Util.h"
#include
#include
namespace interface{
using namespace EpollSever;
int recver(EventItem*);
int sender(EventItem*);
int error(EventItem*);
int accepter(EventItem*item){
while(true){
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(item->sock,(struct sockaddr*)&peer,&len);
if(sock<0){//->
if(errno==EAGAIN||errno==EWOULDBLOCK){//底层没有连接了
//std::cout<<"no link need to accept"<
return 0;
}
if(errno==EINTR){
//std::cout<<"interrupted by a signal"<
//读取过程被信号打断
continue;
}
else{
//读取出错
item->error_hander(item);
return -1;
}
}
else{
std::cout<<"get a new link sock="<<sock<<std::endl;
//设置非阻塞
Util::SetNoBlock(sock);
//读取成功,添加到epoller中,并添加这个套接字的注册方法
EventItem tmp_item;
tmp_item.sock=sock;
tmp_item.Epoller=item->Epoller;
tmp_item.RegisterCallBack(recver,sender,error);
epoller* tmp_Epoller=item->Epoller;
tmp_Epoller->AddEvent(sock,EPOLLIN|EPOLLET,tmp_item);
// std::cout<<"add done"<
}
}
return 0;
}
int recv_sock(int sock,std::string&out){
//返回0 读取成功,-1读取失败
while(true){
char buff[1024]={0};
ssize_t size=recv(sock,buff,sizeof(buff)-1,0);
if(size<0){
if(errno==EAGAIN||errno==EWOULDBLOCK){
//读取完毕->
return 0;
}
else if(errno==EINTR){
//被信号中断
continue;
}
else{
//读取出错
return -1;
}
}
else{
buff[size]=0;
out+=buff;//将读取到的内容添加到inbuff中
}
}
}
int recver(EventItem*item){
//数据读取
//1.非阻塞读取
// std::cout<<"recver redy"<sock<
if(recv_sock(item->sock,item->inbuff)<0){
//读取失败->
item->error_hander(item);
return -1;
}
//std::cout<<"client# "<inbuff<
//2.根据发来的数据流进行分包,防止粘包,涉及到协议定制,约定以|标定报文之间的分割符
std::vector<std::string>MessArray;
Util::SplitStr(item->inbuff,MessArray,"|");
//std::cout<inbuff<
//3.针对每个报文,进行协议反序列化,这里要处理加法运算
struct Date{
int x=0;int y=0;
};
for(auto&mes:MessArray){
std::cout<<mes<<std::endl;
struct Date date;
//将a+b字符串反序列化成 struct Date{int x=a;int y=b;};
Util::Deserialize(mes,date.x,date.y);
//std::cout<
//4.业务处理,可以与线程池拓展处理,这里不考虑(构建任务类,将任务插入到线程池中)
int ret=date.x+date.y;
//5.形成响应报文,协议序列化成字符串
std::string respon;
respon+=std::to_string(date.x);
respon+="+";
respon+=std::to_string(date.y);
respon+="=";
respon+=std::to_string(ret);
//添加响应报文的分隔符
respon+="|";
//6.写回
//添加到输出缓冲区上
item->outbuff+=respon;
}
//修改文件描述符的写
if(!item->outbuff.empty()){
item->Epoller->EnableReadWrite(item->sock,true,true);//关心这个文件描述符的读写
}
return 0;
}
//ET模式一次全部写完
//返回0代表写完了,1代表没写完,下次继续写,-1写入失败
int sender_sock(int sock,std::string&in){
size_t total=0;//当前写入的字数,//不能直接全部发出,因为对端可能不能一次全部接受。
while(true){
size_t size=send(sock,in.c_str()+total,in.size()-total,0);
if(size>0){
total+=size;
if(total>=in.size()){
//写完了
return 0;
}
}
else if(size<0){
if(errno==EAGAIN||errno==EWOULDBLOCK){
in.erase(0,total);
return 1;//对端无法再接受,但是写成功了,需要将发送缓冲区移除所有已经发送的数据->
}
else if(errno==EINTR){//被信号中断
continue;
}
else{
//写入失败
return -1;
}
}
}
}
int sender(EventItem*item){
//发送数据
int ret=sender_sock(item->sock,item->outbuff);
if(ret==0){
//发送完毕,不在关心这个文件描述符的写
item->Epoller->EnableReadWrite(item->sock,true,false);
}
else if(ret==1){
//还没有全部发送完,继续关注这个文件描述符的读写,虽然这里默认文件描述符已经被设置了关心读写,这里为了保险再设置一次
item->Epoller->EnableReadWrite(item->sock,true,true);
}
else{
//出错
item->error_hander(item);
}
return 0;
}
int error(EventItem*item){
//出错
//此时文件描述符被设置成了可读可写,可自行返回错误响应
close(item->sock);
item->Epoller->DelEvent(item->sock);
return 0;
}
}
启动服务器,并将监听套接字设置到epoll模型中,循环执行epoll任务分派函数
sever.cpp
#include"epoller.h"
#include
#include"sock.h"
#include"app_interface.h"
#include"Util.h"
const int Back_Log=5;
void UsrHelp(const char*name){
std::cout<<"UsrHelp# "<<name<<" +port"<<std::endl;
}
int main(int argc,char*argv[]){
if(argc!=2){
UsrHelp(argv[0]);
exit(5);
}
int port=atoi(argv[1]);
//创建listen sock
int listen_sock=NetWork_Socket::Sock::Socket();
std::cout<<"listen sock="<<listen_sock<<std::endl;
//设置非阻塞
Util::SetNoBlock(listen_sock);
NetWork_Socket::Sock::Bind(listen_sock,port);
NetWork_Socket::Sock::Listen(listen_sock,Back_Log);
//调用epollsever事件管理器
EpollSever::epoller epoll;
EpollSever::EventItem item;
item.sock=listen_sock;
item.Epoller=&epoll;
//这里只关注读事件,注册回调函数
item.RegisterCallBack(interface::accepter,nullptr,nullptr);
//将监听套接字托管到epoller,ET模式工作
epoll.AddEvent(listen_sock,EPOLLIN|EPOLLET,item);
int timeout=1000;
while(true){
epoll.DisPatch(timeout);
}
return 0;
}
其中,还可以对Reactor模型进行拓展。即Reactor只负责读取数据流,将报文和报文进行分离,后续数据处理交给线程池。
这就是Reactor半同步半异步的工作方式,是Linux中最常用的工作方式。
后端开发就是在上述服务器代码的 业务处理开始到发送响应这块范围进行工作的。/(ㄒoㄒ)/~~