简介:本资源是一个基于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 写了个 Server,WSAStartup成功、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::vector<std::shared_ptr<CClientSession>>),提供线程安全的Add(),Remove(),Broadcast()方法。所有广播逻辑、在线数统计、遍历操作均由此统一调度。 - CMainFrm / CChatDlg:UI 层,通过自定义 Windows 消息(如
WM_CLIENT_CONNECTED,WM_MSG_RECEIVED,WM_CLIENT_DISCONNECTED)接收后台线程通知,更新列表控件、聊天框、状态栏,绝不直接调用CClientSession的send()或recv()。
提示:MFC 中
CSocket的OnReceive()是异步回调,但其执行上下文仍在 UI 线程。若在此回调中做耗时解析或直接调用Send()给其他客户端,极易导致 UI 卡顿甚至死锁。本方案彻底弃用CSocket的事件回调机制,改用WSAEventSelect+WaitForMultipleObjects配合工作线程轮询,将网络 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_shared<CClientSession>(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::queue<CString>,UI 线程调用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::vector<std::shared_ptr<CClientSession>> 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 秒内(60+5)触发recv()返回错误,工作线程捕获并清理。
3.3 UI 线程安全更新:PostMessage+CString拷贝 +CMapStringToString缓存
UI 控件(如CListCtrl,CRichEditCtrl)只能由创建它的线程(UI 线程)访问。工作线程若直接调用m_listUsers.InsertItem(),程序大概率崩溃。安全做法是:工作线程PostMessage发送消息,UI 线程在消息处理器中操作控件。
但PostMessage的wParam/lParam只能传 32/64 位整数,无法直接传CString。解决方案是:在工作线程堆上分配CString,PostMessage传地址,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_ptr<CString>),但需确保 MFC 项目支持 C++11。
3.4 异常断连检测:WSAGetLastError()错误码精准识别与分级处理
recv()返回 -1 时,WSAGetLastError()的值决定后续动作。不能一概closesocket(),必须分级:
| 错误码 | 含义 | 处理方式 |
|---|---|---|
WSAEWOULDBLOCK | 非阻塞套接字无数据 | 忽略,继续循环 |
WSAECONNRESET | 对端强行关闭(RST包) | 立即标记m_bConnected=FALSE,退出循环 |
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显式转换为 UTF8:CString strMsg = _T("你好"); // Unicode 字符串 CT2CA pszUtf8(strMsg); // 转为 const char* send(hSocket, pszUtf8, strlen(pszUtf8), 0); - 接收后,用
CA2CT将 UTF8 转回 Unicode:char 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 == 0:if (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()时间必须 ≥ 10ms:if (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.lib(Windows 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 控件布局:在对话框资源中,拖入
ListCtrl(ID:IDC_LIST_USERS,风格:Report)、RichEditCtrl(ID:IDC_RICHEDIT_CHAT)、EditBox(ID:IDC_EDIT_INPUT)、Button(ID: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. 启动 Server;2. 启动 ClientA、ClientB;3. ClientA 发送“测试1”;4.手动结束 ClientB 进程(任务管理器 → 结束任务);5. ClientA 再发“测试2” | Server 界面“在线用户”数从 2 变为 1;ClientA 收到“测试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. 启动 Server;2. 启动 5 个 Client(可用 Python 脚本模拟);3. 所有 Client 同时发送 10 条消息(如 “MSG:A | 1”, “MSG:A | 2”, …);4. 观察 Server RichEdit 中消息顺序 |
5.3 答辩演示技巧:3句话讲清架构,1个截图锁定亮点
导师没时间听你逐行讲代码。我的经验是:用一张图 + 三句话建立技术可信度,再用一个动态截图证明功能真实。
- 第一句话(定位):“这是一个基于 MFC 框架的 C/S 即时通讯最小系统,核心目标是验证‘一个服务器稳定支撑多个客户端’的工程可行性,而非追求百万并发。”
- 第二句话(架构):“它采用三线程模型:UI 线程管界面、监听线程管接入、每个客户端一个工作线程管通信,所有跨线程数据通过
PostMessage和线程安全队列传递,彻底规避 MFC 跨线程调用陷阱。” - 第三句话(亮点):“我们实现了应用层
本文还有配套的精品资源,点击获取