在内核将数据准备好之前, 系统调用会一直等待。 所有的套接字, 默认都是阻塞方式。
阻塞IO是最常见的IO模型
如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对CPU来说是较大的浪费, 一般只有特定场景下才使用。
内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作。
虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态
由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
小结: 任何IO过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少。
什么叫做高效的IO?
IO=等待+数据拷贝,而高效的IO就是在整个周期内,等的比重特别少,一直在做拷贝。提高IO效率就是减少IO过程等待的比重。
同步和异步关注的是消息通信机制
另外, 我们回忆在讲多进程多线程的时候, 也提到同步和互斥. 这里的同步通信和进程之间的同步是完全不相干的概念
同学们以后在看到 “同步” 这个词, 一定要先搞清楚大背景是什么. 这个同步, 是同步通信异步通信的同步, 还是同步与互斥的同步。
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态
非阻塞IO,纪录锁,系统V流机制, I/O多路转接(也叫I/O多路复用) ,readv和writev函数以及存储映射IO(mmap),这些统称为高级IO。
fcntl
一个文件描述符, 默认都是阻塞IO
函数原型:
int fcntl(int fd,int cmd,.../* arg */ );
传入的cmd的值不同, 后面追加的参数也不相同
fcntl函数有5种功能
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞
实现函数SetNoBlock
基于fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞
void SetNonBlock(int fd)
{
int f1=fcntl(fd,F_GETFL);
if(f1<0){
std::cerr<<"获取文件标记位失败..."<<std::endl;
return;
}
fcntl(fd,F_SETFL,f1|O_NONBLOCK);
}
轮询方式读取标准输入
实现代码
#include
#include
#include
#include
#include
void SetNonBlock(int fd)
{
int f1=fcntl(fd,F_GETFL);
if(f1<0){
std::cerr<<"获取文件标记位失败..."<<std::endl;
return;
}
fcntl(fd,F_SETFL,f1|O_NONBLOCK);
}
int main()
{
char buffer[1024];
SetNonBlock(0);
while(true){
ssize_t s=read(0,buffer,sizeof(buffer)-1);
if(s>0){
buffer[s]=0;
std::cout<<"buffer: "<<buffer<<std::endl;
}
else{
if(errno==EAGAIN || errno == EWOULDBLOCK)
{
sleep(2);
std::cout << "当前没有出错,仅仅底层数据没有就绪罢了..." << std::endl;
continue;
}
if(errno == EINTR){
std::cout << "读取被信号中断" << std::endl;
continue;
}
std::cout<<"read error"<<std::endl;
break;
}
}
return 0;
}
运行结果
系统提供select函数来实现多路复用输入/输出模型
#include
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
参数解释
关于timeval结构
timeval结构用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0。
参数timeout取值
fd_set结构
fd_set是操作系统提供的文件描述符集,其这个结构就是一个整型数组, 更严格的说, 是一个 “位图”. 使用位图中对应的位来表示要监视的文件描述符。
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的全部位
函数返回值
错误值可能为:
常见的程序片段如下
fs_set readset;
FD_SET(fd,&readset);
select(fd+1,&readset,NULL,NULL,NULL);
if(FD_ISSET(fd,readset)){......}
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd
读就绪
写就绪
套接字封装 sock.hpp
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
namespace ns_sock
{
enum{
SOCKET_ERR=2,
BIND_ERR,
LISTEN_ERR
};
const int g_backlog=5;
class Sock
{
public:
static int Socket()
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
std::cerr<<"socket error!"<<std::endl;
exit(SOCKET_ERR);
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));// 设置套接口选项值
return sock;
}
static void Bind(const int &sock,const u_int16_t &port)
{
struct sockaddr_in local;
bzero(&local,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::cerr<<"bind error!"<<std::endl;
exit(BIND_ERR);
}
}
static void Listen(const int &sock)
{
if(listen(sock,g_backlog)<0)
{
std::cerr<<"bind error!"<<std::endl;
exit(LISTEN_ERR);
}
}
};
}
select 服务端头文件 select_server.hpp
#pragma once
#include"sock.hpp"
#include
#include
#include
namespace ns_select
{
using namespace ns_sock;
#define NUM (sizeof(fd_set)*8)
const int g_default = 8080;
class SelectServer
{
private:
u_int16_t port_;
int listen_sock_;
int fd_arrar_[NUM];
// EndPoint fd_arrar_[NUM];
public:
SelectServer(int port=g_default):port_(port),listen_sock_(-1)
{
for(int i=0;i<NUM;i++)
{
fd_arrar_[i]=-1;
}
}
void InitSelectServer()
{
listen_sock_=Sock::Socket();
Sock::Bind(listen_sock_,port_);
Sock::Listen(listen_sock_);
fd_arrar_[0]=listen_sock_;
}
std::ostream& PrintFd()
{
for(int i=0;i<NUM;i++)
{
if(fd_arrar_[i]!=-1) std::cout<<fd_arrar_[i]<<' ';
}
return std::cout;
}
void HandlerEvent(fd_set &rfds)
{
//判断我的有效sock,是否在rfds中
for(int i=0;i<NUM;i++)
{
if(-1==fd_arrar_[i]){
continue;
}
//区分新链接和数据
if(FD_ISSET(fd_arrar_[i],&rfds))// 测试描述词组set中相关fd的位是否为真
{
if(fd_arrar_[i]==listen_sock_)
{
//新链接
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(listen_sock_,(struct sockaddr*)&peer,&len);
if(sock<0)
{
std::cout<<"accept error"<<std::endl;
}
else
{
//将新的sock添加到文件描述符中
int j=0;
for(;j<NUM;j++)
{
if(fd_arrar_[j]==-1)
{
break;
}
}
if(j==NUM)
{
std::cout<<"fd_arrar 已经满了"<<std::endl;
close(sock);
}
else{
fd_arrar_[j]=sock;
std::cout<<"获取链接成功,sock: "<<sock<<" 已经添加到数组中了,当前:"<<std::endl;
PrintFd() << " [当前]" << std::endl;
}
}
}
else
{
//数据
// 1.网络通信,定制协议,和业务场景有关
// 2.是不是每一个sock,都必须有自己独立的buffer
char buffer[1024];
ssize_t s=recv(fd_arrar_[i],buffer,sizeof(buffer),0);
if(s>0)
{
buffer[s]='\0';
std::cout<<"clint say#"<<buffer<<std::endl;
}
else if(s==0)
{
std::cout<<"client quit------sock: "<<fd_arrar_[i]<<std::endl;
// 对端链接关闭
close(fd_arrar_[i]);
// 从rfds中,去掉sock
fd_arrar_[i]=-1;
PrintFd()<<"[当前]"<<std::endl;
}
else{
// 读取异常
std::cerr<<"recv error"<<std::endl;
}
}
}
}
}
void Loop()
{
//在服务器最开始的时候,我们只有一个sock,listen_sock, 有读事件就绪,读文件描述符看待的!
fd_set rfds;
//FD_SET(listen_sock_,&rfds);
while(true)
{
//struct timeval timeout={3,0};
// 对位图结构进行清空
FD_ZERO(&rfds);
int max_fd=-1;
for(int i=0;i<NUM;i++)
{
if(-1==fd_arrar_[i]) continue;
FD_SET(fd_arrar_[i],&rfds);
if(max_fd<fd_arrar_[i]) max_fd=fd_arrar_[i];
}
int n=select(max_fd+1,&rfds,nullptr,nullptr,nullptr);
switch(n)
{
case 0:
std::cout<<"timeout ... "<<std::endl;
break;
case -1:
std::cout<<"select error"<<std::endl;
break;
default:
// select成功,至少有一个fd是就绪的
HandlerEvent(rfds);
//select成功,至少有一个fd是就绪的
//std::cout<<"有事件发生"<
break;
}
}
}
~SelectServer()
{
if(listen_sock_>=0)
close(listen_sock_);
}
};
}
select 服务端文件
#include"select_server.hpp"
using namespace ns_select;
int main()
{
SelectServer* svr=new SelectServer();
svr->InitSelectServer();
svr->Loop();
return 0;
}
运行结果
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */ // 用户通知内核,你要帮我关心fd,上面的所有的events事件
short revents; /* returned events */ // 内核通知用户,底层fd,对应的那些事件都已经就绪了
};
参数说明
events和revents的取值
socket就绪条件同select
不同与select使用三个位图来表示三个fdset的方式,poll使用一个pollfd的指针实现
poll中监听的文件描述符数目增多时
select :只负责等待(nums),就绪事件通知机制
poll:只负责等待(nums),就绪事件通知机制
poll :
1.解决了:检测的文件描述符有上限的问题
2解决了︰将输入和输出分离,解决编码的时候,必须的重新设置关心的文件描述符,poll不在需要每次都重新设置
我们编写代码的时候,编译器是如何得知我的代码有语法报错的呢??
因为我们在写代码的时候,编辑器(vscode) or集成环境(vs2019 vs2022),会自动在后台调动编译器,用编译的语法检测功能,来进行对你正在写的代码进行扫描。自动补齐,语法提示,也是类似的道理。
编辑器(vscode)or集成环境(vs2019 vs2022)也会在你写代码的时候,在特定的路径下进行搜索头文件,通过头文件的内容来给我们进行语法提示
poll 服务端头文件 poll_server.hpp
#pragma once
#include"sock.hpp"
#include
#include
#include
namespace ns_poll
{
using namespace ns_sock;
const int g_default = 8080;
#define NUM 1024
class PollServer
{
private:
u_int16_t port_;
int listen_sock_;
struct pollfd pollfds_[NUM];
public:
PollServer(int port=g_default):port_(port),listen_sock_(-1)
{
for(int i=0;i<NUM;i++)
{
pollfds_[i].fd=-1;
pollfds_[i].events=0;
pollfds_[i].revents=0;
}
}
void InitPollServer()
{
listen_sock_=Sock::Socket();
Sock::Bind(listen_sock_,port_);
Sock::Listen(listen_sock_);
pollfds_[0].fd=listen_sock_;
pollfds_[0].events=POLLIN;
}
std::ostream& PrintFd()
{
return std::cout;
}
void HandlerEvent(struct pollfd pollfds[],int num)
{
for(int i=0;i<num;i++)
{
if(pollfds[i].fd==-1) continue;
if(pollfds[i].revents & POLLIN)
{
if(pollfds[i].fd==listen_sock_)
{
//新链接
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(listen_sock_,(struct sockaddr*)&peer,&len);
if(sock<0)
{
std::cout<<"accept error"<<std::endl;
}
else
{
int j=0;
for(;j<num;j++)
{
if(pollfds[j].fd==-1)
{
break;
}
}
if(j==num)
{
close(sock);
}
else
{
std::cout<<"get a new sock : "<<sock<<std::endl;
pollfds[j].fd=sock;
pollfds[j].events=POLLIN;
pollfds[j].revents=0;
close(pollfds[i].fd);
}
}
}
else
{
char buffer[1024];
ssize_t s=recv(pollfds[i].fd,buffer,1024,0);
if(s>0){
buffer[s]=0;
std::cout<<"client say# "<<buffer<<std::endl;
}
else if(s<=0)
{
std::cout<<"client quit ... sock : "<<pollfds[i].fd<<std::endl;
pollfds[i].fd=-1;
pollfds[i].events=0;
pollfds[i].revents=0;
}
}
}
}
}
void Loop()
{
int timeout = 1000;// 1s
while(true)
{
int n=poll(pollfds_,NUM,timeout);
switch(n)
{
case 0:
//std::cout<<"timeout ... "<
break;
case -1:
std::cout<<"poll error"<<std::endl;
break;
default:
// select成功,至少有一个fd是就绪的
HandlerEvent(pollfds_,NUM);
//select成功,至少有一个fd是就绪的
//std::cout<<"有事件发生"<
break;
}
}
}
~PollServer()
{
if(listen_sock_>=0)
close(listen_sock_);
}
};
}
服务端文件 server.cc
#include"poll_server.hpp"
using namespace ns_poll;
int main()
{
PollServer* svr=new PollServer();
svr->InitPollServer();
svr->Loop();
return 0;
}
运行结果
epoll 有3个相关的系统调用
函数原型
int epoll_create(int size);
功能: 创建一个epoll的句柄
说明:
函数原型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能: epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型。
参数:
第二个参数的取值
struct epoll_event结构如下
events可以是以下几个宏的集合
函数原型
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
功能: 收集在epoll监控的事件中已经发送的事件
参数:
返回值:
如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时, 返回小于0表示函数失败.
struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
总结一下, epoll的使用过程就是三部曲
注意:
网上有这样的说法, epoll中使用了内存映射机制(内核直接将就绪队列通过mmap的方式映射到用户态. 避免了拷贝内存这样的额外性能开销)
这种说法是不准确的. 我们定义的struct epoll_event是我们在用户空间中分配好的内存. 势必还是需要将内核的数据拷贝到这个用户空间的内存中的
epoll有2种工作方式-水平触发(LT)和边缘触发(ET)
假如有这样一个例子:
epoll默认状态下就是LT工作模式
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志, epoll进入ET工作模式
select和poll其实也是工作在LT模式下. epoll既可以支持LT, 也可以支持ET
使用 ET 模式的 epoll, 需要将文件描述设置为非阻塞. 这个不是接口上的要求, 而是 “工程实践” 上的要求
假设这样的场景: 服务器接受到一个10k的请求, 会向客户端返回一个应答数据. 如果客户端收不到应答, 不会发送第二个10k请求
如果服务端写的代码是阻塞式的read, 并且一次只 read 1k 数据的话(read不能保证一次就把所有的数据都读出来, 可能被信号打断), 剩下的9k数据就会待在缓冲区中
此时由于 epoll 是ET模式, 并不会认为文件描述符读就绪. epoll_wait 就不会再次返回. 剩下的 9k 数据会一直在缓冲区中. 直到下一次客户端再给服务器写数据. epoll_wait 才能返回
但是这样就存在如下问题
所以, 为了解决上述问题(阻塞read不一定能一下把完整的请求读完), 于是就可以使用非阻塞轮训的方式来读缓冲区,保证一定能把完整的请求都读出来。
如果是LT没这个问题. 只要缓冲区中的数据没读完, 就能够让 epoll_wait 返回文件描述符读就绪
epoll的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll的性能可能适得其反
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll
例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP的入口服务器, 这样的服务器就很适合epoll.
如果只是系统内部, 服务器和服务器之间进行通信, 只有少数的几个连接, 这种情况下用epoll就并不合适. 具体要根据需求和场景特点来决定使用哪种IO模型
利用epoll实现一个简单的英汉互译
Accepter.hpp 文件
#pragma once
#include
#include
#include
#include
#include
#include
#include"Reactor.hpp"
#include"Callback.hpp"
using namespace ns_reactor;
using namespace ns_sock;
void Accepter(Event &event)
{
std::cout << "Accepter 回调方法被调用" << std::endl;
//连接事件到来,在同一个时间段,有很多的连续到来
while(true)
{
struct sockaddr_in peer;
socklen_t len=sizeof(peer);
int sock=accept(event.sock_,(struct sockaddr*)&peer,&len);
if(sock>0)
{
//0.设置fd为非阻塞
Sock::SetNonBlock(sock);
//1.构建新的与sock对应的Event对象
Event ev;
ev.sock_=sock;
ev.r_=event.r_;
ev.RegisterCallback(Recver,Sender,Errorer);
//2.将新的ev拖管给Epoll,必须知道曾今的Epoll对象
(event.r_)->AddEvent(ev,EPOLLIN | EPOLLET);
}
else
{
if(errno==EINTR)
{
//当前的accept调用,被信号中断,并不代表底层没有新的链接了
continue;
}
else if(errno==EAGAIN || errno==EWOULDBLOCK)
{
//当前出错返回,但是不是真正意义上出错了,而是底层没有连接了
break;
}
else
{
//真正意义上面的accept读取出错
std::cerr<<"accept error : "<<errno<<std::endl;
continue;
}
}
}
}
Callback.hpp 文件
#pragma once
#include
#include
#include
#include
#include
#include"Reactor.hpp"
#include"Util.hpp"
std::unordered_map<std::string,std::string> dict{
{"hello" , "你好"},
{"apple" , "苹果"},
{"pear", "梨"},
{"banana", "香蕉"},
{"peach","桃"}
};
using namespace ns_reactor;
/***********************************
* return :
* 0 本轮读取完毕
* -1 本轮读取出错
*
* sock : 要读取的fd
* out : 输出型参数
*
******************************/
static int RecvHepler(int sock,std::string *out)
{
while(true)
{
char buffer[1024];
ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);
if(s > 0){
buffer[s] = 0;
std::string str;
for(int i=0;i<s;i++){
if((buffer[i]>='a'&&buffer[i]<='z')||(buffer[i]>='A'&&buffer[i]<='Z'))
{
str+=buffer[i];
}
}
(*out) = str;
// (*out) += buffer;
}
else if(s < 0){
if(errno == EINTR)// 读取时出现中断错误
{
continue;
}
else if(errno == EAGAIN || errno == EWOULDBLOCK)// 没有数据可读
{
// 本轮读取完毕
return 0;
}
else
{
// 读取出错
return -1;
}
}
else{
return -1;
}
}
}
// 1.网络就绪事件与事件派发,和网络数据进行解耦
void Recver(Event &event)
{
// 你怎么知道你本轮读完了?依旧属于网络通信部分(是否就绪(epoll),是否读取(recv))
if( -1 == RecvHepler(event.sock_,&(event.inbuffer_)))// 不做业务处理,只负责进行读取
{
// 本轮读取出错(sock被关闭了,sock读取出错)
if(event.error_callback_) event.error_callback_(event);
return ;
}
// 往后写的内容已经与Reactor无关了,都是数据分析与处理
// 协议:你怎么知道你拿到了一个完整的报文?我们不知道,因为我们没有定制协议
// 但是,所有的数据全部已经在inbuffer中(无论是现在,还是未来)
// 协议:如果读取数据在协议层面,没有读完完整报文,应该如何?
std::string message = "词库中没有该单词";
std::cout<<event.inbuffer_.c_str()<<std::endl;
auto iter = dict.find(event.inbuffer_);
if(iter!=dict.end()){
message=iter->second;
}
event.outbuffer_ = message+'\n';
(event.r_)->EnableReadWrite(event.sock_,true,true);
std::cout<<"call Recver..."<<std::endl;
}
int SendHelper(int sock,std::string &send_string)
{
int total = 0;//目前累计发送多少
const char* start = send_string.c_str();
int size = send_string.size();
while(true)
{
ssize_t s = send(sock,start+total,size-total,0);
if(s>0)
{
total+=s;
// 本轮缓冲区足够大,数据已全部发送
if(total == size){
return 1;
}
}
else
{
if(errno == EINTR)
{
continue;
}
else if(errno == EAGAIN || errno == EWOULDBLOCK)
{
// 发送缓冲区已满
return 0;// 本轮发送完毕
}
else
{
return -1;// 发送失败
}
}
}
}
void Sender(Event &event)
{
// 本质是发送outbuffer_中的内容
int ret = SendHelper(event.sock_,event.outbuffer_);
if(ret == -1)
{
if(event.error_callback_)
event.error_callback_(event);
}
else if(ret == 1)
{
(event.r_)->EnableReadWrite(event.sock_,true,false);
}
else if(ret == 0)
{
(event.r_)->EnableReadWrite(event.sock_,true,true);
}
std::cout<<"call Sender..."<<std::endl;
}
void Errorer(Event &event)
{
std::cout<<"call Errorer..."<<std::endl;
(event.r_)->DelEvent(event.sock_);
}
Reactor.hpp 文件
#pragma once
#include
#include
#include
#include
#include
#include
namespace ns_reactor
{
class Event;
class Reactor;
typedef void(*callback_t)(Event &);
class Event
{
public:
int sock_; //特定一个文件描述符
Reactor *r_; //指向该Event对应的epoll
std::string inbuffer_; //对应的sock,私有的读取缓冲区
std::string outbuffer_; //对应的sock,私有的发送缓冲区
callback_t recv_callback_; //对应的sock,读回调
callback_t send_callback_; //对应的sock,写回调
callback_t error_callback_; //对应的sock,异常回调
public:
Event():sock_(-1),r_(nullptr)
{
recv_callback_ = nullptr;
send_callback_ = nullptr;
error_callback_ = nullptr;
}
void RegisterCallback(callback_t _recv,callback_t _send,callback_t _error)
{
recv_callback_=_recv;
send_callback_=_send;
error_callback_=_error;
}
~Event()
{
}
};
class Reactor
{
private:
int epfd_;
std::unordered_map<int,Event> events_; //sock : Event
public:
Reactor():epfd_(-1)
{}
void InitReactor()
{
epfd_=epoll_create(128);
if(epfd_<0)
{
std::cerr<<"epoll_create error"<<std::endl;
exit(1);
}
}
void AddEvent(const Event &ev,uint32_t events)
{
//1.将ev中的sock添加到epoll中,默认我们认为,所有添加的事件默认全部都要关心read事件
//2.将ev本身添加到unordered_map
struct epoll_event epoll_ev;
epoll_ev.events=events;
epoll_ev.data.fd=ev.sock_;
if(epoll_ctl(epfd_,EPOLL_CTL_ADD,ev.sock_,&epoll_ev)<0)
{
std::cerr<<"epoll_ctl add event error: "<<ev.sock_<<std::endl;
return ;
}
else
{
events_.insert({ev.sock_,ev});
}
std::cout<<"添加事件成功,sock : "<<ev.sock_<<std::endl;
}
void DelEvent()
{
auto iter = events_.find(sock);
if(iter == events_.end())
{
return ;
}
epoll_ctl(epfd_,EPOLL_CTL_DEL,sock,nullptr);
events_.erase(iter);
close(sock);
}
// 使能读写接口
void EnableReadWrite(int sock,bool readable,bool writeable)
{
struct epoll_event ev;
ev.events = (EPOLLET | (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0));
ev.data.fd = sock;
if(epoll_ctl(epfd_,EPOLL_CTL_MOD,sock,&ev) == 0){
std::cout<<"更改:"<<sock<<" 关心的事件成功"<<std::endl;
}
}
// 检测指定sock是否存在
bool IsExists(int sock)
{
auto iter = events_.find(sock);
return iter == events_.end() ? false : true;
}
// 对就绪事件进行事件派发
void Dispatcher(int timeout)
{
#define NUM 128
struct epoll_event revs[NUM];
int num=epoll_wait(epfd_,revs,NUM,timeout);
for(int i=0;i<num;i++)
{
//就绪事件派发
int sock=revs[i].data.fd;
uint32_t events=revs[i].events;
std::cout<<"sock: "<<sock<<" 这个fd上面有数据"<<std::endl;
// 将所有异常都交给读写处理
if(events & EPOLLERR) events |= (EPOLLIN|EPOLLOUT);
if(events & EPOLLHUP /*对端关闭链接*/) events |= (EPOLLIN|EPOLLOUT);
if(IsExists(sock) && (events&EPOLLIN))
if(events_[sock].recv_callback_) events_[sock].recv_callback_(events_[sock]);
if(IsExists(sock) && (events&EPOLLOUT))
if(events_[sock].send_callback_) events_[sock].send_callback_(events_[sock]);
}
}
~Reactor()
{
if(epfd_>=0) close(epfd_);
}
};
}
sock.hpp 文件
#pragma once
#include
#include
#include
#include
#include
#include
#include
#include
#include
namespace ns_sock
{
enum{
SOCKET_ERR=2,
BIND_ERR,
LISTEN_ERR
};
const int g_backlog=5;
class Sock
{
public:
static int Socket()
{
int sock=socket(AF_INET,SOCK_STREAM,0);
if(sock<0){
std::cerr<<"socket error!"<<std::endl;
exit(SOCKET_ERR);
}
int opt = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
return sock;
}
static void Bind(const int &sock,const u_int16_t &port)
{
struct sockaddr_in local;
bzero(&local,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::cerr<<"bind error!"<<std::endl;
exit(BIND_ERR);
}
}
static void Listen(const int &sock)
{
if(listen(sock,g_backlog)<0)
{
std::cerr<<"bind error!"<<std::endl;
exit(LISTEN_ERR);
}
}
static void SetNonBlock(int sock)
{
int fl=fcntl(sock,F_GETFL);
fcntl(sock, F_SETFL , fl | O_NONBLOCK);
}
};
}
EpollServer.cc 文件
#include"Reactor.hpp"
#include"sock.hpp"
#include"Accepter.hpp"
using namespace ns_reactor;
using namespace ns_sock;
int main()
{
//1.创建Epoll对象
Reactor* R=new Reactor();
R->InitReactor();
//2.创建网络套接字
int listen_sock=Sock::Socket();
Sock::SetNonBlock(listen_sock);// 为了支持ET模式,需要把文件描述符设置为非阻塞
Sock::Bind(listen_sock,8080);
Sock::Listen(listen_sock);
//3.创建Event对象
Event ev;
ev.sock_=listen_sock;
ev.r_=R;
//Accepter:链接管理器
ev.RegisterCallback(Accepter,nullptr,nullptr);// listen_sock,只需监听就绪
//4.将Event ev注册进入Epoll中
R->AddEvent(ev,EPOLLIN | EPOLLET);//让sock的工作方式是ET模式
//5.进入事件派发逻辑,服务器启动
int timeout=1000;
while(true)
{
R->Dispatcher(timeout);
}
return 0;
}