简介:本资源是一个基于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 ← ★ 实现 OnAccept:accept() 后 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_strRecvBuffer(CString 成员) 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 6(1998年)格式,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__closesocket@4:
- 右键项目 → “属性” → “链接器” → “输入” → “附加依赖项”
- 添加:
ws2_32.lib✅(注意:不是wsock32.lib,那是 Win95 旧库) - 同时确认 “常规” → “Windows SDK 版本” ≥
10.0(VS2019 默认满足)
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), // CA2CT:ANSI to CString (Unicode) m_nPort);参数说明:
CA2CT是 ATL 转换宏,头文件<atlconv.h>;_T()确保字符串字面量适配 Unicode;Format()比+=更安全,避免内存越界。
3.6 步骤六:调试配置与启动项设置(让 F5 真正跑起来)
- 右键
CSocketcli项目 → “设为启动项目” - “调试” → “命令行参数”:留空(客户端无需参数)
- “调试” → “工作目录”:
$(ProjectDir)✅(确保ReadMe.txt等资源可读) - 关键:
CSocket服务端项目也按同样流程创建,并设为另一个启动项目(调试时可右键 → “调试” → “开始新实例” 启动服务端)
验证技巧:启动服务端后,在 CMD 执行
netstat -ano | findstr :8080(假设端口8080),能看到LISTENING状态及 PID;客户端连接后,状态变为ESTABLISHED—— 这是比 UI 更可靠的连通性证据。
4. 避坑:五个真实踩过的雷区与对应解法(从编译失败到运行时崩溃)
这些坑我都亲手踩过,不是文档抄来的“理论上可能”。每一条都附带现象、根因、解法,照做即避。
4.1 现象:编译通过,但运行时CSocket::Create()返回 FALSE,WSAGetLastError()= 10093(WSANOTINITIALISED)
- 原因:
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 占用 80,Skype 占用 80/443)。 - 解法:
- 检查
CSocketDlg::OnBnClickedButtonStart()中Create()参数:
必须含m_listenSocket->Create(m_nPort, SOCK_STREAM, IPPROTO_TCP, NULL, 0, FD_ACCEPT | FD_CLOSE);FD_ACCEPT; - 换端口测试(如
20000),CMD 执行netstat -ano | findstr :20000确认无冲突; - 服务端启动后,用
telnet 127.0.0.1 20000测试端口是否可达(若 telnet 未启用,Control Panel → 程序 → 启用或关闭 Windows 功能 → Telnet 客户端)。
- 检查
4.3 现象:客户端 Send 按钮点击后,服务端OnReceive()收不到数据,或只收到前几个字节
- 原因:TCP Nagle 算法合并小包,或
Send()未检查返回值导致部分数据未发出。 - 解法:
- 在客户端 socket
Create()后,禁用 Nagle:BOOL 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(连接重置) }
- 在客户端 socket
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派生类对象未正确存储在CArray<CSocket*, CSocket*>中,或OnReceive()中PostMessage的m_hWndParent为空。 - 解法:
- 服务端
CSocketDlg.h中定义:CArray<CSocket*, 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 字节:
| 字段 | 长度 | 说明 | 示例(HEX) |
|---|---|---|---|
| Transaction ID | 2B | 客户端自增,服务端原样返回 | 00 01 |
| Protocol ID | 2B | 固定00 00 | 00 00 |
| Length | 2B | 后续字节数(单元ID + 功能码 + 数据) | 00 06(6字节) |
| Unit ID | 1B | 从站地址(PLC ID) | 01 |
| Function Code | 1B | 03=读保持寄存器,10=写多个寄存器 | 03 |
| Data | ≥2B | 地址+数量(读)或地址+数量+字节数+数据(写) | 00 00 00 01(读地址0,1个寄存器) |
关键:我们不实现完整 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 = 6(UnitID+FuncCode+4字节Data) mbap[6] = static_cast<BYTE>(m_nModbusUnitId); // Unit ID // 2. 构造 Modbus ADU(Application 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_cast<BYTE>(m_nModbusStartAddr >> 8); adu[nAdus++] = static_cast<BYTE>(m_nModbusStartAddr & 0xFF); adu[nAdus++] = static_cast<BYTE>(m_nModbusCount >> 8); adu[nAdus++] = static_cast<BYTE>(m_nModbusCount & 0xFF); break; case 1: // 10 - Write Multiple Registers adu[nAdus++] = 0x10; adu[nAdus++] = static_cast<BYTE>(m_nModbusStartAddr >> 8); adu[nAdus++] = static_cast<BYTE>(m_nModbusStartAddr & 0xFF); adu[nAdus++] = static_cast<BYTE>(m_nModbusCount >> 8); adu[nAdus++] = static_cast<BYTE>(m_nModbusCount & 0xFF); adu[nAdus++] = static_cast<BYTE>(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 回传 + Length=3+2*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 == 502(Modbus 默认端口) - 客户端点击 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 抓包 + 十六进制对比。不是为了炫技,是产线环境里,一个字节的偏差就可能导致设备停机。希望帮到你。
本文还有配套的精品资源,点击获取