学习github上的项目 flamingo 自己的笔记。
flamingo原作者的csdn是: analogous_love
flamingo是多线程的,但是本人能力有限,只是单线程的还算能理解一点。
自己参照flamingo实现的基于epoll的单线程服务端,git地址:https://gitee.com/storm_zy/StServerFrame
实现了简单的echo功能,很多代码直接拷贝自flamingo。
一、框架结构
- 网络事件模块
EventLoop
- Channel
- EventModule
- TcpServer模块
TcpServer
- TcpConnections
- TcpSession
- Listener
- EventLoop
- main 函数
#define LISTEN_PORT 12345
CEventLoop g_mainLoop;
int main(int argc, char *argv[])
{
CEventModule *emd = nullptr;
emd = new CEpollModule(&g_mainLoop);
emd->Init();
g_mainLoop.setEventModule(emd);
CEchoServer *server = new CEchoServer();
server->Init(&g_mainLoop, "0.0.0.0", LISTEN_PORT);
server->Start();
g_mainLoop.loop();
return 0;
}
- CEventLoop::loop函数
void CEventLoop::loop()
{
m_stoped = false;
while (!m_stoped)
{
m_activeChannels.clear();
// 获取当前有事件的Channel
if (m_eventModule->GetActiveChannels(50, m_activeChannels) != ST_OK) {
}
// 处理有时间的Channel
for (CChannel *channel : m_activeChannels)
{
m_curActiveChannel = channel;
m_curActiveChannel->handleEvents();
}
m_curActiveChannel = nullptr;
// 做日常任务
if (runtimeCallBack_)
runtimeCallBack_(this);
}
}
二、执行流程
从上面的main函数可以看到,最终的执行是在loop中,而在loop中是调用了GetActiveChannels来获取
当前有事件发生的Channel列表,而后对Channel列表进行处理(通过调用 Channel::handleEvents)。
在GetActiveChannels中则是调用了epoll_wait系统调用进行对当前有事件的socket fd列表的获取。
-
首先来看一下fd的Channel是如何通过epoll_ctrl添加到epoll模块的关注列表中的。
从main函数中一行一行的开始走:
1> 首先创建全局的 EventLoop用于事件循环。
2> 创建EventModule模块并初始化,然后添加到EventLoop中进行绑定。
3> 创建EchoServer并用 EventLoop和监听ip和端口port进行初始化。
4> 开启服务。
看一下CEchoServer::Init(…)中都做了什么?void CEchoServer::Init(CEventLoop *loop, const std::string& ip, unsigned short port) { m_server = new CTcpServer(loop, ip, port); m_server->setConnectionCallBack(std::bind(&CEchoServer::OnNewConnection, this, std::placeholders::_1)); m_server->setDailyCleanUpCallBack(std::bind(&CEchoServer::HandleDailyResCleanUp, this)); }
绑定了两个回调,一个是绑定在TcpServer中新连接到来的回调,另一个是绑定到日常执行任务的回调。
我们主要看第一个,绑定新连接到来的回调。void CTcpServer::OnNewConnection(int fd) { CTcpConnection *conn = new CTcpConnection(m_loop, fd); if (!AddConnection(conn)) { return; } conn->setCloseCallBack(std::bind(&CTcpServer::OnConnectionClose, this, std::placeholders::_1)); CSocket s(fd); s.SetNonBlock(true); conn->connectEstablished(); if (connectionCallBack_) connectionCallBack_(conn); }
在TcpServer中新连接到来的函数中,会调用 connectionCallBack_函数,此时就是CEchoServer::OnNewConnection函数。
然后看一下 CTcpServer::OnNewConnection是在何时调用的:CTcpServer::CTcpServer(CEventLoop *loop, const std::string& ip, st_port_t port) : m_loop(loop), m_listener(new CListener(loop, ip, port)) { m_listener->setNewConnectionCallBack(std::bind(&CTcpServer::OnNewConnection, this, std::placeholders::_1)); m_loop->setRuntimeCallBack(std::bind(&CTcpServer::HandleDailyResCleanUp, this, std::placeholders::_1)); }
在TcpServer的构造函数中,会将 CTcpServer::OnNewConnection 函数设为 Listener的回调函数。
void CListener::OnNewConnection() { int fd = StAccept(m_fd.fd()); if (fd <= 0) { return; } if (newConnectionCallBack_) newConnectionCallBack_(fd); }
在 CListener::OnNewConnection 中会先接受新的连接,然后调用这个回调并将新建socket的fd传过去。
那么什么时候调用的 CListener::OnNewConnection 呢?CListener::CListener(CEventLoop *loop, const std::string& ip, st_port_t port) : m_fd(ip.c_str(), port), m_ip(ip), m_port(port), m_loop(loop) { m_fd.Init(); m_channel = new CChannel(loop, m_fd.fd()); m_fd.SetNonBlock(true); m_fd.SetReuseAddr(true); m_fd.SetReusePort(true); m_fd.Bind(); m_channel->setReadCallBack(std::bind(&CListener::OnNewConnection, this)); }
CListener的构造函数中会将 CListener::OnNewConnection 设为 自己的 Channel 的读回调函数,此处可以看出已经将新建连接的函数和 Channel 的 readCallBack_ 绑定的一起了。
那Channel的readCallBack_是在什么时候被调用的呢?void CChannel::handleEvents() { if ((m_revents & XPOLLHUP) && !(m_revents & XPOLLIN)) { if (closeCallBack_) closeCallBack_(); } if (m_revents & (XPOLLERR | XPOLLNVAL)) { if (errorCallBack_) errorCallBack_(); } if (m_revents & (XPOLLIN | XPOLLPRI | XPOLLRDHUP)) { if (readCallBack_) readCallBack_(); } if (m_revents & XPOLLOUT) { if (writeCallBack_) writeCallBack_(); } }
handleEvent在最上面的CEventLoop::loop函数中被调用,所以就串联起来了。
然后在TcpServer::start()中调用 CListener::StartListening(),CListener::StartListening会调用 Channel::enableReading(); 将CListener的fd的读事件添加到 epoll(EventModule)中。对于Listener来说,事件发生的调用流程是这样的:
CEventLoop::loop -> Channel::handleEvent -> Channel::readCallBack_(实为 CListener::OnNewConnection) 此时调用accept系统调用拿到新的fd -> CListener::newConnectionCallBack_(实为 CTcpServer::OnNewConnection) 此时TcpServer接受到 来自CListener传来的新的fd,然后新建CTcpConnection对象与之关联并添加到连接列表m_connections中 -> CTcpServer::connectionCallBack_(实为 CEchoServer::OnNewConnection)此时就来到了用户定义的 业务层代码。
在上面流程中的 CTcpServer::OnNewConnection 函数中,会将对新建的连接调用 CTcpConnection::connectEstablished函数,该函数会调用 Channel::enableReading();将新建连接的fd的读事件添加到epoll(EventModule)中,自此就将新的连接(TcpConnection)关联到了 EventModule中。
三、简洁执行流程
CEventLoop::loop
-> EventModule::epoll_wait
-> Channle::handleEvent
-> Channel::readCallBack_
也就是说,关于收到读事件后应该做什么事的读事件回调函数只要绑定到对应fd的Channel上就可以了。
CListener是在构造函数中设置的读事件回调函数,在StartListening中将读事件添加到epoll(EventModule)中的。
CTcpConnection因为涉及具体的业务层,所以要在上层的业务层设置 读写事件回调。
*以上只是伪码,示例大概怎样使用。
欢迎关注 [懒人漫说] 公众号,分享Java、Android、C/C++ 技术,包括基础、自己遇到的问题解决过程。
当然如果关注并留言问题的话,我们力所能及的话会帮你解决并回复哟。我们和你一样,是正在成长的程序员,我们也会分享自己的成长路上的感想,希望可以和你一起努力成长。