ARTICLE DETAIL

资讯详情

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

MFC CSocket 文件传输实战:从源码到避坑

MFC CSocket 文件传输实战:从源码到避坑 简介这份资源面向学习网络编程与MFC框架的C开发者提供一套完整的文件传输实验方案包含客户端与服务端两个可运行程序。实验以Socket编程为核心借助MFC封装的CSocket类完成连接建立、文件读取、数据收发与本地保存覆盖TCP/IP通信、文件I/O操作及面向对象代码组织等知识点适合课程实验、自学练手或项目参考。压缩包共63个文件约109.68MB内含cpp与h源码、vcxproj与sln工程文件、exe可执行程序以及obj、pdb、tlog等编译中间与日志文件另有rc、ico等资源文件结构完整便于直接编译调试。目前已有316人学习下载。通过研读源码读者可掌握客户端连接服务器、服务端监听并接收文件的完整流程理解CSocket各接口的调用时机与资源释放方式并借鉴工程目录组织与排错思路快速搭建自己的网络传输实验环境。1. 从一份 MFC 文件传输源码说起它到底能跑通什么很多人第一次接触 socket 网络编程是在控制台里敲send/recv数据在终端里滚一屏就完事。可一旦要交付一个带界面的东西——选文件、点按钮、看进度——控制台那套就不够用了。这份MFCFileUpload压缩包解决的正是这个断层它用 MFC 的CSocket类把 TCP 通信包起来配上一个客户端和一个服务端客户端负责挑本地文件发出去服务端负责监听端口、收数据、落盘成文件。压缩包里能看到MFCFileUpload1S服务端和MFCFileUpload1C客户端两个工程各自带.sln和Debug目录是典型的 Visual Studio MFC 解决方案结构。它适合两类人一类是正在上网络编程实验课、需要一份能编译能跑通的参考实现的学生另一类是想在 Windows 桌面端快速验证「文件从 A 机器传到 B 机器」这条链路的开发者。核心知识点集中在三块——TCP/IP 协议栈上的 socket 通信、MFC 对 WinSock 的封装、以及 C 的文件流读写。下面按「先搞懂它怎么搭起来再动手复现最后避开那些必踩的坑」的顺序拆开讲。2. CSocket 通信骨架从 CAsyncSocket 到阻塞式收发的选型逻辑2.1 为什么是 CSocket 而不是裸 WinSockMFC 里做网络通信绕不开CAsyncSocket和CSocket这两个类。CAsyncSocket是对 WinSock API 的薄封装事件驱动你得自己处理OnReceive、OnConnect这些回调稍不留神就掉进异步状态机的坑里。CSocket则继承自CAsyncSocket但它在内部加了一层阻塞式同步机制——当你调用Receive时它会挂起当前线程直到数据到达代码写起来接近同步流程对实验和中小型工具来说心智负担小得多。这份源码用的是CSocket路线。选它的理由很实际文件传输本身是「发完一段再发下一段」的顺序逻辑用阻塞式收发天然契合不需要为了异步去维护一堆状态标志。代价是 UI 线程会被阻塞所以正规做法是把收发放到工作线程里或者至少在传输期间禁用界面按钮。源码里如果直接在按钮响应函数里跑Send/Receive循环传输大文件时界面会卡住——这是后面避坑章节要重点说的。2.2 服务端Bind、Listen、Accept 三步不能乱服务端的启动流程是固定的三段式。先创建CSocket对象调Create指定端口再Bind绑定到本机地址然后Listen进入监听状态。有客户端连进来时Accept会返回一个新的CSocket对象专门服务这条连接——注意监听 socket 和通信 socket 是两个对象别混用。// 服务端初始化创建监听 socket 并绑定端口 CSocket serverSock; if (!serverSock.Create(6000)) { // 6000 是监听端口可改 AfxMessageBox(_T(创建 socket 失败)); return; } if (!serverSock.Bind(6000)) { // 绑定到本机 6000 端口 AfxMessageBox(_T(绑定端口失败可能被占用)); return; } if (!serverSock.Listen(5)) { // backlog5等待队列长度 AfxMessageBox(_T(监听失败)); return; } // 阻塞等待客户端连接 CSocket clientSock; if (serverSock.Accept(clientSock)) { // clientSock 现在专门负责和这个客户端通信 }Create的参数是端口号Bind把它和本机地址关联Listen的 backlog 参数决定同时允许多少个连接排队等待。Accept是阻塞的没有客户端来就一直等——所以服务端界面在等待连接时是「无响应」状态这是正常现象不是死机。端口选 6000 只是示例实际用的时候避开 1024 以下的系统保留端口也别撞上已知服务占用的端口。2.3 客户端Connect 之后才能 Send客户端比服务端简单核心就两步Create一个 socket然后Connect到服务端的 IP 和端口。连接建立后就可以打开本地文件、循环读取、逐块Send出去。// 客户端连接并发送文件 CSocket clientSock; clientSock.Create(); // 不指定端口系统自动分配 if (!clientSock.Connect(_T(192.168.1.100), 6000)) { AfxMessageBox(_T(连接服务端失败检查 IP 和端口)); return; } CFile file; if (!file.Open(_T(D:\\test.dat), CFile::modeRead)) { AfxMessageBox(_T(打开文件失败)); return; } const int BUF_SIZE 4096; char buf[BUF_SIZE]; UINT nRead; while ((nRead file.Read(buf, BUF_SIZE)) 0) { clientSock.Send(buf, nRead); // 逐块发送nRead 是实际读到的字节数 } file.Close(); clientSock.Close(); // 发完关闭连接Connect的 IP 要填服务端所在机器的实际地址本机测试用127.0.0.1。Send的第二个参数是本次发送的字节数必须是Read实际读到的长度不能固定写BUF_SIZE——最后一块数据往往不满缓冲区写死会把垃圾数据也发过去。这个细节在源码里如果处理对了说明作者是踩过坑的。3. 文件收发闭环缓冲区、文件流与收尾信号的配合3.1 发送端分块读取与边界处理文件传输的本质是把一个连续的字节流切成若干块通过 socket 一块块送出去。缓冲区大小是个权衡太小则Send调用次数多开销大太大则单次阻塞时间长内存占用高。4096 字节是常见起点局域网内可以调到 8192 甚至 16384跨网络传输则建议保守一些。// 发送端完整循环含文件大小预发送 CFile file; file.Open(filePath, CFile::modeRead); ULONGLONG fileSize file.GetLength(); // 先发文件大小让接收端知道要收多少字节 clientSock.Send(fileSize, sizeof(fileSize)); const int BUF_SIZE 8192; char* buf new char[BUF_SIZE]; UINT nRead; while ((nRead file.Read(buf, BUF_SIZE)) 0) { int nSent 0; while (nSent (int)nRead) { int ret clientSock.Send(buf nSent, nRead - nSent); if (ret SOCKET_ERROR) { /* 错误处理 */ } nSent ret; // Send 可能只发出去一部分 } } delete[] buf; file.Close();这里有个容易被忽略的点Send不保证一次把你要发的数据全发完它返回的是实际发送的字节数。所以内层要有一个while循环把剩余部分继续发直到nSent等于nRead。很多实验代码直接调一次Send就完事小文件碰巧能过大文件就丢数据——这是血泪经验。3.2 接收端怎么知道文件收完了接收端面临的核心问题是「什么时候算收完」。TCP 是流式协议没有消息边界你不能靠「这次Receive没收到数据」来判断结束因为那可能只是网络暂时没数据。可靠的做法有两种一是发送端先发一个固定长度的文件大小字段接收端收满这么多字节就停二是发送端发完后关闭连接接收端Receive返回 0 表示对端关闭。// 接收端先读文件大小再按大小收满 ULONGLONG fileSize 0; int nRecv 0; // 确保收满 sizeof(fileSize) 个字节 while (nRecv (int)sizeof(fileSize)) { int ret clientSock.Receive(((char*)fileSize) nRecv, sizeof(fileSize) - nRecv); if (ret 0) { /* 连接断开或出错 */ } nRecv ret; } CFile outFile; outFile.Open(_T(D:\\recv.dat), CFile::modeCreate | CFile::modeWrite); const int BUF_SIZE 8192; char* buf new char[BUF_SIZE]; ULONGLONG totalRecv 0; while (totalRecv fileSize) { int toRead (int)min((ULONGLONG)BUF_SIZE, fileSize - totalRecv); int ret clientSock.Receive(buf, toRead); if (ret 0) break; outFile.Write(buf, ret); totalRecv ret; } delete[] buf; outFile.Close();Receive同样不保证一次收满你请求的字节数所以读文件大小那段要用循环凑齐。主循环里用fileSize - totalRecv控制每次最多读多少避免多收。这套「先发大小、再发内容」的协议虽然简单但足够可靠也是这份源码能跑通的关键。3.3 资源释放与异常出口socket 和文件句柄都是系统资源用完必须关。CSocket的析构函数会调Close但依赖析构时机不如显式关闭稳妥。文件对象CFile也一样Close之后才能保证数据刷到磁盘。异常路径上——比如Connect失败、Open失败——要确保已经创建的对象被清理否则反复重试会耗尽句柄。// 推荐的清理顺序先关通信 socket再关监听 socket if (clientSock.m_hSocket ! INVALID_SOCKET) { clientSock.Shutdown(1); // 1 禁止后续发送 clientSock.Close(); } if (serverSock.m_hSocket ! INVALID_SOCKET) { serverSock.Close(); }Shutdown先于Close是个好习惯它通知对端「我不再发了」让对端能干净地结束接收循环。直接Close有时会让对端收到 RST 而不是正常的 FIN导致对方报「连接被重置」。4. 避坑与排查MFC 文件传输里最容易翻车的五件事4.1 现象客户端显示发送成功服务端文件却比原文件小原因几乎总是Send返回值没检查。TCP 发送缓冲区满了之后Send只发出去一部分就返回了剩余数据被丢弃。解决方法是像 3.1 节那样用内层循环把没发完的继续发直到全部发出。4.2 现象服务端Accept之后界面卡死无法响应Accept、Receive、Send在CSocket默认模式下都是阻塞的。如果这些调用发生在 UI 线程消息循环就被卡住了。常见做法是把服务端的监听和收发逻辑放到AfxBeginThread创建的工作线程里UI 线程只负责更新界面。源码如果没做线程分离传输大文件时界面假死是必然的。4.3 现象本机测试正常两台机器之间连不上先查防火墙。Windows 防火墙默认会拦截入站的陌生端口连接服务端所在机器需要放行对应端口。再查 IPConnect里填的必须是服务端实际监听的网卡地址如果服务端Bind时绑的是127.0.0.1那只有本机能连外部机器连不上。Bind时传INADDR_ANY才能监听所有网卡。4.4 现象传输到一半报 10053 或 10054 错误10053是本地主动断开10054是对端强制关闭。常见诱因是接收端写文件失败磁盘满、路径无权限后直接退出发送端还在发就撞上了。排查时先看接收端的文件写入路径是否存在、磁盘是否有空间再看两边缓冲区大小是否匹配——接收端Receive的缓冲区如果比发送端Send的块还小虽然不会丢数据但会频繁触发窗口调整极端情况下引发超时。4.5 现象中文路径或文件名导致打开文件失败CFile::Open在 Unicode 工程下需要宽字符路径如果源码里写的是D:\\测试.dat这种窄字符串编译能过但运行时找不到文件。统一用_T()宏包裹路径字符串或者确认工程字符集设置与代码一致。这个坑在 VS 不同版本间迁移时尤其常见。5. 进阶技巧把这份源码改成能显示进度的可靠传输工具源码能跑通只是起点。实际用的时候你大概率会想要一个进度条知道传了多少、还剩多少。改造思路不复杂发送端在循环里每发完一块就更新一个计数器通过自定义消息PostMessage通知 UI 线程刷新进度条接收端同理用totalRecv / fileSize算百分比。// 发送端每块发完后通知进度 #define WM_UPDATE_PROGRESS (WM_USER 100) ULONGLONG totalSent 0; while ((nRead file.Read(buf, BUF_SIZE)) 0) { // ... 发送逻辑 ... totalSent nRead; int percent (int)(totalSent * 100 / fileSize); PostMessage(WM_UPDATE_PROGRESS, percent, 0); // 通知 UI 线程 }UI 线程里映射WM_UPDATE_PROGRESS消息处理函数中更新进度条控件的SetPos。用PostMessage而不是SendMessage是因为前者不阻塞工作线程后者会等 UI 处理完才返回在高速传输时反而拖慢速度。另一个值得加的改进是超时控制。CSocket的阻塞调用默认没有超时网络断了可能一直挂着。可以在Create之后调SetSockOpt设置SO_RCVTIMEO和SO_SNDTIMEO让收发在指定毫秒数后自动返回错误避免线程永久卡死。int timeout 5000; // 5 秒超时 clientSock.SetSockOpt(SO_RCVTIMEO, timeout, sizeof(timeout)); clientSock.SetSockOpt(SO_SNDTIMEO, timeout, sizeof(timeout));验证改造是否成功最简单的办法是传一个几百 MB 的文件同时用任务管理器看内存占用是否稳定——如果内存持续上涨说明缓冲区没释放或者接收端在无限累积数据。正常的实现内存占用应该在一个小范围内波动。从那以后我每次拿到这类 socket 传输代码都强制先跑一遍「大文件 断网重连」的测试确认发送循环有返回值检查、接收循环有大小边界、超时设置有兜底才敢往实际环境里放。希望这份拆解能帮你少走几个弯路。本文还有配套的精品资源点击获取
返回列表