广州总部电话:020-85564311
广州总部电话:020-85564311
20年
互联网应用服务商
请输入搜索关键词
知识库 知识库

优网知识库

探索行业前沿,共享知识宝库

客户端软件结构设计思考:适配性与扩展性的双重考量

发布日期:2026-01-24 08:48:41 浏览次数: 803 来源:CppGuide
推荐语
从银狐远控源码出发,剖析客户端软件设计的核心痛点与优化思路。

核心内容:
1. 银狐主控现存三大架构问题:数据结构传递混乱、插件扩展性差、网络协议维护困难
2. Windows程序UI流畅性的底层原理与线程设计误区
3. 跨平台客户端结构设计的通用方法论与实践心得
小优 网站建设顾问
专业来源于二十年的积累,用心让我们做到更好!

写在前面的话

最近拿到了一份银狐远控的源码,阅读之后,大为赞赏。 但其稳定性也存在诸多问题,这些稳定性有整个框架上的设计缺陷,也有细粒度的编码问题,主控最大的问题有如下:

  1. 部分数据结构在网络层和UI层随意传递,生命周期不明,可能引起各类崩溃;
  2. 被控每增加一个插件,主控就要在主程序中固化一段针对该插件的逻辑,可扩展性差;
  3. 网络收包和解包逻辑分散的到处都是,缺乏统一的出入口,需要修改协议或者做其他的处理改造工作量巨大,维护困难。


于是借这个优化契机,分享一些架构或者设计性的总结,希望对做软件结构设计规划的同学有一些启发。

正文

关于这个标题的内容我思考了很多年,也求索了很多年,每次遇到一份新的质量看起来不错客户端软件的源码时,我总是忍不住地去学习和研究,以期能解决我的困惑,希望能达到我心中“完美”方案的样子。但是直到今天,我仍然没找到所谓的“完美”的答案,但是在这个过程中,因为借鉴、融合和吸纳了许多其他客户端软件的设计思想和技巧,我在做PC软件整体结构设计时越来越得心应手。这个系列文章将是我的成长的心路历程,故事很长,有太多前程往事,如果你准备好听我说一说,那咱们就开始吧。当然,这是这个系列的第一篇。

注意:下面的软件内容是关于PC端软件的,但是不局限于PC端,移动端也类似。

一、困惑我的问题

互联网从业多年,设计过很多PC端产品,让这个产品性能卓越、界面流畅、用户体验好一直是我追求的目标。然而,从接触Windows程序设计以来,我一直被这样一些问题困惑着:

1. 对UI流畅性的追求

软件UI流畅性直接影响到软件的用户体验。Windows程序的消息机制原理决定着主线程必然是UI线程(当然你的程序如果没有GUI那就另当别论了),那么决定一款软件的界面的流畅性很大程度上取决于这个主线程中对Windows消息处理的时耗,对一个消息处理的时耗越长,界面将越卡顿,反之,可能会越流畅。(注意,我这里只是说“可能会越流畅”,因为除了这里讨论的因素外,还有其他因素决定着Windows UI的流畅性,所以这里讨论的因素只是UI流畅性的必要因素之一。)

所以,为了达到这个目标,一般比较耗时的操作,例如网络收发数据、磁盘读写较大文件,我都会开启新的工作线程来处理这些耗时的操作。在这种认知和实践过程中,我走了很多弯路,有段时间,我甚至认为所有的非UI逻辑都应该搬到工作线程里面去,这样才能最大程度地保证UI在用户操作时响应的及时性。

这种想法其实有点极端了,毫不委婉地说这种想法也是错误的,理由有以下两点:

a. 并非所有非UI逻辑都需要放到线程中

只要UI线程在处理事件时不阻塞,不做那些明显耗时(人能感知)的操作,不一定要把非UI逻辑移到工作线程里面去;因为现代计算机在执行这些代码时耗相对于人类所能感知的基本上是可以忽略不计的;反过来,因为计算机执行这些消息处理代码非常快,所以消息队列大多数情况下是空的(没有消息),有大量的空闲时间(Idle time)。所以即使你利用这些空闲时间,也不会对界面流畅性有任何影响。

反过来说,如果不用,却是“暴殄天物”,大大的浪费了。为什么这么说呢?因为理由b:

b. 线程过多会增加复杂度和开发成本

将非UI逻辑移到工作线程,不仅要开启新的线程,而且新线程在逻辑处理完之后通知UI线程更新界面(线程之间通信),这样的步骤明显地加大了实际的项目代码的复杂度和开发周期,写出来的代码从结构上来说更加复杂、更容易出错。

所以我的观点是,让UI流畅,只要不是长时间阻塞UI线程,即使是千行万行代码,放到主线程的消息处理函数中执行又何妨。而 FileZilla 的 CAsyncSocketEx 类代码就是这么做的(FileZilla 项目下载地址:https://github.com/baloonwj/filezilla,电驴客户端也用了 CAsyncSocketEx 这个类:https://github.com/baloonwj/easyMule)。

我这里简化一下细枝末节,抽出主干框架:

// Processes event notifications sent by the sockets or the layers
static LRESULT CALLBACK WindowProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    if (message >= WM_SOCKETEX_NOTIFY)
    {
        // 此处省略195行代码
    }
    elseif (message == WM_USER) // Notification event sent by a layer
    {
        // 此处省略138行代码
    }
    elseif (message == WM_USER + 1)
    {
        // 此处省略40行代码
    }
    elseif (message == WM_USER + 2)
    {
        // 此处省略23行代码
    }
    elseif (message == WM_TIMER)
    {
        // 此处省略21行代码
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

以上面的代码为例,省略195行那个地方的是收发 socket 数据的,因为产生这个窗口消息之前,已经检测了相应的 socket 是否可读可写了。所以不会执行的时候不会阻塞,虽然代码量比较大,因为不会阻塞或者很耗时,所以 FileZilla 的界面还是非常流畅的。

当然上面的代码有 Windows 平台特有的特点,Windows 平台专门提供了一个 WSAAsyncSelect 函数来将网络收发数据的通知与 Windows 窗口关联起来,甚至提供了专门用于处理非UI逻辑的窗口类型(HWND_MESSAGE,术语叫 Message-Only Windows:https://msdn.microsoft.com/en-us/library/windows/desktop/ms632599(v=vs.85).aspx#message_only)。

2. PC端网络通信层的设计

可能有人会觉得这个问题有什么好令你苦恼的呢?请听我说。我理想中的PC端网络通信层应该是专注于网络通信细节,不涉及任何业务细节。其他模块只要调用它封装好的接口就可以了,但是呢,实际上做到这一点很难。举几个例子:

a. 解包与业务逻辑的边界

在通信协议不明确的情况下,解包操作到底应该属于网络层本身还是其他层呢?我说的通信协议不明确,指的是数据包无法按统一格式来解包,比如逐个解析字段,但是字段数量和类型因协议类型不同而发生变化。

再比如重连机制,重连一般不仅是操作系统 API connect() 函数的重连,还有业务上的重连(比如重新发送登录数据包请求),那必然需要记录用户名、密码等信息,但是这样的信息却是不属于网络层。即使封装成 Relogin() 这样的函数,网络层在涉及到重连时,很多内部状态也依赖于UI层。

b. 心跳包归属问题

心跳包的产生到底应该由网络层本身产生还是其他层,比如UI层。心跳包一般和断线重连、维持连接存活有关,而网络状态一般需要反映在UI界面上,甚至用户可能需要加以干预的(比如用户主动下线或者用户点击界面上重连或者重试按钮)。

用户的干预,可能会影响心跳包的发送(例如用户主动下线,就不要发送心跳包),从这个意义上说心跳包似乎不单属于网络层。

c. 收发是否需要分离线程

我曾经存在这样一个认识,假如现在有一个 socket 与服务器保持连接,这个 socket 上不仅有主动发包应答,还有服务器随时给你推送的数据。因为 socket 是全双工的,收和发我一般都会单独开一个线程来操作,这样收数据和发数据就会独立开,在一定的情况下互不影响。如果某一方(收或者发)比较频繁,也不会影响对方。

但是,因为同时存在收发两个线程,逻辑处理时就会非常复杂了。举个例子,如果收发数据任一个的过程中出错(不一定是网络出错,可能是逻辑上认为不正确),到底要不要关闭 socket,如果关闭了 socket,那么在更新 m_bConnected 这样表示网络状态的字段时,可能会涉及到两个线程同时操作这个字段,那么势必要加锁保护这个变量,有锁的话,势必对性能有影响。

这还不是大问题,假设这个软件需要定时重连,那到底是放在收线程重连还是发线程重连呢?重连过程中,另外一个线程需不需要停止或者暂停呢?而这样的设计导致重连逻辑非常复杂,因为:如果是用户主动下线或者被踢下线,那么就不该重连。

综合上面,在设计代码结构和逻辑时,变得非常复杂。所以,回过头来认真地想一想,对于客户端软件真的有必要做成开启两个线程、收发分离吗?如果我们开启两个线程真能带来效率上的提升,那我们这样做也是可以考虑的。但是对于大多数PC端软件来说,就算你这么做,我觉得带来的效率提升也是上层界面或者用户无法察觉的。

既然用户无法察觉,那就不会带来用户体验的提升。然而这种结构,在网络通信层代码结构设计上却非常麻烦。那不如收发数据都放在一个线程中,这样不仅没有了收发两个线程同时读写状态标志需要加锁带来性能损失的问题,而且代码结构也简单许多。

3. 工作线程与UI层如何通信的问题

为了避免在非UI线程直接操作UI元素,目前Windows上的主流做法是工作线程通过 PostMessage() 携带堆内存数据给UI线程指定的元素发消息。当然在Windows上从来没有规定不能在非UI线程直接操作界面元素,有的时候迫不得已,我们还得这么做(当然这是一种不好的使用方案,应该尽量避免)。

举个迫不得已的例子,比如一款即时通讯软件聊天中的窗口抖动效果,实现原理是几个 MoveWindow 函数调用之间,嵌入几个 Sleep 函数,代码实例如下:

RECT rtWindow;
::GetWindowRect(hwnd, &rtWindow);
long x = rtWindow.left;
long y = rtWindow.top;
long cxWidth = rtWindow.right - rtWindow.left;
long cyHeight = rtWindow.bottom - rtWindow.top;
constlong nOffset = 9;
constlong SLEEP_INTERAL = 60;

for (long i = 0; i <= 2; ++i)
{
    ::MoveWindow(hwnd, x + nOffset, y - nOffset, cxWidth, cyHeight, FALSE);
    ::Sleep(SLEEP_INTERAL);
    ::MoveWindow(hwnd, x - nOffset, y - nOffset, cxWidth, cyHeight, FALSE);
    ::Sleep(SLEEP_INTERAL);
    ::MoveWindow(hwnd, x - nOffset, y + nOffset, cxWidth, cyHeight, FALSE);
    ::Sleep(SLEEP_INTERAL);
    ::MoveWindow(hwnd, x + nOffset, y + nOffset, cxWidth, cyHeight, FALSE);
    ::Sleep(SLEEP_INTERAL);
    ::MoveWindow(hwnd, x, y, cxWidth, cyHeight, FALSE);
    ::Sleep(SLEEP_INTERAL);
}

如果这段代码放在主线程里面,由于 Sleep 的使用会导致消息队列中其他消息不能及时处理,导致界面卡顿,尤其是用户在频繁地抖动窗口时(假设允许用户频繁地发送窗口抖动)。所以 TeamTalk 这款蘑菇街开源的即时通讯软件里面(PC端源码,源码下载:https://github.com/baloonwj/TeamTalk)是开启单独一个线程来调用上面的代码。

但是随着技术的发展,后来的操作系统比如安卓就未必允许这么做了,安卓操作系统是一直不允许在非UI线程直接操作UI元素,即使是一个小小的 toast 提示。

所以,我们只能老老实实地给UI线程 PostMessage(),但是这样也存在问题——如果 PostMessage 时需要携带复杂类型数据,那么我们必须使用堆内存,以确保消息到达UI线程并处理的时候,这块数据的内存仍然存在。

这样做存在两个问题:

  1. 很容易造成内存泄漏
    因为工作线程不断 new 出内存,UI线程不断 delete,尤其是在多个中间窗口中转时,万一哪一个中间步骤处理后不继续往下抛消息,这块内存就不会被释放了。我的意思是工作线程A new 出数据通过 PostMessage 发给窗口A,窗口A处理或者不处理再 PostMessage 给窗口B,B同样处理或不处理再给窗口C,等等。这个过程非常容易造成内存泄漏,比如窗口B被关闭了。

    为了做到万无一失,我们也许会在各个窗口的事件处理函数里面出现大量半途 delete 的代码,非常分散,很难管理。我曾经尝试着用 C++11 std::shared_ptr 或 std::unique_ptr 这样的智能指针去解决,但是这些智能指针是否在多个线程之间工作的很好,显然是一个问题。当然你可以采用内存池技术,但是让大多数人设计一个不出问题且性能不错的内存池还是有点难度的。这是这种方法存在的缺点之一。

  2. 频繁的 new/delete 会产生内存碎片
    虽然客户端软件一般不用考虑这个问题,但是我有时候还是会表示忧虑的。

既然这种传堆内存的方法不好,另外一种可以使用全局对象,这个全局对象有大量 getter 和 setter 方法。但是在每次 getter 和 setter 时,由于涉及到多个线程操作,还是同样得加上锁,这又回归到上面提的锁带来的性能降低问题。

上面的讨论,让我想起仓央嘉措的诗:“自恐多情损梵行,入山又恐失倾城。世间安得两全法,不负如来不负卿?”如何二者都兼顾呢?真是个头痛的问题。

那到底该如何解决呢?我目前的做法是自己定义一个智能指针对象(实现技术是引用计数),PostMessage 需要 new 出来并传递的对象都继承自这个智能指针对象,这样就能做到自释放了。因为是自己定义的智能指针,所以我们可以自己加上额外的代码保证多线程之间的操作的原子性。例如下面的代码可以参考一下:

class TTPUBAPI atomic_count
{

public:
    explicit atomic_countlong v = 0);
    long increment();
    long decrement();
    long value() const;
private:
    atomic_count( atomic_count const & );
    atomic_count & operator=( atomic_count const & );
    longvolatile value_;
};

atomic_count::atomic_count( long v )
{
    value_ = v;
}

long atomic_count::increment()
{
    return InterlockedIncrement( &value_ );
}

long atomic_count::decrement()
{
    return InterlockedDecrement( &value_ );
}

long atomic_count::value() const
{
    returnstatic_cast<longconstvolatile &>( value_ );
}

class TTPUBAPI safe_object :public tt::atomic_count {
public:
    virtual ~safe_object() {}
};

class TTPUBAPI safe_object_ref {
private:
    safe_object * object_;
    bool auto_free_;
public:
    safe_object_ref();
    safe_object_ref(safe_object * object, bool auto_free = true);
    safe_object_ref(const safe_object_ref &ref);
    virtual ~safe_object_ref();

    safe_object * get() const;
    bool get_auto_free() const;

    void attach(safe_object *object, bool auto_free = true);
    void detach();

    safe_object_ref& operator=(const safe_object_ref& ref);
    booloperator==(const safe_object_ref& ref);

    safe_object * operator->() const;

    bool check_valid() const;
};

safe_object_ref::safe_object_ref()
{
    object_ = NULL;
    auto_free_ = true;
}

safe_object_ref::safe_object_ref(safe_object * object, bool auto_free)
{
    object_ = NULL;
    attach(object, auto_free);
}

safe_object_ref::safe_object_ref(const safe_object_ref &ref)
{
    object_ = NULL;
    attach(ref.object_, ref.auto_free_);
}

safe_object_ref& safe_object_ref::operator=(const safe_object_ref& ref)
{
    attach(ref.object_, ref.auto_free_);
    return (*this);
}

safe_object_ref::~safe_object_ref()
{
    detach();
}

safe_object * safe_object_ref::get() const
{
    return object_;
}

bool safe_object_ref::get_auto_free() const
{
    return auto_free_;
}

void safe_object_ref::attach(safe_object *object, bool auto_free)
{
    if(object != NULL)
    {
        object->increment();
    }

    detach();
    object_ = object;
    auto_free_ = auto_free;
}

void safe_object_ref::detach()
{
    if(object_ != NULL)
    {
        long val = object_->decrement();
        if(val == 0 && auto_free_ == true)
        {
            delete object_;
        }
        object_ = NULL;
    }
}

safe_object * safe_object_ref::operator->() const
{
    return object_;
}

bool safe_object_ref::check_valid() const
{
    return (object_ != NULL);
}

bool safe_object_ref::operator==(const safe_object_ref& ref)
{
    return (object_ == ref.object_);
}

二、案例分析

上面讨论的一些疑惑以及解决办法只是一些具体而微的东西,下面我们来实际讨论一个PC端软件的架构,先看我个人的一款即时通讯软件的PC端(Flamingo:源码下载地址:https://github.com/baloonwj/flamingo,关于 Flamingo 的介绍,您可以参考这篇文章:http://blog.csdn.net/analogous_love/article/details/69481542),这个软件的基础功能和 QQ 一样,可以进行单聊和群聊,当然也可以自定义用户信息,工程代码结构如下:

这个软件的框架结构图如下:

理论上说只要有UI层和网络层就够了,但是为了保证UI界面的流畅和网络通信层单纯高效地收发网络数据,加了一个中间层,即数据加工层。从上往下看,UI层产生调用网络请求或者数据处理请求,如果这些数据需要进行加工,而加工过程比较耗时,那么无论放在UI层还是网络层都不合适;从下往上看,网络上收到数据以后,将这些数据解包后,必须加工成界面层需要的格式,这些数据加工工作也放在数据加工层。

我们现在来详细介绍一下每一层如何实现的:

1. 数据加工层

该层实际上是一组线程组成的,每一个线程都是一个从自己的任务队列中取任务执行,这些任务处理完之后产生网络数据包,或者直接放到网络层的发送队列中去,或者直接调用网络层接口函数将数据发出去。

其中 SendMsgThread 会将自己的队列中任务加工成网络数据格式放到网络层的发送缓冲区中去,RecvMsgThread 会自己任务队列中的数据加工成界面层需要的样子,然后 PostMessage 给界面。而 FileTaskThreadImageTaskThread 分别处理即时通讯聊天中与文件和图片相关的任务,根据任务类型,或者发送出去,或者显示到界面上去。

每个任务队列的结构都是差不多的(这里以 SendMsgThread 为例):

// 处理任务的线程函数
void CSendMsgThread::Run()
{
    while (!m_bStop)
    {
        CNetData* lpMsg;
        {
            std::unique_lock<std::mutex> guard(m_mtItems);
            while (m_listItems.empty())
            {
                if (m_bStop)
                    return;

                m_cvItems.wait(guard);
            }

            lpMsg = m_listItems.front();
            m_listItems.pop_front();
        }

        HandleItem(lpMsg);
    }
}

// 供UI层调用的、产生新任务的接口函数
void CSendMsgThread::AddItem(CNetData* pItem)
{
    std::lock_guard<std::mutex> guard(m_mtItems);
    m_listItems.push_back(pItem);
    m_cvItems.notify_one();
}

每个任务处理好之后,会产生界面需要的数据,然后 new 出堆对象,用 PostMessage 携带发给UI层,这里以创建群组成功的代码为例:

BOOL CRecvMsgThread::HandleCreateNewGroupResult(const std::string& strMsg)
{
    // {"code":0, "msg": "ok", "groupid": 12345678, "groupname": "我的群名称"}
    Json::Reader JsonReader;
    Json::Value JsonRoot;
    if (!JsonReader.parse(strMsg, JsonRoot))
        return FALSE;

    if (!JsonRoot["code"].isInt() || !JsonRoot["groupid"].isInt() || !JsonRoot["groupname"].isString())
        return FALSE;

    CCreateNewGroupResult* pResult = new CCreateNewGroupResult();
    pResult->m_uAccountID = JsonRoot["groupid"].asInt();
    strcpy_s(pResult->m_szGroupName, ARRAYSIZE(pResult->m_szGroupName), JsonRoot["groupname"].asCString());

    // 发给主线程
    ::PostMessage(m_lpUserMgr->m_hProxyWnd, FMG_MSG_CREATE_NEW_GROUP_RESULT, 0, (LPARAM)pResult);

    return TRUE;
}

// 具体每个任务的处理过程
void CSendMsgThread::HandleItem(CNetData* pNetData)
{
    if(pNetData == NULL)
        return;

    switch(pNetData->m_uType)
    {
    case NET_DATA_REGISTER:
        HandleRegister((const CRegisterRequest*)pNetData);
        break;

    case NET_DATA_LOGIN:
        HandleLogon((const CLoginRequest*)pNetData);
        break;

    case NET_DATA_USER_BASIC_INFO:
        HandleUserBasicInfo((const CUserBasicInfoRequest*)pNetData);
        break;

    case msg_type_creategroup:
        HandleCreateNewGroupResult(data);
        break;

    // 创建群
    case msg_type_creategroup:
        HandleCreateNewGroupResult(data);
        break;

    // 类似代码省略

    default:
#ifdef _DEBUG
        ::MessageBox(::GetForegroundWindow(), _T("Be cautious! Unhandled data type in send queen."), _T("Warning"), MB_OK|MB_ICONERROR);
#else
        LOG_WARNING("Be cautious! Unhandled data type in send queen.");
#endif
    }

    m_seq++;

    delete pNetData;
}

2. 网络层

网络层就是按照我上面介绍的同一个 socket 收发分开成两个线程。

// 网络层发送数据的线程函数
void CIUSocket::SendThreadProc()
{
    LOG_INFO("Recv data thread start...");

    while (!m_bStop)
    {
        std::unique_lock<std::mutex> guard(m_mtSendBuf);
        while (m_strSendBuf.empty())
        {
            if (m_bStop)
                return;

            m_cvSendBuf.wait(guard);
        }

        if (!Send())
        {
            // 进行重连,如果连接不上,则向客户报告错误
        }
    }

    LOG_INFO("Recv data thread finish...");
}

// 供数据加工层调用的、产生网络数据包的接口函数
void CIUSocket::Send(const std::string& strBuffer)
{
    std::lock_guard<std::mutex> guard(m_mtSendBuf);
    // 插入包头
    int32_t length = (int32_t)strBuffer.length();
    msg header = { length };
    m_strSendBuf.append((constchar*)&header, sizeof(header));
    m_strSendBuf.append(strBuffer.c_str(), length);
    m_cvSendBuf.notify_one();
}

// 接收数据的网络线程
void CIUSocket::RecvThreadProc()
{
    LOG_INFO("Recv data thread start...");

    int nRet;
    // 上网方式
    DWORD   dwFlags;
    BOOL    bAlive;
    while (!m_bStop)
    {
        // 检测到数据则收数据
        nRet = CheckReceivedData();
        // 出错
        if (nRet == -1)
        {
            m_pRecvMsgThread->NotifyNetError();
        }
        // 无数据
        elseif (nRet == 0)
        {
            bAlive = ::IsNetworkAlive(&dwFlags); // 是否在线
            if (!bAlive && ::GetLastError() == 0)
            {
                // 网络已经断开
                m_pRecvMsgThread->NotifyNetError();
                LOG_ERROR("net error, exit recv and send thread...");
                Uninit();
                break;
            }

            long nLastDataTime = 0;
            {
                std::lock_guard<std::mutex> guard(m_mutexLastDataTime);
                nLastDataTime = m_nLastDataTime;
            }

            if (m_nHeartbeatInterval > 0)
            {
                if (time(NULL) - nLastDataTime >= m_nHeartbeatInterval)
                    SendHeartbeatPackage();
            }
        }
        // 有数据
        elseif (nRet == 1)
        {
            if (!Recv())
            {
                m_pRecvMsgThread->NotifyNetError();
                continue;
            }

            // 解包,并将得到的业务数据交给 RecvMsgThread 的任务队列
            DecodePackages();
        } // end if
    } // end while-loop

    LOG_INFO("Recv data thread finish...");
}

bool CIUSocket::DecodePackages()
{
    // 一定要放在一个循环里面解包,因为可能一片数据中有多个包,
    // 对于数据收不全,这个地方我纠结了好久 T_T
    while (true)
    {
        // 接收缓冲区不够一个包头大小
        if (m_strRecvBuf.length() <= sizeof(msg))
            break;

        msg header;
        memcpy_s(&header, sizeof(msg), m_strRecvBuf.data(), sizeof(msg));
        // 防止包头定义的数据是一些错乱的数据,这里最大限制每个包大小为10M
        if (header.packagesize >= MAX_PACKAGE_SIZE || header.packagesize <= 0)
        {
            LOG_ERROR("Recv a strange packagesize in header, packagesize=%d", header.packagesize);
            m_strRecvBuf.clear();
            returnfalse;
        }

        // 接收缓冲区不够一个整包大小(包头+包体)
        if (m_strRecvBuf.length() < sizeof(msg) + header.packagesize)
            break;

        // 去除包头信息
        m_strRecvBuf.erase(0sizeof(msg));
        std::string strBody;
        strBody.append(m_strRecvBuf.c_str(), header.packagesize);
        // 去除包体信息
        m_strRecvBuf.erase(0, header.packagesize);

        m_pRecvMsgThread->AddMsgData(strBody);
    }

    returntrue;
}

3. UI层

UI层实际上包含两部分,一部分是包含各种程序的界面,还有一个所谓的代理窗口,数据加工层将数据先发到这个窗口上,然后可能在这里进一步处理一下界面的逻辑,然后再发给目标窗口。当然有些数据只是原封不动地转发:

LRESULT CALLBACK CFlamingoClient::ProxyWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    CFlamingoClient* lpFMGClient = (CFlamingoClient*)::GetWindowLong(hWnd, GWL_USERDATA);
    if (NULL == lpFMGClient)
        return ::DefWindowProc(hWnd, message, wParam, lParam);

    if (message < FMG_MSG_FIRST || message > FMG_MSG_LAST)
        return ::DefWindowProc(hWnd, message, wParam, lParam);

    switch (message)
    {
    // 网络错误
    case FMG_MSG_NET_ERROR:
        ::PostMessage(lpFMGClient->m_UserMgr.m_hCallBackWnd, FMG_MSG_NET_ERROR, 00);
        break;

    case FMG_MSG_HEARTBEAT:
        lpFMGClient->OnHeartbeatResult(message, wParam, lParam);
        break;

    case FMG_MSG_NETWORK_STATUS_CHANGE:
        lpFMGClient->OnNetworkStatusChange(message, wParam, lParam);
        break;

    case FMG_MSG_REGISTER:                // 注册结果
        lpFMGClient->OnRegisterResult(message, wParam, lParam);
        break;
    case FMG_MSG_LOGIN_RESULT:            // 登录返回消息
        lpFMGClient->OnLoginResult(message, wParam, lParam);
        break;
    case FMG_MSG_LOGOUT_RESULT:           // 注销返回消息
    case FMG_MSG_UPDATE_BUDDY_HEADPIC:    // 更新好友头像
        //::MessageBox(NULL, _T("Change headpic"), _T("Change head"), MB_OK);
    case FMG_MSG_UPDATE_GMEMBER_HEADPIC:  // 更新群成员头像
    case FMG_MSG_UPDATE_GROUP_HEADPIC:    // 更新群头像
        ::SendMessage(lpFMGClient->m_UserMgr.m_hCallBackWnd, message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_USER_BASIC_INFO:  // 收到用户的基本信息
        lpFMGClient->OnUpdateUserBasicInfo(message, wParam, lParam);
        break;

    case FMG_MSG_UPDATE_GROUP_BASIC_INFO:
        lpFMGClient->OnUpdateGroupBasicInfo(message, wParam, lParam);
        break;

    case FMG_MSG_MODIFY_USER_INFO:                // 修改个人信息结果
        lpFMGClient->OnModifyInfoResult(message, wParam, lParam);
        break;
    case FMG_MSG_RECV_USER_STATUS_CHANGE_DATA:
        lpFMGClient->OnRecvUserStatusChangeData(message, wParam, lParam);
        break;

    case FMG_MSG_USER_STATUS_CHANGE:
        lpFMGClient->OnUserStatusChange(message, wParam, lParam);
        break;

    case FMG_MSG_UPLOAD_USER_THUMB:
        lpFMGClient->OnSendConfirmMessage(message, wParam, lParam);
        break;

    case FMG_MSG_UPDATE_USER_CHAT_MSG_ID:
        lpFMGClient->OnUpdateChatMsgID(message, wParam, lParam);
        break;
    case FMG_MSG_FINDFREIND:
        lpFMGClient->OnFindFriend(message, wParam, lParam);
        break;

    case FMG_MSG_DELETEFRIEND:
        lpFMGClient->OnDeleteFriendResult(message, wParam, lParam);
        break;

    case FMG_MSG_RECVADDFRIENDREQUSET:
        lpFMGClient->OnRecvAddFriendRequest(message, wParam, lParam);
        break;

    case FMG_MSG_CUSTOMFACE_AVAILABLE:
        lpFMGClient->OnBuddyCustomFaceAvailable(message, wParam, lParam);
        break;

    case FMG_MSG_MODIFY_PASSWORD_RESULT:
        lpFMGClient->OnModifyPasswordResult(message, wParam, lParam);
        break;

    case FMG_MSG_CREATE_NEW_GROUP_RESULT:
        lpFMGClient->OnCreateNewGroupResult(message, wParam, lParam);
        break;

    case FMG_MSG_UPDATE_BUDDY_LIST:                // 更新好友列表
        lpFMGClient->OnUpdateBuddyList(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_GROUP_LIST:        // 更新群列表消息
        lpFMGClient->OnUpdateGroupList(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_RECENT_LIST:        // 更新最近联系人列表消息
        lpFMGClient->OnUpdateRecentList(message, wParam, lParam);
        break;
    case FMG_MSG_BUDDY_MSG:                // 好友消息
        lpFMGClient->OnBuddyMsg(message, wParam, lParam);
        break;
    case FMG_MSG_GROUP_MSG:                // 群消息
        lpFMGClient->OnGroupMsg(message, wParam, lParam);
        break;
    case FMG_MSG_SESS_MSG:                // 临时会话消息
        lpFMGClient->OnSessMsg(message, wParam, lParam);
        break;
    case FMG_MSG_STATUS_CHANGE_MSG:        // 好友状态改变消息
        lpFMGClient->OnStatusChangeMsg(message, wParam, lParam);
        break;
    case FMG_MSG_SELF_STATUS_CHANGE:    // 自己的状态发生改变,例如被踢下线消息
        lpFMGClient->OnKickMsg(message, wParam, lParam);
        break;
    case FMG_MSG_SCREENSHOT:    // 截屏消息
        lpFMGClient->OnScreenshotMsg(message, wParam, lParam);
        break;
    case FMG_MSG_SYS_GROUP_MSG:            // 群系统消息
        lpFMGClient->OnSysGroupMsg(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_BUDDY_NUMBER:    // 更新好友号码
        lpFMGClient->OnUpdateBuddyNumber(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_GMEMBER_NUMBER:    // 更新群成员号码
        lpFMGClient->OnUpdateGMemberNumber(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_GROUP_NUMBER:    // 更新群号码
        lpFMGClient->OnUpdateGroupNumber(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_BUDDY_SIGN:    // 更新好友个性签名
        lpFMGClient->OnUpdateBuddySign(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_GMEMBER_SIGN:    // 更新群成员个性签名
        lpFMGClient->OnUpdateGMemberSign(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_BUDDY_INFO:    // 更新用户信息
        lpFMGClient->OnUpdateBuddyInfo(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_GMEMBER_INFO:    // 更新群成员信息
        lpFMGClient->OnUpdateGMemberInfo(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_GROUP_INFO:    // 更新群信息
        lpFMGClient->OnUpdateGroupInfo(message, wParam, lParam);
        break;
    case FMG_MSG_UPDATE_C2CMSGSIG:    // 更新临时会话信令
        //lpFMGClient->OnUpdateC2CMsgSig(message, wParam, lParam);
        break;
    case FMG_MSG_CHANGE_STATUS_RESULT:    // 改变在线状态返回消息
        lpFMGClient->OnChangeStatusResult(message, wParam, lParam);
        break;
    case FMG_MSG_TARGET_INFO_CHANGE:        // 有用户信息发生改变:
        lpFMGClient->OnTargetInfoChange(message, wParam, lParam);
        break;

    case FMG_MSG_INTERNAL_GETBUDDYDATA:
        lpFMGClient->OnInternal_GetBuddyData(message, wParam, lParam);
        break;
    case FMG_MSG_INTERNAL_GETGROUPDATA:
        lpFMGClient->OnInternal_GetGroupData(message, wParam, lParam);
        break;
    case FMG_MSG_INTERNAL_GETGMEMBERDATA:
        lpFMGClient->OnInternal_GetGMemberData(message, wParam, lParam);
        break;
    case FMG_MSG_INTERNAL_GROUPID2CODE:
        return lpFMGClient->OnInternal_GroupId2Code(message, wParam, lParam);
        break;

    default:
        return ::DefWindowProc(hWnd, message, wParam, lParam);
    }
    return0;
}

这里的代理窗口其实就是上文中说的 HWND_MESSAGE 窗口。之所以在UI层创建一个窗口进行中转一下的原因是,给所有消息有一个集中处理的机会,方便因数据变化带来的UI逻辑的统一处理(说白了,就是将界面逻辑的代码集中在这个模块这里)。

UI层与数据加工层通信、往数据加工层的各个线程加工队列中产生数据,是通过一个专门的类提供的各种接口:

class CFlamingoClient
{

public:
    static CFlamingoClient& GetInstance();

public:
    CFlamingoClient(void);
    ~CFlamingoClient(void);

public:
    bool InitProxyWnd();                                        // 初始化代理窗口
    bool InitNetThreads();                                      // 初始化网络线程
    void Uninit();                                               // 反初始化客户端

    void SetServer(PCTSTR pszServer);
    void SetFileServer(PCTSTR pszServer);
    void SetImgServer(PCTSTR pszServer);
    void SetPort(short port);
    void SetFilePort(short port);
    void SetImgPort(short port);

    void SetUser(LPCTSTR lpUserAccount, LPCTSTR lpUserPwd);      // 设置UTalk号码和密码
    void SetLoginStatus(long nStatus);                           // 设置登录状态
    void SetCallBackWnd(HWND hCallBackWnd);                     // 设置回调窗口句柄
    void SetRegisterWindow(HWND hwndRegister);                   // 设置注册结果的反馈窗口
    void SetModifyPasswordWindow(HWND hwndModifyPassword);       // 设置修改密码结果反馈窗口
    void SetCreateNewGroupWindow(HWND hwndCreateNewGroup);       // 设置创建群组结果反馈窗口
    void SetFindFriendWindow(HWND hwndFindFriend);               // 设置查找用户结果反馈窗口

    void StartCheckNetworkStatusTask();
    //void StartGetUserInfoTask(long nType);                    // 获取好友
    void StartHeartbeatTask();

    void Register(PCTSTR pszAccountName, PCTSTR pszNickName, PCTSTR pszPassword);
    void Login(int nStatus = STATUS_ONLINE);                     // 登录
    BOOL Logout();                                               // 注销
    void CancelLogin();                                          // 取消登录
    void GetFriendList();                                       // 获取好友列表
    void GetGroupMembers(int32_t groupid);                      // 获取群成员
    void ChangeStatus(int32_t nNewStatus);                      // 更改自己的登录状态

    BOOL FindFriend(PCTSTR pszAccountName, long nType, HWND hReflectionWnd);// 查找好友
    BOOL AddFriend(UINT uAccountToAdd);
    void ResponseAddFriendApply(UINT uAccountID, UINT uCmd);     // 回应加好友请求任务
    BOOL DeleteFriend(UINT uAccountID);                          // 删除好友
    BOOL UpdateLogonUserInfo(PCTSTR pszNickName,
                             PCTSTR pszSignature,
                             UINT uGender,
                             long nBirthday,
                             PCTSTR pszAddress,
                             PCTSTR pszPhone,
                             PCTSTR pszMail,
                             UINT uSysFaceID,
                             PCTSTR pszCustomFacePath,
                             BOOL bUseCustomThumb)
;

    void SendHeartbeatMessage();
    void ModifyPassword(PCTSTR pszOldPassword, PCTSTR pszNewPassword);
    void CreateNewGroup(PCTSTR pszGroupName);
    void ChangeStatus(long nStatus);                            // 改变在线状态
    void UpdateBuddyList();                                      // 更新好友列表
    void UpdateGroupList();                                      // 更新群列表
    void UpdateRecentList();                                     // 更新最近联系人列表
    void UpdateBuddyInfo(UINT nUTalkUin);                        // 更新好友信息
    void UpdateGroupMemberInfo(UINT nGroupCode, UINT nUTalkUin)// 更新群成员信息
    void UpdateGroupInfo(UINT nGroupCode);                       // 更新群信息
    void UpdateBuddyNum(UINT nUTalkUin);                         // 更新好友号码
    void UpdateGroupMemberNum(UINT nGroupCode, UINT nUTalkUin);  // 更新群成员号码
    void UpdateGroupMemberNum(UINT nGroupCode, std::vector<UINT>* arrUTalkUin)// 更新群成员号码
    void UpdateGroupNum(UINT nGroupCode);                        // 更新群号码
    void UpdateBuddySign(UINT nUTalkUin);                        // 更新好友个性签名
    void UpdateGroupMemberSign(UINT nGroupCode, UINT nUTalkUin)// 更新群成员个性签名
    void ModifyUTalkSign(LPCTSTR lpSign);                        // 修改UTalk个性签名
    void UpdateBuddyHeadPic(UINT nUTalkUin, UINT nUTalkNum);     // 更新好友头像
    void UpdateGroupMemberHeadPic(UINT nGroupCode, UINT nUTalkUin, UINT nUTalkNum)// 更新群成员头像
    void UpdateGroupHeadPic(UINT nGroupCode, UINT nGroupNum);    // 更新群头像
    void UpdateGroupFaceSignal();                                // 更新群表情信令

    BOOL SendBuddyMsg(UINT nFromUin, const tstring& strFromNickName, UINT nToUin, const tstring& strToNickName, time_t nTime, const tstring& strChatMsg, HWND hwndFrom = NULL);// 发送好友消息
    BOOL SendGroupMsg(UINT nGroupId, time_t nTime, LPCTSTR lpMsg, HWND hwndFrom)// 发送群消息
    BOOL SendSessMsg(UINT nGroupId, UINT nToUin, time_t nTime, LPCTSTR lpMsg);     // 发送临时会话消息
    BOOL SendMultiChatMsg(const std::set<UINT> setAccountID, time_t nTime, LPCTSTR lpMsg, HWND hwndFrom=NULL);// 群发消息

    BOOL IsOffline();                                             // 是否离线状态

    long GetStatus();                                             // 获取在线状态
    BOOL GetVerifyCodePic(const BYTE*& lpData, DWORD& dwSize);    // 获取验证码图片
    void SetBuddyListAvailable(BOOL bAvailable);
    BOOL IsBuddyListAvailable();

    CBuddyInfo* GetUserInfo(UINT uAccountID=0);                  // 获取用户信息
    CBuddyList* GetBuddyList();                                  // 获取好友列表
    CGroupList* GetGroupList();                                  // 获取群列表
    CRecentList* GetRecentList();                                // 获取最近联系人列表
    CMessageList* GetMessageList();                              // 获取消息列表
    CMessageLogger* GetMsgLogger();                              // 获取消息记录管理器

    tstring GetUserFolder();                                      // 获取用户文件夹存放路径

    tstring GetPersonalFolder(UINT nUserNum = 0);                // 获取个人文件夹存放路径
    tstring GetChatPicFolder(UINT nUserNum = 0);                 // 获取聊天图片存放路径

    tstring GetUserHeadPicFullName(UINT nUserNum = 0);           // 获取用户头像图片全路径文件名
    tstring GetBuddyHeadPicFullName(UINT nUTalkNum);             // 获取好友头像图片全路径文件名
    tstring GetGroupHeadPicFullName(UINT nGroupNum);             // 获取群头像图片全路径文件名
    tstring GetSessHeadPicFullName(UINT nUTalkNum);              // 获取群成员头像图片全路径文件名
    tstring GetChatPicFullName(LPCTSTR lpszFileName);            // 获取聊天图片全路径文件名
    tstring GetMsgLogFullName(UINT nUserNum = 0);                // 获取消息记录全路径文件名

    BOOL IsNeedUpdateBuddyHeadPic(UINT nUTalkNum);               // 判断是否需要更新好友头像
    BOOL IsNeedUpdateGroupHeadPic(UINT nGroupNum);               // 判断是否需要更新群头像
    BOOL IsNeedUpdateSessHeadPic(UINT nUTalkNum);                // 判断是否需要更新群成员头像

    void RequestServerTime();                                    // 获取服务器时间
    time_t GetCurrentTime();                                     // 获取当前时间(以服务器时间为基准)
    void LoadUserConfig();                                       // 加载用户设置信息
    void SaveUserConfig();                                       // 保存用户设置信息

    void GoOnline();
    void GoOffline();                                             // 掉线或者下线

    long ParseBuddyStatus(long nFlag);                           // 解析用户在线状态
    void CacheBuddyStatus();                                     // 缓存用户在线状态
    BOOL SetBuddyStatus(UINT uAccountID, long nStatus);
    BOOL SetBuddyClientType(UINT uAccountID, long nNewClientType);

private:
    void OnHeartbeatResult(UINT message, WPARAM wParam, LPARAM lParam);
    void OnNetworkStatusChange(UINT message, WPARAM wParam, LPARAM lParam);
    void OnRegisterResult(UINT message, WPARAM wParam, LPARAM lParam);
    void OnLoginResult(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateUserBasicInfo(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateGroupBasicInfo(UINT message, WPARAM wParam, LPARAM lParam);
    void OnModifyInfoResult(UINT message, WPARAM wParam, LPARAM lParam);
    void OnRecvUserStatusChangeData(UINT message, WPARAM wParam, LPARAM lParam);
    void OnRecvAddFriendRequest(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUserStatusChange(UINT message, WPARAM wParam, LPARAM lParam);
    void OnSendConfirmMessage(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateChatMsgID(UINT message, WPARAM wParam, LPARAM lParam);
    void OnFindFriend(UINT message, WPARAM wParam, LPARAM lParam);
    void OnBuddyCustomFaceAvailable(UINT message, WPARAM wParam, LPARAM lParam);
    void OnModifyPasswordResult(UINT message, WPARAM wParam, LPARAM lParam);
    void OnCreateNewGroupResult(UINT message, WPARAM wParam, LPARAM lParam);
    void OnDeleteFriendResult(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateBuddyList(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateGroupList(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateRecentList(UINT message, WPARAM wParam, LPARAM lParam);
    void OnBuddyMsg(UINT message, WPARAM wParam, LPARAM lParam);
    void OnGroupMsg(UINT message, WPARAM wParam, LPARAM lParam);
    void OnSessMsg(UINT message, WPARAM wParam, LPARAM lParam);
    void OnSysGroupMsg(UINT message, WPARAM wParam, LPARAM lParam);
    void OnStatusChangeMsg(UINT message, WPARAM wParam, LPARAM lParam);
    void OnKickMsg(UINT message, WPARAM wParam, LPARAM lParam);
    void OnScreenshotMsg(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateBuddyNumber(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateGMemberNumber(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateGroupNumber(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateBuddySign(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateGMemberSign(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateBuddyInfo(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateGMemberInfo(UINT message, WPARAM wParam, LPARAM lParam);
    void OnUpdateGroupInfo(UINT message, WPARAM wParam, LPARAM lParam);
    //void OnUpdateC2CMsgSig(UINT message, WPARAM wParam, LPARAM lParam);
    void OnChangeStatusResult(UINT message, WPARAM wParam, LPARAM lParam);
    void OnTargetInfoChange(UINT message, WPARAM wParam, LPARAM lParam);

    void OnInternal_GetBuddyData(UINT message, WPARAM wParam, LPARAM lParam);
    void OnInternal_GetGroupData(UINT message, WPARAM wParam, LPARAM lParam);
    void OnInternal_GetGMemberData(UINT message, WPARAM wParam, LPARAM lParam);
    UINT OnInternal_GroupId2Code(UINT message, WPARAM wParam, LPARAM lParam);

    BOOL CreateProxyWnd();        // 创建代理窗口
    BOOL DestroyProxyWnd();       // 销毁代理窗口
    static LRESULT CALLBACK ProxyWndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);

public:
    CUserMgr                        m_UserMgr;
    CCheckNetworkStatusTask         m_CheckNetworkStatusTask;

    CSendMsgThread                  m_SendMsgThread;
    CRecvMsgThread                  m_RecvMsgThread;
    CFileTaskThread                 m_FileTask;
    CImageTaskThread                m_ImageTask;

    CUserConfig                     m_UserConfig;

    std::vector<AddFriendInfo*>     m_aryAddFriendInfo;

private:
    time_t                          m_ServerTime;             // 服务器时间
    DWORD                           m_StartTime;              // 开始计时的时间

    BOOL                            m_bNetworkAvailable;      // 网络是否可用

    HWND                            m_hwndRegister;           // 注册窗口
    HWND                            m_hwndFindFriend;         // 查找好友窗口
    HWND                            m_hwndModifyPassword;     // 修改密码窗口
    HWND                            m_hwndCreateNewGroup;     // 创建群组窗口

    BOOL                            m_bBuddyIDsAvailable;     // 用户好友ID是否可用
    BOOL                            m_bBuddyListAvailable;    // 用户好友列表信息是否可用

    std::map<UINT, long>            m_mapUserStatusCache;     // 好友在线状态缓存:key是账户ID,value是状态码
    std::map<UINT, UINT>            m_mapAddFriendCache;      // 加好友操作缓存: key是账户ID,value是操作码

    long                            m_nGroupCount;
    BOOL                            m_bGroupInfoAvailable;
    BOOL                            m_bGroupMemberInfoAvailable;
};

举个具体的例子,以删除好友为例:

// 删除好友
BOOL CFlamingoClient::DeleteFriend(UINT uAccountID)
{
    // TODO: 先判断是否离线
    COperateFriendRequest* pRequest = new COperateFriendRequest();
    pRequest->m_uCmd = Delete;
    pRequest->m_uAccountID = uAccountID;

    m_SendMsgThread.AddItem(pRequest);

    return TRUE;
}

实际上也是产生一个任务丢到 SendMsgThread 所属的消息队列中去。

我的即时通讯软件 Flamingo PC端的基本框架大致介绍完毕了。这种架构,基本上是目前大多数客户端软件的通用结构(包括安卓,上文中介绍的 FileZilla 是将利用 Windows API WSAAsyncSelect 将UI层与网络层合并了),只不过不同的软件在细节上可能做的比 Flamingo 好。

那么我们来看下 Flamingo PC端的这种设计在细节上有哪些地方需要优化的呢?

  1. 缺点一:请求是无状态无反馈的
    比如某个请求,加到 SendMsgThread 中之后,如果处理失败(可能在数据加工层或者网络层没发出去),对应的产生这个请求的UI元素无法得到任何反馈,所以也就没法重试。解决办法是,UI元素记录一下自己发送的请求,然后开启一个定时器,在一定时间后,该请求无应答,则重试。

  2. 缺点二:网络层收发不必分开
    原因上文也具体分析过了。这种分开,导致我现在给 Flamingo 增加断线重连机制非常麻烦。因为涉及到用户主动下线、掉线和被同名用户在另外机器上登录踢下线。这也是今后 Flamingo 要重点优化的一个地方。

  3. 缺点三:可以抽象出公共模块变量
    有些数据可以抽象出一个公共的模块变量出来,允许在多个线程(UI线程和数据加工层)中读写(当然需要加锁),这样可以减少一部分通过 PostMessage 携带的、new 出来的对象。(安卓中这种方法就很舒服了,因为 Java 中不需要自己做内存回收)。其实这种方法是结合上文中讨论的两个方法的综合运用。

  4. 缺点四:缺少定时器
    缺点一我介绍了UI层需要的定时器,其实网络层也需要一个定时器,可用于网络层心跳包的发送、或者自动重连。所以定时器是PC端软件中非常重要的一个模块。

三、pc端软件中很有意思的设计技巧

当然,客户端软件还涉及到一些具体的细节设计技巧。这里介绍几个:

1. 放入任务队列中、new出来的任务对象是使用它的父模块删除,还是自我删除

所以有的客户端软件给每个这样的任务提供一个自我删除的接口,里面 delete this。 看看 teamtalk pc 版就是这么做的:

struct MODULE_API IHttpOperation : public ICallbackOpertaion
{
public:
    IHttpOperation(IOperationDelegate& callback)
        :ICallbackOpertaion(callback)
    {

    }
    inline void cancel() { m_bIsCancel = TRUE; }
    inline BOOL isCanceled() const return m_bIsCancel; }

    virtual void release() 0;

private:
    BOOL        m_bIsCancel = FALSE;
};

注意 release() 和 cancel() 接口:

  • release() 接口一般就是自我释放资源和删除自己
  • cancel() 允许取消任务的执行

2. 同步的、且带超时时间的网络通信接口该怎么设计

一个系统在启动之前,或者在登录成功之前,其实一般不需要启动上面所说数据加工层和网络层。 所以登录成功之前,尤其是有登录界面的客户端,都可以使用同步的登录接口,因为登录之前服务器一般不会有推送的数据给你,一般你发生的什么请求,收到的数据就是该请求的应答。

具体做法如下:

a. 用户点击了登录按钮之后,开启一个新的线程(开启新线程是为了防止网络通信阻塞界面)

b. 新线程函数中,调用网络接口 API 发送数据和接收应答。 当然涉及的 socket,你仍然需要设置成非阻塞的,这样,你可以在网络接口中不必等待或者调用 select 函数等待 socket 可写或者可读一段你规定的时间。

c. 这样的接口设计,你可以先连接服务器,再检测 socket 是否可写,如果可写,发送数据,接着检测 socket 是否可读,如果可读,收取一个包头数据大小,接着根据包头数据中指定的包体大小收取一个包体大小,然后解包数据,最后判断是否是正确的应答。

d. 需要注意的是,新开启的工作线程得到结果以后或者一定时间内超时,向界面元素反馈信息,也是通过 PostMessage 返回给界面的。

以 flamingo 里面的登录过程为例:

“登录”按钮

void CLoginDlg::OnBtn_Login(UINT uNotifyCode, int nID, CWindow wndCtl)
{
    if (m_cboUid.IsDefaultText())
    {
        MessageBox(_T("请输入账号!"), _T("提示"), MB_OK|MB_ICONINFORMATION);
        m_cboUid.SetFocus();
        return;
    }

    if (m_edtPwd.IsDefaultText())
    {
        MessageBox(_T("请输入密码!"), _T("提示"), MB_OK|MB_ICONINFORMATION);
        m_edtPwd.SetFocus();
        return;
    }

    m_cboUid.GetWindowText(m_stAccountInfo.szUser, ARRAYSIZE(m_stAccountInfo.szUser));
    m_edtPwd.GetWindowText(m_stAccountInfo.szPwd, ARRAYSIZE(m_stAccountInfo.szPwd));
    m_stAccountInfo.bRememberPwd = (m_btnRememberPwd.GetCheck() == BST_CHECKED);
    m_stAccountInfo.bAutoLogin = (m_btnAutoLogin.GetCheck() == BST_CHECKED);

    // 记录当前用户信息
    m_lpFMGClient->m_UserMgr.m_UserInfo.m_strAccount = m_stAccountInfo.szUser;

    //开启线程
    HANDLE hLoginThread = (HANDLE)::_beginthreadex(NULL0, LoginThreadProc, this0NULL);
    if (hLoginThread != NULL)
        ::CloseHandle(hLoginThread);

    EndDialog(IDOK);
}

登录线程函数

UINT CLoginDlg::LoginThreadProc(void* pParam)
{
    CLoginDlg* pLoginDlg = (CLoginDlg*)pParam;
    if (pLoginDlg == NULL)
        return0;

    char szUser[64] = { 0 };
    EncodeUtil::UnicodeToUtf8(pLoginDlg->m_stAccountInfo.szUser, szUser, ARRAYSIZE(szUser));
    char szPassword[64] = { 0 };
    EncodeUtil::UnicodeToUtf8(pLoginDlg->m_stAccountInfo.szPwd, szPassword, ARRAYSIZE(szPassword));

    std::string strReturnData;
    //调用网络接口,超时时间设置为3秒
    bool bRet = CIUSocket::GetInstance().Login(szUser, szPassword, 113000, strReturnData);
    int nRet = LOGIN_FAILED;
    CLoginResult* pLoginResult = new CLoginResult();
    pLoginResult->m_LoginResultCode = LOGIN_FAILED;
    if (bRet)
    {
        //{"code": 0, "msg": "ok", "userid": 8}
        Json::Reader JsonReader;
        Json::Value JsonRoot;
        if (JsonReader.parse(strReturnData, JsonRoot) && !JsonRoot["code"].isNull() && JsonRoot["code"].isInt())
        {
            int nRetCode = JsonRoot["code"].asInt();

            if (nRetCode == 0)
            {
                if (!JsonRoot["userid"].isInt() || !JsonRoot["username"].isString() || !JsonRoot["nickname"].isString() ||
                    !JsonRoot["facetype"].isInt() || !JsonRoot["gender"].isInt() || !JsonRoot["birthday"].isInt() ||
                    !JsonRoot["signature"].isString() || !JsonRoot["address"].isString() ||
                    !JsonRoot["customface"].isString() || !JsonRoot["phonenumber"].isString() ||
                    !JsonRoot["mail"].isString())
                {
                    LOG_ERROR(_T("login failed, login response json is invalid, json=%s"), strReturnData.c_str());
                    pLoginResult->m_LoginResultCode = LOGIN_FAILED;
                }
                else
                {
                    pLoginResult->m_LoginResultCode = 0;
                    pLoginResult->m_uAccountID = JsonRoot["userid"].asInt();
                    strcpy_s(pLoginResult->m_szAccountName, ARRAYSIZE(pLoginResult->m_szAccountName), JsonRoot["username"].asCString());
                    strcpy_s(pLoginResult->m_szNickName, ARRAYSIZE(pLoginResult->m_szNickName), JsonRoot["nickname"].asCString());
                    //pLoginResult->m_nStatus = JsonRoot["status"].asInt();
                    pLoginResult->m_nFace = JsonRoot["facetype"].asInt();
                    pLoginResult->m_nGender = JsonRoot["gender"].asInt();
                    pLoginResult->m_nBirthday = JsonRoot["birthday"].asInt();
                    strcpy_s(pLoginResult->m_szSignature, ARRAYSIZE(pLoginResult->m_szSignature), JsonRoot["signature"].asCString());
                    strcpy_s(pLoginResult->m_szAddress, ARRAYSIZE(pLoginResult->m_szAddress), JsonRoot["address"].asCString());
                    strcpy_s(pLoginResult->m_szCustomFace, ARRAYSIZE(pLoginResult->m_szCustomFace), JsonRoot["customface"].asCString());
                    strcpy_s(pLoginResult->m_szPhoneNumber, ARRAYSIZE(pLoginResult->m_szPhoneNumber), JsonRoot["phonenumber"].asCString());
                    strcpy_s(pLoginResult->m_szMail, ARRAYSIZE(pLoginResult->m_szMail), JsonRoot["mail"].asCString());
                }
            }
            elseif (nRetCode == 102)
                pLoginResult->m_LoginResultCode = LOGIN_UNREGISTERED;
            elseif (nRetCode == 103)
                pLoginResult->m_LoginResultCode = LOGIN_PASSWORD_ERROR;
            else
                pLoginResult->m_LoginResultCode = LOGIN_FAILED;
        }
    }
    //m_lpUserMgr为野指针
    ::PostMessage(pLoginDlg->m_lpFMGClient->m_UserMgr.m_hProxyWnd, FMG_MSG_LOGIN_RESULT, 0, (LPARAM)pLoginResult);

    return1;
}

登录网络接口

bool CIUSocket::Login(const char* pszUser, const char* pszPassword, int nClientType, int nOnlineStatus, int nTimeout, std::string& strReturnData)
{
    if (!Connect())
        returnfalse;

    char szLoginInfo[256] = { 0 };
    sprintf_s(szLoginInfo,
        ARRAYSIZE(szLoginInfo),
        "{\"username\": \"%s\", \"password\": \"%s\", \"clienttype\": %d, \"status\": %d}",
        pszUser,
        pszPassword,
        nClientType,
        nOnlineStatus);

    std::string outbuf;
    BinaryWriteStream writeStream(&outbuf);
    writeStream.WriteInt32(msg_type_login);
    writeStream.WriteInt32(0);
    //std::string data = szLoginInfo;
    writeStream.WriteCString(szLoginInfo, strlen(szLoginInfo));
    writeStream.Flush();

    LOG_INFO("Request logon: Account=%s, Password=*****, Status=%d, LoginType=%d.", pszUser, pszPassword, nOnlineStatus, nClientType);

    int32_t length = (int32_t)outbuf.length();
    msg header = { length };
    std::string strSendBuf;
    strSendBuf.append((constchar*)&header, sizeof(header));
    strSendBuf.append(outbuf.c_str(), length);

    //超时时间设置为3秒
    if (!SendData(strSendBuf.c_str(), strSendBuf.length(), nTimeout))
        returnfalse;

    memset(&header, 0sizeof(header));
    if (!RecvData((char*)&header, sizeof(header), nTimeout))
        returnfalse;

    if (header.packagesize <= 0)
        returnfalse;

    CMiniBuffer minBuff(header.packagesize);
    if (!RecvData(minBuff, header.packagesize, nTimeout))
    {
        returnfalse;
    }

    BinaryReadStream readStream(minBuff, header.packagesize);
    int32_t cmd;
    if (!readStream.ReadInt32(cmd))
        returnfalse;

    int32_t seq;
    if (!readStream.ReadInt32(seq))
        returnfalse;

    size_t datalength;
    if (!readStream.ReadString(&strReturnData, 0, datalength))
    {
        returnfalse;
    }

    returntrue;
}

发送数据

bool CIUSocket::SendData(const char* pBuffer, int nBuffSize, int nTimeout)
{
    //TODO:这个地方可以先加个select判断下socket是否可写

    int64_t nStartTime = time(NULL);

    int nSentBytes = 0;
    int nRet = 0;
    while (true)
    {
        nRet = ::send(m_hSocket, pBuffer, nBuffSize, 0);
        if (nRet == SOCKET_ERROR)
        {
            //对方tcp窗口太小暂时发布出去,同时没有超时,则继续等待
            if (::WSAGetLastError() == WSAEWOULDBLOCK && time(NULL) - nStartTime < nTimeout)
            {
                continue;
            }
            else
                returnfalse;
        }
        elseif (nRet < 1)
        {
            //一旦出现错误就立刻关闭Socket
            LOG_ERROR("Send data error, disconnect server:%s, port:%d.", m_strServer.c_str(), m_nPort);
            Close();
            returnfalse;
        }

        nSentBytes += nRet;
        if (nSentBytes >= nBuffSize)
            break;

        pBuffer += nRet;
        nBuffSize -= nRet;

        ::Sleep(1);
    }

    returntrue;
}

接收数据

bool CIUSocket::RecvData(char* pszBuff, int nBufferSize, int nTimeout)
{
    int64_t nStartTime = time(NULL);

    fd_set writeset;
    FD_ZERO(&writeset);
    FD_SET(m_hSocket, &writeset);

    timeval timeout;
    timeout.tv_sec = nTimeout;
    timeout.tv_usec = 0;

    int nRet = ::select(m_hSocket + 1NULL, &writeset, NULL, &timeout);
    if (nRet != 1)
    {
        Close();
        returnfalse;
    }

    int nRecvBytes = 0;
    int nBytesToRecv = nBufferSize;
    while (true)
    {
        nRet = ::recv(m_hSocket, pszBuff, nBytesToRecv, 0);
        if (nRet == SOCKET_ERROR)              //一旦出现错误就立刻关闭Socket
        {
            if (::WSAGetLastError() == WSAEWOULDBLOCK && time(NULL) - nStartTime < nTimeout)
                continue;
            else
            {
               LOG_ERROR("Recv data error, disconnect server:%s, port:%d.", m_strServer.c_str(), m_nPort);
                Close();
                returnfalse;
            }
        }
        elseif (nRet < 1)
        {
            LOG_ERROR("Recv data error, disconnect server:%s, port:%d.", m_strServer.c_str(), m_nPort);
            Close();
            returnfalse;
        }

        nRecvBytes += nRet;
        if (nRecvBytes >= nBufferSize)
            break;

        pszBuff += nRet;
        nBytesToRecv -= nRet;

        ::Sleep(1);
    }

    returntrue;
}

3. 网络数据接收者接口设计

不少 PC 客户端软件,会设计一个接口,这个接口含有所有可以处理收到的网络数据的方法,也就是说所有继承自这个接口的子类都可以处理收到的网络数据。 这些子类会在程序初始化的时候,被加入到网络层或者数据加工层的一个接受者集合中去。 这样的话,网络层在收到数据后,遍历这个对象,然后利用 C++ 多态,分别调用各个改写的方法去处理具体的数据。

举个代码例子:

interface IMessageRevcer
{
    virtual CString GetRecverName() 0;
    virtual void OnDataPackRecv(GWBasePack *pPack);
    virtual void OnConnectEvent(GWConnectEventType nEvent) 0;
};

网络层分发数据时:

else if(pPack->nType == PT_MARKETINIT)
{
    GWMarketInitPack* pMarketInitPack = (GWMarketInitPack* )pPack;
    ASSERT(pMarketInitPack != NULL);
    // 广播市场初始化信息
    CAutoCritical lock(&m_lockRecver);
    //注意这一行,m_vecRecver就是一个存放IMessageRevcer具体子类实例指针的stl容器
    std::for_each(m_vecRecver.begin(), m_vecRecver.end(), bind2nd(mem_fun(&IMessageRevcer::OnDataPackRecv), pMarketInitPack));
    lock.UnLock();
}

而 flamingo 中是抛携带数据的消息到一个代理窗口中统一处理的,然后再分发到各处,这两种做法各有千秋吧。

这篇文章是这个系列的第一篇,是优化银狐主控框架的理论篇。限于篇幅有限,下一篇我们继续。欢迎关注。




优网科技,优秀企业首选的互联网供应服务商

优网科技秉承"专业团队、品质服务" 的经营理念,诚信务实的服务了近万家客户,成为众多世界500强、集团和上市公司的长期合作伙伴!

优网科技成立于2001年,擅长网站建设、网站与各类业务系统深度整合,致力于提供完善的企业互联网解决方案。优网科技提供PC端网站建设(品牌展示型、官方门户型、营销商务型、电子商务型、信息门户型、微信小程序定制开发、移动端应用(手机站APP开发)、微信定制开发(微信官网、微信商城、企业微信)等一系列互联网应用服务。


我要投稿

姓名

文章链接

提交即表示你已阅读并同意《个人信息保护声明》

专属顾问 专属顾问
扫码咨询您的优网专属顾问!
专属顾问
马上咨询