学习github上的项目 flamingo 自己的笔记。
flamingo原作者的csdn是: analogous_love
flamingo是多线程的,但是本人能力有限,只是单线程的还算能理解一点。
自己参照flamingo实现的基于epoll的单线程服务端,git地址:https://gitee.com/storm_zy/StServerFrame
实现了简单的echo功能,很多代码直接拷贝自flamingo。
EventLoop
- Channel
- EventModule
TcpServer
- TcpConnections
- TcpSession
- Listener
- EventLoop
#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;
}
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++ 技术,包括基础、自己遇到的问题解决过程。
当然如果关注并留言问题的话,我们力所能及的话会帮你解决并回复哟。我们和你一样,是正在成长的程序员,我们也会分享自己的成长路上的感想,希望可以和你一起努力成长。