简介本资源是一个基于MFC框架的Windows平台Socket即时通讯编程示例面向具备Java Socket开发经验、希望转向更高性能C网络编程的中高级开发者。项目完整实现单服务器与多个客户端间的异步消息广播核心在于服务端使用CPtrList管理客户端socket连接并依托CSocket/CSocketFile/CArchive类体系构建轻量级通信模型代码结构清晰、注释详尽特别适合理解MFC网络编程与Java多线程模型的对应设计思想。压缩包共75个文件含15个头文件h、13个源码文件cpp、2个可执行程序exe及配套资源文件ico、rc、res等总大小3.44MB其中util目录封装了ChatSocket、Message等关键辅助类工程按功能分层组织便于快速定位服务端onAccept回调与客户端OnSendButton入口逻辑。目前已有756人学习下载是深入掌握MFC异步Socket编程、构建简易IM底层通信模块的优质实践范例。1. 一个服务器对多个客户端的MFC Socket编程示例实现简单的即时通讯功能不是写个accept()就算多线程真跑起来卡死、消息乱序、客户端断连不通知——这套方案专治 MFC 网络编程里的“玄学崩溃”你是不是也试过用 MFC 写了个 ServerWSAStartup成功、socket()成功、bind()成功、listen()成功……一调accept()界面就卡住加个AfxBeginThread结果发出去的消息在客户端显示成乱码或者 A 发的“你好”被 B 收到C 却收不到更糟的是某个客户端手滑关了程序服务器端毫无反应recv()一直阻塞整个服务线程挂死其他用户全掉线这不是代码没写完是架构没立住。这套「一个服务器对多个客户端的MFC Socket编程示例」就是为解决这些真实翻车现场而生它不堆砌 Win32 API 调用而是以可交付、可调试、可答辩为目标完整实现带连接管理、消息广播、异常断连检测、UI 线程安全更新的即时通讯最小闭环。适合正在做课程设计、毕业设计或需要快速验证网络通信逻辑的开发者——它不追求高并发百万级但保证 50 个客户端稳定在线时每条消息从发送到所有在线用户 UI 刷新延迟可控、日志可查、断连可感知。核心不在“能连”而在“连得稳、断得清、发得准”。2. 总体架构设计单服务进程 多工作线程 主UI线程三线协同模型与四类核心对象职责划分这套方案没有采用 IOCP 或完成端口这类重型机制而是基于 MFC 框架特性构建了一个轻量但健壮的三层协作模型主线程UI线程专注界面响应与用户操作服务监听线程专职accept()新连接每个客户端连接由独立的工作线程承载负责该连接的recv/send循环与协议解析所有跨线程数据交互通过线程安全队列与 Windows 消息机制完成。这种结构既规避了 MFC 窗口句柄跨线程调用的致命陷阱又避免了单线程轮询带来的高 CPU 占用和响应延迟。整个系统围绕四个核心对象展开职责划分CServerSocket继承自CSocket仅封装监听套接字的创建、绑定、监听逻辑不处理任何客户端通信纯粹作为“守门人”。CClientSession每个已连接客户端对应一个实例持有该连接的SOCKET句柄、客户端 IP/端口、连接时间、最后心跳时间并提供SendMsg()和Close()接口。它是数据收发的最小单元。CSessionManager全局单例管理所有活跃的CClientSession*指针容器std::vectorstd::shared_ptrCClientSession提供线程安全的Add(),Remove(),Broadcast()方法。所有广播逻辑、在线数统计、遍历操作均由此统一调度。CMainFrm / CChatDlgUI 层通过自定义 Windows 消息如WM_CLIENT_CONNECTED,WM_MSG_RECEIVED,WM_CLIENT_DISCONNECTED接收后台线程通知更新列表控件、聊天框、状态栏绝不直接调用CClientSession的send()或recv()。提示MFC 中CSocket的OnReceive()是异步回调但其执行上下文仍在 UI 线程。若在此回调中做耗时解析或直接调用Send()给其他客户端极易导致 UI 卡顿甚至死锁。本方案彻底弃用CSocket的事件回调机制改用WSAEventSelectWaitForMultipleObjects配合工作线程轮询将网络 I/O 完全剥离出 UI 线程。2.1 监听线程启动与连接接纳AfxBeginThread启动ListenThreadProc并传递CServerSocket*监听线程是整个服务的入口必须确保其永不退出且能及时响应新连接。我们不使用CSocket::AsyncSelect()而是手动创建监听套接字并启用事件驱动// ListenThreadProc.cpp UINT ListenThreadProc(LPVOID pParam) { CServerSocket* pServer (CServerSocket*)pParam; if (!pServer || !pServer-m_hAcceptEvent) return 1; // 设置监听套接字为非阻塞并关联事件对象 WSAEventSelect(pServer-m_hSocket, pServer-m_hAcceptEvent, FD_ACCEPT); while (pServer-m_bRunning) { DWORD dwRet WaitForSingleObject(pServer-m_hAcceptEvent, 1000); if (dwRet WAIT_OBJECT_0) { // 有新连接到达 sockaddr_in addrClient; int nAddrLen sizeof(addrClient); SOCKET hClientSock accept(pServer-m_hSocket, (sockaddr*)addrClient, nAddrLen); if (hClientSock ! INVALID_SOCKET) { // 创建会话对象并加入管理器 auto pSession std::make_sharedCClientSession(hClientSock, addrClient); CSessionManager::GetInstance()-Add(pSession); // 通知UI新客户端接入 ::PostMessage(AfxGetMainWnd()-m_hWnd, WM_CLIENT_CONNECTED, (WPARAM)pSession.get(), 0); } // 重置事件对象等待下一次FD_ACCEPT WSAResetEvent(pServer-m_hAcceptEvent); } else if (dwRet WAIT_TIMEOUT) { // 定期检查服务运行状态避免假死 continue; } else { break; // 其他错误退出线程 } } return 0; }这段代码的关键在于WSAEventSelect将套接字事件与内核事件对象绑定WaitForSingleObject实现低开销等待避免accept()阻塞主线程。PostMessage是跨线程通知 UI 的唯一安全方式参数WPARAM传入CClientSession*地址注意仅作标识UI 层不可解引用操作因会话对象生命周期由CSessionManager管理。2.2 客户端工作线程每个CClientSession独立RecvSendLoop每个客户端连接启动一个专属工作线程负责该连接的全部 I/O。这是实现“一个服务器对多个客户端”的核心载体。线程函数需处理三类事件接收数据、发送队列消费、连接异常检测。// CClientSession.cpp UINT CClientSession::WorkThreadProc(LPVOID pParam) { CClientSession* pThis (CClientSession*)pParam; if (!pThis) return 1; // 设置套接字为非阻塞 u_long nonBlocking 1; ioctlsocket(pThis-m_hSocket, FIONBIO, nonBlocking); char szBuffer[4096] {0}; int nBytes 0; DWORD dwLastActive GetTickCount(); while (pThis-m_bConnected) { // 1. 尝试接收数据 nBytes recv(pThis-m_hSocket, szBuffer, sizeof(szBuffer)-1, 0); if (nBytes 0) { szBuffer[nBytes] \0; // 解析协议假设为 MSG:用户名|内容 格式 pThis-ParseAndDispatch(szBuffer); dwLastActive GetTickCount(); } else if (nBytes 0) { // 对端正常关闭连接 pThis-m_bConnected FALSE; break; } else { int nError WSAGetLastError(); if (nError WSAEWOULDBLOCK) { // 无数据继续循环 Sleep(10); } else if (nError WSAECONNRESET || nError WSAENETDOWN) { // 连接被重置或网络中断 pThis-m_bConnected FALSE; break; } else { // 其他错误记录日志 LogError(_T(recv error %d on client %s), nError, pThis-m_strIP); pThis-m_bConnected FALSE; break; } } // 2. 检查是否超时心跳机制 if (GetTickCount() - dwLastActive 60000) { // 60秒无活动 pThis-m_bConnected FALSE; break; } // 3. 尝试发送队列中的消息线程安全队列 pop pThis-TrySendFromQueue(); } // 连接终止清理资源并通知管理器 CSessionManager::GetInstance()-Remove(pThis); ::PostMessage(AfxGetMainWnd()-m_hWnd, WM_CLIENT_DISCONNECTED, (WPARAM)pThis, 0); return 0; }TrySendFromQueue()内部使用CCriticalSection保护的std::queueCStringUI 线程调用CClientSession::EnqueueSend(CString msg)将待发消息压入队列工作线程在空闲时pop并send()。这种“生产者-消费者”模式彻底解耦 UI 与网络线程避免send()阻塞 UI。2.3 消息广播与会话管理CSessionManager的线程安全广播实现广播不是简单遍历send()必须考虑目标客户端是否仍在线、send()是否成功、失败后如何清理。CSessionManager::Broadcast()是核心业务逻辑出口// CSessionManager.cpp void CSessionManager::Broadcast(const CString strMsg, CClientSession* pExcept /* nullptr*/) { std::vectorstd::shared_ptrCClientSession vecOnline; // 1. 快照当前在线会话避免遍历时容器被修改 { CSingleLock lock(m_csLock, TRUE); vecOnline m_vecSessions; } // 2. 遍历快照对每个会话尝试发送 for (auto pSession : vecOnline) { if (!pSession || pSession.get() pExcept || !pSession-IsConnected()) { continue; } // 使用会话自身的发送队列而非直接 send() // 因为 send() 可能阻塞或失败交由工作线程处理更安全 pSession-EnqueueSend(strMsg); } }关键点在于“快照”先加锁拷贝m_vecSessions到局部vecOnline再释放锁然后遍历快照。这样即使在遍历过程中有其他线程调用Remove()也不会导致迭代器失效或访问已释放内存。EnqueueSend()将消息推入该会话的工作线程队列由其TrySendFromQueue()异步发出保证广播操作本身毫秒级返回不拖慢调用方。2.4 UI 线程消息响应CChatDlg中处理WM_CLIENT_CONNECTED等自定义消息MFC 对话框需在ON_MESSAGE宏中注册自定义消息处理器。这是 UI 与后台唯一合法的数据通道// CChatDlg.h afx_msg LRESULT OnClientConnected(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnMsgReceived(WPARAM wParam, LPARAM lParam); afx_msg LRESULT OnClientDisconnected(WPARAM wParam, LPARAM lParam); // CChatDlg.cpp BEGIN_MESSAGE_MAP(CChatDlg, CDialogEx) ON_MESSAGE(WM_CLIENT_CONNECTED, CChatDlg::OnClientConnected) ON_MESSAGE(WM_MSG_RECEIVED, CChatDlg::OnMsgReceived) ON_MESSAGE(WM_CLIENT_DISCONNECTED, CChatDlg::OnClientDisconnected) END_MESSAGE_MAP() LRESULT CChatDlg::OnClientConnected(WPARAM wParam, LPARAM lParam) { CClientSession* pSession (CClientSession*)wParam; if (pSession) { // 更新在线用户列表ListCtrl int nItem m_listUsers.InsertItem(m_listUsers.GetItemCount(), pSession-m_strIP); m_listUsers.SetItemText(nItem, 1, pSession-m_strPort); m_listUsers.SetItemText(nItem, 2, _T(在线)); // 更新状态栏 CString strStatus; strStatus.Format(_T(在线用户%d), CSessionManager::GetInstance()-GetCount()); m_wndStatusBar.SetPaneText(0, strStatus); } return 0; } LRESULT CChatDlg::OnMsgReceived(WPARAM wParam, LPARAM lParam) { MSG_INFO* pMsg (MSG_INFO*)wParam; // MSG_INFO 结构体含 senderIP, content 等 if (pMsg) { // 格式化消息并追加到聊天框RichEditCtrl CString strLog; strLog.Format(_T([%s] %s\r\n), pMsg-strSenderIP, pMsg-strContent); m_richEdit.AppendText(strLog); // 滚动到底部 m_richEdit.PostMessage(EM_SCROLLCARET, 0, 0); delete pMsg; // 由UI线程释放 } return 0; }注意OnMsgReceived中wParam传入的是new MSG_INFO分配的堆内存地址UI 线程处理完必须delete否则内存泄漏。这是跨线程传递复杂数据的常用手法比CString更灵活可携带结构化信息。3. 关键模块实现协议设计、心跳保活、UI线程安全更新与异常断连检测即时通讯的“即时”二字不仅指发送快更指状态准、断连清、消息不丢。本节聚焦四个让项目从“能跑”升级到“可靠”的硬核模块。3.1 文本协议设计MSG:前缀 |分隔 UTF8 编码兼顾可读性与扩展性不采用二进制协议是因为课程设计/毕设阶段首要目标是可调试、可验证、可演示。明文协议让 Wireshark 抓包一目了然也让测试脚本如 Python socket 脚本能轻松模拟客户端。协议格式定义为MSG:用户名|消息内容 HEARTBEAT: LIST_USERS:MSG:开头表示普通聊天消息|左侧为发送者标识可为 IP 或自定义昵称右侧为 UTF8 编码的正文。服务端收到后自动在消息前添加[IP:端口]前缀再广播确保客户端无需解析即可显示来源。HEARTBEAT:是客户端主动发送的心跳包服务端收到后刷新该会话的dwLastActive时间戳用于超时检测。LIST_USERS:是客户端请求在线用户列表服务端回复USERS:IP1:PORT1|IP2:PORT2|...格式字符串。所有字符串均以\r\n结尾recv()时按行解析find(\r\n)避免粘包。UTF8 编码通过CT2CA转换实现// 发送前编码 CString strToSend _T(MSG:张三|你好世界\r\n); CT2CA pszConverted(strToSend); // CT2CA 自动转为 ANSI/UTF8 send(m_hSocket, pszConverted, strlen(pszConverted), 0);注意MFC 默认使用 ANSI 编码若需支持中文项目属性中必须设置“字符集”为“使用 Unicode 字符集”否则CT2CA转换会丢失中文。这是学生项目里最高频的“消息变问号”原因。3.2 心跳保活与超时检测双机制兜底拒绝“僵尸连接”网络不稳定时TCP 连接可能处于半开状态recv()不返回 0但send()会失败。仅靠recv()返回值无法及时发现。本方案采用“应用层心跳 TCP KeepAlive”双保险应用层心跳客户端每 30 秒发送HEARTBEAT:\r\n服务端工作线程维护dwLastActive时间戳。若GetTickCount() - dwLastActive 60000即 60 秒无任何活动强制断开连接。此机制可捕获网络中断、客户端崩溃等场景。TCP KeepAlive在accept()后立即启用作为底层兜底// 启用 TCP KeepAlive DWORD dwKeepAlive 1; setsockopt(hClientSock, SOL_SOCKET, SO_KEEPALIVE, (char*)dwKeepAlive, sizeof(dwKeepAlive)); // Windows 下需通过 ioctlsocket 设置间隔非标准但有效 tcp_keepalive ka; ka.onoff 1; ka.keepalivetime 60000; // 60秒后开始探测 ka.keepaliveinterval 5000; // 每5秒探测一次 DWORD dwBytes; WSAIoctl(hClientSock, SIO_KEEPALIVE_VALS, ka, sizeof(ka), NULL, 0, dwBytes, NULL, NULL);双机制确保网络闪断时应用层心跳在 60 秒内发现客户端进程僵死但 TCP 连接未关闭时TCP KeepAlive 在 65 秒内605触发recv()返回错误工作线程捕获并清理。3.3 UI 线程安全更新PostMessageCString拷贝 CMapStringToString缓存UI 控件如CListCtrl,CRichEditCtrl只能由创建它的线程UI 线程访问。工作线程若直接调用m_listUsers.InsertItem()程序大概率崩溃。安全做法是工作线程PostMessage发送消息UI 线程在消息处理器中操作控件。但PostMessage的wParam/lParam只能传 32/64 位整数无法直接传CString。解决方案是在工作线程堆上分配CStringPostMessage传地址UI 线程处理完delete。为防重复释放使用CMapStringToString缓存待处理消息// 工作线程中 CString* pMsg new CString(_T([192.168.1.100:5000] 你好)); ::PostMessage(AfxGetMainWnd()-m_hWnd, WM_MSG_RECEIVED, (WPARAM)pMsg, 0); // UI 线程中CChatDlg.cpp LRESULT CChatDlg::OnMsgReceived(WPARAM wParam, LPARAM lParam) { CString* pStr (CString*)wParam; if (pStr) { m_richEdit.AppendText(*pStr _T(\r\n)); delete pStr; // 必须释放 } return 0; }提示“忘记 delete”是导致内存泄漏的头号原因。建议在CChatDlg析构函数中遍历CMapStringToString清理残留或改用智能指针如std::shared_ptrCString但需确保 MFC 项目支持 C11。3.4 异常断连检测WSAGetLastError()错误码精准识别与分级处理recv()返回 -1 时WSAGetLastError()的值决定后续动作。不能一概closesocket()必须分级错误码含义处理方式WSAEWOULDBLOCK非阻塞套接字无数据忽略继续循环WSAECONNRESET对端强行关闭RST包立即标记m_bConnectedFALSE退出循环WSAENETDOWN网络接口断开同上需记录日志WSAETIMEDOUT连接超时罕见同上WSAENOTCONN套接字未连接理论不应出现记严重错误工作线程中必须对每个错误码编写明确分支而非笼统if (nBytes SOCKET_ERROR)。例如else { int nError WSAGetLastError(); switch (nError) { case WSAEWOULDBLOCK: Sleep(10); break; case WSAECONNRESET: case WSAENETDOWN: case WSAETIMEDOUT: LogInfo(_T(Client %s disconnected due to network error %d), m_strIP, nError); m_bConnected FALSE; break; default: LogError(_T(Unexpected recv error %d from %s), nError, m_strIP); m_bConnected FALSE; break; } }精准识别错误码是写出健壮网络程序的基本功。很多“随机崩溃”都源于对WSAETIMEDOUT或WSAENOTCONN的忽略。4. 常见问题排查MFC Socket编程中5个最易翻车的点与血泪解决方案写 MFC Socket 最痛苦的不是写不出而是写出来后“时好时坏”“重启就崩”“消息偶尔乱序”。这些问题往往源于对 MFC 线程模型和 Windows 网络栈的误解。以下是我在带多个模拟项目X 时学生踩坑频率最高的 5 个点每一条都附带现象、根因和可立即执行的修复方案。4.1 现象UI 界面卡死鼠标变成沙漏但accept()之后的代码明明没写死循环原因CSocket::Create()或socket()创建后未调用ioctlsocket(hSocket, FIONBIO, nonBlocking)设置为非阻塞模式导致accept()或recv()在 UI 线程中阻塞。解决彻底弃用CSocket的AsyncSelect()机制它依赖 UI 线程消息泵易受阻塞影响。所有套接字监听套接字、客户端套接字创建后立即设置非阻塞u_long nonBlocking 1; ioctlsocket(hSocket, FIONBIO, nonBlocking);监听逻辑必须放入AfxBeginThread启动的独立线程accept()调用必须在该线程内。4.2 现象客户端发送中文服务器收到乱码如“浣犲ソ”或部分中文显示为方块原因项目字符集设置为“多字节字符集MBCS”但网络传输使用 UTF8 编码CString默认构造函数按 ANSI 解析。解决项目属性 → 常规 → 字符集 →选择“使用 Unicode 字符集”强制要求。发送前用CT2CA显式转换为 UTF8CString strMsg _T(你好); // Unicode 字符串 CT2CA pszUtf8(strMsg); // 转为 const char* send(hSocket, pszUtf8, strlen(pszUtf8), 0);接收后用CA2CT将 UTF8 转回 Unicodechar szBuf[1024]; int n recv(hSocket, szBuf, sizeof(szBuf)-1, 0); if (n 0) { szBuf[n] \0; CA2CT pszUnicode(szBuf); // UTF8 - Unicode CString strUnicode pszUnicode; // 此时 strUnicode 可正确显示中文 }4.3 现象客户端关闭后服务器端recv()一直返回 0但CSessionManager中该会话未被移除Broadcast()仍向其发送导致send()失败原因recv()返回 0 表示对端正常关闭FIN 包但工作线程未及时跳出循环并调用CSessionManager::Remove()。解决工作线程recv()循环中必须显式检查nBytes 0if (nBytes 0) { // 对端优雅关闭立即终止本线程 m_bConnected FALSE; break; // 退出 recv 循环 }break后线程函数末尾必须执行CSessionManager::GetInstance()-Remove(this)和PostMessage(WM_CLIENT_DISCONNECTED)。严禁在nBytes 0时只打印日志而不break这是“僵尸连接”的根源。4.4 现象多个客户端同时发送消息服务器广播时A 的消息被 C 收到B 却收不到或消息顺序错乱原因CSessionManager::Broadcast()直接遍历std::vector并调用send()而send()在非阻塞套接字上可能返回SOCKET_ERROR且WSAGetLastError() WSAEWOULDBLOCK此时消息实际未发出但代码未重试也未入队。解决Broadcast()绝不直接调用send()必须调用CClientSession::EnqueueSend()将消息压入该会话的线程安全发送队列。每个CClientSession工作线程在TrySendFromQueue()中循环pop并send()若send()返回SOCKET_ERROR且WSAGetLastError() WSAEWOULDBLOCK则push_back()回队列尾部稍后重试。队列使用CCriticalSection保护确保push/pop原子性。4.5 现象程序运行一段时间后CPU 占用飙升至 100%任务管理器看到多个AfxWinMain线程原因工作线程中Sleep(0)或Sleep(1)时间过短导致线程频繁唤醒陷入“忙等”busy-waiting。解决工作线程recv()循环中WSAEWOULDBLOCK分支的Sleep()时间必须 ≥ 10msif (nError WSAEWOULDBLOCK) { Sleep(10); // 关键不能是 Sleep(0) 或 Sleep(1) }更优方案使用select()或WSAEventSelect配合WaitForMultipleObjects让线程真正休眠直到有 I/O 事件发生。本方案监听线程已采用此法客户端工作线程亦可升级。5. 方案落地与验收从编译通过到答辩演示的6步闭环流程与3个必验场景一份能交差的课程设计/毕设不在于代码行数而在于能否在导师面前稳定、清晰、可解释地跑通核心流程。我带过的某高校课程设计中80% 的学生倒在“能编译但跑不通”或“跑通了但讲不清为什么”。以下是我提炼的 6 步闭环流程以及答辩前必须亲自验证的 3 个硬核场景——它们直接对应评分标准里的“功能完整性”、“稳定性”、“可维护性”。5.1 6步闭环流程从零开始到可演示的标准化路径这 6 步不是理想化步骤而是我每次帮学生搭环境时强制执行的 checklist确保不遗漏任何隐性依赖环境初始化安装 Visual Studio 2019或 VS2022创建 MFC 应用程序基于对话框项目属性 → 常规 → 字符集 → 使用 Unicode 字符集此步遗漏后续中文全废。网络库链接项目属性 → 链接器 → 输入 → 附加依赖项 → 添加ws2_32.libWindows Sockets 2 库无此库socket()等函数链接失败。头文件与宏定义在stdafx.h或pch.h中添加#include winsock2.h #include ws2tcpip.h #pragma comment(lib, ws2_32.lib) #define WM_CLIENT_CONNECTED (WM_USER 101) #define WM_MSG_RECEIVED (WM_USER 102) #define WM_CLIENT_DISCONNECTED (WM_USER 103)UI 控件布局在对话框资源中拖入ListCtrlID:IDC_LIST_USERS风格Report、RichEditCtrlID:IDC_RICHEDIT_CHAT、EditBoxID:IDC_EDIT_INPUT、ButtonID:IDC_BTN_SEND。线程安全类实现新建CSessionManager.h/cpp实现单例、Add()/Remove()/Broadcast()内部std::vector加CCriticalSection保护新建CClientSession.h/cpp实现工作线程、EnqueueSend()、TrySendFromQueue()。消息映射与启动在CChatDlg中ON_MESSAGE注册三个自定义消息在OnInitDialog()中调用AfxBeginThread(ListenThreadProc, pServerSocket)启动监听线程在OnBtnSend()中获取EditBox文本调用CSessionManager::Broadcast()。提示第 3 步的#define WM_XXX必须放在stdafx.h否则不同.cpp文件中宏值不一致PostMessage会发错消息。这是“消息收不到”的隐藏元凶。5.2 3个必验场景答辩前亲手跑通覆盖90%扣分点不要只测“两个客户端聊天”导师最可能问的问题都藏在这三个极端场景里。每个场景我都附上验证方法和预期结果场景验证步骤预期结果为什么必验场景1异常断连韧性1. 启动 Server2. 启动 ClientA、ClientB3. ClientA 发送“测试1”4.手动结束 ClientB 进程任务管理器 → 结束任务5. ClientA 再发“测试2”Server 界面“在线用户”数从 2 变为 1ClientA 收到“测试1”和“测试2”且“测试2”不显示 ClientB 的头像/名称因 ClientB 已离线Server 日志显示 “Client 127.0.0.1:5001 disconnected due to network error 10054”验证WSAECONNRESET捕获、CSessionManager::Remove()执行、广播逻辑自动过滤离线用户。这是“稳定性”核心指标。场景2中文与特殊字符1. Server 启动2. ClientA 发送MSG:小明Hello! 你好世界#$%^*()3. ClientB 发送MSG:小红测试换行第二行注意 是纯文本非 HTML场景3高负载下的消息时序1. 启动 Server2. 启动 5 个 Client可用 Python 脚本模拟3. 所有 Client 同时发送 10 条消息如 “MSG:A1”, “MSG:A2”, …4. 观察 Server RichEdit 中消息顺序5.3 答辩演示技巧3句话讲清架构1个截图锁定亮点导师没时间听你逐行讲代码。我的经验是用一张图 三句话建立技术可信度再用一个动态截图证明功能真实。第一句话定位“这是一个基于 MFC 框架的 C/S 即时通讯最小系统核心目标是验证‘一个服务器稳定支撑多个客户端’的工程可行性而非追求百万并发。”第二句话架构“它采用三线程模型UI 线程管界面、监听线程管接入、每个客户端一个工作线程管通信所有跨线程数据通过PostMessage和线程安全队列传递彻底规避 MFC 跨线程调用陷阱。”第三句话亮点“我们实现了应用层本文还有配套的精品资源点击获取