C++ MFC Socket通信骨架:异步网络编程与线程安全设计
简介这是一份基于C与MFC框架开发的仿QQ即时通讯系统源码包面向C初学者及课程设计实践者聚焦Socket网络编程、多线程通信、客户端文件传输等核心能力训练。资源共114个文件涵盖16个头文件.h与14个实现文件.cpp构成完整C/S架构逻辑另有11个位图.bmp与6个图标.ico支撑界面资源2个可执行文件.exe提供直接运行能力辅以工程配置.sln、.vcproj、调试符号.pdb、资源脚本.rc等结构完整、开箱即用。压缩包大小为28.67MB目录组织规范便于理解MFC消息机制与网络模块分层设计。已有1040人学习下载读者可获得一套功能完备、注释清晰、支持文字聊天与文件互传的实战级C项目特别适合作为《网络程序设计》《面向对象编程》等课程的综合性实践范例快速掌握进程间通信、异步Socket处理与UI线程协同等关键技能。1. 这不是个“仿QQ界面”而是一套可调试的C Socket通信骨架你打开这个基于C的mfc的仿QQ聊天系统.zip第一眼看到的是.bmp图标、.aps编译中间文件、一堆带数字编号的位图资源——很容易误以为这是个“画UI练手”的MFC入门项目。但真正拆进去会发现它用纯 Win32 API MFC 封装的 CAsyncSocket 实现了完整的 TCP 长连接心跳、消息序列化、文件分块传输、多线程收发分离甚至包含一个简易的客户端注册/登录状态机。它不依赖 Qt 或 Boost.Asio所有网络逻辑都扎根在CWinThread派生类和CSocket的重载回调中。适合正在学《Windows 网络编程》第6章、刚写完send()/recv()阻塞模型但卡在“如何不卡死主线程”的同学也适合课程设计里需要交出“有真实通信行为、能跑通文件传输”的 C 工程作业——因为它的SendFile()不是调用系统 API 就完事而是手动切片、加校验头、重传计时器全在CFileTransferThread里实现。2. 从 MFC Socket 架构到线程安全消息队列为什么选 CAsyncSocket 而非 CSocket2.1 CAsyncSocket 是 MFC 中唯一能兼顾 UI 响应性与网络事件驱动的底层封装MFC 提供三种 Socket 封装CSocket同步阻塞、CAsyncSocket异步事件驱动、以及 ATL 的CAtlSocket。本项目选用CAsyncSocket并非偶然——它通过 Windows 消息机制WM_SOCKET_NOTIFY将网络事件连接建立、数据到达、断开转为OnConnect()、OnReceive()、OnClose()等虚函数回调避免了CSocket在Accept()或Receive()时阻塞 UI 线程。项目中的CChatSocket类继承自CAsyncSocket并在OnReceive()中直接调用AfxGetMainWnd()-PostMessage(WM_RECV_MESSAGE, ...)向主窗口投递自定义消息这比用SetTimer()轮询或CreateThread()手动管理更符合 MFC 消息泵范式。提示CAsyncSocket的Create()必须在主线程调用且需在PreTranslateMessage()中处理WM_SOCKET_NOTIFY。项目中CChatSocket::Create()的第二个参数SOCKET_ERROR表示使用默认端口实际连接时才指定服务器 IP 和端口这种延迟绑定设计让客户端可动态切换服务器。2.2 消息收发分离主线程只管 UI子线程专责 Socket I/O项目未采用CSocketCreateThread()的粗暴方案而是构建了双线程模型主线程运行CMainFrame响应用户输入、渲染聊天记录、触发CChatSocket::Send()接收线程CRecvThread继承自CWinThread在InitInstance()中调用CChatSocket::AsyncSelect(FD_READ | FD_CLOSE)持续监听 socket 可读事件。关键在于线程间数据传递CRecvThread收到原始字节流后不直接操作 UI 控件违反 MFC 线程安全规则而是将解析后的MSG_STRUCT结构体含消息类型、发送者ID、内容长度、正文指针压入全局CMsgQueue队列。该队列使用CCriticalSection加锁CMainFrame::OnTimer()每 50ms 从队列取一条消息再通过PostMessage(WM_SHOW_MSG, ...)更新列表控件。// CMsgQueue.h 中的关键成员 class CMsgQueue { private: CPtrArray m_MsgArray; // 存储 MSG_STRUCT* 指针 CCriticalSection m_Cs; // 临界区保护读写 public: void Push(MSG_STRUCT* pMsg) { m_Cs.Lock(); m_MsgArray.Add(pMsg); m_Cs.Unlock(); } MSG_STRUCT* Pop() { m_Cs.Lock(); if (m_MsgArray.GetSize() 0) { MSG_STRUCT* p (MSG_STRUCT*)m_MsgArray.GetAt(0); m_MsgArray.RemoveAt(0); m_Cs.Unlock(); return p; } m_Cs.Unlock(); return nullptr; } };这段代码说明Push()和Pop()必须成对加锁且Pop()返回后由调用方负责delete pMsg。项目中CMainFrame::OnTimer()在取到消息后立即delete避免内存泄漏——这是 MFC 多线程中最易踩的坑new出来的结构体必须在同一线程delete否则可能触发 CRT 断言。2.3 消息协议设计二进制封包而非文本协议支撑文件传输扩展项目定义了紧凑的二进制协议头MSG_HEADER#pragma pack(1) struct MSG_HEADER { WORD wType; // 消息类型LOGIN_REQ0x01, CHAT_MSG0x02, FILE_HEAD0x03 DWORD dwLen; // 后续数据长度不含 header DWORD dwFromID; // 发送者 ID DWORD dwToID; // 接收者 ID DWORD dwTime; // 时间戳秒级 }; #pragma pack()#pragma pack(1)强制按字节对齐确保跨平台解析一致。wType占 2 字节而非intdwLen明确限定为 4 字节避免因编译器默认对齐导致sizeof(MSG_HEADER)在不同机器上变化。当wType FILE_HEAD时dwLen表示整个文件大小后续数据即为文件名UTF-16 编码 文件内容分块当wType CHAT_MSG时dwLen为 UTF-8 文本长度正文直接追加在 header 后。这种设计使单次recv()调用即可判断是否收完整包先收 12 字节 header再根据dwLen循环recv()直至收满。字段类型说明典型值wTypeWORD消息类型枚举0x02普通聊天dwLenDWORDpayload 长度24你好世界 UTF-8 编码长度dwFromIDDWORD客户端唯一标识1001登录时服务器分配dwToIDDWORD目标客户端 ID1002在线好友3. 文件传输实现分块校验、断点续传与 UI 进度反馈3.1 文件分块策略固定 64KB 分片 CRC32 校验头项目不采用TransmitFile()系统 API而是手动分片传输原因在于TransmitFile()无法在传输中途插入进度通知且难以实现断点续传。CFileTransferThread将文件按 64KB65536 字节切片每片前附加 8 字节校验头struct FILE_CHUNK_HEADER { DWORD dwChunkIndex; // 当前分片序号从 0 开始 DWORD dwChunkSize; // 本片实际字节数末片可能小于 64KB DWORD dwCRC32; // 本片数据 CRC32 校验值 };发送端流程打开文件获取总大小 → 计算总片数nTotalChunks (fileSize 65535) / 65536循环for (int i 0; i nTotalChunks; i)fread(buffer, 1, 65536, fp)读取一片CalcCRC32(buffer, dwChunkSize)计算校验值构造FILE_CHUNK_HEADERbuffer调用CChatSocket::Send()发送接收端流程收到FILE_HEAD消息后创建临时文件记录nTotalChunks每收到一个FILE_CHUNK先校验dwCRC32DWORD crc CalcCRC32(pData sizeof(FILE_CHUNK_HEADER), header.dwChunkSize); if (crc ! header.dwCRC32) { // 发送重传请求 MSG_RESEND_CHUNK携带 header.dwChunkIndex SendResendRequest(header.dwChunkIndex); continue; }校验通过后fwrite()写入临时文件对应偏移位置fseek(fp, (LONGLONG)header.dwChunkIndex * 65536, SEEK_SET)注意fseek()的偏移量必须用LONGLONG类型否则超过 4GB 文件时高位截断。项目中dwChunkIndex为DWORD最大支持2^32 * 64KB ≈ 256TB理论无上限。3.2 UI 进度同步通过 PostMessage 传递实时进度主线程无法直接访问子线程的CFileTransferThread实例因此进度更新采用消息机制。CFileTransferThread::Run()中每完成一片调用// 向主窗口发送进度消息 ::PostMessage(AfxGetMainWnd()-GetSafeHwnd(), WM_FILE_PROGRESS, (WPARAM)nCurrentChunk, (LPARAM)nTotalChunks);CMainFrame::OnFileProgress()响应此消息更新CProgressCtrl控件void CMainFrame::OnFileProgress(WPARAM wParam, LPARAM lParam) { int nCurrent (int)wParam; int nTotal (int)lParam; int nPercent (nCurrent * 100) / nTotal; m_Progress.SetPos(nPercent); // m_Progress 是 CProgressCtrl 成员变量 CString str; str.Format(_T(传输中%d/%d 分片 (%d%%)), nCurrent, nTotal, nPercent); m_StaticProgress.SetWindowText(str); // m_StaticProgress 是 CStatic 控件 }这里SetPos()和SetWindowText()必须在主线程调用PostMessage()保证了线程安全。若改用SendMessage()会导致接收线程阻塞失去异步优势。3.3 断点续传实现服务端维护分片接收状态表项目虽为 C/S 架构但断点续传逻辑集中在客户端。CFileTransferThread在开始传输前先向服务器发送MSG_FILE_RESUME_REQ消息携带文件名哈希MD5和已接收字节数。服务器查表返回MSG_FILE_RESUME_ACK包含已成功接收的分片索引数组。客户端据此跳过已传分片从第一个缺失索引开始重传。关键代码在CFileTransferThread::ResumeFromFile()// 解析服务器返回的已接收分片列表 BYTE* pBuf GetRecvBuffer(); // 假设已收到 ACK 数据 int nReceivedCount *(int*)pBuf; // 第 4 字节为已接收分片数 int* pReceivedList (int*)(pBuf 4); // 后续为 int 数组 for (int i 0; i nTotalChunks; i) { bool bAlreadyReceived false; for (int j 0; j nReceivedCount; j) { if (pReceivedList[j] i) { bAlreadyReceived true; break; } } if (!bAlreadyReceived) { // 加入待发送队列 m_SendQueue.Add(i); } }此设计使客户端重启后能自动恢复传输无需重新上传整个文件。服务端状态表用std::mapCString, std::setint实现键为文件 MD5值为已接收分片索引集合。4. 多线程调试技巧定位 CAsyncSocket 回调丢失与临界区死锁4.1OnReceive()不触发检查 WSACleanup() 调用时机常见错误在CChatSocket::OnClose()中直接调用closesocket(m_hSocket)随后执行WSACleanup()。这会导致后续CAsyncSocket实例无法注册事件。正确做法是OnClose()中只调用Close()MFC 封装的清理不调用WSACleanup()WSACleanup()应在CWinApp::ExitInstance()中统一调用且仅一次项目中CChatApp::ExitInstance()包含int CChatApp::ExitInstance() { // 先销毁所有 socket 实例 if (g_pChatSocket) { delete g_pChatSocket; g_pChatSocket nullptr; } // 再清理 Winsock WSACleanup(); return CWinApp::ExitInstance(); }若WSACleanup()提前调用CAsyncSocket::Create()会失败并返回FALSE但 MFC 不抛异常仅静默失败——此时AsyncSelect()无效OnReceive()永不触发。4.2 临界区死锁诊断用CCriticalSection::Lock()超时检测CMsgQueue::Pop()若在CMainFrame::OnTimer()中被频繁调用而CRecvThread正在Push()可能因临界区争用导致 UI 卡顿。项目未实现超时机制调试时可临时修改BOOL bLocked m_Cs.Lock(100); // 最多等待 100ms if (!bLocked) { // 记录日志临界区争用超时 OutputDebugString(_T(CMsgQueue lock timeout!\n)); return nullptr; }生产环境应避免Lock()长时间阻塞改为TryLock()或改用CMutex支持命名便于跨进程调试。4.3 线程局部存储TLS替代全局变量解决多客户端实例冲突项目当前设计为单客户端单 socket若需支持多账号登录全局g_pChatSocket会冲突。升级方案是使用 TLS// 在 CChatApp::InitInstance() 中 m_dwTlsIndex TlsAlloc(); // 在 CChatSocket 构造函数中 TlsSetValue(m_dwTlsIndex, this); // 在任意线程中获取当前 socket CChatSocket* pSocket (CChatSocket*)TlsGetValue(m_dwTlsIndex);TlsAlloc()返回唯一索引TlsSetValue()为当前线程绑定 socket 实例彻底解耦线程与 socket 关系。此方案比std::thread_local更兼容 VS2013项目默认工具集。5. VS2013 环境配置与 MFC 项目打包部署要点5.1 VS2013 中启用 MFC 支持及 Unicode 设置项目基于 VS2013 开发需确认以下设置项目属性 → 配置属性 → 常规 → 使用 MFC选择“在共享 DLL 中使用 MFC”字符集必须设为“使用 Unicode 字符集”项目摘要中CString默认为wchar_t*若设为多字节会导致LoadString()加载中文资源失败C/C → 语言 → 启用运行时类型信息设为“是/GR”因CAsyncSocket涉及 RTTI若忽略 Unicode 设置CChatSocket::Send()发送中文时会乱码因为CString::GetBuffer()返回char*而网络协议要求 UTF-8 编码。正确做法是在发送前转换CString strUtf8; CT2CA pszConverted(str); // CT2CA 将 CString 转为 ANSIUTF-8 strUtf8 pszConverted; // 再 send(strUtf8.GetBuffer());5.2 打包时必须包含的 MFC 与 CRT 运行库VS2013 生成的 MFC 程序依赖以下 DLL需随 exe 一起分发DLL 名称来源是否必须mfc120u.dllMicrosoft Visual C 2013 Redistributable (x86)是Unicode 版msvcr120.dll同上是CRT 运行库vcruntime120.dll同上是VC 运行时核心项目未使用静态链接/MT故不能省略这些 DLL。打包脚本如 Inno Setup应包含[Files] Source: mfc120u.dll; DestDir: {app}; Flags: ignoreversion Source: msvcr120.dll; DestDir: {app}; Flags: ignoreversion Source: vcruntime120.dll; DestDir: {app}; Flags: ignoreversion提示若目标机器已安装 Visual C Redistributable可跳过复制但需在安装程序中检测注册表HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\VisualStudio\12.0\Setup\VC下ProductDir值是否存在。5.3 资源文件路径硬编码问题与解决方案项目中图标、位图路径写死为相对路径如1.bmp导致打包后运行报错。修复方法将所有资源文件.bmp,.ico添加到项目资源中设Resource Type为BITMAP/ICON用AfxFindResourceHandle()获取模块句柄再LoadBitmap()HINSTANCE hInst AfxFindResourceHandle(MAKEINTRESOURCE(IDB_BITMAP1), RT_BITMAP); HBITMAP hBmp ::LoadBitmap(hInst, MAKEINTRESOURCE(IDB_BITMAP1));IDB_BITMAP1为资源视图中定义的 ID编译后嵌入 exe彻底摆脱外部文件依赖。此法也适用于对话框模板、字符串表等所有资源。调试时若发现LoadBitmap()返回NULL用GetLastError()查错误码ERROR_RESOURCE_DATA_NOT_FOUND表示资源 ID 错误ERROR_RESOURCE_TYPE_NOT_FOUND表示资源类型不匹配如把RT_ICON当RT_BITMAP用。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →