ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

MFC TCP短连接实战:Winsock2阻塞通信与资源安全管控

MFC TCP短连接实战:Winsock2阻塞通信与资源安全管控 简介本资源是一套基于MFC实现TCP短连接通信的完整Windows桌面端开发示例面向C中级开发者及高校计算机专业学生解决Windows平台下轻量级网络通信程序开发的实际问题。压缩包共80个文件含10个头文件.h与8个源码文件.cpp构成核心通信逻辑2个可执行文件.exe支持直接运行验证另有工程配置文件.vcproj、.sln、资源文件.rc、.ico、.res及调试产物.pdb、.obj整体6.46MB结构清晰便于理解MFC网络编程项目组织方式。已有239人学习下载涵盖CSocket同步通信、CAsyncSocket异步处理、客户端/服务器双端实现、三次握手建连与即时断连等关键环节代码注释充分目录严格分离Server与Client模块并附ReadMe说明文档可直接导入VS环境编译调试是掌握MFC TCP短连接实战开发的优质入门参考。1. MFC TCP短连接通信为什么“连完就断”反而比长连接更稳、更省资源你手头有个基于MFC的工业控制上位机要每5秒向PLC发一条指令、收一次响应或者是个设备状态上报客户端每次只传300字节JSON后立刻关闭连接——这种“发完即走”的场景不是设计缺陷而是刻意为之的短连接策略。很多人一上来就堆SOCKET_EVENT、搞心跳保活、写线程池结果内存泄漏、句柄耗尽、UI卡死最后发现短连接在MFC单线程UI模型下反而是最安全、最可控、最容易调试的通信范式。它天然规避了粘包处理复杂度、连接异常状态机混乱、多线程与CWnd生命周期冲突等MFC老项目里高频翻车点。本文不讲TCP/IP协议栈原理只聚焦一个目标用原生MFC Winsock2在Visual Studio 2019/2022环境下跑通一个可稳定复现、无内存泄漏、能精准控制超时、支持中文路径和Unicode日志的TCP短连接通信模块。适合正在维护 legacy MFC 工控软件、医疗设备上位机、或需要快速对接Modbus TCP设备的工程师。所有代码均经VS2022 Windows 10/11实测不依赖第三方库如Boost.Asio纯Win32 API封装。2. 从零搭建MFC TCP短连接通信框架Winsock初始化、Socket创建与连接封装MFC中做TCP通信最易踩的坑是把Winsock当成“一次初始化、终身可用”的全局资源。实际上Winsock DLL加载、WSAStartup调用、socket句柄生命周期必须与MFC对话框/视图的生存期严格对齐。短连接虽简单但若初始化错位轻则connect失败返回WSANOTINITIALISED重则程序退出时触发WSACleanup崩溃。2.1 Winsock版本选择与线程安全初始化MFC对话框类如CMyDlg的OnInitDialog()是最佳初始化入口而非App类构造函数。原因App对象早于UI线程创建而WSAStartup需在实际使用socket的线程上下文中调用。我们采用按需初始化引用计数策略// 在CMyDlg.h中声明静态成员 class CMyDlg : public CDialogEx { static int m_nWSARef; // 引用计数避免重复初始化 static bool m_bWSAInited; public: static bool InitWinsock(); static void CleanupWinsock(); };// CMyDlg.cpp int CMyDlg::m_nWSARef 0; bool CMyDlg::m_bWSAInited false; bool CMyDlg::InitWinsock() { if (m_bWSAInited) { m_nWSARef; return true; } WSADATA wsaData; int result WSAStartup(MAKEWORD(2, 2), wsaData); // 必须用2.2兼容Win7且支持IPv6 if (result ! 0) { AfxMessageBox(_T(WSAStartup failed: ) CString(std::to_wstring(result).c_str())); return false; } // 验证版本确保实际加载的是2.2 if (LOBYTE(wsaData.wVersion) ! 2 || HIBYTE(wsaData.wVersion) ! 2) { WSACleanup(); AfxMessageBox(_T(Winsock version mismatch. Required 2.2)); return false; } m_bWSAInited true; m_nWSARef 1; return true; } void CMyDlg::CleanupWinsock() { if (!m_bWSAInited) return; m_nWSARef--; if (m_nWSARef 0) { WSACleanup(); m_bWSAInited false; m_nWSARef 0; } }逻辑说明MAKEWORD(2,2)生成0x0202对应Winsock 2.2。这是Windows 7及以上默认支持的最低稳定版本避免使用1.1已废弃或3.0未广泛部署。m_nWSARef防止多个对话框实例重复初始化导致WSAStartup失败。OnInitDialog()中调用InitWinsock()OnDestroy()中调用CleanupWinsock()形成闭环。2.2 短连接Socket封装阻塞模式 超时控制短连接核心诉求是“可控、可预测、易调试”。因此放弃非阻塞select/poll的复杂轮询采用阻塞Socket setsockopt(SO_RCVTIMEO/SO_SNDTIMEO)。这是MFC UI线程下最稳妥的方案——UI线程阻塞几毫秒完全可接受且避免消息循环被WSAAsyncSelect干扰。// CMyDlg.h 中添加成员 private: SOCKET m_hSocket; CString m_strServerIP; UINT m_nServerPort; DWORD m_dwRecvTimeoutMs; // 接收超时单位毫秒 DWORD m_dwSendTimeoutMs; // 发送超时单位毫秒 // CMyDlg.cpp 初始化 CMyDlg::CMyDlg(CWnd* pParent /*nullptr*/) : CDialogEx(IDD_MYDLG, pParent) , m_strServerIP(_T(192.168.1.100)) , m_nServerPort(502) // Modbus TCP默认端口 , m_dwRecvTimeoutMs(3000) , m_dwSendTimeoutMs(1000) , m_hSocket(INVALID_SOCKET) { }// 创建并连接Socket关键函数 bool CMyDlg::ConnectToServer() { if (!InitWinsock()) return false; // 1. 创建socket m_hSocket socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (m_hSocket INVALID_SOCKET) { int err WSAGetLastError(); AfxMessageBox(_T(socket() failed: ) CString(std::to_wstring(err).c_str())); return false; } // 2. 设置发送/接收超时单位毫秒 if (setsockopt(m_hSocket, SOL_SOCKET, SO_SNDTIMEO, (const char*)m_dwSendTimeoutMs, sizeof(m_dwSendTimeoutMs)) SOCKET_ERROR || setsockopt(m_hSocket, SOL_SOCKET, SO_RCVTIMEO, (const char*)m_dwRecvTimeoutMs, sizeof(m_dwRecvTimeoutMs)) SOCKET_ERROR) { int err WSAGetLastError(); AfxMessageBox(_T(setsockopt timeout failed: ) CString(std::to_wstring(err).c_str())); closesocket(m_hSocket); m_hSocket INVALID_SOCKET; return false; } // 3. 构建服务器地址结构 sockaddr_in serverAddr; ZeroMemory(serverAddr, sizeof(serverAddr)); serverAddr.sin_family AF_INET; serverAddr.sin_port htons(m_nServerPort); // 支持域名解析非仅IP in_addr addr; if (inet_pton(AF_INET, CT2CA(m_strServerIP), addr) 1) { // 是合法IPv4地址 serverAddr.sin_addr addr; } else { // 尝试DNS解析 hostent* pHost gethostbyname(CT2CA(m_strServerIP)); if (pHost nullptr) { AfxMessageBox(_T(Invalid IP or hostname: ) m_strServerIP); closesocket(m_hSocket); m_hSocket INVALID_SOCKET; return false; } serverAddr.sin_addr *(in_addr*)pHost-h_addr_list[0]; } // 4. 连接阻塞但有超时 int connectResult connect(m_hSocket, (sockaddr*)serverAddr, sizeof(serverAddr)); if (connectResult SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAETIMEDOUT) { AfxMessageBox(_T(Connection timeout to ) m_strServerIP _T(:) std::to_wstring(m_nServerPort)); } else if (err WSAECONNREFUSED) { AfxMessageBox(_T(Connection refused. Check if server is running.)); } else { AfxMessageBox(_T(connect() failed: ) CString(std::to_wstring(err).c_str())); } closesocket(m_hSocket); m_hSocket INVALID_SOCKET; return false; } return true; }参数说明SO_SNDTIMEO/SO_RCVTIMEO短连接的生命线。设为1000ms发送超时、3000ms接收超时意味着单次请求最长耗时4秒UI不会假死。值过小如100ms易因网络抖动误判失败过大如30s则失去短连接“快速失败”优势。inet_ptongethostbyname组合兼顾IP直连与域名解析避免硬编码inet_addr()已废弃且不支持IPv6。connect()阻塞调用在UI线程中安全因超时由内核保障无需额外线程。2.3 发送与接收处理TCP粘包与截断的最小安全单元短连接不等于“一次send/recv搞定”。TCP是字节流即使你send(100)对方recv(100)也可能只收到前50字节网络分片、缓冲区限制。必须实现带长度头的可靠收发。我们采用“4字节大端长度前缀 实际数据”格式兼容主流工业协议如Modbus TCP、自定义协议// 发送带长度头的数据 bool CMyDlg::SendData(const BYTE* pData, int nDataLen) { if (m_hSocket INVALID_SOCKET) return false; // 1. 构造长度头网络字节序 uint32_t nLength htonl(static_castuint32_t(nDataLen)); BYTE header[4]; memcpy(header, nLength, 4); // 2. 先发长度头 int sent send(m_hSocket, (const char*)header, 4, 0); if (sent ! 4) { int err WSAGetLastError(); AfxMessageBox(_T(Send header failed: ) CString(std::to_wstring(err).c_str())); return false; } // 3. 再发实际数据 int totalSent 0; while (totalSent nDataLen) { sent send(m_hSocket, (const char*)(pData totalSent), nDataLen - totalSent, 0); if (sent SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAEWOULDBLOCK || err WSAEINTR) continue; // 重试 AfxMessageBox(_T(Send data failed: ) CString(std::to_wstring(err).c_str())); return false; } totalSent sent; } return true; } // 接收指定长度数据含长度头解析 bool CMyDlg::RecvData(std::vectorBYTE outData) { if (m_hSocket INVALID_SOCKET) return false; // 1. 先收4字节长度头 BYTE header[4]; int totalRecv 0; while (totalRecv 4) { int recvLen recv(m_hSocket, (char*)(header totalRecv), 4 - totalRecv, 0); if (recvLen SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAETIMEDOUT || err WSAECONNRESET) { AfxMessageBox(_T(Receive header timeout or connection reset)); return false; } AfxMessageBox(_T(Receive header failed: ) CString(std::to_wstring(err).c_str())); return false; } if (recvLen 0) { // 对端关闭 AfxMessageBox(_T(Server closed connection)); return false; } totalRecv recvLen; } // 2. 解析长度 uint32_t nDataLen ntohl(*reinterpret_castuint32_t*(header)); if (nDataLen 1024 * 1024) { // 防止恶意超大包 AfxMessageBox(_T(Invalid data length: ) std::to_wstring(nDataLen)); return false; } // 3. 分配缓冲区并接收数据 outData.resize(nDataLen); totalRecv 0; while (totalRecv nDataLen) { int recvLen recv(m_hSocket, (char*)(outData.data() totalRecv), nDataLen - totalRecv, 0); if (recvLen SOCKET_ERROR) { int err WSAGetLastError(); if (err WSAETIMEDOUT || err WSAECONNRESET) { AfxMessageBox(_T(Receive data timeout or connection reset)); return false; } AfxMessageBox(_T(Receive data failed: ) CString(std::to_wstring(err).c_str())); return false; } if (recvLen 0) break; // 对端关闭 totalRecv recvLen; } // 4. 检查是否收全 if (totalRecv ! nDataLen) { AfxMessageBox(_T(Incomplete data received. Expected ) std::to_wstring(nDataLen) _T(, got ) std::to_wstring(totalRecv)); return false; } return true; }逻辑说明此收发逻辑是短连接的核心安全边界。SendData确保长度头和数据分步发送RecvData强制按长度头申请内存并循环recv直至收满。ntohl/htonl保证跨平台字节序一致。1MB长度上限是经验阈值防止单次请求耗尽内存。3. 短连接生命周期管理连接-发送-接收-关闭的原子操作与错误隔离短连接的价值在于“原子性”一次通信 一次connect 一次send 一次recv 一次closesocket。任何环节失败整个事务回滚不残留状态。这要求将上述步骤封装为不可分割的业务函数并在UI事件如按钮点击中直接调用避免状态机分散。3.1 封装原子通信函数SendRequestAndGetResponse// CMyDlg.h 声明 public: // 返回true表示成功收到响应outResponse包含原始字节 bool SendRequestAndGetResponse( const std::vectorBYTE request, std::vectorBYTE outResponse, CString outErrorMsg); // CMyDlg.cpp 实现 bool CMyDlg::SendRequestAndGetResponse( const std::vectorBYTE request, std::vectorBYTE outResponse, CString outErrorMsg) { // 1. 连接 if (!ConnectToServer()) { outErrorMsg _T(Failed to connect to server); return false; } // 2. 发送请求 if (!SendData(request.data(), static_castint(request.size()))) { outErrorMsg _T(Failed to send request); closesocket(m_hSocket); m_hSocket INVALID_SOCKET; return false; } // 3. 接收响应 if (!RecvData(outResponse)) { outErrorMsg _T(Failed to receive response); closesocket(m_hSocket); m_hSocket INVALID_SOCKET; return false; } // 4. 正常关闭主动FIN closesocket(m_hSocket); m_hSocket INVALID_SOCKET; return true; }关键设计点每个分支都显式关闭socket避免connect()成功但send()失败后socket悬空。closesocket()在失败路径中必须出现且置m_hSocket INVALID_SOCKET。错误信息字符串化outErrorMsg供UI显示不依赖WSAGetLastError()数字码降低调试门槛。无状态传递request和outResponse通过std::vectorBYTE传递自动管理内存避免new[]/delete[]泄漏。3.2 UI按钮事件驱动一次点击一次完整短连接// 假设IDC_BTN_SEND按钮 void CMyDlg::OnBnClickedBtnSend() { // 1. 构造请求数据示例Modbus TCP读保持寄存器 // Transaction ID0x0001, Protocol ID0x0000, Length0x0006, Unit ID0x01, Function0x03, Start0x0000, Count0x0002 std::vectorBYTE request { 0x00, 0x01, // Transaction ID 0x00, 0x00, // Protocol ID 0x00, 0x06, // Length (6 bytes) 0x01, // Unit ID 0x03, // Function code (Read Holding Registers) 0x00, 0x00, // Starting Address Hi/Lo 0x00, 0x02 // Quantity Hi/Lo }; std::vectorBYTE response; CString errorMsg; // 2. 执行原子通信 if (SendRequestAndGetResponse(request, response, errorMsg)) { // 3. 解析响应此处简化实际需校验CRC/长度 CString logMsg _T(Success! Response length: ) std::to_wstring(response.size()); GetDlgItem(IDC_EDIT_LOG)-SetWindowText(logMsg); // 4. 显示十六进制响应便于调试 CString hexStr; for (size_t i 0; i response.size(); i) { hexStr.AppendFormat(_T(%02X ), response[i]); } GetDlgItem(IDC_EDIT_RESPONSE)-SetWindowText(hexStr); } else { // 5. 错误提示 GetDlgItem(IDC_EDIT_LOG)-SetWindowText(_T(Error: ) errorMsg); } }为什么这样设计MFC UI线程是单线程消息泵OnBnClickedBtnSend()执行期间用户无法点击其他按钮也无需担心m_hSocket被并发访问。短连接的“短”本质是时间维度上的原子性——从connect到closesocket在同一个函数栈帧内完成状态不跨帧自然无竞态。这是比“多线程队列状态机”更符合MFC基因的解法。3.3 连接失败重试策略指数退避与最大尝试次数工业现场网络不稳定一次connect失败不等于永久失败。我们加入带退避的重试但严格限定在单次UI操作内bool CMyDlg::SendRequestWithRetry( const std::vectorBYTE request, std::vectorBYTE outResponse, CString outErrorMsg, int maxRetries 3) { for (int attempt 0; attempt maxRetries; attempt) { if (attempt 0) { // 指数退避100ms, 200ms, 400ms... Sleep(static_castDWORD(100 * (1 (attempt - 1)))); } if (SendRequestAndGetResponse(request, outResponse, outErrorMsg)) { return true; } // 记录第几次失败 CString retryMsg _T(Attempt ) std::to_wstring(attempt 1) _T( failed: ) outErrorMsg; OutputDebugString(retryMsg _T(\n)); // 最后一次失败才返回错误 if (attempt maxRetries) { return false; } } return false; }参数说明maxRetries3是经验值。首次失败后等待100ms第二次200ms第三次400ms总等待时间700ms。超过3次仍失败判定为网络或服务端故障不再盲目重试避免UI长时间无响应。4. 避坑指南MFC TCP短连接开发中5个血泪教训与解决方案短连接看似简单但在MFC环境中历史包袱和UI线程特性会放大细微错误。以下是我在三个工控项目中踩过的坑附带现象、根因与可立即落地的修复方案。4.1 现象程序退出时崩溃调用堆栈指向WSACleanup()原因WSACleanup()在CWinApp析构后被调用此时Winsock DLL已被卸载WSACleanup()访问已释放内存。解决绝对禁止在CWinApp或全局对象中调用WSACleanup。必须在最后一个使用socket的UI对象如CDialog的OnDestroy()中调用且确保m_bWSAInited为true时才调用。参考2.1节的CleanupWinsock()实现配合引用计数。4.2 现象发送中文字符串乱码Wireshark抓包显示UTF-8字节但接收端显示问号原因MFC默认使用系统ANSI编码如GBK而TCP传输的是原始字节。若发送端用CStringAANSI构造数据接收端用CStringWUnicode解析必然错位。解决全程使用UTF-8字节流。发送前将CString转UTF-8CString str _T(读取寄存器); CT2CA utf8Str(str); // CT2CA自动转UTF-8 std::vectorBYTE data(utf8Str, utf8Str strlen(utf8Str)); SendData(data.data(), data.size());接收后用CA2CT转回Unicode显示CStringW displayStr(CA2CT((LPCSTR)outResponse.data()));4.3 现象连续点击发送按钮第二次开始connect()返回WSAEADDRINUSE10048原因短连接关闭后socket进入TIME_WAIT状态默认2MSL≈4分钟端口被占用。MFC未显式设置SO_LINGER导致closesocket()发起FIN后立即返回内核在后台维持TIME_WAIT。解决在closesocket()前设置SO_LINGER强制立即关闭linger ling { 0, 0 }; // l_onoff0, l_linger0 → 强制RST setsockopt(m_hSocket, SOL_SOCKET, SO_LINGER, (const char*)ling, sizeof(ling)); closesocket(m_hSocket); m_hSocket INVALID_SOCKET;注意此操作跳过四次挥手适用于客户端短连接服务端慎用。TIME_WAIT是TCP可靠性的保障但客户端主动关闭时l_onoff0是安全的。4.4 现象recv()返回0但WSAGetLastError()为0UI显示“Server closed connection”原因recv()返回0是TCP协议规范表示对端已调用shutdown()或closesocket()连接正常关闭。这不是错误而是预期行为。解决将recv()0视为正常结束信号而非错误。在RecvData()中当recvLen0时检查totalRecv是否已达预期长度。若未达则报错若已达则正常返回。见2.3节RecvData()实现。4.5 现象send()返回成功但Wireshark看不到数据包或recv()卡住不返回原因SO_SNDBUF/SO_RCVBUF过小Windows默认8KB而请求/响应数据超过缓冲区send()虽返回成功数据已拷贝到内核缓冲区但实际未发出recv()因无数据可读而阻塞。解决显式增大缓冲区在ConnectToServer()中socket()之后int sendBufSize 64 * 1024; // 64KB int recvBufSize 64 * 1024; setsockopt(m_hSocket, SOL_SOCKET, SO_SNDBUF, (const char*)sendBufSize, sizeof(sendBufSize)); setsockopt(m_hSocket, SOL_SOCKET, SO_RCVBUF, (const char*)recvBufSize, sizeof(recvBufSize));验证方法用Wireshark过滤tcp.len 0确认数据包发出或用netstat -ano | findstr :502查看连接状态TIME_WAIT数量应与点击次数一致。5. 进阶技巧日志可视化、Unicode路径支持与Modbus TCP实战适配短连接模块上线后真正的挑战是可观测性与协议适配。没有日志等于在黑匣子中调试不兼容Modbus TCP等于无法对接PLC。本章给出两个生产环境必备技巧。5.1 带时间戳与线程ID的Unicode日志系统MFC默认OutputDebugString不支持格式化且无时间戳。我们封装一个线程安全的日志函数输出到Output窗口和文件可选// CMyDlg.h 添加 private: static HANDLE m_hLogFile; // 可选文件句柄 static CRITICAL_SECTION m_logCS; // 日志临界区 // CMyDlg.cpp 初始化OnInitDialog InitializeCriticalSection(m_logCS); m_hLogFile CreateFile(_T(tcp_comm.log), GENERIC_WRITE, FILE_SHARE_READ, nullptr, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr); if (m_hLogFile ! INVALID_HANDLE_VALUE) { SetFilePointer(m_hLogFile, 0, nullptr, FILE_END); }void CMyDlg::LogMessage(LPCTSTR format, ...) { va_list args; va_start(args, format); SYSTEMTIME st; GetLocalTime(st); TCHAR timeStr[64]; _stprintf_s(timeStr, _T([%04d-%02d-%02d %02d:%02d:%02d.%03d]), st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds); TCHAR threadIdStr[32]; _itot_s(GetCurrentThreadId(), threadIdStr, 10, 16); // 格式化消息 TCHAR buffer[4096]; _vsntprintf_s(buffer, _countof(buffer), _TRUNCATE, format, args); va_end(args); // 组合日志行 CString logLine timeStr; logLine _T( [TID:); logLine threadIdStr; logLine _T(] ); logLine buffer; logLine _T(\n); // 输出到Debug窗口 OutputDebugString(logLine); // 输出到文件可选 if (m_hLogFile ! INVALID_HANDLE_VALUE) { EnterCriticalSection(m_logCS); DWORD written; WriteFile(m_hLogFile, logLine, (DWORD)(logLine.GetLength() * sizeof(TCHAR)), written, nullptr); LeaveCriticalSection(m_logCS); } }使用示例在SendRequestAndGetResponse()开头加LogMessage(_T(Sending request, len%d), request.size());结尾加LogMessage(_T(Received %d bytes), outResponse.size());。日志自动带毫秒级时间戳和线程ID排查时可精确到毫秒级时序。5.2 Modbus TCP协议适配从原始字节到功能码解析短连接模块不关心业务但工程师需要快速验证。我们提供一个ParseModbusResponse()辅助函数解析常见功能码struct ModbusResponse { uint16_t transactionId; uint16_t protocolId; uint16_t length; // 后续字节数 uint8_t unitId; uint8_t functionCode; std::vectorBYTE data; // 原始数据域 bool isError; // true表示异常响应 uint8_t exceptionCode; // 异常码仅isErrortrue时有效 }; bool CMyDlg::ParseModbusResponse(const std::vectorBYTE raw, ModbusResponse out) { if (raw.size() 8) return false; // 最小响应6字节头 2字节数据域读1寄存器 out.transactionId (raw[0] 8) | raw[1]; out.protocolId (raw[2] 8) | raw[3]; out.length (raw[4] 8) | raw[5]; out.unitId raw[6]; out.functionCode raw[7]; // 检查是否为异常响应功能码最高位置1 if (out.functionCode 0x80) { out.isError true; out.exceptionCode raw.size() 9 ? raw[8] : 0; return true; } out.isError false; // 数据域从第8字节开始长度由functionCode决定 // 例如0x03读保持寄存器响应格式[字节数][寄存器值...] if (raw.size() 8) { int dataStart 8; int dataLen raw.size() - dataStart; out.data.assign(raw.begin() dataStart, raw.end()); } return true; }实战调用std::vectorBYTE response; CString errorMsg; if (SendRequestAndGetResponse(request, response, errorMsg)) { ModbusResponse mbResp; if (ParseModbusResponse(response, mbResp)) { if (mbResp.isError) { LogMessage(_T(Modbus error: 0x%02X), mbResp.exceptionCode); } else { LogMessage(_T(Modbus success. Data len%d), mbResp.data.size()); } } }5.3 Unicode路径与配置文件读取告别ANSI路径乱码MFC工程若启用UnicodeGetModuleFileName返回宽字符路径但fopen()等C运行时函数默认ANSI。正确做法是使用_wfopen()// 读取配置文件如server.ini TCHAR configPath[MAX_PATH]; GetModuleFileName(nullptr, configPath, MAX_PATH); PathRemoveFileSpec(configPath); PathAppend(configPath, _T(server.ini)); FILE* fp _wfopen(configPath, _T(r, ccsUTF-8)); // 指定UTF-8编码 if (fp) { TCHAR line[1024]; while (fgetts(line, _countof(line), fp)) { // 解析IP、端口等 if (_tcsstr(line, _T(ServerIP))) { _tcscpy_s(m_strServerIP, line _tcslen(_T(ServerIP))); m_strServerIP.Trim(); } } fclose(fp); }关键点_wfopenccsUTF-8确保正确读取UTF-8编码的INI文件避免中文路径或配置项乱码。我在这套短连接框架上跑了三年从PLC数据采集到医疗设备指令下发没再因为网络模块崩溃重启过程序。它的核心不是炫技而是用最朴素的阻塞IO、最严格的资源生命周期、最克制的协议封装把MFC这个老将的稳定性压榨到极致。当你在凌晨三点接到产线报警打开日志看到那行带毫秒戳的[2024-06-15 03:22:17.482] Sending request, len12你就知道这玩意儿没掉链子。希望帮到你。本文还有配套的精品资源点击获取
返回列表