
简介这份资源面向学习网络编程与MFC框架的C开发者尤其是需要完成课程实验或想动手实践Socket通信的初学者。内容围绕基于TCP/IP的文件传输展开客户端负责选择本地文件并发送服务端负责监听连接、接收数据并写入本地文件完整覆盖CSocket类的创建、连接、绑定、监听、收发与资源释放等关键环节帮助读者理解网络通信原理与MFC封装机制。压缩包共63个文件约109.68MB包含cpp与h源码、vcxproj与sln工程文件、exe可执行程序以及obj、pdb、tlog等编译中间产物和rc、ico等资源文件工程结构完整可直接编译运行。目前已有316人学习下载。通过研读源码读者能掌握文件I/O与Socket结合的实现思路理解客户端与服务端双工程的组织方式并积累网络编程调试与排错经验适合作为课程设计或自学练手的参考案例。1. 从一次课堂实验说起MFC 文件传输到底在练什么很多人第一次接触 socket 网络编程是在控制台里敲send和recv跑通了却总觉得隔了一层——数据到底怎么从界面按钮走到网卡又怎么落到对面磁盘上心里没底。MFC 实现文件传输客户端 服务端这个题目恰恰是把这层窗户纸捅破的练手项目用 Windows 原生框架把 TCP 通信、文件读写、界面交互串成一条完整链路。它解决的不是能不能传而是怎么把网络字节流可靠地变成磁盘上的文件适合刚学完 socket API、想找一个能看见进度条、能手动断线重连的实战场景的在校生和转行工程师。热词里 socket 网络编程、客户端和服务端这些词在这个项目里全都能落到具体代码行上不是空概念。2. 动手前先把 TCP 文件传输的骨架搭清楚2.1 为什么选 TCP 而不是 UDP 传文件文件传输的第一决策是传输层协议。UDP 无连接、不保证顺序、不重传用它传文件意味着你得在应用层自己实现序号、确认、超时重传和乱序重组——这本身就是另一个大工程。TCP 把这些全包了代价是延迟略高、有拥塞控制但对文件这种完整性优先于实时性的场景TCP 是默认答案。常见做法是服务端listen在一个固定端口客户端connect上来后先发一个定长文件头文件名长度、文件名、文件大小再发文件体。这个先头后体的约定是整个项目的地基后面所有进度计算、断点判断都依赖它。2.2 最小可跑的通信骨架先不碰 MFC 界面用纯 Win32 socket 把收发跑通确认链路没问题再往对话框里搬。下面这段是服务端接收的核心逻辑客户端对称处理即可。// 服务端接收文件头 文件体 SOCKET listenSock socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); sockaddr_in addr{}; addr.sin_family AF_INET; addr.sin_port htons(9527); // 端口选 1024 以上避开系统保留段 addr.sin_addr.s_addr INADDR_ANY; // 监听所有网卡 bind(listenSock, (sockaddr*)addr, sizeof(addr)); listen(listenSock, SOMAXCONN); SOCKET clientSock accept(listenSock, nullptr, nullptr); // 先收 8 字节文件大小网络字节序转主机字节序 uint64_t fileSize 0; recv(clientSock, (char*)fileSize, sizeof(fileSize), 0); fileSize ntohll(fileSize); // 再收 4 字节文件名长度然后收文件名 uint32_t nameLen 0; recv(clientSock, (char*)nameLen, sizeof(nameLen), 0); nameLen ntohl(nameLen); std::string fileName(nameLen, \0); recv(clientSock, fileName.data(), nameLen, 0); // 循环收文件体直到收满 fileSize std::ofstream out(fileName, std::ios::binary); char buf[8192]; uint64_t received 0; while (received fileSize) { int n recv(clientSock, buf, sizeof(buf), 0); if (n 0) break; // 0 表示对端关闭负数表示出错 out.write(buf, n); received n; }逻辑说明recv不保证一次收满你想要的长度所以文件体必须用循环累加直到received fileSize。参数上缓冲区 8192 是经验值太小系统调用频繁太大栈上分配有风险端口 9527 只是示例实际选一个没被占用的即可。ntohll不是标准库函数Windows 下需要自己用ntohl拼高低位或者直接用htonll的对称实现这是新手第一个容易翻车的点。2.3 把 socket 逻辑搬进 MFC 对话框MFC 里网络操作不能放在 UI 线程否则recv阻塞时界面直接假死。标准做法是开一个工作线程跑收发通过PostMessage把进度回传给对话框更新进度条。下面是在对话框类里启动线程的骨架。// 对话框头文件里声明线程函数和消息 static UINT RecvThread(LPVOID pParam); // 线程入口必须是静态或全局 #define WM_UPDATE_PROGRESS (WM_USER 100) // 点击开始接收按钮 void CFileTransferDlg::OnBnClickedRecv() { m_bRunning TRUE; AfxBeginThread(RecvThread, this); // this 传进去线程里能拿到对话框指针 } UINT CFileTransferDlg::RecvThread(LPVOID pParam) { CFileTransferDlg* pDlg (CFileTransferDlg*)pParam; // ... 上面那套 accept/recv 逻辑 ... // 每收一块就通知界面 pDlg-PostMessage(WM_UPDATE_PROGRESS, (WPARAM)percent, 0); return 0; }逻辑说明AfxBeginThread是 MFC 封装的工作线程创建接口比裸CreateThread更安全因为它会正确处理 MFC 的线程状态。PostMessage是异步投递不会阻塞工作线程界面在消息映射里处理WM_UPDATE_PROGRESS更新进度条即可。参数上WM_USER 100是自定义消息的惯用起点避免和系统消息冲突。注意线程里绝对不能直接调用SetDlgItemText之类的 UI 函数跨线程操作控件是 MFC 的经典崩溃来源。3. 文件头协议设计与粘包处理3.1 定长头 变长体的协议格式TCP 是字节流没有消息边界。如果客户端连发两次send服务端可能一次recv全收到这就是粘包。解决办法是让接收方知道要收多少。本项目的协议设计成固定 8 字节文件大小 4 字节文件名长度 变长文件名 变长文件体。接收方先读满 12 字节定长头解析出文件名长度和文件大小再按长度精确读取后续内容。这样无论 TCP 怎么合并或拆分接收逻辑都不会错。字段长度说明文件大小8 字节网络字节序uint64文件名长度4 字节网络字节序uint32文件名变长UTF-8 编码不含路径文件体变长二进制原始字节3.2 封装一个可靠的 recvAll 函数裸recv只保证收到至少 1 字节要收满指定长度必须自己循环。把这件事封装成一个函数后面所有读取都调它能省掉大量重复判断。// 收满 len 字节才返回返回实际收到的字节数 int recvAll(SOCKET s, char* buf, int len) { int total 0; while (total len) { int n recv(s, buf total, len - total, 0); if (n 0) return n; // 对端关闭或出错直接返回 total n; } return total; }逻辑说明这个函数是文件传输可靠性的核心。参数len是期望收满的长度返回值等于len表示成功小于len说明连接中断。有了它读文件头就是recvAll(s, header, 12)读文件名就是recvAll(s, nameBuf, nameLen)读文件体就是循环调recvAll每次收一块。注意recv返回 0 表示对端正常关闭返回SOCKET_ERROR才是出错两者要区分处理否则断线时日志会误导你。3.3 发送端的分块与进度计算发送端相对简单但进度计算有个坑send的返回值是本次实际发出的字节数可能小于你请求发送的长度所以发送也要循环。// 发送文件体并更新进度 uint64_t sent 0; while (sent fileSize) { int chunk (int)min((uint64_t)8192, fileSize - sent); int n send(clientSock, buf sent, chunk, 0); if (n 0) break; sent n; int percent (int)(sent * 100 / fileSize); pDlg-PostMessage(WM_UPDATE_PROGRESS, percent, 0); }逻辑说明min保证最后一块不会越界读取。进度用sent * 100 / fileSize计算注意先乘后除避免整数除法丢精度。参数 8192 和接收端保持一致方便对照调试。如果文件很大超过 4GBsent * 100可能溢出 32 位要强制转成uint64_t再算。4. 避坑与排查那些让实验卡住的真实问题4.1 现象客户端显示发送完成服务端文件却少一截原因发送端send返回后数据只是进了内核发送缓冲区不代表对端已收到。如果发送完立刻closesocket缓冲区里没发完的数据会被丢弃。解决发送完成后调用shutdown(s, SD_SEND)通知对端我没数据了然后等对端关闭或自己recv到 0 再closesocket。这个顺序是血泪经验少了shutdown大文件必丢尾巴。4.2 现象界面进度条卡住不动点关闭直接无响应原因网络操作写在了 UI 线程recv阻塞时消息循环停转。解决所有阻塞 socket 调用放进AfxBeginThread开的工作线程UI 线程只负责响应消息和刷新控件。如果非要在 UI 线程用 socket就设成非阻塞模式配合WSAAsyncSelect但那样代码复杂度翻倍实验项目没必要。4.3 现象中文文件名传过去变成乱码原因MFC 默认用CString的TCHAR在 Unicode 工程里是宽字符直接send宽字符缓冲区对端按单字节解析必然乱码。解决发送前统一转成 UTF-8。用WideCharToMultiByte(CP_UTF8, ...)转换接收端再用MultiByteToWideChar(CP_UTF8, ...)转回来显示。协议里约定死编码别一边 GBK 一边 UTF-8。4.4 现象本机测试正常两台机器连不上原因Windows 防火墙默认拦截入站连接或者服务端bind用了127.0.0.1只监听回环。解决bind时地址用INADDR_ANY首次运行服务端时在防火墙弹窗里允许专用网络访问。如果还不行用netstat -ano | findstr 9527确认端口确实在监听再看客户端connect的返回值WSAECONNREFUSED说明对面没监听WSAETIMEDOUT多半是防火墙。4.5 现象传大文件时内存暴涨原因一次性new出整个文件大小的缓冲区再收发。解决固定 8KB 或 64KB 缓冲区循环读写内存占用与文件大小无关。这个习惯在实验阶段就要养成否则以后做视频传输会吃大亏。5. 进阶技巧断点续传与传输校验5.1 用文件偏移实现断点续传基础版传完就结束但真实场景经常断线。断点续传的思路是接收端先检查目标文件是否已存在存在就取当前大小作为偏移量把这个偏移发给发送端发送端seek到对应位置继续发。协议头里加一个 8 字节的起始偏移字段即可。// 接收端检查已存在文件算出偏移 uint64_t offset 0; std::ifstream exist(fileName, std::ios::binary | std::ios::ate); if (exist.is_open()) { offset exist.tellg(); // ate 模式打开tellg 直接给文件大小 exist.close(); } // 把 offset 发给发送端发送端从该位置继续 uint64_t netOffset htonll(offset); send(clientSock, (char*)netOffset, sizeof(netOffset), 0);逻辑说明std::ios::ate让文件指针初始就在末尾tellg返回的就是文件大小比先seekg(0, end)再tellg简洁。发送端收到偏移后seekg(offset)再开始读。注意续传的前提是文件内容没被改动过实验里够用生产环境还得加 MD5 校验。5.2 传输完成后做一次哈希校验进度条到 100% 不代表文件正确网络抖动、磁盘写满都可能导致内容损坏。最省事的校验是在发送端算好整个文件的 MD5放在协议头里一起发接收端收完后本地也算一遍对比。// 接收端收完后校验 std::string localHash CalcMD5(fileName); // 自己实现的 MD5 封装 if (localHash ! expectedHash) { AfxMessageBox(_T(文件校验失败请重传)); // 删除损坏文件避免下次续传基于错误数据 DeleteFile(CString(fileName.c_str())); }逻辑说明MD5 计算可以边收边算不必等文件落盘后再读一遍省一次 IO。参数上校验失败一定要删掉半成品文件否则下次续传会基于损坏数据继续越传越错。这个后悔药机制在实验报告里也是加分项。5.3 几个值得固化的习惯第一所有 socket 操作检查返回值SOCKET_ERROR时用WSAGetLastError()打日志别让程序静默失败。第二WSAStartup和WSACleanup成对出现放在InitInstance和ExitInstance里。第三端口、缓冲区大小、超时时间这些魔法数字抽成常量或配置项调参时不用满代码找。第四测试顺序永远是先本机回环、再局域网两台机、最后跨网段每步确认再往下走能省掉大量到底是代码问题还是网络问题的纠结。我自己带实验时最常看到的一幕是学生盯着进度条 100% 却打不开文件最后发现是忘了shutdown导致尾部数据丢失。这个坑我当年也踩过后来养成的习惯是只要涉及发完就关先想清楚对端怎么知道发完了。希望帮到你。本文还有配套的精品资源点击获取