flamingo是一个即时通讯项目,作者开源的服务端代码是基于C++实现,客户端提供PC、安卓,由于笔者学习的是服务端开发,所以目前仅分析服务端的实现。作者GitHub地址https://github.com/balloonwj/flamingo
flamingo服务端的网络库是基于muduo库开发的,作者进行改进实现了跨平台,服务端分为3个部分,分别是chatserver(聊天服务)、fileserver(文件服务)、imgserver(图片服务)。
这里将服务端部署在Centos上,然后以守护进程的方式启动三个服务
通过lsof -i -Pn命令查看监听端口的状态,已经进入监听模式
当服务端启动成功后,接着启动PC客户端,进行连接,PC客户端代码在Visual Studio 2019上进行编译,编译成功后运行可执行文件,设置ip,这里的ip就是笔者的服务端地址。
先注册一个账号
登录该账号
添加好友
聊天
下面就开始分析聊天服务的具体实现
1、chatserver部分
可以看到chatserver模块不仅支持聊天服务,而且支持HTTP、实现监控服务,并且支持缓存,这里的缓存并没有使用第三方数据库,作者也是为了便于新手学习,直接利用内存实现的缓存。对任何一个项目,我们首先从main函数开始分析,下面进入main.cpp。
......
int main(int argc, char* argv[])
{
#ifndef WIN32
//设置信号处理
signal(SIGCHLD, SIG_DFL); //子进程退出,默认忽略
signal(SIGPIPE, SIG_IGN); //对端关闭,忽略处理,因为它默认终止进程
signal(SIGINT, prog_exit); //终端信号(Ctrl + C),退出
signal(SIGTERM, prog_exit); //kill发送,退出
......
//初始化数据库配置
const char* dbserver = config.GetConfigName("dbserver");
const char* dbuser = config.GetConfigName("dbuser");
const char* dbpassword = config.GetConfigName("dbpassword");
const char* dbname = config.GetConfigName("dbname");
......
}
如果不是Windows平台,则设置以上四个信号处理函数,关于这几个信号的意义可以参考《Linux系统编程手册》,接下来就是初始化数据库部分,这里的数据库使用的是Mysql,读取了数据库的配置信息(主要就是数据库IP、用户名、密码、数据库名称),然后就是连接数据库。
if (!Singleton<CMysqlManager>::Instance().Init(dbserver, dbuser, dbpassword, dbname))
{
LOGF("Init mysql failed, please check your database config..............");
}
if (!Singleton<UserManager>::Instance().Init(dbserver, dbuser, dbpassword, dbname))
{
LOGF("Init UserManager failed, please check your database config..............");
}
这里使用了单例模式,在Init()函数中进行数据库的连接。
bool CMysqlManager::Init(const char* host, const char* user, const char* pwd, const char* dbname)
{
m_strHost = host;
m_strUser = user;
//数据库密码可能为空
if (pwd != NULL)
m_strPassword = pwd;
m_strDataBase = dbname;
//注意:检查数据库是否存在时,需要将数据库名称设置为空
m_poConn.reset(new CDatabaseMysql());
if (!m_poConn->Initialize(m_strHost, m_strUser, m_strPassword, ""))
{
//LOG_FATAL << "CMysqlManager::Init failed, please check params(" << m_strHost << ", " << m_strUser << ", " << m_strPassword << ")";
return false;
}
// 1. 检查库是否存在 /
if (!_IsDBExist())
{
if (!_CreateDB())
{
return false;
}
}
//再次确定是否可以连接上数据库
m_poConn.reset(new CDatabaseMysql());
if (!m_poConn->Initialize(m_strHost, m_strUser, m_strPassword, m_strDataBase))
{
//LOG_FATAL << "CMysqlManager::Init failed, please check params(" << m_strHost << ", " << m_strUser
// << ", " << m_strPassword << ", " << m_strDataBase << ")";
return false;
}
// 2. 检查库中表是否正确 /
for (size_t i = 0; i < m_vecTableInfo.size(); i++)
{
STableInfo table = m_vecTableInfo[i];
if (!_CheckTable(table))
{
//LOG_FATAL << "CMysqlManager::Init, table check failed : " << table.m_strName;
return false;
}
}
// 2. 检查库中表是否正确 /
m_poConn.reset();
return true;
}
登录数据库后检查数据库是否存在,如果对应的数据库名称不存在,则重新创建一个数据库,来看一下如何查询数据库是否存在的,进入_IsDBExist()函数。
bool CMysqlManager::_IsDBExist()
{
if (NULL == m_poConn)
{
return false;
}
QueryResult* pResult = m_poConn->Query("show databases");
if (NULL == pResult)
{
//LOGI << "CMysqlManager::_IsDBExist, no database(" << m_strDataBase << ")";
return false;
}
Field* pRow = pResult->Fetch();
while (pRow != NULL)
{
string name = pRow[0].GetString();
if (name == m_strDataBase)
{
//LOGI << "CMysqlManager::_IsDBExist, find database(" << m_strDataBase << ")";
pResult->EndQuery();
return true;
}
if (pResult->NextRow() == false)
{
break;
}
pRow = pResult->Fetch();
}
//LOGI << "CMysqlManager::_IsDBExist, no database(" << m_strDataBase << ")";
pResult->EndQuery();
delete pResult;
return false;
}
可以看出,就是直接查看数据库中所有库,并把查询结果与当前的数据库名称进行比较。数据库部分初始化完成后,接着就是获取监听的IP与Port信息。
//获取聊天服务监听的IP与端口
const char* listenip = config.GetConfigName("listenip");
short listenport = (short)atol(config.GetConfigName("listenport"));
Singleton<ChatServer>::Instance().Init(listenip, listenport, &g_mainLoop);
然后进入Init()函数,进行初始化操作。
bool ChatServer::Init(const char* ip, short port, EventLoop* loop)
{
InetAddress addr(ip, port);
m_server.reset(new TcpServer(loop, addr, "FLAMINGO-SERVER", TcpServer::kReusePort));
m_server->setConnectionCallback(std::bind(&ChatServer::OnConnection, this, std::placeholders::_1));
//启动侦听
m_server->start(6);
return true;
}
这里封装了一个InetAddress类对ip与port进行管理,然后new一个TcpServer类,TcpServer是属于muduo网络库中的管理tcp连接的类,这里创建了TcpServer,并通过m_server指针指向它,创建TCPServer并传递了(loop, addr, "FLAMINGO-SERVER", TcpServer::kReusePort)这几个参数,进入TcpServer类看看如何构建一个TCPServer对象。
TcpServer::TcpServer(EventLoop* loop,
const InetAddress& listenAddr,
const std::string& nameArg,
Option option)
: loop_(loop),
hostport_(listenAddr.toIpPort()),
name_(nameArg),
acceptor_(new Acceptor(loop, listenAddr, option == kReusePort)),
//threadPool_(new EventLoopThreadPool(loop, name_)),
connectionCallback_(defaultConnectionCallback),
messageCallback_(defaultMessageCallback),
started_(0),
nextConnId_(1)
{
acceptor_->setNewConnectionCallback(std::bind(&TcpServer::newConnection, this, std::placeholders::_1, std::placeholders::_2));
}
在构造TcpServer过程中又new出了一个Acceptor,可以猜测在这个类中是实现对fd的监听,进入Acceptor,这里设置了一个新连接的回调函数newConnection()。
Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
: loop_(loop),
acceptSocket_(sockets::createNonblockingOrDie()),
acceptChannel_(loop, acceptSocket_.fd()),
listenning_(false)
{
#ifndef WIN32
idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
#endif
acceptSocket_.setReuseAddr(true);
acceptSocket_.setReusePort(reuseport);
acceptSocket_.bindAddress(listenAddr);
acceptChannel_.setReadCallback(std::bind(&Acceptor::handleRead, this));
}
这里将fd设置为非阻塞的,并创建一个channel,在muduo中,一个fd就绑定一个channel,在channel中设置fd读写事件的回调函数,这里设置复用地址与端口号,调用bindAddress()函数进行地址进行绑定,其实底层调用的还是bind()函数,并绑定了读回调函数handleRead()。
void Socket::bindAddress(const InetAddress& addr)
{
sockets::bindOrDie(sockfd_, addr.getSockAddrInet());
}
void sockets::bindOrDie(SOCKET sockfd, const struct sockaddr_in& addr)
{
int ret = ::bind(sockfd, sockaddr_cast(&addr), static_cast<socklen_t>(sizeof addr));
if (ret < 0)
{
LOGF("sockets::bindOrDie");
}
}
地址绑定好了,应该就开始监听了,下面回到chatserver::Init()函数
bool ChatServer::Init(const char* ip, short port, EventLoop* loop)
{
......
//启动侦听
m_server->start(6);
return true;
}
void TcpServer::start(int workerThreadCount/* = 4*/)
{
if (started_ == 0)
{
eventLoopThreadPool_.reset(new EventLoopThreadPool());
eventLoopThreadPool_->Init(loop_, workerThreadCount);
eventLoopThreadPool_->start();
//threadPool_->start(threadInitCallback_);
//assert(!acceptor_->listenning());
loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
started_ = 1;
}
}
在start()函数中创建了6个线程,每个线程对应一个loop,也就是muduo中的one loop per thread思想,然后将listen()函数绑定到loop上进行监听。
void Acceptor::listen()
{
loop_->assertInLoopThread();
listenning_ = true;
acceptSocket_.listen();
acceptChannel_.enableReading();
}
在listen中不仅调用底层的listen()函数,enableReading将监听fd的读事件,至此,chatserver服务的第一个服务端的监听流程至此结束了,然后就可以在loop中调用IO复用函数监听对应的事件。