基于EventLoop的tcp服务端 C++实现(四) ——完整的流程

学习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列表的获取。
  1. 首先来看一下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++ 技术,包括基础、自己遇到的问题解决过程。
基于EventLoop的tcp服务端 C++实现(四) ——完整的流程_第1张图片
当然如果关注并留言问题的话,我们力所能及的话会帮你解决并回复哟。我们和你一样,是正在成长的程序员,我们也会分享自己的成长路上的感想,希望可以和你一起努力成长。

你可能感兴趣的:(C++,C++服务端框架)