ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MFC中TCP短连接通信实战:CAsyncSocket实现与避坑指南

MFC中TCP短连接通信实战:CAsyncSocket实现与避坑指南 简介这份资源面向Windows平台下学习C网络编程的开发者聚焦MFC框架中基于TCP协议的短连接通信实现适合已具备一定C与MFC基础、希望掌握客户端-服务器通信机制的中级学习者。内容围绕CSocket与CAsyncSocket类展开涵盖连接建立、数据收发与断开释放等关键环节并配有服务端与客户端两套完整工程示例便于对照理解三次握手与短连接的生命周期。压缩包共80个文件约6.46MB以h头文件、cpp源文件、obj与pdb编译调试文件、vcproj与sln工程文件、rc资源脚本及exe可执行文件为主同时包含ReadMe说明文档工程结构清晰可直接编译运行验证。目前已有238人学习下载。通过研读示例代码与工程配置读者能够掌握MFC下TCP短连接的编程思路、异步事件处理方式以及资源释放要点为构建可靠的网络通信程序打下实践基础。1. TCP.rar 里的 MFC 短连接通信一个被低估的工程起点接手一个老工控上位机项目时我在交接目录里翻到一个叫TCP.rar的压缩包解压后是一套基于 MFC 的对话框工程里面用 CAsyncSocket 封装了一套 TCP 短连接通信。当时第一反应是这年头谁还用 MFC 写网络通信但把代码跑起来、对着现场设备联调了两天之后我改主意了——在产线设备、仪器仪表、老旧 PLC 这类场景里MFC TCP 短连接反而是最省心的组合。所谓短连接就是每次发一条指令就建立一次连接、收完应答立刻关闭不维持长连接状态。它牺牲了一点握手开销换来的是连接状态永远干净、不会因为设备重启或网络抖动留下半死不活的 socket。这套方案适合谁适合手上有 VS 环境、要快速给一台 Windows 上位机加网络通信能力、又不想引入 Qt 或第三方网络库的工程师。MFC 自带的 CAsyncSocket 和 CSocket 虽然老但胜在零依赖、随 VS 一起装、调试信息全配合 TCP 短连接模型能把发指令-收数据-断连这条链路做得非常可控。下面我按自己实际落地的顺序把选型、实现、参数和踩过的坑讲清楚。2. 为什么在 MFC 里选短连接而不是长连接2.1 短连接和长连接在 MFC 场景下的真实差别先把概念对齐。TCP 长连接是三次握手建立通道后一直保持双方随时可以发数据短连接是每次通信都走一遍连接-发送-接收-关闭的完整流程。热词里常出现的 tcp三次握手、tcp三次握手四次挥手在短连接里是每次请求都要真实发生的这也是它最大的开销来源。在 MFC 里这个差别会被放大。MFC 的 CAsyncSocket 是基于 Windows 消息机制的异步 socket长连接意味着你要维护一个常驻的 socket 对象、处理 OnReceive 的粘包、处理断线重连、处理心跳超时。而短连接把这些状态全部消灭连接建立后同步等待应答收到就关超时就重试没有中间态。对于上位机每隔几秒读一次设备寄存器这种典型工控轮询场景短连接的代码量和出错概率都远低于长连接。代价是每次通信多出约 1 个 RTT 的握手时间。在局域网内这个开销通常小于 1ms完全可以接受如果是跨公网、RTT 几十毫秒的场景短连接就会明显拖慢轮询频率这时候才需要考虑长连接。2.2 用 CAsyncSocket 还是 CSocket选型理由MFC 提供两个 socket 封装类。CSocket 是同步阻塞模型内部靠消息泵模拟阻塞写起来像同步代码但在对话框程序里容易和 UI 消息循环打架出现界面卡死。CAsyncSocket 是异步非阻塞模型所有事件通过回调通知更适合短连接这种发完就等、等完就关的流程。我一般用 CAsyncSocket 做短连接原因是它的事件驱动模型天然适配一次请求一个生命周期OnConnect 里发数据OnReceive 里收数据并关闭OnClose 里清理对象。整个流程是线性的不需要额外的线程和锁。如果非要用 CSocket务必把它放在工作线程里别在主 UI 线程直接调用阻塞的 Receive否则界面会假死这是血泪经验。2.3 最小可跑的短连接客户端骨架下面这段代码是我从TCP.rar那套工程里抽出来的最小骨架去掉业务逻辑后可以直接编译。它演示了 CAsyncSocket 短连接的核心连接、发送、接收、关闭四个阶段。// ShortConnSocket.h #pragma once #include afxsock.h class CShortConnSocket : public CAsyncSocket { public: CShortConnSocket(); virtual ~CShortConnSocket(); // 发起一次短连接请求连接 - 发送 - 接收 - 关闭 BOOL Request(const CString strIP, UINT nPort, const CString strSend); protected: virtual void OnConnect(int nErrorCode); virtual void OnReceive(int nErrorCode); virtual void OnClose(int nErrorCode); virtual void OnConnectError(int nErrorCode); private: CString m_strSend; // 待发送数据 CString m_strRecv; // 接收缓冲 BOOL m_bConnected; // 连接状态标记 };// ShortConnSocket.cpp #include pch.h #include ShortConnSocket.h CShortConnSocket::CShortConnSocket() : m_bConnected(FALSE) {} CShortConnSocket::~CShortConnSocket() {} BOOL CShortConnSocket::Request(const CString strIP, UINT nPort, const CString strSend) { m_strSend strSend; m_strRecv.Empty(); // Create 必须在 Connect 之前端口传 0 表示由系统分配本地端口 if (!Create()) return FALSE; // 短连接连接目标地址成功后触发 OnConnect return Connect(strIP, nPort); } void CShortConnSocket::OnConnect(int nErrorCode) { if (nErrorCode ! 0) { Close(); return; } m_bConnected TRUE; // 连接成功立即发送短连接不等待 Send(m_strSend, m_strSend.GetLength()); } void CShortConnSocket::OnReceive(int nErrorCode) { if (nErrorCode ! 0) { Close(); return; } char buf[4096] { 0 }; int nRet Receive(buf, sizeof(buf) - 1); if (nRet 0) { buf[nRet] \0; m_strRecv buf; } // 短连接收到一次应答就关闭不等对端先关 ShutDown(2); // 2 禁止收发 Close(); } void CShortConnSocket::OnClose(int nErrorCode) { m_bConnected FALSE; // 这里可以通知上层本次短连接结束m_strRecv 是结果 } void CShortConnSocket::OnConnectError(int nErrorCode) { Close(); }逻辑说明Request是唯一入口调用方只关心发什么、发给谁不关心 socket 生命周期。OnConnect里连接成功后立刻Send这是短连接的关键——不等待、不缓存。OnReceive里收到数据后主动ShutDown(2)再Close避免进入 TIME_WAIT 之外的半关闭状态。参数说明Create()不传端口时系统自动分配本地端口短连接不需要固定本地端口ShutDown(2)的 2 表示同时禁止发送和接收比直接 Close 更干净接收缓冲 4096 是经验值工控指令应答一般不超过几百字节如果协议帧更大要相应调大。提示CAsyncSocket 的回调都在主线程消息循环里执行OnReceive 里不要做耗时操作否则会阻塞 UI。短连接因为生命周期短通常没问题但如果要连续高频轮询建议把 socket 放到工作线程。3. 把短连接封装成可复用的请求-应答模块3.1 一次请求的完整时序与状态机短连接虽然简单但如果没有明确的状态划分代码会退化成到处 Close的混乱局面。我一般给它定义四个状态Idle空闲、Connecting连接中、Sending已发送待应答、Closing关闭中。状态迁移只允许单向流动任何错误都直接回到 Idle 并关闭 socket。这个状态机的好处是排错时一目了然。现场出问题时看一眼当前状态就知道卡在哪一步卡在 Connecting 说明目标不可达或端口不对卡在 Sending 说明对端没回数据或协议不匹配卡在 Closing 说明关闭流程有异常。热词里那个error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address就是典型的端口占用问题在短连接里表现为 Connect 失败状态会停在 Connecting。3.2 超时控制短连接最容易被忽略的参数短连接最大的风险是连接建立了但对端不回数据这时候 OnReceive 永远不触发socket 就挂在那里。必须加超时。MFC 本身没有给 CAsyncSocket 提供超时接口常见做法是用SetTimer在发起请求时启动一个定时器超时后强制 Close。// 在对话框类里管理超时 #define TIMER_SHORTCONN_TIMEOUT 1001 #define SHORTCONN_TIMEOUT_MS 3000 // 3 秒无应答即超时 void CMyDlg::OnBnClickedSend() { m_sock.Request(_T(192.168.1.100), 502, _T(READ_REG_01\r\n)); // 启动超时定时器3 秒后如果还没收到应答就强制关闭 SetTimer(TIMER_SHORTCONN_TIMEOUT, SHORTCONN_TIMEOUT_MS, nullptr); } void CMyDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent TIMER_SHORTCONN_TIMEOUT) { KillTimer(TIMER_SHORTCONN_TIMEOUT); m_sock.Close(); // 强制关闭触发 OnClose } CDialogEx::OnTimer(nIDEvent); }参数说明SHORTCONN_TIMEOUT_MS设 3000 是工控场景的常用值局域网设备应答通常在几十毫秒内3 秒足够覆盖偶发卡顿如果是跨网段或设备处理慢可以放宽到 5000。注意超时后要KillTimer否则定时器会反复触发。收到应答时也要记得KillTimer这个容易漏漏了会导致下一次请求被上一次的定时器误杀。3.3 粘包与半包短连接也躲不掉的问题很多人以为短连接就没有粘包问题这是误解。TCP 是字节流协议短连接只是减少了连接状态不改变字节流本质。如果一次应答数据超过接收缓冲或者对端分两次发送OnReceive 会触发多次你需要自己判断数据收完了没有。常见做法是约定协议帧格式定长帧、或者带长度字段、或者用结束符。工控里最常用的是结束符方案比如以\r\n结尾。下面是在 OnReceive 里判断帧完整的逻辑void CShortConnSocket::OnReceive(int nErrorCode) { if (nErrorCode ! 0) { Close(); return; } char buf[4096] { 0 }; int nRet Receive(buf, sizeof(buf) - 1); if (nRet 0) { buf[nRet] \0; m_strRecv buf; // 判断是否收到完整帧以 \r\n 结尾 if (m_strRecv.Right(2) _T(\r\n)) { ShutDown(2); Close(); // 帧完整关闭连接 } // 否则继续等下一次 OnReceive } }逻辑说明每次收到数据追加到m_strRecv然后检查是否以结束符结尾。是则关闭否则继续等。参数说明结束符要和设备协议一致有的设备用\n有的用固定长度。如果协议是定长帧就判断m_strRecv.GetLength() 预期长度。注意不要用Receive的返回值判断帧完整返回值只代表本次收到的字节数不代表整帧。注意如果对端发送的数据永远不以结束符结尾这个逻辑会一直等下去必须配合 3.2 的超时定时器兜底。4. 现场联调最容易翻车的五个坑4.1 坑一Connect 返回成功但实际没连上现象Connect返回 TRUE但 OnConnect 回调里nErrorCode非 0或者干脆不触发。原因CAsyncSocket 的 Connect 是异步的返回 TRUE 只表示请求已提交不代表连接成功。真正的结果在 OnConnect 里。解决所有连接后的逻辑都放在 OnConnect 里不要在 Connect 返回后直接 Send。这是新手最常翻的车。4.2 坑二OnReceive 里 Close 导致崩溃现象程序偶发崩溃堆栈指向 socket 相关代码。原因在 OnReceive 回调里直接delete this或删除 socket 对象而此时 MFC 内部还在使用这个对象。解决不要在回调里销毁对象用PostMessage通知上层由上层在消息处理里安全销毁。或者用Close()而不是delete对象复用。4.3 坑三短连接频率过高导致端口耗尽现象程序跑一段时间后 Connect 失败报only one usage of each socket address或类似错误。原因短连接每次关闭后本地端口进入 TIME_WAIT 状态默认持续 2MSL约 4 分钟。如果每秒发起几十次短连接端口会被快速耗尽。解决降低轮询频率或者设置SO_REUSEADDR或者改用长连接。工控场景一般轮询间隔在几百毫秒以上不会触发这个问题但如果做高频采集就要注意。4.4 坑四中文乱码现象发送的中文指令设备收到乱码或接收的中文数据显示为问号。原因MFC 默认使用 Unicode而Send/Receive操作的是字节流直接把 CString 传进去会按宽字符处理。解决发送前用CStringA或WideCharToMultiByte转成 UTF-8 或 GBK接收后反向转换。参数说明转换编码要和设备协议一致国内工控设备多用 GBK国际设备多用 UTF-8。4.5 坑五对话框关闭时 socket 未清理现象关闭程序时偶发崩溃或进程残留。原因对话框销毁时 socket 对象还在回调可能触发到已销毁的窗口。解决在OnDestroy里显式Close所有 socket并KillTimer所有定时器。顺序很重要先 KillTimer 再 Close socket避免定时器回调里访问已关闭的 socket。5. 让短连接更稳的两个进阶技巧5.1 用 Wireshark 验证短连接行为调试短连接最有效的手段是抓包。热词里有人问 tcp三次握手在wireshark中怎么筛选出来答案是过滤表达式tcp.flags.syn 1 tcp.flags.ack 0。短连接的抓包特征非常明显每次请求都能看到完整的 SYN、SYN-ACK、ACK 三次握手然后 PSH 传数据最后 FIN、ACK 四次挥手。如果抓包发现只有 SYN 没有 SYN-ACK说明目标端口没开或被防火墙拦了如果握手完成但迟迟没有 PSH说明你的 Send 没发出去或对端没处理。我一般会在联调时同时开 Wireshark 和程序日志对照时间戳看每一步。有一次现场设备偶尔不回数据抓包发现是设备在收到指令后 2.8 秒才应答刚好卡在我 3 秒超时边缘把超时改成 5 秒就稳定了。这种问题不看抓包根本定位不到。5.2 重试策略别用固定间隔短连接失败后的重试不能简单循环。固定间隔重试会在设备重启期间持续打空包浪费资源还可能触发设备保护。我一般用指数退避第一次失败等 200ms第二次 400ms第三次 800ms最多重试 3 次。这样既能覆盖偶发丢包又不会在设备真故障时疯狂重连。// 指数退避重试 int nRetry 0; int nDelay 200; while (nRetry 3) { if (m_sock.Request(ip, port, cmd)) { // 等待应答由 OnReceive 或超时定时器决定成败 break; } Sleep(nDelay); nDelay * 2; // 200 - 400 - 800 nRetry; }参数说明初始延迟 200ms 适合局域网跨网段可以设 500ms最大重试 3 次是经验值再多说明链路本身有问题应该报错而不是继续重试。注意Sleep会阻塞 UI 线程实际项目里应该用定时器实现异步重试这里只是为了演示退避逻辑。最后说个我自己的习惯每次接手一套 MFC 网络通信代码第一件事不是看业务逻辑而是把 socket 的创建、连接、关闭三个点找出来确认没有泄漏和悬空回调。这个习惯帮我省了无数次深夜排查。短连接这套东西不新但在 MFC 这个老框架里它是最不容易出玄学问题的方案。希望帮到你。本文还有配套的精品资源点击获取
返回列表