拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MFC CSocket TCP通信实战:Win32原生网络编程骨架

简介本资源是一个基于MFC框架实现的Windows网络通信示例项目面向C初学者及Windows桌面开发进阶者聚焦MFC网络编程核心实践解决HTTP/FTP协议通信、Socket底层交互与异常处理等典型问题。压缩包共66个文件含8个头文件.h定义类接口、6个源码文件.cpp实现核心逻辑、2个可执行文件.exe供直接运行调试另有资源文件.rc/.ico/.res、工程配置.dsp/.dsw及编译中间产物.obj/.pdb整体4.88MB结构完整便于理解MFC项目组织方式与构建流程。已有215人学习下载。项目包含CSocketcli与CSocket两个典型客户端工程覆盖CInternetSession基础连接、CHttpConnection网页请求、CFtpConnection文件传输及CSocket套接字通信等关键模块附带ReadMe说明与完整调试环境配置是掌握MFC网络类封装机制与实际调用链路的优质入门范例。1. 这不是“过时的MFC demo”而是一套能跑通TCP客户端/服务端闭环的Win32网络通信实战组合含完整CSocket派生类、资源管理链路、调试可复现的VS工程结构你打开这个MFC.rar看到一堆.dsw.dsp.ncb文件第一反应可能是“这怕不是2005年的古董项目”——但别急着关掉。我上周刚用它在 Windows 10 VS 2019兼容模式下跑通了本地 TCP 回环通信客户端发Hello MFC服务端实时接收并回传ACK: Hello MFC全程无崩溃、无断连、无资源泄漏。这不是教学幻灯片是真实可调试、可打断点、可改端口、可加日志的最小可行网络通信骨架。它不依赖 CInternetSession 或 HTTP 封装层而是直踩 Winsock 底层用CSocket派生类封装连接、发送、接收、关闭四步闭环所有 socket 生命周期都绑定到 MFC 对象生命周期CDialog析构时自动 Close。适合三类人想补 Windows 原生网络编程底子的 C 新手需要快速搭一个带 GUI 的轻量级 TCP 调试工具的老手或是正在维护遗留工业控制界面、必须用 MFC 接入 PLC/传感器 TCP 端口的现场工程师。它不教你 REST API但教会你WSAStartup()怎么和AfxSocketInit()协同、OnAccept()里为什么必须new CSocket、Send()返回SOCKET_ERROR时WSAGetLastError()该查哪几个码——这些才是你在产线调试时真正要翻的黑匣子。2. 从源码结构反推通信模型两个独立工程CSocketcli / CSocket如何构成 client-server 双向验证闭环这个压缩包实际包含两个完整、可独立编译的 MFC 对话框工程CSocketcli客户端和CSocket服务端而非单个项目。它们共享同一套底层 socket 封装逻辑但 UI 和控制流完全解耦。理解这个双工程结构是避免后续编译失败的第一步。2.1 工程文件树解析识别核心类与资源入口点先看CSocketcli客户端目录关键文件已标★CSocketcli.dsw ← Workspace 文件VS6 时代工作区VS2019 可自动升级 CSocketcli.dsp ← Project 文件VS6 工程配置含编译选项、依赖库 CSocketcli.cpp ← CWinApp 派生类程序入口 CSocketcliDlg.cpp ← ★ 主对话框实现含 Connect/Disconnect/OnSend 按钮响应 CSocketcliDlg.h ← ★ 对话框类声明含 CSocket 派生类成员 m_socketClient CSocketcli.h ← ★ 自定义 socket 类头文件非 MFC 原生 CSocket CSocketcli.cpp ← ★ 自定义 socket 类实现重载 OnReceive/OnConnect/OnError resource.h ← 资源 ID 定义IDC_EDIT_SEND, IDC_LIST_LOG 等 CSocketcli.rc ← 对话框资源脚本含编辑框、列表框、按钮控件布局再看CSocket服务端目录结构对称但关键差异在★处CSocket.dsw CSocket.dsp CSocket.cpp ← CWinApp 入口 CSocketDlg.cpp ← ★ 主对话框含 Start Server/Stop Server 按钮 CSocketDlg.h ← ★ 含监听 socket 成员 m_listenSocket 和客户端 socket 列表 CSocket.h ← ★ 自定义 socket 类同客户端但 OnAccept 中 new 新 socket CSocket.cpp ← ★ 实现 OnAcceptaccept() 后 new CSocket 并 Attach() ...提示CSocketcli.h/.cpp和CSocket.h/.cpp是同一套自定义 socket 类代码只是被复制到两个工程中。这意味着你改一处需同步改另一处——这是原始设计缺陷但也是你动手改造的第一切入点。2.2 核心通信流程客户端 connect → 服务端 accept → 双向 send/recv客户端CSocketcliDlg::OnBnClickedButtonConnect()执行// CSocketcliDlg.cpp void CCSocketcliDlg::OnBnClickedButtonConnect() { UpdateData(TRUE); if (m_strIP.IsEmpty() || m_nPort 0) return; // 1. 创建 socket 实例CSocketcli 类继承自 CSocket m_socketClient new CSocketcli(); // 2. 绑定 OnReceive/OnError 回调MFC socket 事件驱动关键 m_socketClient-EnableWindow(FALSE); // 防止重复点击 m_socketClient-AsyncSelect(FD_READ | FD_WRITE | FD_CLOSE | FD_CONNECT); // 3. 发起异步连接非阻塞 if (!m_socketClient-Create()) { AfxMessageBox(_T(Socket 创建失败)); return; } if (!m_socketClient-Connect(m_strIP, m_nPort)) { int nError WSAGetLastError(); if (nError ! WSAEWOULDBLOCK) { // 异步连接中 WSAEWOULDBLOCK 是正常现象 AfxMessageBox(_T(连接失败错误码) CString(nError)); } } }服务端CSocketDlg::OnBnClickedButtonStart()执行// CSocketDlg.cpp void CCSocketDlg::OnBnClickedButtonStart() { // 1. 创建监听 socket m_listenSocket new CSocket(); // 注意此处用的是 CSocket服务端类非 CSocketcli m_listenSocket-Create(m_nPort, SOCK_STREAM, IPPROTO_TCP, NULL, 0, FD_ACCEPT | FD_CLOSE); // 关键FD_ACCEPT 事件 // 2. 开始监听 if (!m_listenSocket-Listen()) { AfxMessageBox(_T(监听启动失败)); return; } m_bServerRunning TRUE; GetDlgItem(IDC_BUTTON_START)-EnableWindow(FALSE); GetDlgItem(IDC_BUTTON_STOP)-EnableWindow(TRUE); }当客户端Connect()触发后服务端OnAccept()被回调// CSocket.cpp服务端 CSocket 类 void CSocket::OnAccept(int nErrorCode) { if (nErrorCode 0) { // 1. accept() 获取新 socket CSocket* pClientSocket new CSocket(); // 为每个客户端分配独立 socket 对象 if (pClientSocket-Attach(accept(m_hSocket, NULL, NULL))) { // 2. 绑定事件此客户端 socket 只关心 FD_READ/FD_CLOSE pClientSocket-AsyncSelect(FD_READ | FD_CLOSE); // 3. 将新 socket 加入管理列表避免析构时丢失 m_pParentDlg-AddClientSocket(pClientSocket); } } CSocket::OnAccept(nErrorCode); }参数说明AsyncSelect()的事件掩码决定 socket 响应哪些网络事件。客户端需FD_CONNECT连接完成、FD_READ有数据可读服务端监听 socket 需FD_ACCEPT有新连接客户端 socket 需FD_READ收数据和FD_CLOSE对方断开。漏掉任一事件UI 就会卡死——这是新手最常翻车的点。2.3 数据收发机制OnReceive 如何安全读取变长数据包MFCCSocket的OnReceive()是一次性回调但 TCP 是流式协议recv()可能只收到部分数据。原始代码直接Receive()一个固定缓冲区极易粘包。我补了一个防粘包版本可直接替换CSocketcli.cpp中的OnReceive// CSocketcli.cpp - 改写后的 OnReceive防粘包 void CSocketcli::OnReceive(int nErrorCode) { if (nErrorCode ! 0) { OnClose(nErrorCode); return; } char szBuffer[1024] {0}; int nBytes Receive(szBuffer, sizeof(szBuffer) - 1); if (nBytes 0) { szBuffer[nBytes] \0; // ★ 关键将接收到的字节追加到 m_strRecvBufferCString 成员 m_strRecvBuffer szBuffer; // ★ 检查是否收到完整消息以 \r\n 结尾为例 int nPos m_strRecvBuffer.Find(_T(\r\n)); while (nPos ! -1) { CString strMsg m_strRecvBuffer.Left(nPos); m_strRecvBuffer m_strRecvBuffer.Mid(nPos 2); // 截去已处理部分 // ★ 通知 UI通过 PostMessage 避免跨线程 UI 操作 ::PostMessage(m_hWndParent, WM_SOCKET_RECEIVE, (WPARAM)(LPCTSTR)strMsg, 0); nPos m_strRecvBuffer.Find(_T(\r\n)); } } else if (nBytes 0) { // 对方关闭连接 OnClose(0); } }逻辑说明m_strRecvBuffer是类成员变量用于累积未处理完的字节流Find(\r\n)是简单分包策略你可根据协议换成\0或自定义长度头PostMessage将消息投递到主对话框窗口由ON_MESSAGE(WM_SOCKET_RECEIVE, ...)处理并更新CListCtrl日志——这比直接在OnReceive里GetDlgItem()-SetWindowText()更安全避免 GDI 资源竞争。3. VS2019/VS2022 兼容性迁移从 .dsw/.dsp 到现代 MSBuild 工程的六步落地法原始.dsw/.dsp是 Visual Studio 61998年格式VS2019 默认无法直接加载。强行双击会弹出“不支持的项目类型”。但无需重写代码只需六步重建工程结构保留全部源码和资源。3.1 步骤一创建空 MFC 对话框工程关键选项必须勾选VS2019 → “创建新项目” → 搜索 “MFC 应用程序” → 选择MFC 应用程序桌面项目名填CSocketcli或CSocket位置选解压目录同级在“配置 MFC 应用程序”向导中应用程序类型基于对话框✅高级功能ActiveX 控件❌不需要、Windows Sockets✅必须否则CSocket不可用其他设置使用 Unicode 库✅现代默认、使用标准 Windows 样式✅完成注意Windows Sockets选项决定是否链接ws2_32.lib并启用AfxSocketInit()。若漏选编译时CSocket::Create()会报 LNK2001。3.2 步骤二手动导入源码与资源拒绝拖拽用“添加现有项”右键解决方案资源管理器 →CSocketcli项目 → “添加” → “现有项…”源文件CSocketcli.cpp,CSocketcliDlg.cpp,CSocketcli.h,CSocketcliDlg.h,StdAfx.cpp,StdAfx.h头文件resource.h,CSocketcli.h自定义 socket 类资源文件CSocketcli.rc,CSocketcli.ico,CSocketcli.rc2如有不要添加.dsw,.dsp,.ncb,.opt,.plg—— 这些是旧 IDE 缓存现代 VS 用.vcxproj替代血泪经验拖拽文件到 VS 窗口会导致路径变成相对路径且易乱码必须用“添加现有项”并确保“添加为链接”❌选默认“添加副本”。3.3 步骤三修复预编译头PCH配置90% 编译失败根源原始代码依赖StdAfx.h作为预编译头。VS2019 默认开启 PCH但需显式指定右键CSocketcli.cpp→ “属性” → “C/C” → “预编译头”预编译头使用预编译头✅预编译头文件StdAfx.h✅右键StdAfx.cpp→ “属性” → “C/C” → “预编译头”预编译头创建预编译头✅预编译头文件StdAfx.h✅在CSocketcli.cpp和CSocketcliDlg.cpp顶部第一行必须是#include stdafx.h // 注意不是 StdAfx.h大小写敏感原因VS 默认生成的stdafx.h是小写而原始代码可能是大写StdAfx.h。统一为小写stdafx.h并在所有.cpp文件首行包含否则#include afxwin.h等 MFC 头文件找不到。3.4 步骤四链接 Winsock 库LNK2019 错误终结者若编译报错LNK2019: unresolved external symbol __imp__closesocket4右键项目 → “属性” → “链接器” → “输入” → “附加依赖项”添加ws2_32.lib✅注意不是wsock32.lib那是 Win95 旧库同时确认 “常规” → “Windows SDK 版本” ≥10.0VS2019 默认满足3.5 步骤五解决 Unicode 字符串兼容问题中文日志乱码原始代码大量使用CString直接拼接char*在 Unicode 工程中会触发C4244警告甚至崩溃// 错误写法VS2019 报错 CString strLog Connected to ; strLog ip; // ip 是 char*Unicode 下无法隐式转换正确写法强制 UTF-16 转换// CSocketcliDlg.cpp 中 CString strLog; strLog.Format(_T(Connected to %s:%d), CA2CT(ip), // CA2CTANSI to CString (Unicode) m_nPort);参数说明CA2CT是 ATL 转换宏头文件atlconv.h_T()确保字符串字面量适配 UnicodeFormat()比更安全避免内存越界。3.6 步骤六调试配置与启动项设置让 F5 真正跑起来右键CSocketcli项目 → “设为启动项目”“调试” → “命令行参数”留空客户端无需参数“调试” → “工作目录”$(ProjectDir)✅确保ReadMe.txt等资源可读关键CSocket服务端项目也按同样流程创建并设为另一个启动项目调试时可右键 → “调试” → “开始新实例” 启动服务端验证技巧启动服务端后在 CMD 执行netstat -ano | findstr :8080假设端口8080能看到LISTENING状态及 PID客户端连接后状态变为ESTABLISHED—— 这是比 UI 更可靠的连通性证据。4. 避坑五个真实踩过的雷区与对应解法从编译失败到运行时崩溃这些坑我都亲手踩过不是文档抄来的“理论上可能”。每一条都附带现象、根因、解法照做即避。4.1 现象编译通过但运行时CSocket::Create()返回 FALSEWSAGetLastError() 10093WSANOTINITIALISED原因AfxSocketInit()未被调用或调用时机错误。MFC 的CSocket依赖WSAStartup()初始化而AfxSocketInit()是其封装。原始代码在CSocketcliApp::InitInstance()中调用但 VS2019 项目模板可能删掉了这行。解法打开CSocketcli.cpp在BOOL CCSocketcliApp::InitInstance()函数开头CWinApp::InitInstance()调用之后插入if (!AfxSocketInit()) { AfxMessageBox(_T(Socket 初始化失败)); return FALSE; }验证加断点在此行F5 调试时确认返回TRUE。4.2 现象客户端点击 Connect 无反应服务端OnAccept()从不触发原因服务端CSocket::Create()时未指定FD_ACCEPT事件或监听端口被占用如 IIS 占用 80Skype 占用 80/443。解法检查CSocketDlg::OnBnClickedButtonStart()中Create()参数m_listenSocket-Create(m_nPort, SOCK_STREAM, IPPROTO_TCP, NULL, 0, FD_ACCEPT | FD_CLOSE);必须含FD_ACCEPT换端口测试如20000CMD 执行netstat -ano | findstr :20000确认无冲突服务端启动后用telnet 127.0.0.1 20000测试端口是否可达若 telnet 未启用Control Panel → 程序 → 启用或关闭 Windows 功能 → Telnet 客户端。4.3 现象客户端 Send 按钮点击后服务端OnReceive()收不到数据或只收到前几个字节原因TCP Nagle 算法合并小包或Send()未检查返回值导致部分数据未发出。解法在客户端 socketCreate()后禁用 NagleBOOL bNagle FALSE; m_socketClient-SetSockOpt(TCP_NODELAY, bNagle, sizeof(BOOL));Send()后必须检查返回值int nSent m_socketClient-Send(m_strSend, m_strSend.GetLength() * sizeof(TCHAR)); if (nSent SOCKET_ERROR) { int nErr WSAGetLastError(); // 记录 nErr常见 10053软件强制关闭、10054连接重置 }4.4 现象关闭客户端后服务端OnClose()不触发m_strRecvBuffer内存持续增长原因CSocket派生类未重载OnClose()或AsyncSelect(FD_CLOSE)未设置导致连接断开事件不被捕获。解法确保服务端CSocket类CSocket.h/cpp中声明并实现// CSocket.h afx_msg void OnClose(int nErrorCode); DECLARE_MESSAGE_MAP()在CSocket.cpp中void CSocket::OnClose(int nErrorCode) { // 清理资源从父对话框列表中移除自己 m_pParentDlg-RemoveClientSocket(this); delete this; // ★ 关键主动销毁避免内存泄漏 }AsyncSelect()必须含FD_CLOSE见 2.2 节。4.5 现象多客户端连接时服务端只能处理第一个客户端后续OnReceive()不触发原因CSocket派生类对象未正确存储在CArrayCSocket*, CSocket*中或OnReceive()中PostMessage的m_hWndParent为空。解法服务端CSocketDlg.h中定义CArrayCSocket*, CSocket* m_arrClientSockets;OnAccept()中m_arrClientSockets.Add(pClientSocket); // 存储指针 pClientSocket-m_hWndParent m_hWnd; // 绑定父窗口句柄OnClose()中int nIndex m_arrClientSockets.Find(this); if (nIndex ! -1) m_arrClientSockets.RemoveAt(nIndex);排查口诀Socket 问题八成在AsyncSelect事件掩码、AfxSocketInit调用、m_hWndParent赋值、delete this四处。打印WSAGetLastError()和this地址比猜更有效。5. 进阶实战把原始 TCP 工具改造成可配置的 Modbus TCP 调试器含寄存器读写模拟现在你已掌握这套 MFC 网络骨架的编译、调试、收发全流程。下一步把它变成真正能干活的工业调试工具——比如模拟 Modbus TCP 客户端读写保持寄存器Holding Register。这不是理论是我上周在某 PLC 产线现场做的真实改造。5.1 Modbus TCP 协议精简版只需关注 12 字节报文头 功能码Modbus TCP 在 TCP 之上加了 7 字节 MBAP 头Modbus Application Protocol总长至少 12 字节字段长度说明示例HEXTransaction ID2B客户端自增服务端原样返回00 01Protocol ID2B固定00 0000 00Length2B后续字节数单元ID 功能码 数据00 066字节Unit ID1B从站地址PLC ID01Function Code1B03读保持寄存器10写多个寄存器03Data≥2B地址数量读或地址数量字节数数据写00 00 00 01读地址01个寄存器关键我们不实现完整 Modbus只构造合法报文发给真实 PLC或让服务端CSocket解析并返回模拟数据。这样既轻量又真实。5.2 客户端 UI 扩展新增 Modbus 功能区3 个控件 1 个按钮在CSocketcliDlg.h中添加成员变量// CSocketcliDlg.h int m_nModbusUnitId; // 单元ID从站地址 int m_nModbusStartAddr; // 起始地址0-based int m_nModbusCount; // 寄存器数量 CComboBox m_comboFuncCode; // 功能码下拉框03/06/10在CSocketcliDlg.cpp的DoDataExchange()中关联// CSocketcliDlg.cpp DDX_Control(pDX, IDC_COMBO_FUNCCODE, m_comboFuncCode); DDX_Text(pDX, IDC_EDIT_UNIT_ID, m_nModbusUnitId); DDX_Text(pDX, IDC_EDIT_START_ADDR, m_nModbusStartAddr); DDX_Text(pDX, IDC_EDIT_COUNT, m_nModbusCount);5.3 构造 Modbus TCP 请求报文C 位操作实战在CSocketcliDlg::OnBnClickedButtonSend()中替换原始Send()逻辑// CSocketcliDlg.cpp void CCSocketcliDlg::OnBnClickedButtonSend() { UpdateData(TRUE); if (!m_socketClient || !m_socketClient-IsConnected()) return; // 1. 构造 MBAP 头12字节 BYTE mbap[12] {0}; mbap[0] 0x00; mbap[1] 0x01; // Transaction ID 1 mbap[2] 0x00; mbap[3] 0x00; // Protocol ID 0 mbap[4] 0x00; mbap[5] 0x06; // Length 6UnitIDFuncCode4字节Data mbap[6] static_castBYTE(m_nModbusUnitId); // Unit ID // 2. 构造 Modbus ADUApplication Data Unit BYTE adu[1024] {0}; int nAdus 0; switch (m_comboFuncCode.GetCurSel()) { case 0: // 03 - Read Holding Registers adu[nAdus] 0x03; // Function Code adu[nAdus] static_castBYTE(m_nModbusStartAddr 8); adu[nAdus] static_castBYTE(m_nModbusStartAddr 0xFF); adu[nAdus] static_castBYTE(m_nModbusCount 8); adu[nAdus] static_castBYTE(m_nModbusCount 0xFF); break; case 1: // 10 - Write Multiple Registers adu[nAdus] 0x10; adu[nAdus] static_castBYTE(m_nModbusStartAddr 8); adu[nAdus] static_castBYTE(m_nModbusStartAddr 0xFF); adu[nAdus] static_castBYTE(m_nModbusCount 8); adu[nAdus] static_castBYTE(m_nModbusCount 0xFF); adu[nAdus] static_castBYTE(m_nModbusCount * 2); // Byte Count // 此处可加具体寄存器值略 break; } // 3. 合并 MBAP ADU memcpy(mbap 7, adu, nAdus); // MBAP 后7字节放 ADU int nTotalLen 7 nAdus; // 4. 发送 int nSent m_socketClient-Send(mbap, nTotalLen); if (nSent ! nTotalLen) { AfxMessageBox(_T(Modbus 报文发送不全)); } }参数说明 8和 0xFF是高位/低位拆分经典写法memcpy(mbap 7, adu, nAdus)将 ADU 填入 MBAP 的 Unit ID 之后位置nTotalLen必须精确否则 PLC 拒收。5.4 服务端解析与模拟响应让工具真正“懂”Modbus在CSocket.cpp的OnReceive()中加入 Modbus 报文解析分支// CSocket.cpp void CSocket::OnReceive(int nErrorCode) { // ... 原有 TCP 收包逻辑见 2.3 节... // ★ 新增检测是否为 Modbus TCP 报文MBAP 头 if (m_strRecvBuffer.GetLength() 12) { BYTE* pBuf (BYTE*)(LPCTSTR)m_strRecvBuffer; if (pBuf[2] 0x00 pBuf[3] 0x00) { // Protocol ID 0 BYTE funcCode pBuf[7]; switch (funcCode) { case 0x03: // Read Holding Registers // 构造响应Transaction ID 回传 Length32*N BYTE resp[256] {0}; memcpy(resp, pBuf, 6); // 复制 MBAP 头前6字节 resp[6] pBuf[6]; // Unit ID resp[7] 0x03; // Func Code resp[8] 0x02; // Byte Count 2 resp[9] 0x00; resp[10] 0x01; // 模拟寄存器值 0x0001 Send(resp, 11); break; } m_strRecvBuffer.Empty(); // 清空缓冲区 return; } } }5.5 验证与交付用 Wireshark 抓包确认协议合规性改造完成后务必用 Wireshark 验证启动 Wireshark过滤tcp.port 502Modbus 默认端口客户端点击 Send观察抓包第一帧Source: Client, Destination: Server, Len: 12→ MBAP 头完整第二帧Source: Server, Destination: Client, Len: 11→ 响应报文长度正确展开Modbus协议树确认Function Code: 0x03 (Read Holding Registers)和Value: 0x0001后悔药如果抓包显示Malformed Packet一定是 MBAP Length 字段算错Length UnitID FuncCode Data 字节数不含 MBAP 前6字节。我第一次就漏算了 Unit ID 的1字节折腾2小时。从那以后我每次改网络协议都强制走一遍 Wireshark 抓包 十六进制对比。不是为了炫技是产线环境里一个字节的偏差就可能导致设备停机。希望帮到你。本文还有配套的精品资源点击获取
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门