简介:这份资源面向Windows平台网络编程初学者与C++开发者,聚焦基于TCP协议的C/S架构通信实现,帮助读者理解Socket套接字在MFC框架下的封装与调用方式。压缩包共4个文件,包含2个cpp源文件与2个h头文件,分别对应客户端连接对话框与服务器端对话框的核心实现,整体约6KB,体积轻量便于快速阅读与移植。资源围绕CSocket类展开,涉及Winsock初始化、套接字创建、绑定监听、接受连接、连接服务器以及数据收发与关闭等关键环节,可作为学习MFC网络编程的入门范例。目前已有1260人学习下载,适合希望掌握TCP通信流程、理解客户端与服务器交互逻辑的开发者参考,也可作为课程实验或小型项目的基础代码骨架,便于在此基础上扩展业务功能。
1. 从零拆一个 MFC TCP 通信程序:C/S 架构到底能解决什么问题
很多人第一次接触 socket 网络编程,是在控制台里敲socket()、bind()、listen()那一套,跑通了却不知道怎么套到 Windows 窗口程序里。这份资源给的是一个基于 socket 通信、用 MFC 实现 TCP 通信的 C/S 架构程序,说白了就是把 TCP 服务端和客户端都塞进 MFC 的对话框框架里,用 Windows 消息机制驱动收发,而不是靠while(1)死循环硬扛。它适合两类人:一类是学过 TCP 三次握手、知道 TCP 和 UDP 区别,但没在 MFC 里真正落地过的学生或转岗工程师;另一类是要给工控上位机、设备调试工具做通信模块的从业者,需要一个能直接改、能编译、能抓包验证的骨架。C/S 架构在这里不是概念,而是服务端先listen等连接、客户端connect发起握手、双方各自维护一个SOCKET句柄并靠OnReceive回调收数据的完整闭环。下面按「资源是什么 → 怎么用 → 坑在哪」的顺序拆开讲。
2. MFC 与 Socket 的接法:为什么不用阻塞循环而用消息驱动
2.1 选型理由:MFC 的 CAsyncSocket 与原生 Winsock 怎么选
MFC 里做 TCP 通信有两条路。一条是直接用 Winsock 的socket、connect、send、recv,另一条是用 MFC 封装好的CAsyncSocket和CSocket。这份资源走的是 MFC 封装路线,原因很实际:MFC 的窗口线程本身要跑消息循环,如果你在 UI 线程里写一个阻塞的recv,窗口立刻卡死,点关闭都没反应。CAsyncSocket把 socket 句柄和窗口消息绑定,底层用WSAAsyncSelect注册网络事件,收到数据时投递一个自定义消息,由OnReceive虚函数处理,UI 线程该刷新刷新、该响应响应。
常见做法是:服务端用CAsyncSocket派生一个CListenSocket负责Listen和OnAccept,再派生一个CClientSocket负责每个连接的OnReceive、OnClose;客户端直接派生一个CClientSocket调Connect。这样每个连接是一个对象,生命周期清晰,不会出现句柄满天飞的情况。
提示:
CAsyncSocket的回调是在窗口线程里执行的,OnReceive里不要做耗时操作,否则照样卡 UI。要处理大块数据,先把数据拷到缓冲区,再投递到工作线程。
2.2 服务端搭建:从 Listen 到 OnAccept 的完整步骤
服务端核心是「监听 socket 只负责接受连接,通信 socket 每个连接一个」。下面是一个可抄的骨架,基于对话框程序,在InitDialog里启动监听。
// ListenSocket.h class CListenSocket : public CAsyncSocket { public: CListenSocket() {} virtual ~CListenSocket() {} virtual void OnAccept(int nErrorCode); }; // ListenSocket.cpp void CListenSocket::OnAccept(int nErrorCode) { if (nErrorCode == 0) { // 有新连接进来,创建一个通信 socket CClientSocket* pClient = new CClientSocket(); // Accept 把新连接的句柄交给 pClient if (Accept(*pClient)) { // 把 pClient 存进列表,方便后续广播或管理 theApp.m_clientList.AddTail(pClient); } else { delete pClient; } } CAsyncSocket::OnAccept(nErrorCode); }逻辑说明:OnAccept是CAsyncSocket在监听 socket 收到连接请求时自动调用的虚函数。Accept的参数是一个空的CClientSocket对象,调用成功后这个对象就持有了新连接的 socket 句柄,后续它的OnReceive会被触发。参数nErrorCode为 0 表示正常,非 0 要查WSAGetLastError。
启动监听的代码放在对话框初始化里:
// 在对话框 OnInitDialog 中 if (!m_listenSocket.Create(8888, SOCK_STREAM)) { AfxMessageBox(_T("监听端口创建失败")); return FALSE; } if (!m_listenSocket.Listen(5)) { AfxMessageBox(_T("Listen 失败")); return FALSE; }Create的第二个参数SOCK_STREAM指定 TCP,端口 8888 可以换成任意未占用端口。Listen(5)里的 5 是等待队列长度,不是最大连接数,别搞混。常见做法是设 5 到 10,太大没意义,太小高并发时会丢连接。
2.3 客户端连接与收发:Connect 和 OnReceive 的配合
客户端比服务端少一个监听环节,直接Create再Connect。注意Connect是异步的,返回FALSE且GetLastError为WSAEWOULDBLOCK是正常的,真正的连接结果在OnConnect里判断。
// ClientSocket.cpp void CClientSocket::OnConnect(int nErrorCode) { if (nErrorCode == 0) { // 连接成功,可以发数据了 CString strMsg = _T("hello server"); Send(strMsg, strMsg.GetLength() * sizeof(TCHAR)); } else { AfxMessageBox(_T("连接服务端失败")); } CAsyncSocket::OnConnect(nErrorCode); } void CClientSocket::OnReceive(int nErrorCode) { if (nErrorCode == 0) { char buf[4096] = {0}; int nRead = Receive(buf, sizeof(buf) - 1); if (nRead > 0) { buf[nRead] = '\0'; // 把收到的数据显示到界面,注意跨线程问题 theApp.m_pMainDlg->AppendText(CString(buf)); } else if (nRead == 0) { // 对端正常关闭 Close(); } } CAsyncSocket::OnReceive(nErrorCode); }Receive返回 0 表示对端关闭了连接,返回SOCKET_ERROR要查错误码,WSAEWOULDBLOCK表示当前没数据可读,不是错误。发送时Send的字节数要按实际数据算,发CString时用GetLength() * sizeof(TCHAR),在 Unicode 工程里TCHAR是wchar_t,占 2 字节,这个点后面避坑章节还会提。
3. 编译环境与工程配置:让 MFC 和 Winsock 真正跑起来
3.1 Visual Studio 里 MFC 组件的安装与工程创建
MFC 不是默认安装的。Visual Studio 安装器里要勾「使用 C++ 的桌面开发」,右侧「安装详细信息」里再勾「适用于最新 v143 生成工具的 C++ MFC」。离线环境的话,安装器支持下载后离线布局,但这一步按官方安装器走就行,不展开。
创建工程时选「MFC 应用」,应用程序类型选「基于对话框」,项目名随意。生成后你会看到App、Dlg、stdafx(或pch)这几个核心文件。MFC 四大类在这里对应:CWinApp是应用对象,CDialogEx是主窗口,CAsyncSocket是网络对象,CString是字符串封装。工程属性里不需要额外链接ws2_32.lib,因为 MFC 的 socket 类已经处理了,但如果你混用原生 Winsock API,就要在链接器输入里加ws2_32.lib。
3.2 端口、IP 与防火墙:三个必须确认的参数
| 参数 | 位置 | 常见值 | 说明 |
|---|---|---|---|
| 端口号 | Create第一个参数 | 8888 | 1024 以下需管理员权限,建议用 8000 以上 |
| IP 地址 | Connect第一个参数 | 127.0.0.1 或局域网 IP | 本机测试用回环,跨机测试用实际网卡 IP |
| 等待队列 | Listen参数 | 5 | 不是最大连接数,是未 accept 的排队上限 |
防火墙是新手最容易翻车的地方。服务端Listen成功后,如果客户端在另一台机器上连不上,先关掉 Windows Defender 防火墙的对应入站规则测试,确认是防火墙问题后再加规则放行端口。本机127.0.0.1测试不受防火墙影响,所以先用回环验证代码逻辑,再上局域网。
3.3 用 TCP 调试助手做交叉验证
不要只信自己的客户端和服务端。常见做法是:服务端跑起来后,用 TCP 调试助手(网上搜「tcp调试助手1.17」这类工具)当客户端连上去,手动发几条数据,看服务端OnReceive有没有正确解析。反过来,用调试助手开一个 TCP 服务端,让你的 MFC 客户端去连,验证Connect和Send是否正常。这样能把「代码问题」和「环境问题」分开。调试助手发十六进制和 ASCII 都支持,测粘包时特别有用。
4. 避坑与排查:MFC TCP 通信里最容易翻车的五件事
4.1 现象:客户端连不上,错误码 10061
原因:服务端没Listen成功,或者端口被占用,或者防火墙拦了。10061是WSAECONNREFUSED,意思是目标端口没有程序在监听。
解决:先在服务端机器上netstat -ano | findstr 8888看端口有没有处于LISTENING。如果没有,检查Create和Listen的返回值,别把AfxMessageBox弹窗当装饰。端口被占用就换一个。跨机测试先临时关防火墙确认。
4.2 现象:OnReceive 收到的数据少一截或乱码
原因:TCP 是字节流,没有消息边界。你发两次,对面可能一次Receive全收到,也可能分两次收到。另外 Unicode 工程里CString发出去是宽字符,对面按char收就会乱。
解决:自定义一个简单的包头,比如前 4 字节写长度,后面跟实际数据。收的时候先收 4 字节,解析出长度,再循环收够为止。或者统一用char数组和sizeof(char)发送,界面显示时再转CString。粘包处理是 TCP 编程的必修课,别指望一次Receive就是一条完整消息。
4.3 现象:关闭窗口时程序崩溃或报内存泄漏
原因:CClientSocket对象是new出来的,连接关闭后没有delete。MFC 的CAsyncSocket析构时会关闭句柄,但对象本身要你管理。
解决:在OnClose里把对应的 socket 对象从列表移除并delete,或者用CArray、CList配合智能指针。调试时看输出窗口的「Detected memory leaks」提示,定位是哪个对象没释放。
4.4 现象:OnReceive 里更新界面没反应或直接崩
原因:如果你把 socket 放到工作线程里,OnReceive就在工作线程执行,直接调SetWindowText或操作控件会跨线程访问 UI,轻则无效重则崩溃。
解决:用PostMessage把数据指针投递到主窗口,在主窗口的自定义消息处理函数里更新界面。PostMessage是异步的,不会阻塞工作线程。数据指针要用new分配,接收方负责delete。
4.5 现象:编译报错「无法打开包括文件 afxsock.h」
原因:工程没有启用 MFC,或者 MFC 组件没装全。
解决:项目属性 → 常规 → 「使用 MFC」设为「在共享 DLL 中使用 MFC」或「在静态库中使用 MFC」。如果选项是灰的,说明 MFC 组件没装,回安装器勾上。头文件包含顺序也有讲究,afxsock.h要放在afxwin.h之后。
5. 进阶技巧:把通信模块从界面里剥出来
写到这儿,程序能跑,但代码全堆在对话框里,加个功能就牵一发动全身。我一般会做一件事:把 socket 通信封装成一个独立的CNetManager类,对话框只负责调StartServer、ConnectServer、SendData和接收回调,不直接碰CAsyncSocket。这样换界面、加协议、上多线程都不用动网络层。
具体做法是定义一个回调接口:
// 网络事件回调接口 class INetCallback { public: virtual void OnNetRecv(const char* data, int len) = 0; virtual void OnNetConnect() = 0; virtual void OnNetClose() = 0; }; // CNetManager 持有 CAsyncSocket 派生对象,把事件转发给 callback class CNetManager { public: void SetCallback(INetCallback* cb) { m_cb = cb; } BOOL StartServer(UINT port); BOOL ConnectTo(LPCTSTR ip, UINT port); int SendData(const char* data, int len); private: INetCallback* m_cb = nullptr; CListenSocket m_listen; CClientSocket m_client; };对话框实现INetCallback,在OnNetRecv里把数据转成CString显示。这样网络层和界面层解耦,测试的时候可以写一个控制台程序实现同样的接口,不用开窗口就能压测收发。
验证方法上,我习惯用两个指标:一是连续发 10000 条 1KB 数据,看有没有丢包和错序(TCP 本身保证不丢不乱,但你的解析逻辑可能出错);二是用任务管理器看句柄数,反复连接断开 100 次,句柄数应该回到初始值附近,如果一直涨,说明 socket 对象没释放干净。
注意:
CAsyncSocket在 MFC 里不是线程安全的,一个 socket 对象只能在一个线程里用。多线程方案要么每个线程独立 socket,要么用CSocket配合工作线程,但CSocket的阻塞特性又和 UI 线程冲突,选型时要先想清楚。
从那以后我每次写 MFC 网络程序,都强制先把「监听、连接、收发、关闭」四个动作的返回值检查一遍,再谈界面。希望帮到你。
本文还有配套的精品资源,点击获取