任何IO过程中,都包含两个步骤:1、等待2、拷贝。在实际的应用场景中,等待消耗的时间往往都远高于拷贝时间,让IO更高效,最核心的办法是让等待的时间尽量少
在内核将数据准备好之前,系统调用会一直等待,所有的套接字,默认都是阻塞方式
阻塞IO是最常见的IO模型:
所谓阻塞,是用户层的感受,在内核中本质是进程被挂起
如果内核还未准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。
非阻塞IO需要通过循环的方式反复尝试读取文件描述符,这个过程称之为轮询,这对CPU来说是较大的浪费,一般只有在特定场景下才会使用。
非阻塞轮询的本质是在做事件就绪(底层OS有数据)的检测工作
内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作
虽然从流程图上看与阻塞IO类似,实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
由内核在数据拷贝完成时,通知应用程序(信号驱动是告诉应用进程何时可以考试拷贝数据)
同步和异步关注的是消息通信机制
多线程的同步和互斥
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
非阻塞IO,记录锁,,系统V流机制,I/O多路转接(I/O多路复用),readv和writev函数以及存储映射IO(mmap)
一个文件描述符,默认都是阻塞IO
#include
#include
int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd不同,后面的追加参数也不同
fcntl函数的5种功能
将获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞
基于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);
}
#include
#include
#include
bool SetNoBlock(int fd)
{
int fl = fcntl(fd,F_GETFL);
if(fl < 0)
{
std::cerr << "fcntl error" << std::endl;
return false;
}
fcntl(fd,F_SETFL,fl | O_NONBLOCK);
return true;
}
#define NUM 1024
int main()
{
SetNoBlock(0);
while(true)
{
char buffer[NUM];
ssize_t size = read(0,buffer,sizeof(buffer) - 1);
if(size < 0)
{
//如果非阻塞,读取数据时,没有就绪,read以出错的形式进行返回
//不一定是出错的有可能是底层没有数据
if(errno == EAGAIN || errno == EWOULDBLOCK)
{
std::cout << "底层的数据没有就绪,轮询检测"<<std::endl;
sleep(1);
}
std::cerr << "read error "<< size << " errno:"<< errno << std::endl;
continue;
}
buffer[size] = 0;
std::cout << "echo# "<< buffer << std::endl;
}
return 0;
}
#include
#include
#include
#include
bool SetNoBlock(int fd)
{
int fl = fcntl(fd,F_GETFL);
if(fl < 0)
{
std::cerr << "fcntl error" << std::endl;
return false;
}
fcntl(fd,F_SETFL,fl | O_NONBLOCK);
return true;
}
void handler(int signo)
{
std::cout << "get a signo" << std::endl;
}
#define NUM 1024
int main()
{
//SetNoBlock(0);
signal(2,handler);
while(true)
{
std::cout<<"do something"<<std::endl;
char buffer[NUM];
ssize_t size = read(0,buffer,sizeof(buffer) - 1);
if(size < 0)
{
//如果非阻塞,读取数据时,没有就绪,read以出错的形式进行返回
//不一定是出错的有可能是底层没有数据
if(errno == EAGAIN || errno == EWOULDBLOCK)
{
std::cout << "底层的数据没有就绪,轮询检测"<<std::endl;
sleep(1);
continue;
}
if(errno == EINTR)
{
std::cout << "底层数据就绪未知,被信号中断"<<std::endl;
continue;
}
else
{
std::cerr << "read error "<< size << " errno:"<< errno << std::endl;
break;
}
continue;
}
buffer[size] = 0;
std::cout << "echo# "<< buffer << std::endl;
}
return 0;
}
系统提供select 函数来实现多路复用输入/输出模型
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
and
struct timespec {
long tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
timeval结构用来描述一段时间长度,如果在这个时间之内,需要监视的描述符没有事件发生则函数返回,返回值为0
是一个整数数组/位图,使用位图中对应的位来表示要监视的文件描述符
fd_set接口
所有的fd_set既是输入型也是输出型
fd_set大小是1024所以select能够同时等待的文件描述符是有上限的
读就绪
写就绪
多路转接适用场景:有大量连接,但只有少量活跃->聊天工具
select_server.hpp
#pragma once
#define BackLog 5
#define NUM 1024
#define DFL_FD -1
#include "sock.hpp"
#include
namespace ns_select
{
class SelectServer
{
private:
int listen_sock;
unsigned short port;
public:
SelectServer(unsigned short _port):port(_port)
{}
void InitSelectServer()
{
listen_sock = ns_sock::Sock::Socket();
ns_sock::Sock::Bind(listen_sock,port);
ns_sock::Sock::Listen(listen_sock,BackLog);
}
void Run()
{
fd_set rfds;
int fd_arr[NUM] = {0};//临时数组,保存对应的所有文件描述符
ClearArray(fd_arr,NUM,DFL_FD);//初始化数组中的所有fd设置为无效
fd_arr[0] = listen_sock;//把监听套接字sock写入数组的第一个元素
for(;;)
{
//时间也是输入输出,如果是间隔性的timeout返回,那么需要对时间进行重新设定
struct timeval timeout = {5,0};//每隔5s timeout一次
int max_fd = DFL_FD;//不断获取新连接的同时文件描述符在不断增多,所以max_fd值是需要更新
FD_ZERO(&rfds);//清空所有的read_fd
//第一次循环的时候fd_arr数组中至少有一个文件描述符:listen_sock;
for(auto i = 0;i < NUM;i++)
{
if(fd_arr[i] == DFL_FD)
{
continue;
}
//需要添加合法fd
FD_SET(fd_arr[i],&rfds);
if(max_fd < fd_arr[i])
{
max_fd = fd_arr[i];//更新最大描述符
}
}
//1、select阻塞等待 :timeout:nullptr
//2、timeout={0} 非阻塞轮询
//3、阻塞+轮询timeval={5,0}5s之内阻塞等待,5s之后select返回,无论是否有事件就绪
switch(select(max_fd+1,&rfds,nullptr,nullptr,&timeout))
{
case 0:std::cout<<"timeout"<<timeout.tv_sec<<std::endl;
break;
case -1:std::cerr<<"select error"<<std::endl;
break;
default:
//正常的事件处理
//std::cout<<"有事件发生 "<<"timeout "<
HandlerEvent(rfds,fd_arr,NUM);//处理事件
break;
}
}
}
void HandlerEvent(const fd_set &rfds,int fd_arr[],int num)
{
//怎样判定哪些文件描述符是否就绪?判定特定的fd是否在rfds中
//都有哪些文件描述符? fd_arr
for(auto i = 0;i < num;i++)
{
if(fd_arr[i] == DFL_FD)
{
continue;
}
//是一个合法fd,但不一定就绪
if(FD_ISSET(fd_arr[i],&rfds) && listen_sock == fd_arr[i])
{
//合法fd且已经就绪,连接事件到来
//accept
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(fd_arr[i],(struct sockaddr*)&peer,&len);//不会阻塞,FD_ISSET已经判定有就绪事件
if(sock < 0)
{
std::cerr<<"accept error"<<std::endl;
continue;
}
//已连接,但是不可以recv,因为连接建立好不代表对端把数据发过来
//recv是IO:等+拷贝,不知道数据发过来,但是select知道数据有没有就绪
//不能直接recv,而是将文件描述符添加到fd_arr中因为handlerEvent处理完后会返回到RUN()的循环处
//下一次循环时,会把所有合法文件描述符重新添加到rfds中-》下一次select就可以检测rfds中读事件是否就绪
//如果就绪-》HandlerEvent遍历扫描进入“处理正常fd处“进行扫描读取
// uint16_t peer_port = htons(peer.sin_port);
//std::string peer_ip = inet_ntoa(peer.sin_addr);
//std::cout<<"get a new link! "<
if(!AddToArray(fd_arr,NUM,sock))
{
close(sock);
std::cout<<"select_server is full,close fd: "<<sock<<std::endl;
}
}
else
{
//处理正常fd
if(FD_ISSET(fd_arr[i],&rfds))
{
//合法fd且已经就绪,读事件就绪
//实现读写不会阻塞
char buffer[1024];
ssize_t s = recv(fd_arr[i],buffer,sizeof(buffer)-1,0);
if(s > 0)
{
buffer[s] = 0;
std::cout<<"echo# "<<buffer<<std::endl;
}
else if(s == 0)
{
std::cout<<"client quit"<<std::endl;
close(fd_arr[i]);
fd_arr[i] = DFL_FD;//清除数组中的文件描述符
}
else
{
std::cout<<"recv error"<<std::endl;
}
}
else
{
//...
}
}
}
}
~SelectServer()
{}
private:
void ClearArray(int fd_arr[],int num,int def_fd)
{
for(auto i = 0;i < num;i++)
{
fd_arr[i] = def_fd;
}
}
bool AddToArray(int fd_arr[],int num,int sock)
{
for(auto i = 0;i < num;i++)
{
if(fd_arr[i] == DFL_FD)
{
//说明该位置没有被使用
fd_arr[i] = sock;
return true;
}
}
//数组内空间被使用完毕
return false;
}
};
}
sock.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
namespace ns_sock
{
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
std::cerr<<"sock error"<<std::endl;
exit(1);
}
// int opt = 1;
//setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
return sock;
}
static bool Bind(int sock,unsigned short 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;
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
std::cout<<"bind error"<<std::endl;
exit(2);
}
return true;
}
static bool Listen(int sock,int backlog)
{
if(listen(sock,backlog) < 0)
{
std::cerr<<"listen error"<<std::endl;
exit(3);
}
return true;
}
};
}
server.cc
#include "select_server.hpp"
#include
#include
#include
static void Usage(std::string proc)
{
std::cerr<<"Usage:"<<"\n\t"<<proc<<" port "<<std::endl;
}
// ./select_server port
int main(int argc,char*argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(4);
}
unsigned short port = atoi(argv[1]);
ns_select::SelectServer *select_svr = new ns_select::SelectServer(port);
select_svr->InitSelectServer();
select_svr->Run();
return 0;
}
#pragma once
#define BackLog 5
#define NUM 1024
#define DFL_FD -1
#include "sock.hpp"
#include
#include
namespace ns_select
{
struct bucket
{
public:
std::string inbuffer;
std::string outbuffer;
int in_curr;//已经接收到了多少数据
int out_curr;//已经发送了多少数据
public:
bucket():in_curr(0),out_curr(0)
{}
};
class SelectServer
{
private:
int listen_sock;
unsigned short port;
std::unordered_map<int,bucket> buckets;//每来一个新连接,构建一个buckets
public:
SelectServer(unsigned short _port):port(_port)
{}
void InitSelectServer()
{
listen_sock = ns_sock::Sock::Socket();
ns_sock::Sock::Bind(listen_sock,port);
ns_sock::Sock::Listen(listen_sock,BackLog);
}
void Run()
{
fd_set rfds;
int fd_arr[NUM] = {0};//临时数组,保存对应的所有文件描述符
ClearArray(fd_arr,NUM,DFL_FD);//初始化数组中的所有fd设置为无效
fd_arr[0] = listen_sock;//把监听套接字sock写入数组的第一个元素
for(;;)
{
//时间也是输入输出,如果是间隔性的timeout返回,那么需要对时间进行重新设定
struct timeval timeout = {5,0};//每隔5s timeout一次
int max_fd = DFL_FD;//不断获取新连接的同时文件描述符在不断增多,所以max_fd值是需要更新
FD_ZERO(&rfds);//清空所有的read_fd
//第一次循环的时候fd_arr数组中至少有一个文件描述符:listen_sock;
for(auto i = 0;i < NUM;i++)
{
if(fd_arr[i] == DFL_FD)
{
continue;
}
//需要添加合法fd
FD_SET(fd_arr[i],&rfds);
if(max_fd < fd_arr[i])
{
max_fd = fd_arr[i];//更新最大描述符
}
}
//1、select阻塞等待 :timeout:nullptr
//2、timeout={0} 非阻塞轮询
//3、阻塞+轮询timeval={5,0}5s之内阻塞等待,5s之后select返回,无论是否有事件就绪
switch(select(max_fd+1,&rfds,nullptr,nullptr,&timeout))
{
case 0:std::cout<<"timeout"<<timeout.tv_sec<<std::endl;
break;
case -1:std::cerr<<"select error"<<std::endl;
break;
default:
//正常的事件处理
//std::cout<<"有事件发生 "<<"timeout "<
HandlerEvent(rfds,fd_arr,NUM);//处理事件
break;
}
}
}
void HandlerEvent(const fd_set &rfds,int fd_arr[],int num)
{
//怎样判定哪些文件描述符是否就绪?判定特定的fd是否在rfds中
//都有哪些文件描述符? fd_arr
for(auto i = 0;i < num;i++)
{
if(fd_arr[i] == DFL_FD)
{
continue;
}
//是一个合法fd,但不一定就绪
if(FD_ISSET(fd_arr[i],&rfds) && listen_sock == fd_arr[i])
{
//合法fd且已经就绪,连接事件到来
//accept
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sock = accept(fd_arr[i],(struct sockaddr*)&peer,&len);//不会阻塞,FD_ISSET已经判定有就绪事件
if(sock < 0)
{
std::cerr<<"accept error"<<std::endl;
continue;
}
//已连接,但是不可以recv,因为连接建立好不代表对端把数据发过来
//recv是IO:等+拷贝,不知道数据发过来,但是select知道数据有没有就绪
//不能直接recv,而是将文件描述符添加到fd_arr中因为handlerEvent处理完后会返回到RUN()的循环处
//下一次循环时,会把所有合法文件描述符重新添加到rfds中-》下一次select就可以检测rfds中读事件是否就绪
//如果就绪-》HandlerEvent遍历扫描进入“处理正常fd处“进行扫描读取
uint16_t peer_port = htons(peer.sin_port);
std::string peer_ip = inet_ntoa(peer.sin_addr);
struct bucket b;
buckets.insert({sock,b});
std::cout<<"get a new link! "<<peer_ip<<": "<<peer_port<<std::endl;
if(!AddToArray(fd_arr,NUM,sock))
{
close(sock);
std::cout<<"select_server is full,close fd: "<<sock<<std::endl;
}
}
else
{
//处理正常fd
if(FD_ISSET(fd_arr[i],&rfds))
{
//合法fd且已经就绪,读事件就绪
//实现读写不会阻塞
char buffer[1024];
//不能确定数据都读完了,可能存在粘包/数据丢失问题
//怎么保证拿到完整的数据?
//1、定制协议
//2、给每一个sock定义缓冲区
buckets[fd.arr[i]].inbuffer = buffer;
ssize_t s = recv(fd_arr[i],buffer,sizeof(buffer)-1,0);
if(s > 0)
{
buffer[s] = 0;
std::cout<<"echo# "<<buffer<<std::endl;
}
else if(s == 0)
{
std::cout<<"client quit"<<std::endl;
close(fd_arr[i]);
fd_arr[i] = DFL_FD;//清除数组中的文件描述符
}
else
{
std::cout<<"recv error"<<std::endl;
}
}
else
{
//...
}
}
}
}
~SelectServer()
{}
private:
void ClearArray(int fd_arr[],int num,int def_fd)
{
for(auto i = 0;i < num;i++)
{
fd_arr[i] = def_fd;
}
}
bool AddToArray(int fd_arr[],int num,int sock)
{
for(auto i = 0;i < num;i++)
{
if(fd_arr[i] == DFL_FD)
{
//说明该位置没有被使用
fd_arr[i] = sock;
return true;
}
}
//数组内空间被使用完毕
return false;
}
};
}
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
不同于select使用三个位图来表示三个fd_set的方式,poll使用一个pollfd指针实现
poll监听的文件描述符增多时
poll_server
#include "sock.hpp"
#include
#define BackLog 5
namespace ns_poll
{
class PollServer
{
private:
int listen_sock;
int port;
public:
PollServer(int _port):port(_port)
{}
void InitServer()
{
listen_sock = ns_sock::Sock::Socket();
ns_sock::Sock::Bind(listen_sock,port);
ns_sock::Sock::Listen(listen_sock,BackLog);
}
void Run()
{
struct pollfd rfds[64];
for(int i = 0;i < 64;i++)
{
rfds[0].fd = listen_sock;
rfds[0].events |= POLLIN;
rfds[0].revents = 0;
}
rfds[0].fd = listen_sock;
rfds[0].events |= POLLIN;
rfds[0].revents = 0;
for(;;)
{
switch(poll(rfds,64,-1))
{
case 0:std::cout<<"timeout"<<std::endl;
break;
case 1:std::cout<<"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 new link"<<std::endl;
}
else
{
//recv()
}
}
}
break;
}
}
}
~PollServer()
{}
};
}
sock.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
namespace ns_sock
{
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
std::cerr<<"sock error"<<std::endl;
exit(1);
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
return sock;
}
static bool Bind(int sock,unsigned short 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;
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
std::cout<<"bind error"<<std::endl;
exit(2);
}
return true;
}
static bool Listen(int sock,int backlog)
{
if(listen(sock,backlog) < 0)
{
std::cerr<<"listen error"<<std::endl;
exit(3);
}
return true;
}
};
}
server.cc
#include "poll_server.hpp"
#include
#include
#include
static void Usage(std::string proc)
{
std::cerr<<"Usage:"<<"\n\t"<<proc<<" port "<<std::endl;
}
// ./select_server port
int main(int argc,char*argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(4);
}
unsigned short port = atoi(argv[1]);
ns_poll::PollServer *ps= new ns_poll::PollServer(port);
ps->InitServer();
ps->Run();
return 0;
}
epoll是为了处理大批量句柄而作了改进的poll
公认为Linux2.6下性能最好的多路I/O就绪通知方法
#include
int epoll_create(int size);
创建一个epoll句柄
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数
第二个参数取值
struct epoll_event 结构:
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
events可以是以下几个宏的集合
#include
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
收集在epoll监听的事件中已经发送的事件
struct eventpoll
{
struct rb_root rbr;
//红黑树的根节点,这棵树中存储着所有添加到epoll的需要监控的事件
struct list_head rdlist;
//双链表中则存放着将要通过epoll_wait返回给客户的满足条件的事件
}
struct epitem
{
struct rb_node rbn;//红黑树节点
struct list_head;//双向链表指针
struct epoll_filefd ffd;//事件句柄信息
struct eventpoll *ep;//指向其所属事件的eventpoll对象
struct epoll_event event;//期待发生的事件类型
}
我们定义的struct epoll_event是我们在用户空间中分配好的内存,还是需要将内核的数据拷贝到这个用户空间的内存中的
epoll_sever.hpp
#include "sock.hpp"
#pragma once
#include
#define max_num 64
#define backlog 5
namespace ns_epoll
{
const int size = 256;
class EpollServer
{
private:
int listen_sock;
int epfd;
uint16_t port;
public:
EpollServer(int _port):port(_port)
{}
public:
void InitEpollServer()
{
listen_sock = ns_sock::Sock::Socket();
ns_sock::Sock::Bind(listen_sock,port);
ns_sock::Sock::Listen(listen_sock,backlog);
std::cout<<"debug,listen_sock "<<listen_sock<<std::endl;//3
if((epfd=epoll_create(size)) < 0)
{
std::cerr<<"epoll error"<<std::endl;
exit(4);
}
std::cout<<"debug epfd"<<epfd<<std::endl;//4
}
void AddEvent(int sock,uint32_t event)
{
struct epoll_event ev;
ev.events = 0;
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::cerr<<"EPOLL_CTL_DEL error fd:"<< sock <<std::endl;
}
}
void Run()
{
//这里目前只有一个socket是能够关心读写的:listen_sock,read event
AddEvent(listen_sock,EPOLLIN);
//int timeout = 1000;
int timeout = -1;//默认情况下,epoll有事件就绪但是没有处理,会一直通知你
struct epoll_event revs[max_num];
for(;;)
{
//返回值num表明有多少个事件就绪,内核会将就绪事件依次放入revs中
int num = epoll_wait(epfd,revs,max_num,timeout);
if(num > 0)
{
for(int i = 0;i < num;i++)
{
int sock = revs[i].data.fd;//哪个文件描述符就绪?
if(revs[i].events & EPOLLIN)
{
//读事件就绪
//1、listen_sock 连接事件就绪
if(sock == listen_sock)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int sk = accept(listen_sock,(struct sockaddr*)&peer,&len);
if(sk < 0)
{
std::cout<<"accept error"<<std::endl;
continue;
}
//不能直接recv
std::cout<<"get a new link" << inet_ntoa(peer.sin_addr)<<":"<<ntohs(peer.sin_port)<<std::endl;
AddEvent(sk,EPOLLIN);//先进行读取,只有需要写的时候才会主动设置EPOLLOUT
}
else
{
//2、sock可读事件就绪
char buffer[1024];
ssize_t s = recv(sock,buffer,sizeof(buffer) - 1,0);
if(s > 0)
{
buffer[s] = 0;
std::cout<<buffer<<std::endl;
}
else
{
std::cout<<"client quit"<<std::endl;
close(sock);
DelEvent(sock);
}
}
}
else if(revs[i].events & EPOLLOUT)
{
}
else
{
}
}
//std::cout<<"有事件发生了"<
}
else if(num == 0)
{
std::cout<<"timeout"<<std::endl;
}
else
{
std::cerr<<"epoll error"<<std::endl;
}
}
}
~EpollServer()
{
if(listen_sock >= 0) close(listen_sock);
if(epfd >= 0) close(epfd);
}
};
}
sock.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
namespace ns_sock
{
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
std::cerr<<"sock error"<<std::endl;
exit(1);
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
return sock;
}
static bool Bind(int sock,unsigned short 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;
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
std::cout<<"bind error"<<std::endl;
exit(2);
}
return true;
}
static bool Listen(int sock,int backlog)
{
if(listen(sock,backlog) < 0)
{
std::cerr<<"listen error"<<std::endl;
exit(3);
}
return true;
}
};
}
server.cc
#include "epoll_server.hpp"
#include
#include
static void Usage(std::string proc)
{
std::cerr<<"Usage:"<<"\n\t"<<proc<<" port "<<std::endl;
}
// ./epoll_server port
int main(int argc,char*argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(4);
}
int short port = atoi(argv[1]);
ns_epoll::EpollServer *es= new ns_epoll::EpollServer(port);
es->InitEpollServer();
es->Run();
return 0;
}
epoll有两种工作方式:水平触发(LT)和边缘触发(ET)
例子:
水平触发(LT)工作模式:数据没被取走一直通知上层
边缘触发(ET)工作模式:数据从无->有,从有->多(变化)会通知上层
select/poll是在工作在LT模式下,epoll既支持LT也支持ET
LT是epoll的默认行为,使用ET能够减少epoll触发的次数,但是代价就是强逼着一次响应就绪过程中就把所有的数据读取完。相当于一个文件描述符就绪之后,不会反复被提示就绪,看起来比LT高效,但是LT情况下如果也能做到每次就绪的文件描述符都立刻处理,不让就绪被反复提示的话,性能是一样的。
ET的代码复杂度更高
使用ET模式的epoll,需要将文件描述符设置为非阻塞
但是问题来了:
所以,为了解决上述问题(阻塞read不一定能把完整的请求读完),于是就可以使用非阻塞轮询的方式来读取缓冲区,保证一定能把完整的请求都读出来
而如果是LT,只要缓冲区的数据没有读完,就够让epoll_wait返回文件描述符读就绪
epoll的高性能,是有一定的特定场景的,如果场景选的不适宜,epoll的性能可能适得其反
eg:需要处理上万个客户端的服务器;各种互联网APP的入口服务器
如果是系统内部,服务器和服务器之间进行通信,只有少数几个连接,这种情况下使用epoll就不合适
Reactor.hpp
#include "sock.hpp"
#pragma once
#include
#include
#include
#define max_num 64
#define backlog 5
namespace ns_epoll
{
class Reactor;
class EventItem;
const int size = 256;
typedef int(*callback_t)(EventItem *);//const &:输入;*:输出;&:输入输出
class EventItem//事件的元素
{
public:
int sock;//通信相关
Reactor *R;//回指Reactor
//有关数据处理的回调函数,用来进行逻辑解耦的
//应用层数据就绪等通信细节和数据的处理模块使用该方法进行解耦
callback_t recv_handler;
callback_t send_handler;
callback_t error_handler;
std::string inbuffer;//读取到该数据的缓冲区
std::string outbuffer;//发生的数据缓冲区
public:
EventItem():sock(0),R(nullptr),recv_handler(nullptr),send_handler(nullptr),error_handler(nullptr)
{}
~EventItem()
{}
void ManagerCallBack(callback_t _recv,callback_t _send,callback_t _error)
{
recv_handler = _recv;
send_handler = _send;
error_handler = _error;
}
};
class Reactor
{
private:
int epfd;
std::unordered_map<int,EventItem> event_items;//将sock映射到EventItem,可以拿着sock直接找到对应的Event_item
public:
Reactor():epfd(-1)
{}
public:
void InitReactor()
{
if((epfd=epoll_create(size)) < 0)
{
std::cerr<<"epoll error"<<std::endl;
exit(4);
}
std::cout<<"debug epfd"<<epfd<<std::endl;//4
}
void AddEvent(int sock,uint32_t event,const EventItem &item)
{
struct epoll_event ev;
ev.events = 0;
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
{
//EventItem item;
event_items.insert({sock,item});
}
std::cout<<"debug:"<<"添加"<<sock<<"到epoller中,成功"<<std::endl;
}
void DelEvent(int sock)
{
if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr) < 0)
{
std::cerr<<"EPOLL_CTL_DEL error fd:"<< sock <<std::endl;
}
event_items.erase(sock);
}
void EnableReadWrite(int sock,bool read,bool write)
{
//之前没有设置EPOLLOUT
struct epoll_event evt;
evt.data.fd = sock;
evt.events = (read ? EPOLLIN: 0)|(write ? EPOLLOUT:0) | EPOLLET;
if(epoll_ctl(epfd,EPOLL_CTL_MOD,sock,&evt) < 0)
{
std::cerr<<"epoll_ctl_mod error,fd"<<sock<<std::endl;
}
}
void Dispatcher(int timeout)//事件分派器
{
//如果底层的事件就绪,就把对应事件分派给指定的回调函数进行同一处理
struct epoll_event revs[max_num];
for(;;)
{
//返回值num表明有多少个事件就绪,内核会将就绪事件依次放入revs中
int num = epoll_wait(epfd,revs,max_num,timeout);
for(int i = 0;i < num;i++)
{
int sock = revs[i].data.fd;
uint32_t mask = revs[i].events;
if(revs[i].events &EPOLLIN)
//读事件就绪
if(event_items[sock].recv_handler)//读事件就绪
event_items[sock].recv_handler(&event_items[sock]);//如果读取回调函数被设置,调用回调函数
if(revs[i].events &EPOLLOUT)
if(event_items[sock].send_handler)
event_items[sock].send_handler(&event_items[sock]);
if((revs[i].events &EPOLLERR)||(revs[i].events & EPOLLHUP))
{
//把所有的异常事件,交给read,write处理
if(event_items[sock].error_handler)
//event_items[sock].error_handler(&event_items[sock]);
mask |= (EPOLLIN|EPOLLOUT);
}
}
}
}
~Reactor()
{
//if(listen_sock >= 0) close(listen_sock);
if(epfd >= 0) close(epfd);
}
};
}
app_interface.hpp
#pragma once
#include
#include
#include
#include "util.hpp"
#include "Reactor.hpp"
namespace ns_app
{
using namespace ns_epoll;
int recver(EventItem* item);
int sender(EventItem* item);
int errorer(EventItem* item);
int accepter(EventItem* item)
{
//std::cout<<"监听套接字来了新连接,执行回调函数"<
std::cout<<"get a new link"<<item->sock << std::endl;
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)
{
//说明没有读取出错,只是底层没有链接
return 0;
}
if(errno == EINTR)
{
//读取的过程被信号打断
continue;
}
else
{
//真正出错
return -1;
}
}
else
{
//读取成功
ns_util::SetNoBlock(sock);
EventItem tmp;
tmp.sock = sock;
tmp.R = item->R;
tmp.ManagerCallBack(recver,sender,errorer);
Reactor *epoller = item->R;
//epoll经常会设置读事件就绪,而写事件会按需打开
epoller->AddEvent(sock,EPOLLIN|EPOLLET,tmp);//将新获取的链接托管给对应的epoller
}
}
return 0;
}
int recv_helper(int sock,std::string *out)
{
//0:读取成功;-1:读取失败
while(true)
{
char buffer[128];
ssize_t size = recv(sock,buffer,sizeof(buffer)-1,0);
if(size < 0)
{
if(errno == EAGAIN || errno == EWOULDBLOCK)
{
//循环读取
return 0;
}
else if(errno == EINTR)
{
//被信号中断,继续尝试
continue;
}
else
{
//真正出错
return -1;
}
}
else
{
buffer[size] = 0;
*out += buffer;
}
}
}
int recver(EventItem* item)
{
std::cout<<"recv event ready:"<<item->sock<<std::endl;
//负责数据读取
//1、需要整体读,非阻塞
if(recv_helper(item->sock,&(item->inbuffer)) < 0)
{
//读取失败
return -1;
}
std::cout <<"client# "<<item->inbuffer<<std::endl;
//2、根据发来的数据流,进行包和包之间的分离,防止粘包问题,涉及到协议定制
std::vector<std::string> message;
ns_util::StringUtil::Split(item->inbuffer,&message,"X");
//3、针对一个一个的报文协议进行反序列化decode
/*
for(auto s : message)
{
std::cout<<"##################################"<inbuffer<
struct data
{
int x;
int y;
};
//已经拿到了所有报文数据
for(auto s: message)
{
struct data d;
ns_util::StringUtil::Deserialize(s,&d.x,&d.y);
std::cout<<d.x<<":"<<d.y<<std::endl;
//Reactor如果只负责读取数据流,进行报文和报文分离,不再进行后续处理,
//形成报文,构建响应等工作,交给软件层或线程进程池处理,基于Reactor的
//半同步半异步的工作方式,是Linux中最常用的工作方式
//4、业务处理
int z = d.x+d.y;
//5、形成响应报文,序列化转化成为一个字符串encode
std::string response;
response += std::to_string(d.x);
response += "+";
response += std::to_string(d.y);
response += " = ";
response += std::to_string(z);
item->outbuffer += response;
//5-1设置响应报文和响应报文之间的分隔符
item->outbuffer += "X";//encode
}
//6、写回
if(!item->outbuffer.empty())
item->R->EnableReadWrite(item->sock,true,true);
return 0;
}
int send_help(int sock,std::string& in)
{
//0:缓冲区打满,并且写完 1:缓冲区打满,下次写入-1:error,写出错
while(true)
{
size_t total = 0;
ssize_t s = send(sock,in.c_str()+total,in.size()-total,0);
if(s > 0)
{
total += s;
if(total >= in.size())
{
return 0;
}
}
else if(s < 0)
{
if(errno == EAGAIN ||errno == EWOULDBLOCK)
{
//无论是否发送完,inbuffer,都需要将已经发送的数据全部移出缓冲区
in.erase(total);
return 1;//已经将缓冲区打满,不能再写入
}
else if(errno == EINTR)
{
continue;
}
else
{
return -1;
}
}
}
}
int sender(EventItem* item)
{
int ret = send_help(item->sock,item->outbuffer);
if(ret == 0)
{
item->R->EnableReadWrite(item->sock,true,false);
}
else if(ret == 1)
{
//只要设置了epollout,默认epoll会在下一次自动触发
item->R->EnableReadWrite(item->sock,true,true);
}
else
{
}
return 0;
}
int errorer(EventItem* item)
{
return 0;
}
}
util.hpp
#pragma once
//工具类
#include
#include
#include
#include
namespace ns_util
{
void SetNoBlock(int sock)
{
int fl = fcntl(sock,F_GETFL);
fcntl(sock,F_SETFL,fl | O_NONBLOCK);
}
class StringUtil
{
public:
static void Split(std::string &in,std::vector<std::string> *out,std::string sep)
{
//aaaXbbbXcccX->X代表一个完整报文结束
//aaXbXcc
//aa
//;;
while(true)
{
size_t pos = in.find(sep);
if(pos == std::string::npos)
{
break;
}
sleep(1);
std::string s = in.substr(0,pos);
// std::cout<
out->push_back(s);
in.erase(0,pos+sep.size());
}
}
static void Deserialize(std::string &in,int *x,int *y)//反序列化
{
size_t pos = in.find("+");
std::string left = in.substr(0,pos);
std::string right = in.substr(pos+1);
*x = atoi(left.c_str());
*y = atoi(right.c_str());
}
};
}
sock.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
namespace ns_sock
{
class Sock
{
public:
static int Socket()
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
std::cerr<<"sock error"<<std::endl;
exit(1);
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
return sock;
}
static bool Bind(int sock,unsigned short 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;
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
std::cout<<"bind error"<<std::endl;
exit(2);
}
return true;
}
static bool Listen(int sock,int backlog)
{
if(listen(sock,backlog) < 0)
{
std::cerr<<"listen error"<<std::endl;
exit(3);
}
return true;
}
};
}
server.cc
#include "sock.hpp"
#include "util.hpp"
#include "Reactor.hpp"
#include "app_interface.hpp"
#include
#include
#define back_log 5
static void Usage(std::string proc)
{
std::cerr<<"Usage:"<<"\n\t"<<proc<<" port "<<std::endl;
}
// ./epoll_server port
int main(int argc,char*argv[])
{
if(argc != 2)
{
Usage(argv[0]);
exit(4);
}
//与listen_sock相关
uint16_t port = atoi(argv[1]);
int listen_sock = ns_sock::Sock::Socket();
ns_util::SetNoBlock(listen_sock);
ns_sock::Sock::Bind(listen_sock,port);
ns_sock::Sock::Listen(listen_sock,back_log);
std::cout<<"listen_sock"<<listen_sock<<std::endl;
//与epoll事件管理器相关
ns_epoll::Reactor R;
R.InitReactor();
//需要把listen_sock添加到epoll事件管理器中
ns_epoll::EventItem item;
item.sock = listen_sock;
item.R = &R;
//listen_sock只需要关系读事件就可以
item.ManagerCallBack(ns_app::accepter,nullptr,nullptr);
//将Listen_sock托管给epoller
R.AddEvent(listen_sock,EPOLLIN | EPOLLET,item);
int timeout = 1000;
while(true)
{
R.Dispatcher(timeout);
}
return 0;
}