即时通讯源码_即时通讯服务端源码分析(一)

本文介绍了即时通讯项目flamingo的C++服务端实现,包括基于muduo库的chatserver、fileserver和imgserver。在Centos上部署并以守护进程启动服务,通过lsof命令查看监听状态。分析从main函数开始,涉及数据库初始化(使用Mysql)、TCP服务器的创建与监听,以及Acceptor和新连接回调函数的设置。整个流程遵循muduo的one loop per thread模型进行监听和事件处理。
摘要由CSDN通过智能技术生成

230b82d8d2784452ef72666e9eb9cecf.png

flamingo是一个即时通讯项目,作者开源的服务端代码是基于C++实现,客户端提供PC、安卓,由于笔者学习的是服务端开发,所以目前仅分析服务端的实现。作者GitHub地址https://github.com/balloonwj/flamingo

flamingo服务端的网络库是基于muduo库开发的,作者进行改进实现了跨平台,服务端分为3个部分,分别是chatserver(聊天服务)、fileserver(文件服务)、imgserver(图片服务)。

这里将服务端部署在Centos上,然后以守护进程的方式启动三个服务

e3a7e3040e37f77596de7f99bbdf2290.png

通过lsof -i -Pn命令查看监听端口的状态,已经进入监听模式

6a3a0fb3e5869a9e2106f7ec0c24b7b6.png

当服务端启动成功后,接着启动PC客户端,进行连接,PC客户端代码在Visual Studio 2019上进行编译,编译成功后运行可执行文件,设置ip,这里的ip就是笔者的服务端地址。

8685e0081faf71d93f4e3b8072c58a07.png

先注册一个账号

208b1679e8eac7ed5d52b74dc47b2fce.png

登录该账号

3c486abe1a564dcfb6d1a4d463ee7adb.png

添加好友

c7857aa8005514d7d1c328928e5a7de3.png

聊天

1e6b4b7b1709fed6020ee50bb6a1c9ed.png

下面就开始分析聊天服务的具体实现

1、chatserver部分

77cb841b8b68ea536332f8282f52b3fa.png

可以看到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复用函数监听对应的事件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值