teamtalk学习笔记——客户端(1)——客户端概述

一、客户端框架概述要点

先将客户端大致框架描述一下,因为这个框架基于网络库,所以该系列笔记先从底层网络库,再一步步往上进行分析,后续再详细讲解这些框架流程,仅供参考,如有不足请及时指出
  1. 客户端网络库通过提前注册的会话反应函数,来进行数据的回调,并根据serviceID来调用相应模块中的与cmdID对应的业务函数
  2. 客户端总体有两个任务队列,四个线程,分别为逻辑任务队列(处理业务逻辑任务)、http任务队列(处理http有关的业务),每个队列有一个对应的线程,http队列有一个线程池,默认线程池线程个数为1,还有一个网络IO线程负责处理IO数据的读写操作,主线程做界面消息循环,用来与用户交互
  3. 主线程中有一个代理窗口,这个代理窗口利用win32的消息队列,使用窗口处理函数来统一接口,负责处理客户端后台业务与界面的数据传递
  4. 客户端各个模块如登录模块、会话模块、文件传输模块等,都抽象成一个个模块,数据包过来的时候会选择对应的模块进行相应的逻辑处理
  5. 基础模块和界面模块之间的数据流通则通过观察者模式来进行通知相应界面的一些视图改变

二、主线程流程

//teamtalk客户端入口函数
BOOL CteamtalkApp::InitInstance()
{
    ......
    //这个做了两个工作,1、启动逻辑任务队列处理线程,2、启动io处理线程,进行io读写事件的监听
    if (!imcore::IMLibCoreRunEvent())
    ......
    //3、启动ui事件代理窗口,从系统消息队列中获取消息并处理一般事件和定时事件
    if (module::getEventManager()->startup() != imcore::IMCORE_OK)
    ......
    //4、 创建用户目录
    _CreateUsersFolder();
    ......
    //5、模态显示登录界面
    if (!module::getLoginModule()->showLoginDialog())
    ......
    //6、等登录操作完成后,登录界面模态返回才执行到这一步,进行创建主窗口
    if (!_CreateMainDialog())
    .....
    //7、主线程进行duilib的消息循环
    CPaintManagerUI::MessageLoop();
}

上述是teamtalk启动的大致流程,下面就简单分析下这些启动流程

2.1. 启动逻辑任务队列处理线程

IMLibCoreRunEvent()中调用 getOperationManager()->startup();来启动逻辑任务队列线程,代码如下

IMCoreErrorCode OperationManager::startup()
{
    m_operationThread = std::thread([&]
    {
        std::unique_lock  lck(m_cvMutex);
        Operation* pOperation = nullptr;
        while (m_bContinue)
        {
            if (!m_bContinue)
                break;
            //操作队列类型为std::list,进行循环从队列中取任务进行处理
            if (m_vecRealtimeOperations.empty())
                m_CV.wait(lck);//队列为空,条件变量在此等待有任务再进行处理
            if (!m_bContinue)
                break;
            {
                std::lock_guard lock(m_mutexOperation);
                if (m_vecRealtimeOperations.empty())
                    continue;
                pOperation = m_vecRealtimeOperations.front();
                m_vecRealtimeOperations.pop_front();
            }
            if (!m_bContinue)
                break;
            if (pOperation)
            {
                pOperation->process();//每个逻辑任务都继承于IOperation,重写process函数,进行相应任务的处理
                pOperation->release();
            }
        }
    });
    return IMCORE_OK;
}

上述OperationManager::startup()中使用c++11中的lambda表达式来创建一个线程运行函数,并立即启动,线程中循环的从逻辑任务队列中取
任务,没有则进行条件等待

###2.2 启动io处理线程,进行io读写事件的监听
IMLibCoreRunEvent()中调用_beginthreadex(0, 0, event_run, 0, 0, (unsigned*)&m_dwThreadID);启动线程运行event_run函数,代码如下:

unsigned int __stdcall event_run(void* threadArgu)
    {
        LOG__(NET,  _T("event_run"));
        netlib_init();
        netlib_set_running();//设置运行标识
        netlib_eventloop();
        return NULL;
    }

上述代码中netlib_eventloop进行事件分发

void netlib_eventloop(uint32_t wait_timeout)
{
    CEventDispatch::Instance()->StartDispatch(wait_timeout);
}

void CEventDispatch::StartDispatch(uint32_t wait_timeout)
{
    fd_set read_set, write_set, excep_set;
    timeval timeout;
    timeout.tv_sec = 1;    //wait_timeout 1 second
    timeout.tv_usec = 0;
    while (running)
    {
        //_CheckTimer();
        //_CheckLoop();
        if (!m_read_set.fd_count && !m_write_set.fd_count && !m_excep_set.fd_count)
        {
            Sleep(MIN_TIMER_DURATION);
            continue;
        }
        m_lock.lock();
        FD_ZERO(&read_set);
        FD_ZERO(&write_set);
        FD_ZERO(&excep_set);
        memcpy(&read_set, &m_read_set, sizeof(fd_set));
        memcpy(&write_set, &m_write_set, sizeof(fd_set));
        memcpy(&excep_set, &m_excep_set, sizeof(fd_set));
        m_lock.unlock();
        if (!running)
            break;
        //for (int i = 0; i < read_set.fd_count; i++) {
        //    LOG__(NET,  "read fd: %d\n", read_set.fd_array[i]);
        //}
        int nfds = select(0, &read_set, &write_set, &excep_set, &timeout);//无事件时超时等待
        if (nfds == SOCKET_ERROR)
        {
            //LOG__(NET,  "select failed, error code: %d\n", GetLastError());
            Sleep(MIN_TIMER_DURATION);
            continue;            // select again
        }
        if (nfds == 0)
        {
            continue;
        }
        for (u_int i = 0; i < read_set.fd_count; i++)
        {
            //LOG__(NET,  "select return read count=%d\n", read_set.fd_count);
            SOCKET fd = read_set.fd_array[i];
            CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);//从全局map中找出对应句柄封装类
            if (pSocket)
            {
                pSocket->OnRead();//进行数据读取的一些操作
                pSocket->ReleaseRef();
            }
        }
        for (u_int i = 0; i < write_set.fd_count; i++)
        {
            //LOG__(NET,  "select return write count=%d\n", write_set.fd_count);
            SOCKET fd = write_set.fd_array[i];
            CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
            if (pSocket)
            {
                pSocket->OnWrite();
                pSocket->ReleaseRef();
            }
        }
        for (u_int i = 0; i < excep_set.fd_count; i++)
        {
            LOG__(NET,  _T("select return exception count=%d"), excep_set.fd_count);
            SOCKET fd = excep_set.fd_array[i];
            CBaseSocket* pSocket = FindBaseSocket((net_handle_t)fd);
            if (pSocket)
            {
                pSocket->OnClose();
                pSocket->ReleaseRef();
            }
        }
    }
}

io线程启动后运行event_run函数,在这个函数里启动事件分发函数StartDispatch,windows下使用select进行socket句柄的事件监听,linux下的服务端是epoll,基本流程就是循环超时等待监听的socket是否有事件发生,有的话就进行相应事件的处理,网络库详细如何进行与业务挂钩的后续章节再进行分析

2.3 启动ui事件代理窗口,从系统消息队列中获取消息并处理一般事件和定时事件

module::getEventManager()->startup()中代码如下:

imcore::IMCoreErrorCode UIEventManager::startup()
{
    IMCoreErrorCode errCode = IMCORE_OK;
    if (0 != m_hWnd)
        return IMCORE_OK;
    else
    {
        //注册窗口类
        if (!_registerClass())
            return IMCORE_INVALID_HWND_ERROR;
        m_hWnd = ::CreateWindowW(uiEventWndClass, _T("uiEvnetManager_window"),
            0, 0, 0, 0, 0, HWND_MESSAGE, 0, GetModuleHandle(0), 0);
        if (m_hWnd)
        {
            //Windows每隔1s给窗口线程发送WM_TIMER消息
            ::SetTimer(m_hWnd, reinterpret_cast(this), 1000, NULL);
        }
    }
    if (FALSE == ::IsWindow(m_hWnd))
        errCode = IMCORE_INVALID_HWND_ERROR;
    return errCode;
}

BOOL UIEventManager::_registerClass()
{
    WNDCLASSEXW wc = { 0 };
    wc.cbSize = sizeof(wc);
    wc.lpfnWndProc = _WindowProc;//代理窗口的窗口过程函数,用来处理普通事件和定时事件
    wc.hInstance = ::GetModuleHandle(0);
    wc.lpszClassName = uiEventWndClass;
    ATOM ret = ::RegisterClassExW(&wc);
    ASSERT(ret != NULL);
    if (NULL == ret || ::GetLastError() == ERROR_CLASS_ALREADY_EXISTS)
        return FALSE;
    return TRUE;
}

LRESULT _stdcall UIEventManager::_WindowProc(HWND hWnd
                                            , UINT message
                                            , WPARAM wparam
                                            , LPARAM lparam)
{
    switch (message)
    {
    case UI_EVENT_MSG:
    //处理普通事件,用于界面和后台数据的一些数据传递
        reinterpret_cast(wparam)->_processEvent(reinterpret_cast(lparam), TRUE);
        break;
    case WM_TIMER:
    //主要做有关时间的任务
        reinterpret_cast(wparam)->_processTimer();
        break;
    default:
        break;
    }
    return ::DefWindowProc(hWnd, message, wparam, lparam);
}

上述过程,就是主线程创建一个代理窗口,无论是用户点击使得界面数据发生变化还是受到服务端数据导致界面需要更新,都会生成事件,投入到代理窗口到这个地方进行处理,怎么进行这些操作的后续章节再详细说明

###2.4 创建用户目录
这个没有多少分析的,调试看看就知道了

BOOL CteamtalkApp::_CreateUsersFolder()
{
    module::IMiscModule* pModule = module::getMiscModule();
    //users目录
    if (!util::createAllDirectories(pModule->getUsersDir()))
    {
        LOG__(ERR, _T("_CreateUsersFolder users direcotry failed!"));
        return FALSE;
    }
    //下载目录
    if (!util::createAllDirectories(pModule->getDownloadDir()))
    {
        LOG__(ERR, _T("_CreateUsersFolder download direcotry failed!"));
        return FALSE;
    }
    return TRUE;
}

2.5 模态显示登录界面,代码如下:

BOOL LoginModule_Impl::showLoginDialog()
{
    BOOL bRet = FALSE;
    LoginDialog* pDialog = new LoginDialog();
    PTR_FALSE(pDialog);
    CString csTitle = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_BTN_LOGIN"));
    pDialog->Create(NULL, csTitle, UI_CLASSSTYLE_DIALOG, WS_EX_STATICEDGE | WS_EX_APPWINDOW, 0, 0, 0, 0);
    pDialog->CenterWindow();
    bRet = (IDOK == pDialog->ShowModal());
    return bRet;
}

上述登录界面显示的时候pDialog->ShowModal(),是进行模态显示,界面显示返回前不会进行后续操作,什么时候进行返回呢,是点击登录按钮,与服务器交互,登录成功后再在如下Close(IDOK)后返回,至于中间做了什么操作,等将队列之间的关系讲完进行分析

void LoginDialog::OnOperationCallback(std::shared_ptr param)
{
    LoginParam* pLoginParam = (LoginParam*)param.get();
    if (LOGIN_OK == pLoginParam->result)    //登陆成功
    {
        Close(IDOK);//这个地方,登录界面才会返回
        //创建用户目录
        _CreateUsersFolder();
        //开启同步消息时间timer
        module::getSessionModule()->startSyncTimeTimer();
        module::getSessionModule()->setTime(pLoginParam->serverTime);
        //通知服务器客户端初始化完毕,获取组织架构信息和群列表
        module::getLoginModule()->notifyLoginDone();
    }
    else    //登陆失败处理
    {
       ......
    }
    CString csTxt = util::getMultilingual()->getStringById(_T("STRID_LOGINDIALOG_BTN_LOGIN"));
    m_pBtnLogin->SetText(csTxt);
    m_pBtnLogin->SetEnabled(true);
}

2.6 进行创建主窗口

创建主窗口,并置顶

BOOL CteamtalkApp::_CreateMainDialog()
{
    m_pMainDialog = new MainDialog();
    PTR_FALSE(m_pMainDialog);
    CString csTitle = util::getMultilingual()->getStringById(_T("STRID_GLOBAL_CAPTION_NAME"));
    if (!m_pMainDialog->Create(NULL, csTitle
        , UI_CLASSSTYLE_DIALOG, WS_EX_STATICEDGE /*| WS_EX_APPWINDOW*/ | WS_EX_TOOLWINDOW, 0, 0, 600, 800))
        return FALSE;
    m_pMainDialog->BringToTop();
    return TRUE;
}

2.7 进行duilib的消息循环

这里需要点一下duilib的消息响应机制,类似qt的qml进行界面和后台逻辑分离,使用方便,teamtalk中的界面类继承INotifyUI,重写Notify()函数进行界面消息的响应具体使用可以自行查看相关资料,下面附带的一个博客地方讲的比较完善
DuiLib消息响应方式:

实现IMessageFilterUI接口,调用CPaintManagerUI:: AddPreMessageFilter,进行消息发送到窗口过程前的过滤。
重载HandleMessage函数,当消息发送到窗口过程中时,最先进行过滤。
实现INotifyUI,调用CPaintManagerUI::AddNotifier,将自身加入Notifier队列。
添加消息代理(其实就是将成员函数最为回到函数加入),MakeDelegate(this, &CFrameWindowWnd::OnAlphaChanged);,当程序某个地方调用了CPaintManagerUI::SendNotify,并且Msg.pSender正好是this,我们的类成员回调函数将被调用

来源: https://blog.csdn.net/kerwinash/article/details/38556069
本博客是在阅读源码过程中做的笔记基础上稍作润色发布,如有纰漏请果断纠正

你可能感兴趣的:(teamtalk源码阅读笔记)