简介这份资源面向具备一定C与Windows开发基础、希望掌握网络编程的开发者聚焦于用MFC封装实现TCP协议的客户端/服务器通信程序。内容围绕CSocket类展开涵盖Winsock初始化、套接字创建、服务器绑定监听与接受连接、客户端发起连接以及数据收发和连接关闭等完整流程帮助读者理解C/S架构下TCP通信的落地方式。压缩包共4个文件以2个cpp源文件和2个h头文件为主分别对应客户端连接对话框与服务器端对话框的实现与声明整体约6KB体量轻巧便于快速阅读与二次改造。目前已有1267人学习下载适合作为MFC网络编程的入门参考或课程实验素材读者可从中获得可直接编译运行的通信骨架、对话框与套接字结合的代码组织思路以及排查连接与收发问题的基本线索。1. 从两个窗口互发消息说起这套 MFC Socket 的 C/S 程序到底能干什么很多人第一次接触网络编程都是从控制台里敲一行send、recv开始的黑底白字跑通了心里却没底——真实项目里客户端和服务端到底怎么组织界面怎么和网络线程配合这套基于 socket 通信、用 MFC 实现 TCP 通信的 C/S 架构程序正好补上这一课。它把服务端和客户端拆成两个独立的 MFC 工程服务端负责监听端口、接受连接、转发消息客户端负责连接服务端、发送文本、接收回显界面用 MFC 对话框搭起来底层走的是标准 Winsock 的 TCP 流式套接字。适合谁适合刚学完 C 语法、想搞明白「一个带界面的聊天程序是怎么跑起来的」的在校开发者也适合需要给老项目补一个轻量 TCP 通信模块的熟手——代码量不大结构清楚改起来不费劲。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序把这份源码拆开讲。2. 工程结构与通信模型两个 MFC 工程如何各司其职2.1 目录与工程划分拿到源码先别急着编译先看目录。这类 C/S 程序通常是一个解决方案下挂两个 MFC 应用程序工程一个叫服务端、一个叫客户端各自有独立的.vcxproj、资源文件和对话框类。常见的目录形态是这样TcpCSharpDemo/ ├── Server/ │ ├── Server.sln │ ├── Server/ │ │ ├── ServerDlg.h / .cpp # 主对话框承载监听与收发逻辑 │ │ ├── Server.rc # 对话框资源、控件布局 │ │ ├── resource.h # 控件 ID 定义 │ │ └── stdafx.h / pch.h # 预编译头含 winsock2.h ├── Client/ │ ├── Client.sln │ ├── Client/ │ │ ├── ClientDlg.h / .cpp # 主对话框承载连接与收发逻辑 │ │ ├── Client.rc │ │ └── resource.h └── README.md服务端和客户端是两个可以独立编译的工程这一点很关键调试的时候你要同时开两个进程一个跑服务端、一个跑客户端才能看到完整链路。工程属性里必须链接ws2_32.lib头文件顺序上winsock2.h要排在windows.h前面否则会出现重定义冲突——这是 MFC 里做网络编程最常见的第一个坑。2.2 TCP 通信的时序模型这套程序的通信模型是最经典的阻塞式 TCP服务端先WSAStartup初始化 Winsock创建监听套接字bind到指定端口listen之后进入accept等待客户端WSAStartup后创建套接字直接connect到服务端 IP 和端口。连接建立后双方各自用一个接收线程循环recv主线程负责界面刷新和send。为什么接收要单独开线程因为recv是阻塞的如果放在 UI 线程里一旦没有数据到达整个对话框就卡死了按钮点不动、窗口拖不动用户以为程序挂了。常见做法是AfxBeginThread起一个工作线程专门收数据收到后通过自定义消息PostMessage回主线程更新控件——MFC 的控件不是线程安全的跨线程直接操作控件是血泪经验里翻车最多的地方。2.3 关键数据结构与端口约定服务端和客户端之间需要约定端口号源码里一般写死一个常量比如#define DEFAULT_PORT 6000。IP 地址在客户端界面上用编辑框输入服务端则绑定INADDR_ANY监听所有网卡。消息格式上这套 Demo 通常直接发原始字符串没有加长度头这意味着它假设每次send的内容能被一次recv完整收到——在小数据量、本机回环测试时没问题跨网络就可能粘包。这一点在二次开发时必须注意后面改造章节会讲怎么补。3. 服务端核心代码拆解从 WSAStartup 到 accept 的完整链路3.1 初始化与监听套接字创建服务端对话框初始化时第一件事是启动 Winsock 并创建监听套接字。典型写法如下// ServerDlg.cpp 初始化部分 BOOL CServerDlg::OnInitDialog() { CDialogEx::OnInitDialog(); WSADATA wsaData; // 请求 Winsock 2.2 版本 if (WSAStartup(MAKEWORD(2, 2), wsaData) ! 0) { AfxMessageBox(_T(Winsock 初始化失败)); return TRUE; } // 创建 TCP 流式套接字 m_listenSocket socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (m_listenSocket INVALID_SOCKET) { AfxMessageBox(_T(创建监听套接字失败)); WSACleanup(); return TRUE; } // 绑定端口INADDR_ANY 表示监听所有网卡 sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(DEFAULT_PORT); addr.sin_addr.S_un.S_addr INADDR_ANY; if (bind(m_listenSocket, (sockaddr*)addr, sizeof(addr)) SOCKET_ERROR) { AfxMessageBox(_T(端口绑定失败可能被占用)); closesocket(m_listenSocket); WSACleanup(); return TRUE; } // 开始监听 backlog 设为 5 listen(m_listenSocket, 5); return TRUE; }这段代码里每个参数都有讲究。MAKEWORD(2, 2)指定 Winsock 版本版本不匹配会导致后续 API 行为异常AF_INET是 IPv4 地址族SOCK_STREAM表示面向连接的流式套接字IPPROTO_TCP明确协议htons把主机字节序转成网络字节序端口号不转的话跨机器通信会连到错误的端口。bind失败最常见的原因是端口被占用换一个端口号或者先关掉占用进程即可。listen的第二个参数是等待队列长度Demo 里设 5 够用真实并发场景要调大。3.2 accept 循环与接收线程监听建立后服务端要在一个循环里accept新连接。因为accept也是阻塞的通常把它放进一个独立线程或者用select做多路复用。这份源码的常见做法是起一个接受线程// 接受连接的线程函数 UINT CServerDlg::AcceptThread(LPVOID pParam) { CServerDlg* pDlg (CServerDlg*)pParam; sockaddr_in clientAddr; int addrLen sizeof(clientAddr); while (pDlg-m_bRunning) { // 阻塞等待客户端连接 SOCKET clientSocket accept(pDlg-m_listenSocket, (sockaddr*)clientAddr, addrLen); if (clientSocket INVALID_SOCKET) continue; // 保存客户端套接字Demo 里只处理单连接 pDlg-m_clientSocket clientSocket; // 通知 UI 有新客户端接入 pDlg-PostMessage(WM_CLIENT_CONNECTED, 0, 0); // 为该连接启动接收线程 AfxBeginThread(RecvThread, pDlg); } return 0; }accept返回一个新的套接字专门用于和这个客户端通信原来的监听套接字继续等待下一个连接。这里 Demo 只保存了一个m_clientSocket意味着同一时刻只服务一个客户端——这是简化设计不是缺陷但二次开发要支持多客户端就得换成std::vectorSOCKET或者用select管理套接字集合。PostMessage把「有新连接」这个事件抛回 UI 线程避免在工作线程里直接操作控件这是 MFC 多线程编程的基本纪律。3.3 消息接收与界面回显接收线程的核心是一个recv循环UINT CServerDlg::RecvThread(LPVOID pParam) { CServerDlg* pDlg (CServerDlg*)pParam; char buffer[1024]; while (pDlg-m_bRunning) { // 阻塞接收返回实际收到的字节数 int ret recv(pDlg-m_clientSocket, buffer, sizeof(buffer) - 1, 0); if (ret 0) { buffer[ret] \0; // 把收到的字符串拷贝到堆上随消息一起投递 CString* pMsg new CString(buffer); pDlg-PostMessage(WM_RECV_DATA, (WPARAM)pMsg, 0); } else if (ret 0) { // 对端正常关闭连接 pDlg-PostMessage(WM_CLIENT_DISCONNECTED, 0, 0); break; } else { // 出错退出循环 break; } } return 0; }recv的返回值有三种情况大于 0 表示收到数据等于 0 表示对端调用了closesocket正常断开小于 0 表示出错。很多新手只判断大于 0结果客户端关掉后服务端线程空转或者行为异常。buffer留一个字节给\0是 C 字符串处理的习惯sizeof(buffer) - 1防止越界。把CString*通过PostMessage的WPARAM传过去UI 线程收到后要记得delete否则每收一条消息泄漏一次内存——这个坑我在早期项目里踩过跑一晚上内存涨到几百兆。4. 客户端核心代码拆解连接、发送与断线处理4.1 连接服务端的参数与流程客户端比服务端简单不需要bind和listen直接connectvoid CClientDlg::OnBnClickedBtnConnect() { // 从界面读取 IP 和端口 CString strIP; GetDlgItemText(IDC_EDIT_IP, strIP); int port GetDlgItemInt(IDC_EDIT_PORT); m_clientSocket socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (m_clientSocket INVALID_SOCKET) { AfxMessageBox(_T(创建套接字失败)); return; } sockaddr_in serverAddr; serverAddr.sin_family AF_INET; serverAddr.sin_port htons(port); // 把点分十进制 IP 转成网络字节序 serverAddr.sin_addr.S_un.S_addr inet_addr(CT2A(strIP)); if (connect(m_clientSocket, (sockaddr*)serverAddr, sizeof(serverAddr)) SOCKET_ERROR) { AfxMessageBox(_T(连接服务端失败请检查 IP 和端口)); closesocket(m_clientSocket); return; } // 连接成功启动接收线程 m_bConnected TRUE; AfxBeginThread(RecvThread, this); }inet_addr负责把192.168.1.100这样的字符串转成 32 位网络字节序整数注意它不校验格式输入非法 IP 会返回INADDR_NONE严谨一点应该用inet_pton。connect是阻塞调用服务端没开或者 IP 写错时会卡几秒才返回失败界面上最好给个「连接中」的提示否则用户以为程序死了。连接成功后立刻起接收线程和服本文还有配套的精品资源点击获取