设计思路
具体来说每一个套接字都会对应一个 Channel 对象,用于对它的事件进行管理。可以对于描述符的监控事件在用户态更容易维护,以及触发事件后的操作流程更加的清晰
Channel模块是用于对一个描述符所需要监控的事件以及事件触发之后要执行的回调函数进行管理的
一个连接触发的事件有哪些?
- 基本事件:EPOLLIN(可读)、EPOLLOUT(可写)。
- 异常事件:EPOLLHUP(挂起)、EPOLLERR(错误)。
- 特殊事件:EPOLLPRI(优先数据)、EPOLLRDHUP(半关闭)。
- 模式修饰:EPOLLET(边缘触发,需配合其他事件)。
对于EPOLLHUP(挂起)EPOLLPRI(优先数据)、EPOLLRDHUP(半关闭)。大家可能见得少
下面我来详细讲解下
EPOLLRDHUP:读关闭,接收缓冲区不会再有新数据到来了,内核不会再向接收缓冲区写数据,但是已经存在的数据还是可以被读取的
读关闭其实就是收到了对方的FIN请求,此时我们进入 CLOSE_WAIT 状态。 对方不会再发送数据给我们了,我们的做法应该是尽快将缓冲区中的数据读取并处理,然后将结果返回,不管这一轮处理之后是否还有不完整的报文,只要我们把发送缓冲区的数据发送完,我们就关闭连接。
EPOLLHUP:读写都关闭。意味着我们不会再收到新数据,同时无法再向内核缓冲区中写入数据或者说无法在将数据发送到对端。 这可能是因为网络等原因,多次重发报文不可达时引起的状态。此时可以理解为连接已经关闭,但是我们还是需要手动关闭文件描述符
当该事件触发时,我们的做法是关闭连接。因为就算还有数据待处理,我们的应答也无法发送到对端了,这时候连接已经无意义了,所以我们直接关闭。
EPOLLPRI:表示文件描述符上有优先或紧急数据(如 TCP 带外数据)可读,不影响普通数据流。
我们可以把 EPOLLRDHUP 看成是 读事件,触发之后调用读方法,但是需要尽快关闭连接。
而 EPOLLHUP 是挂断事件,他的做法其实就跟错误事件的做法类似,直接关闭连接。
一般情况下我们也无需太关心EPOLLRDHUP,就把它当读事件就行了,因为触发EPOLLRDHUP的时候,有两种情况,一种是没有触发读事件,那么说明没有新数据到来,那么缓冲区此时没有一个完整报文,我们只需要把输出缓冲区的数据发出去就可以直接关闭连接了。 另一种是读事件先触发,然后触发EPOLLRDHUP,这时候,由于读事件触发,我们会调用读事件的回调方法将缓冲区的所有完整报文都处理完,所以此时我们也是一样的,把输出缓冲区的数据全部发送出去就可以关闭连接了。
最后,我们还需要一个任意事件的回调方法。因为我们可能会启动超时连接的销毁机制,那么任意事件触发我们都需要刷新定时任务。同时,用户也可能会设置任意事件回调,那么任意事件到来时,我们也需要执行用户设置的任意回调(这一点我们在Channel模块不考虑,这是Connection模块该考虑的,Channel的回调方法全部都是Connection设置进来的,同时被Connection管理)。
在 epoll 模型中,我们对一个描述符的事件管理是使用一个 uint32_t events 来管理的,后续事件就绪时,我们也是通过一个 uint32_t revents 来获取出来的,所以我们的Channel模块中除了五个回调函数,还需要两个 uint32_t的变量。 一个是监控的事件 ,一个是就绪的事件
我们需要设置的功能性接口:
- 判断读/写事件是否已经监控
- 开启读/写事件监控
- 关闭读/写事件监控
- 关闭所有事件监控
- 移除监控
- 以及设置五个回调方法的接口
这个模块就好比是邮局里面有:快递盒子(channel),物品(fd),信息单(各种事件)。用户沟通(回调函数)
包括的步骤:
检查快递盒贴上物品信息了吗?(是否启动了读事件/写事件...)
查看信息单知道某个快递何时派发,需要怎么发送(启动读/写等事件监控)
快递已经被取走(关闭事件监控)
用户取消了某个快递派发(移除监控)
用户说发了这个快递请给我回个电话(设置事件回调)
注意:关闭所有事件监控是指在 epoll 模型中将所有的事件监控取消,不再监控。但是我们的文件描述符还仍在epoll模型的红黑树节点中!! 而移除监控则是要在epoll模型中移除我们的节点,在移除节点指点,所以要先取消所有事件监控,然后进行移除监控
还有需要注意的是,其实移除所有事件监控并不是真的就不监控任何事件了,我们的文件描述符在epoll中,默认是自动开启 EPOLLERR ,ERPOLLHUP 这两个事件的监控的,但是EPOLLRDHUP需要我们自己进行监控。
接下来还需要一个接口用于执行就绪事件,当我们要监控的事件就绪了,肯定要让去做别的事情。也就是按照一定的逻辑来调用回调方法。
同时,未来我们获取到就绪的事件是在EventLoop 模块中获取,那么我们也需要一个接口用于设置就绪事件。
而我们也需要一个私有接口,因为我们的Channel模块只是对事件进行管理,真正进行监控的还是Poller模块以及EventLoop模块,我们未来会通过调用EventLoop模块的接口来对EventLoop模块中的Poller模块内的epoll模型中监控的事件做调整。
我们所谓的启动和取消事件监控,其实就是对 epoll 模型中的文件描述符所监控的事件作更新,所以对事件的修改只需要一个接口就行了,因为未来调用的都是 epoll_ctl 这个接口。
类的设计
class Channel
{
private:
using EventCallBack = std::function<void()>; // 回调函数别名
int fd; // 文件描述符
uint32_t _event; // 监控的事件
uint32_t _revents; // 就绪的事件
EventCallBack _read_cb; // 读事件回调
EventCallBack _write_cb; // 写事件回调
EventCallBack _error_cb; // 错误事件回调
EventCallBack _close_cb; // 挂断事件回调
EventCallBack _event_cb; // 任意事件回调
private:
void UpdateEvents();
public:
bool HasRead(); // 判断是否启动了读事件
bool HasWrite(); // 判断是否启动了写事件
void EnableRead(); // 启动读事件监控
void EnableWrite(); // 启动写事件监控
void DisableRead(); // 取消读事件监控
void DisableWrite(); // 取消写事件监控
void DisableAll(); // 取消所有事件监控
void SetRevents(); // 设置就绪事件
void HandlerEvents(); // 处理就绪事件
void SetReadCallBack(); // 设置读事件回调
void SetWriteCallBack(); // 设置写事件回调
void SetErrorCallBack(); // 设置错误事件回调
void SetClosedCallBack(); // 设置挂断事件回调
void SetEventCallBack(); // 设置任意事件回调
void Remove(); // 移除监控
};
当然,后续我们实现了EventLoop模块之后,我们会在Channel模块中再添加一个成员,就是 EventLoop* loop ,因为我们对事件的实际操作是需要通过 EventLoop 的 接口来完成的。
模块实现
私有接口
//私有接口,用于真正和 Poller 模块和 EventLoop 模块联动,进行事件监控的调整
void UpdateEvents() //op 就是未来传递给 epoll_ctl 的op参数
{
//后续调用EventLoop提供的接口
//_loop->UpdateEvents(this);
}
简单接口
public:
bool HasRead() // 判断是否启动了读事件
{
return _events & EPOLLIN;
}
bool HasWrite() // 判断是否启动了写事件
{
return _events & EPOLLOUT;
}
void EnableRead() // 启动读事件监控
{
_events = _events | EPOLLIN;
}
void EnableWrite() // 启动写事件监控
{
_events = _events | EPOLLOUT;
}
void DisableRead() // 关闭读事件监控
{
_events = _events & (~EPOLLIN);
}
void DisableWrite() // 关闭写事件监控
{
_events = _events & (~EPOLLOUT);
}
void DisableAll() // 关闭所有事件监控
{
_events = 0;
//调用EventLoop里面封装的poller模块函数
}
void SetRevents(uint32_t revents) // 设置就绪事件
{
_revents = revents;
}
void Remove()
{
//调用EventLoop里面封装的poller模块函数
} // 移除监控
};
复杂接口
稍微复杂一点的接口就是处理就绪事件的接口。在处理就绪事件的时候,我们需要判断哪些事件就绪了。 同时,我们要注意一个点
读事件中间出错,我们是不会直接关闭连接的,因为可能对端不让读了,但是我们还能写入
例如一个服务器收到客户端的请求数据(触发 EPOLLIN
),但读取时出错(例如客户端突然断开)。服务器可能还有响应数据(如 HTTP 响应)未发送,不能直接关闭连接,而是应尝试发送完数据。
但是如果写事件出错,那么我们是直接关闭连接了,因为写失败通常意味着连接已不可用(例如对端已关闭或网络中断),继续尝试发送数据没有意义,因为数据无法到达对端。
服务器尝试向客户端发送响应数据(触发 EPOLLOUT
),但写操作失败(例如客户端已断开)。此时继续读取或处理数据无意义,直接关闭连接是合理的选择。
而如果挂断事件和错误事件,我们也是关闭连接。
而如果发生了挂断事件或者错误事件,那么一定会在写事件中体现出来,但是关闭连接的操作我们只需要进行一次,所以我们在处理事件的时候,写事件,挂断事件和错误事件只需要处理一种就行了,如果连接有问题,那么在任意一个事件的处理中就会关闭连接。
void HandlerEvents() // 处理就绪事件
{
if((_revents & EPOLLIN) | (_revents & EPOLLHUP) | (_revents & EPOLLPRI))
{
if(_read_callback)
_read_callback();
if(_event_callback)
_event_callback();
}
if((_revents & EPOLLOUT))
{
if(_write_callback)
_write_callback();
if(_event_callback)
_event_callback();
}
else if((_revents & EPOLLERR))
{
if(_error_callback)
_error_callback();
}
else if((_revents & EPOLLHUP))
{
if(_close_callback)
_close_callback();
}
}
回调函数接口
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 SetClosedCallBack(const EventCallBack &cb) // 设置挂断事件回调
{
_close_callback = cb;
}
void SetEventCallBack(const EventCallBack &cb) // 设置任意事件回调
{
_event_callback = cb;
}
疑惑点
using EventCallBack = std::function<void()>;前面的using是干嘛的?
using EventCallBack = std::function<void()>;
不是已经取消事件了嘛?为什么还要移除事件呢?
_events & EPOLLIN这个是咋计算的
都有才为真
_events |= EPOLLOUT怎么计算的?
有就为真