Welcome to 9ilk's Code World

(๑•́ ₃ •̀๑) 个人主页:9ilk
(๑•́ ₃ •̀๑) 文章专栏: 项目
本篇博客主要是对SERVER模块进行梳理总结,SERVER模块的最终目的是封装成一个主从Reactor模型的支持协议替换的高性能TCP服务器。
Buffer模块
该模块是一个缓冲区模块,用来实现通信中用户态的接收缓冲区和发送缓冲区,其主要的功能就是存放外界数据/供外界读取数据。
我们的主要实现思想是用STL容器vector<char>作为一块内存空间维护,它底层是线性的方便维护。同时还要维护两个指针,一个读指针read,一个写指针write。一个缓冲区可读区域的写指针与读指针之间的距离,因此write一般是大于等于read的。而一个缓冲区可写区域大小,除了写指针后续的内存空间,其实读指针之前的也算是空闲空间,也可以覆盖写。
基于此,在扩容的时候,如果在写入数据时,写指针后续剩余空闲空间不足,首先应该考虑整体缓冲区空闲空间(写指针后+读指针前)是否足够,足够则将原来区域的可读数据移动到起始位置 ,更新指针,然后再写入新数据;如果整体空闲空间不足,则进行扩容,从当前写位置开始扩容足够大小,再写入新数据,更新指针。
class Buffer
{
private:
vector _buffer;//使用vector进行内存管理
uint64_t _read_index;//读位置
uint64_t _write_index;//写位置
public:
Buffer()
:_buffer(defaultbuffersize),
_read_index(0),
_write_index(0)
{}
//提供一个公共的缓冲区起始位置: 获取起始地址还可以使用data() at(0) [0] front()
char* Begin(){return &*_buffer.begin();}
//当前读位置
char* ReadPos(){return Begin() + _read_index;}
//当前写位置
char* WritePos(){return Begin() + _write_index;}
//移动读指针
void MoveReadIdx(uint64_t len)
{
if(len == 0) return;
assert(len<=ReadAbleSize());//要读取长度不可超过可以读取数据大小
_read_index += len;
}
//移动写指针
void MoveWriteIdx(uint64_t len)
{
if(len == 0) return;
//1.需要先保证后续空闲空间足够
assert(TailSpace()>=len);
//2.移动写指针
_write_index += len;
}
//获取可读数据大小
uint64_t ReadAbleSize(){return _write_index - _read_index;}
//获取缓冲区末尾空闲空间大小---写偏移之后的空闲空间
uint64_t TailSpace(){return _buffer.size()-_write_index;}
//获取缓冲区起始空闲空间大小 --- 读偏移之前的空闲空间
uint64_t HeadSpace(){return _read_index;}
//确保可写空间足够(移动+扩容)
void EnsureWriteSpace(uint64_t len)
{
//1.先判断末尾空间大小是否足够
if(len<=TailSpace()) return;
//2.末尾空间不足,再加上起始空闲空间判断,此时向后移动
if(len<=TailSpace()+HeadSpace())
{
uint64_t readsize = ReadAbleSize();//先保存可读数据位置
std::copy(ReadPos(),ReadPos()+readsize,Begin());
_read_index = 0;
_write_index = readsize;
return;
}
//3.起始+末尾空闲时间都不足,则扩容
_buffer.resize(_write_index+len);
}
//写入数据
void Write(const void*data,uint64_t len)
{
if(len == 0) return;
//1.确保空间足够
EnsureWriteSpace(len);
//2.拷贝数据
const char* d = (const char*)data;
std::copy(d,d+len,WritePos());
}
//读取数据
void Read(void* buf,uint64_t len)
{
//1.确保可读数据足够
assert(len<=ReadAbleSize());
//2.读取数据到buf
std::copy(ReadPos(),ReadPos()+len,(char*)buf);
}
//清空缓冲区
void clear()
{
_write_index = _read_index = 0;
}
//将string内容写入
void WriteString(const string& data)
{
Write(data.c_str(),data.size());
}
//将另一个Buffer对象内容写入
void WriteBuffer(Buffer& data)
{
Write(data.ReadPos(),data.ReadAbleSize());
}
//缓冲区数据当作string返回
string ReadAsString(uint64_t len)
{
assert(len<=ReadAbleSize());//确保len不超过可读数据大小
string str;
str.resize(len);
Read(&str[0],len);
return str;
}
//读完并弹出
void ReadAndPop(void*buf,uint64_t len)
{ //读取
Read(buf,len);
//移动指针
MoveReadIdx(len);
}
uint64_t Size()
{
return _buffer.size();
}
//读取到string并弹出
string ReadStringAndPop(uint64_t len)
{
//读取到字符串
string str = ReadAsString(len);
MoveReadIdx(len);
return str;
}
//写入数据并弹出
void WriteAndPush(const void* data,uint64_t len)
{
Write(data,len);
MoveWriteIdx(len);
}
//写入string并弹出
void WriteStringPush(const string& data)
{
WriteString(data);
MoveWriteIdx(data.size());
}
//写入另一个Buffer对象并弹出
void WriteBufferPush(Buffer& data)
{
WriteBuffer(data);
MoveWriteIdx(data.ReadAbleSize());
}
//获取一行数据
//1.获取换行符位置
char* FindCRLF()
{
void* res = memchr(ReadPos(),'\n',ReadAbleSize());
return (char*)res;
}
//2.获取一行
string GetLine()
{
char* pos = FindCRLF();
if(pos == nullptr) return "";
//把换行也取出来
return ReadAsString(pos-ReadPos()+1);
}
//3.获取一行并弹出
string GetLineAndPop()
{
string str = GetLine();
MoveReadIdx(str.size());
return str;
}
};
Socket模块
这个模块主要是对Socket操作的封装,方便灵活操作。主要封装的功能如下:
- 创建套接字
- 绑定地址信息
- 开始监听
- 向服务器发起连接
- 获取新连接
- 接收数据
- 发送数据
- 关闭套接字
- 设置套接字选项(开启地址端口重用)
- 设置套接字阻塞属性 (设置为非阻塞,使用套接字接收数据时,取到没有数据为止,但套接字默认是阻塞操作,当缓冲区没有数据就会阻塞)
除了上述功能,为了更方便创建连接,因此可以再封装两个集成功能:
- 创建一个服务端连接
- 创建一个客户端连接
class Socket
{
private:
int _sockfd;
public:
Socket():_sockfd(-1){}
~Socket(){Close();}
Socket(int fd):_sockfd(fd){}
int Fd()
{return _sockfd;}
//创建套接字
bool Create()
{
_sockfd = ::socket(PF_INET,SOCK_STREAM,0);
if(_sockfd < 0)
{
ERR_LOG("创建套接字失败!\n");
return false;
}
return true;
}
//绑定地址信息
bool Bind(const string& ip,uint16_t port)
{
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(addr);
int n = ::bind(_sockfd,(struct sockaddr*)&addr,len);
if(n < 0)
{
ERR_LOG("绑定失败!\n");
return false;
}
return true;
}
//开始监听
bool listen(int backlog = MAX_LISTEN)
{
int n = ::listen(_sockfd,backlog);
if(n < 0)
{
ERR_LOG("监听失败\n");
return false;
}
return true;
}
//向服务器发起链接
bool Connect(const string& ip,uint16_t port)
{
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = inet_addr(ip.c_str());
socklen_t len = sizeof(addr);
int n = ::connect(_sockfd,(struct sockaddr*)&addr,len);
if(n < 0)
{
ERR_LOG("连接失败!");
return false;
}
return true;
}
//获取新连接
int Accept() //新连接直接返回fd方便后续操作
{
int newfd = ::accept(_sockfd,NULL,NULL);
if(newfd < 0)
{
ERR_LOG("获取新连接失败\n");
return -1;
}
return newfd ;
}
//关闭套接字
void Close()
{
if(_sockfd != -1)
{
::close(_sockfd);
_sockfd = -1;
}
}
//创建一个服务端连接:默认非阻塞
bool CreateServer(uint16_t port,const string& ip = "0.0.0.0",bool flag = 0) //服务器默认接收所有网卡的数据
{
//1.创建套接字 2.bind地址信息 3.监听 4.开启地址复用 5.设置非阻塞
if(Create() == false)
{
ERR_LOG("Create Error");
return false;
}
if(flag) NonBlock();
ReuseAddress();
if(Bind(ip,port) == false)
{
ERR_LOG("Bind Error");
return false;
}
if(listen() == false)
{
ERR_LOG("Listen Error");
return false;
}
return true;
}
//创建一个客户端连接
bool CreateClient(uint16_t port,const string& ip)
{
//1.创建套接字 2.连接
if(Create() == false) return false;
if(Connect(ip,port) == false) return false;
return true;
}
//设置套接字选项 --- 开启地址端口重用
void ReuseAddress()
{
int val = 1;
setsockopt(_sockfd,SOL_SOCKET,SO_REUSEADDR,(void*)&val,sizeof(int));
val = 1;
setsockopt(_sockfd,SOL_SOCKET,SO_REUSEPORT,(void*)&val,sizeof(int));
}
//设置套接字阻塞属性
void NonBlock()
{
int flag = ::fcntl(_sockfd,F_GETFL,0);
::fcntl(_sockfd,F_SETFL,flag|O_NONBLOCK);
}
//接收数据
ssize_t Recv(void* buf,size_t len,int flag = 0) //缺省为0,默认阻塞读写
{
ssize_t ret = ::recv(_sockfd,buf,len,flag);
if(ret <= 0)
{ //EAGAIN:当前socket接收缓冲区中没有数据,在非阻塞情况下才有这个错误
//EINTER:表示当前scoket的阻塞等待,被信号打断
if(errno == EAGAIN || errno == EINTR)
return 0;
ERR_LOG("socket recv failed\n");
return -1;
}
return ret;
}
//发送数据
ssize_t Send(const void* buf,size_t len,int flag = 0)
{
if(len == 0) return 0;
ssize_t ret = ::send(_sockfd,buf,len,flag);
if(ret < 0)
{
if(errno == EINTR || errno == EAGAIN)
return 0;
ERR_LOG("scoket send error\n");
return -1;
}
return ret;
}
//非阻塞接收数据
ssize_t NonBlockRecv(void*buf,size_t len)
{
return Recv(buf,len,MSG_DONTWAIT);
}
//非阻塞发送数据
ssize_t NonBlockSend(void*buf,size_t len)
{
return Send(buf,len,MSG_DONTWAIT);
}
};
Channel模块
该模块主要是对描述符/连接的事件监控管理,它主要完成的功能是下面几大类:
1. 判断当前描述符是否监控了某事件
2. 对当前描述符开启某事件的监控
3. 对当前描述符关闭对某事件的监控
4. 设置事件回调
5. 事件分发/事件出来:一旦连接事件触发,根据不同的事件调用不同的函数
由于我们采用的是epoll机制来进行事件监控,因此我们需要了解其相关的事件标志:
- EPOLLIN:fd可读
- EPOLLOUT:fd可写
- EPOLLRDHUB:fd连接断开(TCP连接被对方关闭或半关闭)
- EPOLLPRI:fd有紧急/带外数据可读
- EPOLLHUB:fd挂断,比如socket被关闭,通常可以理解为描述符已经失效或关闭,不再使用。
- EPOLLERR:fd发生错误,不能作为事件请求(即不能监控该种事件),只能作为返回事件。
这几个事件都是用uint32_t类型保存的,通过是一个bitmask,每个事件占一个bit位,因此可以通过位运算来组合或判断事件,同时我们需要为这几种事件注册对应的回调函数来处理。
class Channel
{
private:
int _fd;
uint32_t _events;//当前链接需要监控的事件
uint32_t _revnts; //当前链接触发的事件
using EventCallBack = std::function;
EventCallBack _write_callback; //可读事件回调
EventCallBack _read_callback; //可写事件回调
EventCallBack _error_callback; //错误事件回调
EventCallBack _close_callback; //关闭事件回调
EventCallBack _any_callback; //任意事件回调
public:
Channel(EventLoop* loop,int fd = -1)
:_fd(fd),
_events(0),
_revnts(0)
{}
int Fd()
{return _fd;}
uint32_t Events()//监控的事件
{return _events;}
bool ReadAble()//当前是否监控可读:判断对应事件的比特位是否为1
{return _events&EPOLLIN; }
bool WriteAble()//当前是否监控可写
{return _events&EPOLLOUT;}
void EnableRead()//启动读事件监控:将对应比特位置1
{_events |= EPOLLIN;}
void EnableWrite() //启动写事件监控
{_events |= EPOLLOUT;}
void DisableRead()//关闭读事件监控:将对应比特位置0
{_events &= ~EPOLLIN;}
void DisableWrite() //关闭写事件监控
{_events &= ~EPOLLOUT;}
void DisableAllEvent()// 关闭任意事件监控:全设置为0
{_events = 0;}
void Update();
void Remove(); //移除监控:目前无法实现,后面调用eventloop移除监控
void SetReadCallBack(const EventCallBack& cb) //设置读事件回调
{_read_callback = cb;}
void SetWriteCallBack(const EventCallBack& cb) //设置读事件回调
{_write_callback = cb;}
void SetErrorCallBack(const EventCallBack& cb) //设置读事件回调
{_error_callback = cb;}
void SetCloseCallBack(const EventCallBack& cb) //设置读事件回调
{_close_callback = cb;}
void SetAnyCallBack(const EventCallBack& cb) //设置读事件回调
{_any_callback = cb;}
void HandlerEvent() //事件处理:一旦链接事件触发就调用该函数,自己触发了什么事件如何处理由自己决定
{
if(_revnts & EPOLLIN || _revnts & EPOLLRDHUP || _revnts & EPOLLPRI) //断开链接认为要求上层读取数据,关闭链接后没有数据发送了
{
if(_any_callback) _any_callback();
if(_read_callback) _read_callback();
}
//有可能链接释放的事件,一次只处理一个因此使用else if
if(_revnts & EPOLLOUT)
{
if(_any_callback) _any_callback();
if(_write_callback) _write_callback();
}
else if(_revnts & EPOLLERR)
{
if(_any_callback) _any_callback();
if(_error_callback) _error_callback();
}
else if(_revnts & EPOLLHUP)
{
if(_any_callback) _any_callback();
if(_close_callback) _close_callback();
}
}
//当Poller模块监控到事件发生时,需要通知Channel哪些事件触发,再去根据对应的事件进行回调,所以我/们需要设置一个SetREvents()告知Channel什么事件发生了
void SetREvents(uint32_t revnts)
{
_revnts = revnts;
}
};
注意:目前我们只是在channel模块内部对该描述符的events和revents进行简单的设置,未来channel模块一定是要和Poller模块结合的,因为设置了事件监控,就要渗透到内核,也就是使用epoll模型来监控,移除监控也一样,而Poller模块就是对epoll操作的封装。
HandlerEvent说明:
1. 对于EPOLLIN、EPOLLRDHUP、EPOLLPRI我们都认为需要读取数据了。EPOLLRDHUP表示连接断开,此时需要上层读取数据,因为对方不可能再发送数据了。
2. 对于可写事件,是可能写入出错,导致链接释放的,此时如果继续往下执行EPOLLERR和EPOLLHUP的事件回调这样是会出错的,因此对于有可能导致链接释放的事件,一次只处理一个,安全第一,下次再处理,因此使用else if。
3. 对于可读事件和可写事件,它们在处理之后是需要调用任意事件回调的,主要是为了刷新连接的活跃度,我们目前认为他们出错也是释放链接的,因此将任意事件回调放在之前调。
4. 对于EPOLLHUP或者EPOLLERR,一旦出错就需要释放连接,因此需要在它们各自的事件回调之前调用任意回调。
Poller模块
这个模块其实是真正的fd事件监控模块,本质是对epoll封装实现对事件的监控操作,比如添加/修改/移除描述符的事件监控。
既然是对epoll封装,那一定是要有一个对epoll模型操作的fd,同时还要有一个struct epoll_event结构体数组,保存所有触发事件的描述符信息。最后还需要一个hash表管理描述符以及描述符对应的事件管理对象channel,当Poller监控到事件发生了,就可以通过channel对象设置revents进行通知,此时channel就可以在HandlerEvent内部进行事件派发。
因此Poller和Channel模块主要结合的流程是:
- Poller通过Channel知道描述符需要监控什么事件,然后对描述符进行事件监控,然后存到将fd和Channel存到hash表中。
- 当描述符事件就绪,通过描述符在hash表中找到对应的Channel,这样才知道什么事件如何处理。
#define MAX_EPOLLSIZE 1024
class Poller
{
private:
int _epfd;
struct epoll_event _events[MAX_EPOLLSIZE];
unordered_map _channels; //一个fd对应一个channel
private: //为实现对外功能的私有接口
bool HasChannel(Channel*channel) //一个Channel是否已经添加了事件监控
{
auto it = _channels.find(channel->Fd());
if(it == _channels.end()) return false;
return true;
}
void Update(Channel*channel,int op) //本质就是对epoll进行操作--epoll_ctl
{
struct epoll_event event;
event.events = channel->Events();
event.data.fd = channel->Fd();
int ret = ::epoll_ctl(_epfd,op,channel->Fd(),&event);
if(ret < 0)
{
ERR_LOG("epoll_ctrL error");
return;
}
}
public: //对外开放接口
Poller()
{
_epfd = ::epoll_create(MAX_EPOLLSIZE);
if(_epfd < 0)
{
ERR_LOG("Epoll Create Error");
exit(1);
}
}
//添加或修改监控事件
void UpdateEvent(Channel* channel)
{
//1.先查询是否channel添加了事件监控
bool ret = HasChannel(channel);
if(ret == false) //不存在则添加
{
_channels[channel->Fd()] = channel;
Update(channel, EPOLL_CTL_ADD);
return;
}
//存在则修改
Update(channel,EPOLL_CTL_MOD);
}
//移除监控事件
void RemoveEvent(Channel* channel)
{
//1.从哈希表中移除
auto it = _channels.find(channel->Fd());
if(it != _channels.end())
_channels.erase(it);
//2.移出红黑树
Update(channel,EPOLL_CTL_DEL);
}
void Poll(vector* active) //开始监控返回活跃链接
{
int ret = ::epoll_wait(_epfd,_events,MAX_EPOLLSIZE,-1);//-1表示调用阻塞直到有事件发生
if(ret < 0)
{
if(ret == EINTR) return;//信号打断
ERR_LOG("epoll_wait error:%s\n",strerror(errno));
return;
}
for(int i = 0; i < ret ; i++)
{
auto it = _channels.find(_events[i].data.fd);
assert(it != _channels.end());//说明channel管理出现问题
it->second->SetREvents(_events[i].events); //设置实际就绪事件
active->push_back(it->second); //放进活跃对立
}
}
};
具体Channel模块和Poll模块怎么结合呢?之前我们在Channel模块提供了设置启动/关闭对某事件监控的接口,我们只是对_events进行修改,但是还要渗透到内核,也就是添加到epoll模型,因此Channel内部需要封装一个Poller模块,修改完events字段之后,使用Poller添加到epoll里:
void EnableRead()//启动读事件监控:将对应比特位置1
{_events |= EPOLLIN;Update();}
void EnableWrite() //启动写事件监控
{_events |= EPOLLOUT;Update();}
void DisableRead()//关闭读事件监控:将对应比特位置0
{_events &= ~EPOLLIN;Update();}
void DisableWrite() //关闭写事件监控
{_events &= ~EPOLLOUT;Update();}
void Channel::Remove()
{
_poller->RemoveEvent(this);
}
void Channel::Update()
{
_poller->UpdateEvent(this);
}
下面我们就可以根据Socket、Poller、Channel这几个模块实现一个简单的单Reactor模型,主要流程如下:
1. 创建监听Socket和Channel对象,Poller对象,将监听套接字添加到Poller中进行管理,设置好可读事件回调(即获取新链接),然后开启可读事件监控。
2. 循环监控,当listen套接字事件就绪,此时就会将新链接获取上来,也为其封装一个Channel对象添加到Poller对象进行事件管理,然后设置好对应的回调,就可以开启事件监控了。
3. 后续就是Poller不断监控监听连接和通信连接的事件。
#include"server.hpp"
void HandlerClose(Channel* channel)
{
cout << "close:" << channel->Fd() << endl;
channel->Remove();//移除监控
delete channel;//释放文件描述符
}
void HandlerAny()
{
cout << "产生了一个事件!" << endl;
return;
}
void HandlerRead(Channel* channel)
{
int fd = channel->Fd();
char buff[1024] = {0};
int ret = recv(fd,buff,1023,0);
if(ret <= 0)
{
cout << "链接释放" << endl;
return HandlerClose(channel);
}//链接释放
channel->EnableWrite();//启动可写事件
cout <<"client say: " << buff << endl;
}
void HandlerWrite(Channel* channel)
{
int fd = channel->Fd();
char* data = "hello Client..";
int ret = ::send(fd,data,strlen(data),0);
if(ret <= 0)
{return HandlerClose(channel);}
channel->DisableWrite();//关闭写监控
}
//监听套接字触发可读事件时,获取新链接 为新链接设置回调,开启他们的可读事件
void Acceptor(Poller* poller,Channel* lis_channel)
{
int fd = lis_channel->Fd();
int newfd = accept(fd,NULL,NULL);
if(newfd < 0)
{
cout << "accept error" << endl;
return;
}
cout << "gain a new link" << endl;
Channel* new_channel = new Channel(poller,newfd);
new_channel->SetReadCallBack(std::bind(HandlerRead,new_channel));
new_channel->SetWriteCallBack(std::bind(HandlerWrite,new_channel));
new_channel->SetErrorCallBack(std::bind(HandlerClose,new_channel));
new_channel->SetCloseCallBack(std::bind(HandlerClose,new_channel));
new_channel->SetAnyCallBack(HandlerAny);
new_channel->EnableRead();
}
int main()
{
Poller poller;
Socket lis_sock;
bool ret = lis_sock.CreateServer(8081);
if(ret == false)
{
ERR_LOG("Create Server Error");
abort();
}
//为监听套接字创建一个Channel对象,对其进行事件监控
Channel lis_channel(&poller,lis_sock.Fd());
lis_channel.SetReadCallBack(std::bind(Acceptor,&poller,&lis_channel));
lis_channel.EnableRead();
while(1)
{ //开始监控
vector actives;
poller.Poll(&actives);
for(const auto& a : actives)
{
a->HandlerEvent();
}
sleep(2);
}
lis_sock.Close();
return 0;
}
#include"server.hpp"
int main()
{
Socket cli_sock;
bool ret = cli_sock.CreateClient(8081,"127.0.0.1");
if(ret == false)
{
ERR_LOG("create newlink error");
abort();
}
cout << "Create Client Success"<< endl;
while(1)
{
string str = "hello world";
cli_sock.Send(str.c_str(),str.size());
char buf[1024] = {0};
int size = cli_sock.Recv(buf,1023);
buf[size] = 0;
DBG_LOG("client recv:%s",buf);
sleep(2);
}
return 0;
}
流程图:

EventLoop模块
EventLoop模块相当于是对前面几个模块的整合。
eventfd
在梳理EventLoop模块之前,我们需要对eventfd进行一个大概的了解。
DESCRIPTION
eventfd - create a file descriptor for event notification
NAME
eventfd - create a file descriptor for event notification
SYNOPSIS
#include
int eventfd(unsigned int initval, int flags);
eventfd用来创建一个文件描述符用于事件通知,是一种Linux提供的事件通知机制。与信号不同的是,信号是针对进程进行事件通知的,但一个信号被进程的哪个线程处理事不一定的,而eventfd就可以被用来在EventLoop模块中实现线程间的事件通知,毕竟一个EventLoop是和一个线程绑定的。- 参数
1. initval:计数初值,不一定要设置为0
2. flags: 一般设置以下两个标志位
EFD_CLOEXEC--- 禁止进程复制,表示返回的eventfd文件描述符在fork后exec其他程序时会自动关闭这个文件描述符。
EFD_NONBLOCK--- 启动非阻塞属性
- 返回值:返回一个文件描述符用于操作eventfd,因此eventfd也是可以通过read/write/close进行操作的。注意:
read&write进行I/O的时候数据只能是一个8字节数据。 - eventfd事件通知的原理:它本质是内核管理的一个计数器,创建eventfd就会在内核中创建对应的计数器结构,计数不为0,那么它会触发可读事件,read之后计数清零,write则会递增计数器。也就是说,eventfd计数不为0就是一直可读,而可写事件是一直可写(因为可以一直累计计数),因此eventfd如果使用epoll监控事件,那么都是监控读事件,因为监控写事件无意义。
- 对于 eventfd 呢?它的阻塞有可能是怎么样的?read eventfd 的时候,如果计数器的值为 0,就会阻塞(这种就等同于没“文件”内容)。这种可以设置 fd 的属性为非阻塞类型,这样读的时候,如果计数器为 0 ,返回 EAGAIN 即可,这样就不会阻塞整个系统。
Q:为什么使用eventfd,而不使用普通的fd进行事件通知呢?
- 不是所有的 fd 类型都可用 epoll 池来监听事件的,只有实现了
file_operation->poll的调用的“文件” fd 才能被 epoll 管理。eventfd 刚好就实现了这个接口。


设计思想
EventLoop是需要对事件监控和事件处理的,但是如果这个描述符在多个线程中都触发了事件进行处理,此时是会存在线程安全问题的,因此这个模块一定是和线程一一对应的,需要将一个连接的事件监控,以及连接事件处理和其他操作都放在同一个线程中处理。具体应该如何处理呢,如何保证对链接的操作和处理回调函数中的操作都在线程中?
muduo库中对于出现的这种问题,为了高性能不加锁,采用了一种设计方案:将对fd的操作封装成一个任务,压入这个EventLoop线程对应的任务队列里,等到监控的事件结束之后,再把任务队列中的任务拿出来执行,这样就只需对任务队列加锁,转移了锁的粒度,所有操作都变成单线程串行执行。因此在EventLoop内部线程具体的工作流程为:
1. 在线程中对描述符进行事件监控。
2. 有描述符就绪则对描述符进行事件处理。
3. 所有就绪事件处理完了,此时再去将任务队列中的所有任务一一执行。
但是需要注意的是,在上面的流程中,由于epoll的事件监控(step 2),可能会因为没有事件到来而持续阻塞,导致任务队列中的任务不能及时得到执行(step 3),因此我们使用eventfd添加到Poller的事件监控中,用于实现每次向任务队列添加任务的时候,通过向eventfd写入数据来唤醒epoll的阻塞。
class EventLoop
{
private:
std::thread::id _thread_id; //线程id
int _eventfd; //eventfd唤醒事件监控可能导致的阻塞
std::unique_ptr _event_channel;//为eventfd创建一个channel进行事件监控
Poller _poller;//进行所有描述发的事件监控
using Functor = function;
vector _tasks; //任务池
std::mutex _mtx; //保证任务队列线程安全的锁
static int CreateEfd()//创建eventfd
{
int efd = ::eventfd(0,EFD_CLOEXEC|EFD_NONBLOCK); //initval flag
if(efd < 0)
{
ERR_LOG("create eventfd error");
abort();
}
return efd;
}
void ReadEvent()
{
uint64_t val = 0;
ssize_t ret = read(_eventfd,&val,sizeof(val));
if(ret < 0)
{ //信号打断 无数据可读
if(errno == EINTR || errno == EAGAIN) return;
ERR_LOG("Read Eventfd Failed");
abort();
}
}
//其实就是向eventfd写入一个数据,达到事件通知的效果,触发可读事件
void WeakEvent()
{
uint64_t val = 1;
ssize_t ret = write(_eventfd,&val,sizeof(val));
if(ret < 0)
{ //信号打断 无数据可读
if(errno == EINTR || errno == EAGAIN) return;
ERR_LOG("Write Eventfd Failed");
abort();
}
}
public:
EventLoop():_thread_id(std::this_thread::get_id()),_eventfd(CreateEfd()),
_event_channel(new Channel(this,_eventfd)) //TODO Channel修改为eventloop
{
//给eventfd设置可读事件回调,读取事件通知次数(注意绑定的时候注意指定类域还有注意this)
_event_channel->SetReadCallBack(std::bind(&EventLoop::ReadEvent,this));
//开启可读监控
_event_channel->EnableRead();
}
//交换任务时需要加锁保证任务队列的线程安全
void RunAllTask() //执行任务池中所有任务
{
vector func;
{
std::unique_lock _lock(_mtx);
func.swap(_tasks);
}
//执行任务
for(const auto& f : func)
{
f();
}
}
void RunInLoop(const Functor& cb) //判断将要执行的任务是否处于当前线程中,如果是则执行,不是则压入队列
{
if(isInLoop()) return cb();//将要执行的任务处于当前线程,直接返回
return QueueInLoop(cb); //不是处于同一线程,压入线程池中
}
void QueueInLoop(const Functor& cb)//将操作压入任务池:需要加锁保证线程安全
{
{
std::unique_lock _lock(_mtx);
_tasks.push_back(cb);
}
//唤醒可能因为事件没就绪,而导致的epoll阻塞
//其实就是给eventfd写入一个数据,eventfd就会触发可读事件,此时epoll不会阻塞,执行RunAlltask()
WeakEvent();
}
bool isInLoop()//用于判断当前线程是否是EventLoop对应的线程
{
return _thread_id == std::this_thread::get_id();
}
bool UpdateEvent(Channel* channel){_poller.UpdateEvent(channel);}//添加/修改描述符的事件监控
void RemoveEvent(Channel* channel){_poller.RemoveEvent(channel);} //移除描述符的监控
void Start() //三步走:1.事件监控 2.就绪事件处理 3.执行任务
{
while(1)
{
//1.监控
vector actives;
_poller.Poll(&actives);
//2.就绪事件处理
for(const auto& a : actives)
{
a->HandlerEvent();
}
//3.执行任务
RunAllTask();
}
};
- Start()就是将来和EventLoop绑定线程的入口函数主要执行的工作。
- 在将对链接的操作封装成一个任务抛进任务队列,不是直接压入任务池中的,如果当前要执行的任务处于当前线程,则直接返回,不是处于同一线程才压入线程池中。
- 双缓冲队列优化:push任务时我们需要加锁,而在RunAllTasks()我们使用两个队列,也就是说锁只保护任务队列的push和swap操作,不保护任务执行,任务执行在EventLoop线程中串行执行,不需要加锁。
EventLoop内部是封装了Poller模块的,因此可以将EventLoop和Channel模块整合,在Channel对事件监控开启/关闭时,调用EventLoop内部封装的Poller实现:
class Poller;
class EventLoop;
class Channel
{
private:
int _fd;
EventLoop* _loop;
uint32_t _events;//当前链接需要监控的事件
uint32_t _revnts; //当前链接触发的事件
///....
};
void Channel::Remove()
{
_loop->RemoveEvent(this);
}
void Channel::Update()
{
_loop->UpdateEvent(this);
}
TimeWheel模块
这个模块主要是为了帮我们完成定时任务的功能,完成类似非活跃连接销毁的任务。因此这个模块的目的是实现一个完整的秒级定时器,通过timerfd决定每隔多少时间滴答一次,timerfd设置为每ns触发一次定时事件,当事件被触发,而运行一次timeWheel的runTimeTask,执行当前槽内的所有定期任务。
首先timerWheel是根据timerfd来决定何时执行过期定时任务,因此我们需要一个timerfd成员;同时每秒钟触发一次事件,那就需要将fd添加到eventloop进行事件监控,所以我们要添加个eventLoop;而eventLoop基于channel进行事件监控,因此我们还要为timerfd创建一个channel对象进行事件监控:
//时间轮对象
class TimeWheel
{
private:
using WeakTask = std::weak_ptr;
using PtrTask = std::shared_ptr;
int _tick;//表示执行定时任务的指针,走到哪里释放哪里,释放哪里相当于执行哪里的任务
int _capacity;//相当于时间轮的一圈多少 --- 最大延迟时间
vector> _wheel;//二维数组作为时间轮注意要比capacity后声明
unordered_map _timers; //注意这里需要使用WeakPtr是因为添加新的定时任务(shared_ptr)再使用shared_Ptr会增加不必要的计数
EventLoop* _loop; //事件循环
int _timerfd;//定时器描述符
unique_ptr _timer_channel;//
//....
};
之前我们已经封装好一个时间轮的,剩下的就是在原来基础上创建好timerfd,timerfd_channel,然后设置好对应可读回调并开启读事件监控:
private:
static int CreateTimerFd()
{
//1.创建定时器
int timerfd = ::timerfd_create(CLOCK_MONOTONIC,0);
if(timerfd < 0)
{
ERR_LOG("timerfd create error!");
abort();
}
//2.启动定时器
struct itimerspec itime;
itime.it_value.tv_sec = 1;//第一次超时
itime.it_value.tv_nsec = 0;
itime.it_interval.tv_sec = 1;//第一次超时之后的超时间隔
itime.it_interval.tv_nsec = 0;
int n = ::timerfd_settime(timerfd,0,&itime,NULL);
if(n < 0)
{
ERR_LOG("start timer error!");
abort();
}
return timerfd;
}
void ReadTimerfd()
{
uint64_t times;
int ret = ::read(_timerfd,×,sizeof(times));//写入数据之前会阻塞没有数据
if(ret < 0)
{
ERR_LOG("read error");;
abort();
}
}
//定时器fd可读事件回调:1.读取定时器数据进行清0 2.执行一波所有的过期定时任务
void TimerCb()
{
ReadTimerfd();
RunTimeTask();
}
public:
TimeWheel(EventLoop* loop)
:_tick(0),_capacity(60),_wheel(_capacity),_loop(loop),
_timerfd(CreateTimerFd()),_timer_channel(new Channel(loop,_timerfd))//注意成员声明顺序
{
_timer_channel->SetReadCallBack(std::bind(&TimeWheel::TimerCb,this));
_timer_channel->EnableRead();
}
//指针移动执行任务
void RunTimeTask()
{
_tick = (_tick+1)%_capacity;
//走到哪释放哪
_wheel[_tick].clear(); //清空指定位置的数组,就会把数组中保存的所有管理定时器对象的shared_ptr軽放掉
}
在时间轮添加定时任务/对指定任务进行刷新/取消定时任务这些接口都是在任何一个线程都有可能进行的,但是定时器是和eventLoop保存在一起的,很多定时任务都是对通信链接进行操作,所以我们定时任务必须在EventLoop线程里执行,即这几个函数最终要放在eventLoop线程中执行:
//在时间轮添加定时任务
void TimerAdd(uint64_t id,uint32_t delay,const TaskFunc& cb)
{
_loop->RunInLoop(std::bind(&TimeWheel::TimerAddInloop,this,id,delay,cb));
return;
}
void TimerAddInloop(uint64_t id,uint32_t delay,const TaskFunc& cb)
{
//1.创建定时任务设置好删除函数
PtrTask pt(new TimeTask(id,delay,cb));
//注意:因为编译器不会将对象的成员函数隐式转换成函数指针,所以必须在TimeWheel::RemoveTimeTask前添加&;
pt->SetRelease(std::bind(&TimeWheel::RemoveTimeTask,this,id));//注意这里需要使用bind这是因为RemoveTimeTask第一个参数是this,而且我们统一了ReleaseFunc
cout << "Create Task.." << _wheel.size() << endl ;
//2.存放到时间轮
int pos = (_tick+delay)%_capacity;
_wheel[pos].push_back(pt);
_timers[id] = WeakTask(pt);
}
//对指定定时任务进行刷新/延迟
void RefreshTask(uint64_t id)
{
_loop->RunInLoop(std::bind(&TimeWheel::RefreshTaskInLoop,this,id));
return;
}
void RefreshTaskInLoop(uint64_t id)
{
auto it = _timers.find(id);
if(it == _timers.end())
return; //没找到指定定时任务 返回
PtrTask pt = it->second.lock();//相当于新建了一个shared_ptr共享计数
int delay = pt->DelayTime();
int pos = (_tick+delay)%_capacity;
_wheel[pos].push_back(pt);
}
//取消定时任务
void CancelTask(uint64_t id)
{
_loop->RunInLoop(std::bind(&TimeWheel::CancelTaskInLoop,this,id));
return;
}
void CancelTaskInLoop(uint64_t id)
{
auto it = _timers.find(id);
if(it == _timers.end())
return; //没找到指定定时任务 返回
PtrTask pt = it->second.lock();//相当于新建了一个shared_ptr共享计数
if(pt) pt->Cancel();
}
//Class TimerWheel: 是否存在某个定时任务
bool HasTimer(uint64_t id)
{
auto it = _timers.find(id);
if(it == _timers.end()) return false;
return true;
}
编写好TimeWheel模块,就可以和EventLoop模块整合了,主要是用于实现定时任务(非活跃连接)的添加、刷新、取消:
void EventLoop::TimerAdd(uint64_t id,uint32_t delay,const TaskFunc& cb)
{return _timer_wheel->TimerAdd(id,delay,cb);}
void RefreshTask(uint64_t id)
{return _timer_wheel->RefreshTask(id);}
void CancelTask(uint64_t id)
{return _timer_wheel->CancelTask(id);}
bool EventLoop::HasTimer(uint64_t id)
{return _timer_wheel->HasTimer(id);}
需要注意的是,添加新链接的定时销毁任务这个操作,必须在启动读事件之前,因为有可能启动了事件监控之后,立即有了事件,需要刷新连接的活跃度,而任意事件回调的操作主要就是刷新定时任务,如果先前没设置定时任务就麻烦了。
主要的流程图如下:

Connection模块
Connection模块的目的是为了对连接进行全方位的管理,对于通信连接的所有操作都是通过这个模块来完成的。
管理的内容主要包括套接字的管理,可以对套接字进行各种操作,还有对于连接事件的管理,比如可读、可写、挂断、任意,也有对于缓冲区的管理,方便和Socket进行数据的发送和接收,还有对于协议上下文的管理,用来记录请求数据的发送过程,以及对用户设置各种回调的功能,最后还有连接的状态方便对连接进行维护。
- DISCONNECTED -- 链接关闭状态
- CONNECTING -- 链接建立成功-待处理状态
- CONNECTED -- 链接建立完成,各种设置已完成,可以通信的状态
- DISCONNECTING --- 链接待关闭状态
之前几个模块来进行通信,都是对新通信连接封装一个channel然后添加到EventLoop中进行管理,设置回调(编写HandleRead,HandlerWrite接收和发送数据)而Connection模块需要对用户更加友好,因此我们需要在内部编写好这几个Channel对应的回调数据,内部帮用户接收/发送数据,而用户只需要关心连接收到数据之后的处理、连接建立成功之后处理、连接关闭之后该如何处理,任意事件的产生如何处理,这些都是由用户设置的。
Connection模块的功能主要由发送数据、关闭连接、启动和取消非活跃连接超时销毁、连接初始化、协议切换的功能,而该模块是对于连接的管理模块,因此对于连接的所有操作都是通过这个模块完成的,Connection内部封装了EventLoop和Channel,因此可以保证对链接的操作都是在对应EventLoop线程中串行执行的,这种做法虽解决了"数据竞争"问题,但是不能解决"逻辑时序"问题,比如对于连接进行操作的时候,排进任务队列,但是释放连接操作比它先排进队列,此时对连接的访问是有风险的,可能导致程序崩溃,其中一个解决方案是使用智能指针来对Connection对象进行管理,这样就能保证任意一个地方对于Connection对象进行操作的时候都会在内部保存一个shared_ptr,即使其他地方进行释放操作,也只是对shared_ptr的计数器减1,修改连接的状态,不会导致连接的实际释放,如果后续其他处使用Send操作,也会根据链接的状态判断是否进行Send操作。
我们连接收到数据之后,用户处理完数据之后,需要调用MessageCallback使用Connection对象再将响应发回去,因此在HandleRead内部需要传递Connection对象给MessageCallBack的,我们是不能直接传递this指针的,因为执行的时候可能对象已经被销毁,导致程序崩溃,因此我们需要值传递对象本身的shared_ptr,此时需要Connection类继承std::enable_shared_from_this,再调用shared_from_this()返回自身的shared_ptr对象。
//链接管理模块Connection
typedef enum {DISCONNECTED,CONNECTING,CONNECTED,DISCONNECTING} ConnStatu;
//DISCONNECTED -- 链接关闭状态
//CONNECTING -- 链接建立成功-待处理状态
//CONNECTED -- 链接建立完成,各种设置已完成,可以通信的状态
//DISCONNECTING --- 链接待关闭状态
class Conncetion;
using PtrConnection = std::shared_ptr;//解决链接释放野指针(?)
class Conncetion : public enable_shared_from_this
{
private:
uint64_t _conn_id;//标识一个链接的id,也作为定时器事件id
//套接字管理
int _sockfd;//链接关联的文件描述符
bool _enable_inactive_release;//链接释放启动非活跃链接销毁的判断标志,默认为false
ConnStatu _statu;
Socket _socket; //套接字操作管理
//链接事件管理
EventLoop* _loop;
Channel _channel; //链接事件管理
//缓冲区管理
Buffer _in_buffer;//输入缓冲区:存放从socket中读取到的数据
Buffer _out_buffer;//输出缓冲区:请求的接收处理上下文
//协议上下文管理
Any _context;//请求的接收处理上下文
//回调函数管理(不同阶段回调)
using ConnectedCallBack = std::function;//连接建立成功后如何处理
using MessageCallBack = std::function;//连接收到数据如何处理
using CloseCallBack = std::function;//连接关闭前如何处理
using AnyCallBack = std::function;//任意事件有没有处理由用户决定
ConnectedCallBack _connected_cb;
MessageCallBack _message_cb;
CloseCallBack _close_cb;
AnyCallBack _any_cb;
CloseCallBack _server_closed_cb;//从服务器组件内移除链接管理信息
private:
//保证在EventLoop线程中执行
void SendInLoop( Buffer& buf)
{
if(_statu == DISCONNECTED) return;
//1.将数据放到缓冲区
_out_buffer.WriteBufferPush(buf);
//2.启动可写事件监控
if(_channel.WriteAble() == false) _channel.EnableWrite();
}
void ShutDownInLoop()
{
_statu = DISCONNECTING;
//细节:在这里设置完DISCONNECTING之后,如果后续发送缓冲区还有数据开启写监控调用回调HandlerWrite发现
//是DISCONNECTING就会调用ReleaseInLoop()
//1.设置待关闭状态
//2.判断接收缓冲区是否还有数据要处理
if(_in_buffer.ReadAbleSize() > 0)
{
if(_message_cb) _message_cb(shared_from_this(),&_in_buffer);
}
//3.判断发送缓冲区是否还有数据
if(_out_buffer.ReadAbleSize() > 0)
{
if(_channel.WriteAble() == false)//开启写监控就会调用HandlerWrite关闭链接
_channel.EnableWrite();
// return;
}
//没有直接关闭
if(_out_buffer.ReadAbleSize() == 0)
Release();
}
void EnableInactiveReleaseInLoop(int sec)
{
//1.设置标记位
_enable_inactive_release = true;
//2.延迟/添加定时任务
if(_loop->HasTimer(_conn_id))
return _loop->RefreshTask(_conn_id);
_loop->TimerAdd(_conn_id,sec,std::bind(&Conncetion::Release,this));
}
void CancelInactiveReleaseInLoop()
{
//1.切换标记位
_enable_inactive_release = false;
//2.取消定时任务
if(_loop->HasTimer(_conn_id))
_loop->CancelTask(_conn_id);
}
void UpgradeInLoop(const Any& context,const ConnectedCallBack& conn,const MessageCallBack&
msg,const CloseCallBack& closed,const AnyCallBack& event)
{
_context = context;
_connected_cb = conn;
_message_cb = msg;
_close_cb = closed;
_any_cb = event;
}
void ReleaseInLoop()//实际释放接口
{
//1.修改状态
_statu = DISCONNECTED;
//2.移除事件监控
_channel.Remove();
//3.关闭文件描述符
_socket.Close();
//4.看是否还存在定时任务需要移除
if(_loop->HasTimer(_conn_id))
CancelInactiveReleaseInLoop();
//5.调用回调:注意为避免因移除服务器管理而导致连接释放而出错,因此先调用用户的
if(_close_cb) _close_cb(shared_from_this());
if(_server_closed_cb) _server_closed_cb(shared_from_this());
}
void EstablishedInLoop()//连接获取之后进行设置
{
assert(_statu == CONNECTING);
//1.修改连接状态
_statu = CONNECTED;
//2.开启读事件监控:开启之后可能立即会有事件触发,如果此时启动了非活跃链接销毁,就会刷新活跃延迟销毁任务执行
//因此启动读事件监控应该在设置非活跃链接是否销毁之后进行
_channel.EnableRead();
//3.调用用户设置的回调
if(_connected_cb)
{
_connected_cb(shared_from_this());
// DBG_LOG("after coonnected_cb");
}
}
public:
Conncetion(EventLoop* loop,uint64_t conn_id,int sockfd)
:_conn_id(conn_id),_sockfd(sockfd),_enable_inactive_release(false),_statu(CONNECTING),
_socket(sockfd),_loop(loop),_channel(loop,sockfd)
{
_channel.SetReadCallBack(std::bind(&Conncetion::HandlerRead,this));
_channel.SetWriteCallBack(std::bind(&Conncetion::HandlerWrite,this));
_channel.SetCloseCallBack(std::bind(&Conncetion::HandlerClose,this));
_channel.SetErrorCallBack(std::bind(&Conncetion::HandlerError,this));
_channel.SetAnyCallBack(std::bind(&Conncetion::HandlerAnyEvent,this));
}
~Conncetion()
{
DBG_LOG("RELEASE CONNECTION:%p",this);
}
void Send(const char* data,size_t len)//发送数据,将数据发送到缓冲区,启动写事件监控
{
//bug:可能外界传入的data是个临时空间,我们现在只是把发送操作传入任务池,有可能并没被立即执行
//因此有可能执行的时候,data指向的空间有可能已经被释放
Buffer buf;
buf.WriteAndPush(data,len);
_loop->RunInLoop(std::bind(&Conncetion::SendInLoop,this,std::move(buf))); //使用move
}
void ShutDown()//提供给组件使用者的关闭接口--并不是实际关闭还需要判断是否有数据待处理
{_loop->RunInLoop(std::bind(&Conncetion::ShutDownInLoop,this));}
void EnableInactiveRelease(int sec)//启动非活跃销毁,并定义多长时间无通信是非活跃,添加定时任务
{_loop->RunInLoop(std::bind(&Conncetion::EnableInactiveReleaseInLoop,this,sec));}
void CancelInactiveRelease()//取消非活跃销毁
{_loop->RunInLoop(std::bind(&Conncetion::CancelInactiveReleaseInLoop,this));}
//切换协议 -- 重置上下文以及阶段性处理函数
void Upgrade(const Any& context,const ConnectedCallBack& conn,const MessageCallBack&
msg,const CloseCallBack& closed,const AnyCallBack& event)
{
//保证必须在eventLoop()线程中执行
_loop->AssertInLoop();
//应该立即执行不能压入队列:能走到这里说明就是同线程,就会直接执行任务
_loop->RunInLoop(std::bind(&Conncetion::UpgradeInLoop,this,context,
conn,msg,closed,event));
}
void Release()
{
_loop->QueueInLoop(std::bind(&Conncetion::ReleaseInLoop,this));
}
void Established()
{_loop->RunInLoop(std::bind(&Conncetion::EstablishedInLoop,this));}
public:
int Fd()//获取链接管理的文件描述符
{return _sockfd;}
int Id()//获取链接ID
{return _conn_id;}
bool Connected()//是否处于Connected状态
{return _statu == CONNECTED;}
void SetContext(const Any& context)//设置上下文
{_context = context;}
Any* GetContext()//获取上下文,返回指针
{return &_context;}
void SetConnectedCallBack(const ConnectedCallBack& cb)
{_connected_cb = cb;}
void SetMessageCallBack(const MessageCallBack& cb)
{_message_cb = cb;}
void SetCloseCallBack(const CloseCallBack& cb)
{_close_cb = cb;}
void SetAnyCallBack(const AnyCallBack& cb)
{_any_cb = cb;}
void SetSvrClosedCallBack(const CloseCallBack& cb)
{_server_closed_cb = cb;}
public: //五个Channel事件回调
void HandlerRead()//描述符可读事件触发的回调,接收socket数据放到接收缓冲区再调用message_cb
{
//1.非阻塞接收socket数据,放到缓冲区
char buf[65536];
int ret = _socket.NonBlockRecv(buf,65535);
if(ret < 0)
{
// DBG_LOG("ret < 0");
//返回-1表示连接断开:出错不能直接关闭还要看缓冲区是否有数据要处理
return ShutDownInLoop();
}
buf[ret] = 0;
// DBG_LOG("%s",buf);
//返回0表示未读取到数据,并不是连接断开
//读取数据到缓冲区,并且移动写偏移
_in_buffer.WriteAndPush(buf,ret);
//2.调用用户message_cb
if(_in_buffer.ReadAbleSize() > 0)
{
// DBG_LOG("message_cb");
_message_cb(shared_from_this(),&_in_buffer);
return;
}
}
void HandlerWrite()//描述符可写事件触发的回调,将发送缓冲区中数据进行发送
{
//1.将输出缓冲区中数据发送
ssize_t ret = _socket.NonBlockSend(_out_buffer.ReadPos(),_out_buffer.ReadAbleSize());
if(ret < 0) //发送错误关闭连接
{ //关闭连接之前查看接收缓冲区是否有数据要处理(出错也就不能发,查看发送缓冲区就没必要了)
if(_in_buffer.ReadAbleSize() > 0)
_message_cb(shared_from_this(),&_in_buffer);
return Release();//直接关闭释放
}
//2.不要忘记移动读偏移(因为我们已经发送了)
_out_buffer.MoveReadIdx(ret);
if(_out_buffer.ReadAbleSize() == 0)
{
_channel.DisableWrite();//关闭写监控
//如果当前连接时待关闭,有数据则处理数据(前面部分代码处理),没有(或处理完了)则释放连接
if(_statu == DISCONNECTING)
return Release();
}
}
void HandlerClose()//触发挂断事件
{
//输入缓冲区有数据就处理
if(_in_buffer.ReadAbleSize() > 0)
_message_cb(shared_from_this(),&_in_buffer);
return Release();
}
void HandlerError()//触发出错事件
{
HandlerClose();
}
void HandlerAnyEvent()//触发任意事件
{
//1.刷新活跃度
if(_enable_inactive_release)
_loop->RefreshTask(_conn_id);
//2.调用用户组件者设置的任意事件回调
if(_any_cb) _any_cb(shared_from_this());
}
};
说明:
1. 对于切换协议的函数这个接口必须在EventLoop()线程中立即执行,因为我们需要防备新事件触发后,处理的时候,切换任务还没有被执行,而导致数据使用原协议处理了。所以这个任务必须保证在eventLoop线程中执行,不能压入队列要立即执行。
2. 除了CloseCallBack,我们还需要设置一个组件内的连接关闭回调,这是在组件内设置的,因为未来服务器组件内会把所有连接管理起来,一旦某个连接关闭就应该从管理的地方移除自己的信息。
3. 注意shutdownInLoop()不是一个实际关闭连接的接口,而是一个判断接口,要判断发送缓冲区有没有数据待处理,有就处理,处理完再调用真正的连接实际释放接口ReleaseInLoop()。
4. 连接获取之后我们需要对连接进行各种设置,比如设置channel事件监控,启动读事件监控等,因此我们还需要设置一个EstablishedInLoop()
Connection内部函数调用关系:

Acceptor模块
该模块其实就是对监听套接字进行管理,开启对监听套接字的读事件监控,当新链接到来需要对新链接创建Connection对象管理,然后为Connection对象设置回调、初始化、开启事件监控等。
我们需要注意的是,必须在新链接获取回调设置好再启动监听套接字的读事件监控,否则可能造成启动监控之后立即有事件发生,但是回调还没有设置好,新链接得不到处理而造成内存泄漏。
class Acceptor
{
private:
Socket _socket;//监听套接字
EventLoop* _loop;//对监听套接字进行事件监控
Channel _channel;//监听套接字事件管理
using AcceptCallBack = std::function;
AcceptCallBack _accept_cb;
private:
void HandlerRead() //可读事件回调 --- 获取新连接,调用accept_cb进行新连接处理
{
//1.获取新连接
int newfd = _socket.Accept();
if(newfd < 0) return;
//2.调用获取新连接回调
if(_accept_cb) _accept_cb(newfd);
}
int CreateServer(int port)
{
bool ret = _socket.CreateServer(port);
assert(ret == true);
return _socket.Fd();
}
public:
int LstFd()
{return _socket.Fd();}
Acceptor(EventLoop* loop,int port)
:_socket(CreateServer(port)),_loop(loop),_channel(loop,_socket.Fd())
{
_channel.SetReadCallBack(std::bind(&Acceptor::HandlerRead,this));
}
void Listen()
{
_channel.EnableRead();
}
void SetAcceptCallBack(const AcceptCallBack& cb)
{_accept_cb = cb;}
};
LoopThread模块
EventLoop是与线程一一对应的,因此这个模块内部肯定要有一个线程然后和EventLoop绑定。EventLoop模块在实例化对象的时候,必须在线程内部,不能在LoopThread这个类构造时初始化(否则实例化构造的线程id不是自己对应的线程,而是主线程),如果我们先创建EventLoop对象再创建线程,将线程id重新给EventLoop进行设置,,这样存在的问题是在构造EventLoop对象到设置新的thread_id期间将是不可控的(构造函数里记录的thread_id是旧线程的id,后面把成员变量改成新线程的id,这两次赋值之间,如果任何代码检查了AssertInLoop就会得到错误结论),因此我们必须先创建线程,然后在线程的入口函数中去实例化EventLoop对象。
将来我们是需要通过LoopThread获取EventLoop对象来对描述符进行事件监控的,因此我们需要互斥锁&&条件变量保证同步,用于实现EventLoop获取的同步互斥关心,避免EventLoop还没实例化之前去获取EventLoop对象指针。
//每个线程对应一个eventLoop
class LoopThread
{
private:
//互斥锁&&条件变量:保证获取EventLoop对象指针的同步关系,防止获取时还没实例化
std::mutex _mtx;
std::condition_variable _cv;
EventLoop* _loop; //EventLoop对象对应指针,需要在线程入口函数初始化
std::thread _thread;//EventLoop对象对应线程
private:
void ThreadEntry()
{
//不new对象的原因是想,EventLoop生命周期随线程
EventLoop loop;
{
unique_lock lck(_mtx);
_loop = &loop;
_cv.notify_all();//唤醒所有在条件变量上等的线程
}
// while(1)
// {
loop.Start();
// }
}
public:
LoopThread()
:_loop(nullptr),_thread(thread(&LoopThread::ThreadEntry,this))
{}
EventLoop* GetLoop()
{
EventLoop* loop = NULL;
{
unique_lock lck(_mtx);
_cv.wait(lck,[&](){
return _loop != NULL;
});//在条件变量上等EventLoop实例化
loop = _loop;
}
return loop;
}
int Eventfd()
{
{
unique_lock lck(_mtx);
_cv.wait(lck,[&](){
return _loop != NULL;
});//在条件变量上等EventLoop实例化
}
return _loop->EventFd();
}
};
有了LoopThread模块,未来Channel模块怎么和EventLoop模块关联呢,或者说Channel模块怎么进行事件监控?在获取新链接之后,创建新的Connection对象,然后通过LoopThread内部的GetLoop获取EventLoop对象指针,传递给Connection内部的channel,后面Connection开启事件监控的时候,内部channel设置好监控事件之后,就可以通过Loop指针调用Update进行事件监控,至此两个模块就关联起来了。
LoopThreadPool模块
该模块其实就是设计一个线程池,对所有的LoopThread进行管理和分配,one thread one Loop其实就是连接从这个线程池中选择一个loop进行事件监控。
该模块主要有以下功能:
1. 线程数量可配置:在服务器中,主从Reactor模型是主线程只负责新连接获取,从线程新连接的事件监控及处理,而当前线程池也有可能从属线程数量为0,也就是实现单Reactor服务器,一个线程既负责连接获取还负责连接的事件监控处理,要什么模型由用户灵活配置。
2. 对所有线程进行管理
3. 提供线程分配功能:当主线程获取新连接,需要将新连接挂到从线程进行事件监控看,假设有0个从属线程,则直接分配给主线程的EventLoop;假设有多个从线程,则采用RR轮转进行线程分配,将对应线程eventLoop返回设置给对应Connection。
class LoopThreadPool
{
private:
int _thread_count;//从属线程数量
int _next_loopix; //分配eventLoop对象
EventLoop* _base_loop;//主Reactor线程
vector _threads;//管理LoopThread线程
vector _loops;//管理EventLoop对象
public:
LoopThreadPool(EventLoop* base_loop)
:_base_loop(base_loop),_thread_count(0),_next_loopix(0)
{}
void SetThreadCount(int count)//设置从属线程数量
{_thread_count = count;}
void Create()//线程池初始化
{
if(_thread_count > 0)
{
_threads.resize(_thread_count);
_loops.resize(_thread_count);
for(int i = 0 ; i < _thread_count ; i++)
{
_threads[i] = new LoopThread();
_loops[i] = _threads[i]->GetLoop();
//细节:在线程入口函数内部实例化EventLoop对象之前GetLoop会阻塞
}
}
}
EventLoop* NextLoop()//给链接分配EventLoop对象进行事件监控
{
if(_thread_count == 0)
return _base_loop;
_next_loopix = (_next_loopix+1) % _thread_count;
return _loops[_next_loopix];
}
};
TcpServer模块
这个模块是对所有模块的整合,通过TcpServer模块实例化的对象,可以非常简单的完成一个服务器搭建。
管理成员:
Acceptor对象:创建一个监听套接字(需要端口信息)EventLoop对象:base_loop,实现对监听套接字的事件监控。vector<PtrConnection> conns:实现对所有新建连接的管理。LoopThreadPool:创建Loop线程池,对新建连接进行事件监控及处理(将新建连接挂到从属EventLoop线程进行事件监控)
功能:
- 设置从属线程池数量(内部创建
Acceptor,base_loop,线程池,但由用户控制线程池线程数量) - 启动服务器:线程池初始化,
acceptor监听,base_loop开启start等(也可放到构造函数) - 设置各种回调函数(连接建立完成回调,消息回调,连接关闭回调,任意回调),这是用户设置给
TcpServer,TcpServer设置给获取的新连接 - 是否启动非活跃连接超时销毁功能(用户设置超时时间)
- 添加定时任务功能
工作流程:
- 在
TcpServer中实例化一个Acceptor对象 , 以及一个EventLoop对象(base_loop) - 将
Acceptor挂到base_loop上进行事件监控(设置好回调再监控!) - 一旦
Acceptor对象就绪可读事件,则执行可读事件回调获取新链接 - Acceptor可读回调对新链接创建一个
Connection进行管理 - 对链接对应的
Connection设置功能回调(链接完成回调,消息回调,关闭回调,任意回调) - 启动
Connection的非活跃链接的超时销毁。 - 将新连接对应的
Connection挂到LoopThread_pool中的从属线程对应的EventLoop中进行事件监控。 - 一旦
Connection对应连接就绪可读事件,则此时执行可读事件回调,读取数据,读取完毕,调用TcpServer设置的消息回调进行业务处理
由于LoopThreadPool的从属线程数量由用户自己指定,因此我们不能在TcpServer构造的时候设置线程数量,需要用户自己显式指定,用户指定完之后,调用Start()接口,内部根据实际线程数量初始化线程池,然后再进行监控。
//TcpServer模块将前面的模块整合
class TcpServer
{
private:
int _next_id; //一个自增长的链接id/定时任务id
int _port;
EventLoop _baseloop; //主Reactor线程
Acceptor _acceptor; //监听套接字
LoopThreadPool _pool;//eventLoop线程池
unordered_map _conns;//保存管理的所有链接shared_Ptr对象
int _timeout;//非活跃链接的超时时间
bool _enable_inactive_release;//表示是否开启非活跃链接销毁
//各种回调
using Functor = function;
using ConnectedCallBack = std::function;//连接建立成功后如何处理
using MessageCallBack = std::function;//连接收到数据如何处理
using CloseCallBack = std::function;//连接关闭前如何处理
using AnyCallBack = std::function;//任意事件有没有处理由用户决定
ConnectedCallBack _connectd_cb;
MessageCallBack _message_cb;
CloseCallBack _close_cb;
AnyCallBack _any_cb;
public:
TcpServer(int port)
:_next_id(0),_port(port),_acceptor(&_baseloop,port),_pool(&_baseloop),
_enable_inactive_release(false)
{
_acceptor.SetAcceptCallBack(std::bind(&TcpServer::NewConnection,this,std::placeholders::_1));
_acceptor.Listen();
}
void Start()//服务器启动
{
//1.初始化线程池
_pool.Create();
//2.开启主线程的事件监控
_baseloop.Start();
}
//回调函数设置
void SetConnectedCallBack(const ConnectedCallBack& cb)
{_connectd_cb = cb;}
void SetMessageCallBack(const MessageCallBack& cb)
{_message_cb = cb;}
void SetCloseCallBack(const CloseCallBack& cb)
{_close_cb = cb;}
void SetAnyCallBack(const AnyCallBack& cb)
{_any_cb = cb;}
//添加定时任务
void RunAfter(const Functor& task,int delay)
{
_baseloop.RunInLoop(std::bind(&TcpServer::RunAfterInLoop,this,task,delay));
}
//启动非活跃超时链接销毁
void EnableInactiveRelease(int timeout)
{
_timeout = timeout;
_enable_inactive_release = true;
}
//用户设置线程池中线程数量
void SetThreadCount(int count){_pool.SetThreadCount(count);}
private:
//监听套接字Acceptror获取新链接之后的回调(给获取的链接创建Connection对象,设置事件回调,分配eventLoop对象监控,开启事件监控等)
void NewConnection(int fd)
{
_next_id++;//自增链接id
//分配线程池的线程给新链接,即分配eventLoop进行事件监控
PtrConnection conn(new Conncetion(_pool.NextLoop(),_next_id,fd));
cout << "gain a new link" << endl;
conn->SetMessageCallBack(_message_cb);
conn->SetConnectedCallBack(_connectd_cb);
conn->SetCloseCallBack(_close_cb);
conn->SetAnyCallBack(_any_cb);
conn->SetSvrClosedCallBack(std::bind(&TcpServer::RemoveConnection,this,std::placeholders::_1));
if(_enable_inactive_release) conn->EnableInactiveRelease(_timeout); //启动-非活跃链接销毁
//先启动非活跃连接销毁再启动读监控
conn->Established();//就绪初始化:设置回调 启动读监控
_conns.insert(std::make_pair(_next_id,conn));//放进服务器组件管理
}
//移除链接的回调(给服务器组件设置的移除链接的回调,不从这删除,链接永远不会释放)
void RemoveConnection(const PtrConnection& conn)
{
_baseloop.RunInLoop(std::bind(&TcpServer::RemoveConnectionInLoop,this,conn));
}
//保证线程安全
void RunAfterInLoop(const Functor& task,int delay)
{
_next_id++;
_baseloop.TimerAdd(_next_id,delay,task);
}
void RemoveConnectionInLoop(const PtrConnection& conn)
{
int id = conn->Id();
auto it = _conns.find(id);
if(it != _conns.end())
_conns.erase(it);
}
};
基于TcpServer实现EchoServer
在封装完TcpServer模块之后,实现一个回显服务器其实就很简单了,其实就是基于TcpServer服务区修改下收到数据的业务处理,即在messageCallback中进行数据的回显。
#include"server.hpp"
class EchoServer
{
private:
TcpServer _server;
public:
EchoServer(int port,int count,bool enable_inactive_release,int timeout)
:_server(port)
{
//设置线程数量和是否启动非活跃链接超时销毁
_server.SetThreadCount(count);
if(enable_inactive_release)
_server.EnableInactiveRelease(timeout);
//设置回调
_server.SetMessageCallBack(std::bind(&EchoServer::OnMessage,this,std::placeholders::_1,std::placeholders::_2));
_server.SetCloseCallBack(std::bind(&EchoServer::OnClosed,this,std::placeholders::_1));
_server.SetConnectedCallBack(std::bind(&EchoServer::OnConnected,this,std::placeholders::_1));
}
void Start()
{
_server.Start();
}
private:
void OnConnected(const PtrConnection& conn)
{
DBG_LOG("NEW Connection:%p",conn.get());
}
void OnClosed(const PtrConnection& conn)
{
DBG_LOG("Close Connection:%p",conn.get());
}
void OnMessage(const PtrConnection& conn,Buffer* buf)
{
conn->Send(buf->ReadPos(),buf->ReadAbleSize());
buf->MoveReadIdx(buf->ReadAbleSize());
// conn->ShutDown();
}
};
模块关系图:

浙公网安备 33010602011771号