找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2616

积分

0

好友

364

主题
发表于 12 小时前 | 查看: 0| 回复: 0

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

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

于是借这个优化契机,分享一些架构与设计层面的思考,希望对从事PC端软件结构设计的同学有所启发。

注意:以下内容聚焦于 PC 端软件,但其核心思想(如分层、线程职责划分、消息通信机制)同样适用于移动端客户端开发。

一、困扰多年的架构困惑

互联网从业多年,设计过多个PC端产品,让软件性能卓越、界面流畅、用户体验好一直是我追求的目标。然而,从接触 Windows 程序设计以来,我一直被以下三类问题持续困扰:

1. 对 UI 流畅性的追求

Windows 程序的消息机制决定了主线程即 UI 线程(无 GUI 程序除外),因此界面是否卡顿,很大程度上取决于 UI 线程处理 Windows 消息的耗时。单个消息处理越长,界面响应越迟滞;反之则更可能保持流畅——注意,这只是 UI 流畅性的必要条件之一,并非全部。

为保障流畅性,常见做法是将耗时操作(如网络收发、大文件磁盘读写)移至工作线程执行。但这一认知曾让我走入极端:一度认为“所有非 UI 逻辑都应搬离主线程”。这种想法看似合理,实则存在两点根本性误区:

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

只要 UI 线程不阻塞、不做用户可感知的耗时操作(如毫秒级以上的同步等待),即使千行代码运行在主线程中,也不会影响体验。现代 CPU 执行效率极高,多数消息处理耗时远低于人类感知阈值(约 16ms)。而 Windows 消息队列绝大多数时间为空闲状态(Idle time),若不加以利用,反而是资源浪费。

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

开启新线程不仅带来创建开销,还需配套实现线程间通信(如 PostMessage + 堆内存管理)、状态同步(如 m_bConnected 字段需加锁保护)、重连逻辑协调等。这直接导致代码结构臃肿、调试困难、出错概率上升。

因此我的观点是:让 UI 流畅的关键,在于避免长时间阻塞 UI 线程,而非机械地将一切非 UI 逻辑外迁。FileZilla 的 CAsyncSocketEx 类正是如此实践的典范(FileZilla 源码,电驴客户端亦复用该类:easyMule 源码)。

其核心思想是:利用 Windows 原生异步 I/O 机制(WSAAsyncSelect),将 socket 可读/可写事件映射为窗口消息,由 UI 线程统一调度处理。虽逻辑量大,但因不阻塞、不等待,界面依然丝滑。

简化后的主干框架如下:

// 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行代码
    }
    else if (message == WM_USER) // Notification event sent by a layer
    {
        // 此处省略138行代码
    }
    else if (message == WM_USER + 1)
    {
        // 此处省略40行代码
    }
    else if (message == WM_USER + 2)
    {
        // 此处省略23行代码
    }
    else if (message == WM_TIMER)
    {
        // 此处省略21行代码
    }
    return DefWindowProc(hWnd, message, wParam, lParam);
}

上述 WM_SOCKETEX_NOTIFY 分支中处理 socket 收发,因前置已通过 WSAAsyncSelect 检测 socket 状态,故实际执行不会阻塞。这种“大块逻辑驻留主线程 + 异步事件驱动”的模式,是 Windows 平台下兼顾性能与简洁性的经典范式。

Windows 还提供了专用的 HWND_MESSAGE(Message-Only Windows)类型窗口,专用于承载非 UI 逻辑,进一步解耦关注点:MSDN 文档

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

理想中的网络层应专注通信细节(连接、收发、心跳、重连),屏蔽业务逻辑。但现实中,边界常被模糊甚至打破。典型矛盾如下:

a. 解包与业务逻辑的归属之争

当通信协议不统一(字段数量/类型随协议类型动态变化)时,“解包”究竟属于网络层还是上层?例如重连机制:除系统级 connect() 重试外,业务重连(如重新发送登录包)必然依赖用户名、密码等信息——而这显然不属于网络层职责。即便封装为 Relogin() 接口,其内部状态(如登录态、token)仍需与 UI 层强耦合。

b. 心跳包的归属问题

心跳包用于维持连接存活、触发断线重连,其状态需实时反映在 UI 上(如显示“已断开”、“重连中”),甚至支持用户干预(点击“重试”按钮、主动下线)。一旦用户手动下线,心跳必须立即停止——这意味着心跳逻辑无法完全独立于 UI 层。

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

传统思路是:socket 全双工,收/发各开一线程,互不影响。但由此引发严重复杂度:

  • 两线程同时操作 m_bConnected 等共享状态,必须加锁 → 性能损耗;
  • 定时重连逻辑需决策:由收线程触发?发线程触发?另一线程是否暂停?
  • 用户主动下线、被踢下线等场景需差异化处理,使重连分支爆炸式增长。

反思后发现:对绝大多数 PC 客户端而言,收发同线程带来的性能损失,用户根本无法感知;而它换来的却是结构清晰、状态可控、调试简易的巨大收益。因此,推荐将收发逻辑收敛至单一线程内统一调度

3. 工作线程与 UI 层的通信难题

为避免跨线程直接操作 UI 控件(Windows 允许但不推荐,Android 则严格禁止),主流方案是工作线程调用 PostMessage 向 UI 线程投递消息。但若需携带复杂数据,则必须使用堆内存(new 分配),并确保 UI 线程处理完毕后 delete ——这极易引发两类问题:

a. 内存泄漏风险高

工作线程 newPostMessage 给窗口 A → A 处理或转发给窗口 B → B 再转给 C……任一中间环节未正确 delete 或窗口提前销毁,内存即永久泄漏。尤其在多级中转、窗口生命周期不一致时,管理成本极高。

尝试用 std::shared_ptr / std::unique_ptr 解决?但智能指针跨线程安全性并非默认保证,需额外同步;而自研内存池对多数团队而言门槛过高。

b. 频繁 new/delete 导致内存碎片

虽客户端通常无需过度担忧,但长期运行的 IM、远程控制类软件,内存碎片累积仍可能影响稳定性。

另一种思路是全局对象 + getter/setter,但多线程读写仍需加锁 → 回归性能瓶颈。

如何破局?我目前的实践是:自定义引用计数型智能指针,确保跨线程安全释放。核心在于使用 Windows 原子操作(InterlockedIncrement/InterlockedDecrement)保障计数器线程安全,并将 PostMessage 携带的对象继承自该基类。示例代码如下:

class TTPUBAPI atomic_count
{
public:
    explicit atomic_count(long v = 0);
    long increment();
    long decrement();
    long value() const;
private:
    atomic_count( atomic_count const & );
    atomic_count & operator=( atomic_count const & );
    long volatile 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
{
    return static_cast<long const volatile &&>( 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);
    bool operator==(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_);
}

该方案将内存生命周期与消息生命周期绑定,彻底规避了手动 new/delete 的风险,且原子操作开销极低,已在多个项目中稳定运行。

二、Flamingo 即时通讯客户端架构剖析

上述理论探讨偏抽象,下面我们以个人开发的即时通讯软件 Flamingo(PC 端)为案例,落地分析其三层架构设计。基础功能对标 QQ,支持单聊、群聊、自定义资料等。

项目目录结构

Flamingo 项目目录结构,突出 UserData 文件夹被高亮选中

三层架构图

Flamingo 三层架构示意图:UI层 → 数据加工层 → 网络层,双向箭头表示数据流动

仅靠 UI 层与网络层无法兼顾流畅性与职责单一性,因此引入数据加工层作为中间枢纽:

  • 自上而下:UI 层发起请求(如“发送消息”),若涉及耗时加工(序列化、加密、压缩),则交由数据加工层处理,再交网络层发出;
  • 自下而上:网络层收到原始数据包,解包后交数据加工层转换为 UI 层可消费的格式(如 CCreateNewGroupResult 结构体),再 PostMessage 至 UI。

该层本质是一组任务线程池,每个线程从专属队列取任务执行:

Flamingo 数据加工层线程列表:FileTaskThread、HeartbeatTask、ImageTaskThread、RecvMsgThread、SendMsgThread 等

  • SendMsgThread:将 UI 请求加工为网络数据格式,放入网络层发送缓冲区;
  • RecvMsgThread:将网络数据加工为 UI 可识别结构,PostMessage 给代理窗口;
  • FileTaskThread / ImageTaskThread:分别处理文件传输、图片上传等专项任务。

1. 数据加工层实现细节

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();
}

任务处理完成后,生成 UI 所需数据并 PostMessage

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;

    // 类似代码省略

    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. 网络层设计(收发同线程)

Flamingo 的网络层采用单 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((const char*)&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();
        }
        // 无数据
        else if (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();
            }
        }
        // 有数据
        else if (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();
            return false;
        }

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

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

        m_pRecvMsgThread->AddMsgData(strBody);
    }

    return true;
}

3. UI 层:代理窗口与统一消息分发

UI 层包含两类实体:可视化界面(对话框、列表窗等)与一个隐藏的 HWND_MESSAGE 代理窗口。所有来自数据加工层的消息均先投递至此窗口,再由其集中分发至目标 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, 0, 0);
        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);
    }
    return 0;
}

该代理窗口的核心价值在于:提供消息统一入口,将 UI 逻辑集中收敛,避免散落在各控件中难以维护

UI 层与数据加工层的交互,通过 CFlamingoClient 单例类提供的丰富接口完成:

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;
};

以删除好友为例,UI 层仅需调用接口,任务即被投入 SendMsgThread 队列:

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

    m_SendMsgThread.AddItem(pRequest);

    return TRUE;
}

Flamingo 的三层架构,代表了当前大多数 Windows 客户端的通用范式(FileZilla 则通过 WSAAsyncSelect 将 UI 层与网络层合并)。其优势在于职责清晰、易于扩展;但细节上仍有优化空间:

  1. 请求无状态、无反馈:任务投入 SendMsgThread 后,若在数据加工层或网络层失败,UI 层无法获知,无法自动重试。改进方向:UI 层记录请求 ID + 启动定时器,超时未响应则重发。
  2. 网络层收发不必分离:前文已论证,单线程收发更简洁可靠。当前 Flamingo 的双线程模型正为此付出高昂维护代价,尤其是断线重连逻辑(需区分用户主动下线、掉线、被踢下线)。
  3. 可抽象公共模块变量:部分高频读写数据(如用户登录态、网络连接标志)可抽为跨线程共享变量(加锁访问),减少 PostMessage + new 的开销。此法在 Java(Android)中天然友好,C++ 中需谨慎设计。
  4. 缺少统一定时器模块:UI 层需定时重试,网络层需定时发心跳、自动重连。一个健壮的定时器服务(如基于 SetTimerWaitForMultipleObjects)是客户端基础设施的重要一环。

三、值得借鉴的客户端设计技巧

1. 任务对象的生命周期管理:自我释放 vs 父模块释放

任务对象(如 COperateFriendRequest)由谁负责 delete?常见两种策略:

  • 父模块释放:由创建者(如 SendMsgThread)在任务执行完毕后统一 delete。优点是所有权清晰;缺点是需维护对象生命周期,易遗漏。
  • 自我释放:任务对象提供 release() 接口,内部执行 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():执行资源清理并 delete this
  • cancel():标记任务取消,供执行中检查。

该模式将释放责任内聚于对象自身,降低外部管理负担,但要求调用方严格遵循“只调用一次 release”原则。

2. 同步且带超时的网络接口设计

登录等初始化操作,常需同步等待服务器响应(如获取 token、用户资料)。此时不宜阻塞 UI 线程,而应:

a. 点击登录按钮后,启动新工作线程;
b. 线程内调用网络 API,socket 设为非阻塞,用 select 控制超时;
c. 收到响应或超时后,PostMessage 将结果返回 UI。

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(NULL, 0, LoginThreadProc, this, 0, NULL);
    if (hLoginThread != NULL)
        ::CloseHandle(hLoginThread);

    EndDialog(IDOK);
}

登录线程函数

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

    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, 1, 1, 3000, 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());
                }
            }
            else if (nRetCode == 102)
                pLoginResult->m_LoginResultCode = LOGIN_UNREGISTERED;
            else if (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);

    return 1;
}

登录网络接口(含超时控制)

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

    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((const char*)&header, sizeof(header));
    strSendBuf.append(outbuf.c_str(), length);

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

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

    if (header.packagesize <= 0)
        return false;

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

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

    int32_t seq;
    if (!readStream.ReadInt32(seq))
        return false;

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

    return true;
}

发送数据(带超时)

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
                return false;
        }
        else if (nRet < 1)
        {
            //一旦出现错误就立刻关闭Socket
            LOG_ERROR("Send data error, disconnect server:%s, port:%d.", m_strServer.c_str(), m_nPort);
            Close();
            return false;
        }

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

        pBuffer += nRet;
        nBuffSize -= nRet;

        ::Sleep(1);
    }

    return true;
}

接收数据(带超时)

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 + 1, NULL, &writeset, NULL, &timeout);
    if (nRet != 1)
    {
        Close();
        return false;
    }

    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();
                return false;
            }
        }
        else if (nRet < 1)
        {
            LOG_ERROR("Recv data error, disconnect server:%s, port:%d.", m_strServer.c_str(), m_nPort);
            Close();
            return false;
        }

        nRecvBytes += nRet;

        if (nRecvBytes >= nBufferSize)
            break;

        pszBuff += nRet;
        nBytesToRecv -= nRet;

        ::Sleep(1);
    }

    return true;
}

3. 网络数据接收者接口设计(观察者模式)

部分客户端采用接口抽象 + 多态分发的方式处理网络数据:

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

初始化时,将各业务模块(如好友管理、群管理、消息管理)的实例注册进网络层的 m_vecRecver 容器。收到数据后,遍历容器,调用对应 OnDataPackRecv 方法:

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 采用的是“代理窗口统一接收 + 分发”模式,二者各有优劣:

  • 观察者模式:松耦合、易扩展(新增业务模块只需实现接口并注册),但需维护容器生命周期、线程安全;
  • 代理窗口模式:逻辑集中、调试直观、消息流清晰,但需维护大量 case 分支,新增消息类型需修改核心分发逻辑。

选择哪种,取决于团队规模、项目阶段及对可维护性的侧重。

本文是“客户端软件结构设计思考”系列的第一篇,聚焦银狐主控的架构痛点与 Flamingo 的实践验证。后续将深入探讨协议抽象、插件热加载、跨平台适配等进阶议题。

如你对 C/C++ 底层开发、网络/系统 编程或 后端 & 架构 设计感兴趣,欢迎加入云栈社区交流讨论。




上一篇:Cloudflare WAF 0Day漏洞分析:攻击者可借ACME挑战路径绕过防护直连后端
下一篇:英国政府ARIA资助AI科学家项目,推动实验室自动化与自主科研新范式
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-1-24 16:35 , Processed in 0.247882 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表