最近拿到了一份银狐远控(winos)的源码,阅读之后,大为赞赏。但其稳定性也存在诸多问题,这些稳定性有整个框架上的设计缺陷,也有细粒度的编码问题。主控最大的问题有如下三点:
- 部分数据结构在网络层和UI层随意传递,生命周期不明,可能引起各类崩溃;
- 被控每增加一个插件,主控就要在主程序中固化一段针对该插件的逻辑,可扩展性差;
- 网络收包和解包逻辑分散各处,缺乏统一出入口,协议改造或功能增强时工作量巨大,维护困难。
于是借这个优化契机,分享一些架构与设计层面的思考,希望对从事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. 内存泄漏风险高
工作线程 new → PostMessage 给窗口 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,支持单聊、群聊、自定义资料等。
项目目录结构

三层架构图

仅靠 UI 层与网络层无法兼顾流畅性与职责单一性,因此引入数据加工层作为中间枢纽:
- 自上而下:UI 层发起请求(如“发送消息”),若涉及耗时加工(序列化、加密、压缩),则交由数据加工层处理,再交网络层发出;
- 自下而上:网络层收到原始数据包,解包后交数据加工层转换为 UI 层可消费的格式(如
CCreateNewGroupResult 结构体),再 PostMessage 至 UI。
该层本质是一组任务线程池,每个线程从专属队列取任务执行:

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 层与网络层合并)。其优势在于职责清晰、易于扩展;但细节上仍有优化空间:
- 请求无状态、无反馈:任务投入
SendMsgThread 后,若在数据加工层或网络层失败,UI 层无法获知,无法自动重试。改进方向:UI 层记录请求 ID + 启动定时器,超时未响应则重发。
- 网络层收发不必分离:前文已论证,单线程收发更简洁可靠。当前 Flamingo 的双线程模型正为此付出高昂维护代价,尤其是断线重连逻辑(需区分用户主动下线、掉线、被踢下线)。
- 可抽象公共模块变量:部分高频读写数据(如用户登录态、网络连接标志)可抽为跨线程共享变量(加锁访问),减少
PostMessage + new 的开销。此法在 Java(Android)中天然友好,C++ 中需谨慎设计。
- 缺少统一定时器模块:UI 层需定时重试,网络层需定时发心跳、自动重连。一个健壮的定时器服务(如基于
SetTimer 或 WaitForMultipleObjects)是客户端基础设施的重要一环。
三、值得借鉴的客户端设计技巧
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++ 底层开发、网络/系统 编程或 后端 & 架构 设计感兴趣,欢迎加入云栈社区交流讨论。