目录
设计思想
实现
设计思想
Channel模块是用于对一个描述符所需要监控的事件以及事件触发之后要执行的回调函数进行管理的
具体来说,它里面会保存该文件描述符所监控的事件,该文件描述符所就绪的事件,以及该描述符的各种事件的处理回调。
每一个套接字都会对应一个 Channel 对象,用于对他的事件进行管理。
那么一个连接可能会触发的事件有哪些呢? 可读事件(EPOLLIN,EPOLLPRI),可写事件(EPOLLOUT),挂断事件,错误事件(EPOLLERR,连接出错)
有两个事件我们会有点不理解,就是EPOLLHUP 和 EPOLLRDHUP
EPOLLRDHUP,读关闭,接收缓冲区不会再有新数据到来了,内核不会再向接收缓冲区写数据,但是已经存在的数据还是可以被读取的
读关闭其实就是收到了对方的FIN请求,此时我们进入 CLOSE_WAIT 状态。 那么此时对方不会再发送数据给我们了,那么此时我们的做法应该是尽快将缓冲区中的数据读取并处理,然后将结果返回,不管这一轮处理之后是否还有不完整的报文,只要我们把发送缓冲区的数据发送完,我们就关闭连接。
EPOLLHUP,读写都关闭。意味着我们不会再收到新数据,同时无法再向内核缓冲区中写入数据或者说无法在将数据发送到对端。 这可能是因为网络等原因,多次重发报文不可达时引起的状态。此时可以理解为连接已经关闭,但是我们还是需要手动关闭文件描述符
那么当该事件触发时,我们的做法是关闭连接。因为就算还有数据待处理,我们的应答也无法发送到对端了,这时候连接已经无意义了,所以我们直接关闭。
那么,我们可以把 EPOLLRDHUP看成是读事件,触发之后调用读方法,但是需要尽快关闭连接。
而EPOLLHUP是挂断事件,他的做法其实就跟错误事件的做法类似,直接关闭连接。
当然其实一般情况下我们也无需太过关心EPOLLRDHUP,就把他当读事件就行了,因为触发 EPOLLRDHUP的时候,有两种情况,一种是没有触发读事件,那么说明没有新数据到来,那么缓冲区此时没有一个完整报文,我们只需要把输出缓冲区的数据发出去就可以直接关闭连接了。 另一种是读事件先触发,然后触发EPOLLRDHUP,这时候,由于读事件触发,我们会调用读事件的回调方法将缓冲区的所有完整报文都处理完,所以此时我们也是一样的,把输出缓冲区的数据全部发送出去就可以关闭连接了。
最后,我们还需要一个任意事件的回调方法。因为我们可能会启动超时连接的销毁机制,那么任意事件触发我们都需要刷新定时任务。同时,用户也可能会设置任意事件回调,那么任意事件到来时,我们也需要执行用户设置的任意回调(当然这一点我们在Channel模块不考虑,这是Connection模块该考虑的,Channel的回调方法全部都是Connection设置进来的,同时被Connection管理)。
而在 epoll 模型中,我们对一个描述符的事件管理是使用一个 uint32_t events 来管理的,后续事件就绪时,我们也是通过一个 uint32_t revents 来获取出来的,所以我们的Channel模块中除了五个回调函数,还需要两个 uint32_t的变量。
我们需要设置的功能性接口:
开启读事件监控,判断读事件是否已经监控,取消读事件监控,开启写事件监控,判断写事件是否已经监控,取消写事件监控,关闭所有事件监控,移除监控 。 以及设置五个回调方法的接口。
注意,关闭所有事件监控是指在 epoll 模型中将所有的事件监控取消,不再监控,但是我们的文件描述符还在epoll模型的红黑树节点中。 而移除监控则是要在epoll模型中移除我们的节点,在移除节点指点,记得要先取消所有事件监控。
还有需要注意的是,其实移除所有事件监控并不是真的就不监控任何事件了,我们的文件描述符在epoll中,默认是自动开启 EPOLLERR ,ERPOLLHUP , ERPOLLRDHUP 这三个事件的监控的。
还需要一个接口用于执行就绪事件,其实就是按照一定的逻辑来调用回调方法。
同时,未来我们获取到就绪的事件是在EventLoop 模块中获取,那么我们也需要一个接口用于设置就绪事件。
而我们也需要一个私有接口,因为我们的Channel模块只是对事件进行管理,真正进行监控的还是Poller模块以及EventLoop模块,我们未来会通过调用EventLoop模块的接口来对EventLoop模块中的Poller模块内的epoll模型中监控的事件做调整。
其实我们所谓的启动和取消事件监控,无非就是对 epoll 模型中的文件描述符所监控的事件作更新,所以对事件的修改只需要一个接口就行了,因为未来调用的都是 epoll_ctl 这个接口。
class Channel
{
private:
using EventCallBack = std::function<void()>;
int _fd; //文件描述符
uint32_t _events; //监控的事件
uint32_t _revents; //就绪的事件
EventCallBack _read_cb; //读事件回调
EventCallBack _write_cb; //写事件回调
EventCallBack _error_cb; //错误事件回调
EventCallBack _close_cb; //挂断事件回调 一般来说,挂断事件和错误事件的处理方式是一样的
EventCallBack _event_cb; //任意事件回调
//EventLoop* _loop //后续会添加的成员
private:
//私有接口,用于真正和 Poller 模块和 EventLoop 模块联动,进行事件监控的调整
void UpdateEvents();
public:
//启动读事件监控
void EnableRead();
//是否启动了读事件
bool HasRead();
//取消读事件监控
void DisableRead();
//启动写事件监控
void EnableWrite();
//是否启动了写事件
bool HasWrite();
//取消写事件监控
void DisableWwrite();
//取消所有事件监控
void DisableAll();
//移除监控
void Remove();
//设置就绪事件
void SetRevents();
//处理就绪事件
void HandlerEvents();
//设置读事件回调
void SetReadCallBack();
//设置写事件回调
void SetWriteCallBack();
//设置错误事件回调
void SetErrorCallBack();
//设置挂断事件回调
void SetCloseCallBack();
//设置任意事件回调
void SetEventCallBack();
};
当然,后续我们实现了EventLoop模块之后,我们会在Channel模块中再添加一个成员,就是 EventLoop* loop ,因为我们对事件的实际操作是需要通过 EventLoop 的 接口来完成的。
实现
//私有接口,用于真正和 Poller 模块和 EventLoop 模块联动,进行事件监控的调整
void UpdateEvents(int op) //op 就是未来传递给 epoll_ctl 的op参数
{
//后续调用EventLoop提供的接口
//_loop->UpdateEvents(_fd,op,_events);
}
简单接口实现
Channel(int fd):_fd(fd),_events(0),_revents(0){}
//启动读事件监控
void EnableRead()
{
if(HasRead()) return ;//说明已经监控了读事件了
_events|= EPOLLIN;
UpdateEvents(EPOLL_CTL_MOD);
}
//是否启动了读事件
bool HasRead()
{
return _events & EPOLLIN;
}
//取消读事件监控
void DisableRead()
{
if(!HasRead()) return ; //说明本来就没有监控读事件
_events &= (~EPOLLIN);
UpdateEvents(EPOLL_CTL_MOD);
}
//启动写事件监控
void EnableWrite()
{
if(HasWrite()) return ;
_events |= EPOLLOUT;
UpdateEvents(EPOLL_CTL_MOD);
}
//是否启动了写事件
bool HasWrite()
{
return _events & EPOLLOUT;
}
//取消写事件监控
void DisableWwrite()
{
if(!HasWrite()) return;
_events &= (~EPOLLOUT);
UpdateEvents(EPOLL_CTL_MOD);
}
//取消所有事件监控
void DisableAll(){_events = 0;}
//移除监控
void Remove(){UpdateEvents(EPOLL_CTL_DEL);}
//设置就绪事件
void SetRevents(uint32_t revents){_revents = revents;}
稍微复杂一点的接口就是处理就绪事件的接口。在处理就绪事件的时候,我们需要判断哪些事件就绪了。 同时,我们要注意一个点:
读事件中间出错,我们是不会直接关闭连接的,因为可能还有数据未发送出去。
但是如果写事件出错,那么我们是直接关闭连接了,因为后续就算再处理数据也没用了。
而如果挂断事件和错误事件,我们也是关闭连接。
而如果发生了挂断事件或者错误事件,那么一定会在写事件中体现出来,但是关闭连接的操作我们只需要进行一次,所以我们在处理事件的时候,写事件,挂断事件和错误事件只需要处理一种就行了,如果连接有问题,那么在任意一个事件的处理中就会关闭连接。
//处理就绪事件
void HandlerEvents()
{
//读事件需要处理
if(_revents & (EPOLLIN | EPOLLPRI | EPOLLRDHUP))
{
if(_read_cb) _read_cb();
if(_event_cb) _event_cb(); //任意事件触发都需要调用,防止事件
}
//剩下三个事件只需要处理其中一种
if(_revents & EPOLLOUT)
{
if(_event_cb) _event_cb(); //因为写事件可能会关闭连接,所以没办法,我们只能放在前面调用了
if(_write_cb) _write_cb();
}
else if(_revents & EPOLLHUP)
{
if(_event_cb) _event_cb();
if(_close_cb) _close_cb();
}
else if(_revents & EPOLLERR)
{
if(_event_cb) _event_cb();
if(_close_cb) _close_cb();
}
return;
}
剩下四个接口就是最简单的设置回调函数的接口了。
//设置读事件回调
void SetReadCallBack(const EventCallBack& cb){ _read_cb = cb;}
//设置写事件回调
void SetWriteCallBack(const EventCallBack& cb){ _write_cb = cb;}
//设置错误事件回调
void SetErrorCallBack(const EventCallBack& cb){ _error_cb = cb;}
//设置挂断事件回调
void SetCloseCallBack(const EventCallBack& cb){ _close_cb = cb;}
//设置任意事件回调
void SetEventCallBack(const EventCallBack& cb){ _event_cb = cb;}
那么Channel类就完成了,由于Channel类不好单独测试,所以这份代码我们只进行编译纠错,等到后续写完Poller模块我们再进行联合调试。