news 2026/9/30 4:28:53

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 ← ★ 实现 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 对话框工程(关键选项必须勾选)

  1. VS2019 → “创建新项目” → 搜索 “MFC 应用程序” → 选择MFC 应用程序(桌面)
  2. 项目名填CSocketcli(或CSocket),位置选解压目录同级
  3. 在“配置 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,但需显式指定:

  1. 右键CSocketcli.cpp→ “属性” → “C/C++” → “预编译头”
    • 预编译头:使用预编译头✅
    • 预编译头文件:StdAfx.h✅
  2. 右键StdAfx.cpp→ “属性” → “C/C++” → “预编译头”
    • 预编译头:创建预编译头✅
    • 预编译头文件:StdAfx.h✅
  3. 在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 真正跑起来)

  1. 右键CSocketcli项目 → “设为启动项目”
  2. “调试” → “命令行参数”:留空(客户端无需参数)
  3. “调试” → “工作目录”:$(ProjectDir)✅(确保ReadMe.txt等资源可读)
  4. 关键: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)。
  • 解法:
    1. 检查CSocketDlg::OnBnClickedButtonStart()中Create()参数:
      m_listenSocket->Create(m_nPort, SOCK_STREAM, IPPROTO_TCP, NULL, 0, FD_ACCEPT | FD_CLOSE);
      必须含FD_ACCEPT;
    2. 换端口测试(如20000),CMD 执行netstat -ano | findstr :20000确认无冲突;
    3. 服务端启动后,用telnet 127.0.0.1 20000测试端口是否可达(若 telnet 未启用,Control Panel → 程序 → 启用或关闭 Windows 功能 → Telnet 客户端)。

4.3 现象:客户端 Send 按钮点击后,服务端OnReceive()收不到数据,或只收到前几个字节

  • 原因:TCP Nagle 算法合并小包,或Send()未检查返回值导致部分数据未发出。
  • 解法:
    1. 在客户端 socketCreate()后,禁用 Nagle:
      BOOL bNagle = FALSE; m_socketClient->SetSockOpt(TCP_NODELAY, &bNagle, sizeof(BOOL));
    2. 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)未设置,导致连接断开事件不被捕获。
  • 解法:
    1. 确保服务端CSocket类(CSocket.h/cpp)中声明并实现:
      // CSocket.h afx_msg void OnClose(int nErrorCode); DECLARE_MESSAGE_MAP()
    2. 在CSocket.cpp中:
      void CSocket::OnClose(int nErrorCode) { // 清理资源:从父对话框列表中移除自己 m_pParentDlg->RemoveClientSocket(this); delete this; // ★ 关键:主动销毁,避免内存泄漏 }
    3. AsyncSelect()必须含FD_CLOSE(见 2.2 节)。

4.5 现象:多客户端连接时,服务端只能处理第一个客户端,后续OnReceive()不触发

  • 原因:CSocket派生类对象未正确存储在CArray<CSocket*, CSocket*>中,或OnReceive()中PostMessage的m_hWndParent为空。
  • 解法:
    1. 服务端CSocketDlg.h中定义:
      CArray<CSocket*, CSocket*> m_arrClientSockets;
    2. OnAccept()中:
      m_arrClientSockets.Add(pClientSocket); // 存储指针 pClientSocket->m_hWndParent = m_hWnd; // 绑定父窗口句柄
    3. 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 ID2B客户端自增,服务端原样返回00 01
Protocol ID2B固定00 0000 00
Length2B后续字节数(单元ID + 功能码 + 数据)00 06(6字节)
Unit ID1B从站地址(PLC ID)01
Function Code1B03=读保持寄存器,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 验证:

  1. 启动 Wireshark,过滤tcp.port == 502(Modbus 默认端口)
  2. 客户端点击 Send,观察抓包:
    • 第一帧:Source: Client, Destination: Server, Len: 12→ MBAP 头完整
    • 第二帧:Source: Server, Destination: Client, Len: 11→ 响应报文长度正确
  3. 展开Modbus协议树,确认Function Code: 0x03 (Read Holding Registers)和Value: 0x0001

后悔药:如果抓包显示Malformed Packet,一定是 MBAP Length 字段算错(Length = UnitID + FuncCode + Data 字节数,不含 MBAP 前6字节)。我第一次就漏算了 Unit ID 的1字节,折腾2小时。

从那以后我每次改网络协议,都强制走一遍 Wireshark 抓包 + 十六进制对比。不是为了炫技,是产线环境里,一个字节的偏差就可能导致设备停机。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 4:28:32

Hindsight记忆层实战:为LLM Agent构建可防御的跨会话记忆系统

1. 从“hindsight”说起&#xff1a;为什么我们需要给Agent装上“后视镜”第一次看到“hindsight”这个词&#xff0c;我脑子里蹦出来的不是词典释义&#xff0c;而是过去一年多在Agent项目里反复踩坑的画面。Hindsight&#xff0c;直译是“事后聪明”&#xff0c;也就是我们常…

作者头像 李华
网站建设 2026/9/30 4:27:57

广电大数据可视化:机顶盒回传、收视指标与ECharts大屏

大屏上的点播曲线在晚上八点整毫无征兆地掉了下去&#xff0c;业务群里的消息一条接一条弹出来&#xff1a;"是不是系统挂了&#xff1f;""昨天这个时候还是峰值。"我打开后台&#xff0c;先看数据链路&#xff0c;再看前端——数据可视化这东西&#xff0…

作者头像 李华
网站建设 2026/9/30 4:27:24

LLM Agent 记忆架构实战:从 working memory 到 MCP 与 Docker 部署

1. 从“hindsight”这个词说起&#xff1a;为什么它值得单独拿出来做一篇文章“hindsight”直译过来是“后见之明”&#xff0c;但在 LLM Agent 的语境里&#xff0c;它指向的是一个非常具体、也非常容易被忽视的问题&#xff1a;Agent 的记忆到底该怎么存、怎么取、怎么用。热…

作者头像 李华
网站建设 2026/9/30 4:26:41

Model-Optimizer:大模型GPU推理的工程方法论与实战调优

1. “Model-Optimizer”不是工具名&#xff0c;而是工程共识的具象化表达 你搜“Model-Optimizer”&#xff0c;首页跳出来的全是TensorRT、vLLM、TensorRT-LLM这些词——没有独立官网、没有GitHub star破万的仓库、没有PyPI上可pip install的包。这恰恰说明一件事&#xff1a…

作者头像 李华
网站建设 2026/9/30 4:25:54

SSM+Layui+ECharts:校园跑腿平台的订单系统实战解析

做这种校园跑腿代办平台&#xff0c;最怕的就是把项目做成“功能堆砌”。系统倒是能跑&#xff0c;但业务逻辑一乱&#xff0c;后面每加一个功能都是在给自己挖坑。这篇文章我从标题里的几个关键词说起&#xff1a;javaweb、ssm、mysql、jsp、layui、echarts&#xff0c;把这套…

作者头像 李华