资讯动态

MFC Socket 文件传输实战:CSocket 封装、粘包处理与断点续传

发布时间:2026/10/9 4:04:14 来源:尧图企业网站定制
简介这份资源面向学习网络编程与MFC框架的C开发者尤其是需要完成课程实验或想动手理解Socket通信的初学者。内容围绕基于TCP的文件传输展开客户端负责选择本地文件并发送服务端负责监听连接、接收数据并写入本地文件完整覆盖CSocket类的创建、连接、收发与资源释放流程同时涉及文件I/O操作与TCP/IP协议栈的基本概念。压缩包共63个文件约109.68MB包含4个cpp源文件、6个h头文件以及vcxproj、sln等工程配置另有exe可执行程序、pdb调试符号、obj中间文件与rc资源脚本方便直接编译运行或对照调试。目前已有316人学习下载。通过这份代码读者可以理清MFC封装下Socket通信的调用顺序掌握客户端与服务端双端协作的工程结构并借助调试文件排查连接与收发环节的常见问题适合作为网络编程实验的参考实现。1. 从一份 MFC 文件传输源码说起Socket 编程到底怎么落地很多人学网络编程卡在“看得懂 TCP 三次握手写不出一个能跑的文件传输”。这份 MFCFileUpload 源码包正好补上这个断层它用 MFC 的 CSocket 封装把客户端选文件、发数据服务端监听、收数据、落盘这条链路完整跑通。压缩包里是 MFCFileUpload1S服务端和 MFCFileUpload1C客户端两个独立工程各自带 .sln 和 Debug 目录用 VS 打开就能编译。适合两类人一是刚学完 Socket API 想找个能跑的 C 实例对照二是要做 Windows 桌面端小工具、需要一套现成的 C/S 通信骨架。它不解决高并发但把“连接—传输—落盘—关闭”这条最小闭环讲清楚了这是后面所有优化的地基。2. 拆开两个工程CSocket 封装下的连接与收发链路2.1 服务端为什么用 CAsyncSocket 派生而不是裸 APIMFC 里做 Socket 有两条路直接用 WinSock 的 socket()/bind()/listen()或者用 MFC 封装的 CAsyncSocket / CSocket。这份源码走的是 CAsyncSocket 派生自定义类再在派生类里重写 OnAccept、OnReceive 这些回调。选它的理由很实际MFC 的消息泵会把网络事件转成窗口消息你不用自己开线程去阻塞 recv界面不会假死。代价是回调里不能做耗时操作否则消息泵被堵住后续连接全卡。服务端启动顺序是固定的先Create指定端口再Listen然后在OnAccept里Accept出一个新的 CAsyncSocket 对象专门伺候这条连接。注意监听 socket 和通信 socket 必须是两个对象很多人图省事在监听对象上直接 Receive结果第一条连接收完就再也接不了新连接。// 服务端监听类派生自 CAsyncSocket关键片段 BOOL CServerSock::StartListen(UINT nPort) { // Create 的参数端口、socket 类型、事件掩码、地址 if (!Create(nPort, SOCK_STREAM, FD_ACCEPT | FD_READ | FD_CLOSE)) { return FALSE; // 端口被占用或权限不足时返回 FALSE } // 第二个参数是 backlog即等待队列长度一般 5~10 够用 return Listen(5); } void CServerSock::OnAccept(int nErrorCode) { CClientSock* pClient new CClientSock(); // 每条连接独立对象 if (Accept(*pClient)) { m_clientList.AddTail(pClient); // 挂进链表方便统一管理 } else { delete pClient; // Accept 失败必须自己释放否则内存泄漏 } CAsyncSocket::OnAccept(nErrorCode); }Create的端口参数是UINT传 0 表示让系统随机分配调试时建议写死一个 6000 以上的端口避免和系统服务撞车。Listen的 backlog 不是越大越好它只是内核等待队列长度真正决定并发的是你 Accept 的速度。OnAccept里new出来的对象一定要有地方存源码用CList挂起来这是常见做法如果只 new 不存连接一多就找不到句柄关都关不掉。2.2 客户端连接与文件分块发送的完整步骤客户端这边流程更直白创建 socket → Connect 到服务端 IP 和端口 → 打开本地文件 → 循环 Send → 关闭。Connect 是异步的调用后立刻返回真正连上会触发OnConnect回调。所以发送逻辑不能写在 Connect 后面得写在OnConnect里否则 socket 还没就绪就 Send直接返回 WSAEWOULDBLOCK。// 客户端连接与发送关键片段 void CFileClientDlg::OnBnClickedBtnSend() { CFileDialog dlg(TRUE); // TRUE 表示打开文件对话框 if (dlg.DoModal() ! IDOK) return; m_strFilePath dlg.GetPathName(); m_client.Create(); // 创建 socket不指定端口 m_client.Connect(m_strServerIP, m_nServerPort); // 异步连接 } void CFileClientSock::OnConnect(int nErrorCode) { if (nErrorCode ! 0) { AfxMessageBox(_T(连接失败)); return; } CFile file; if (!file.Open(m_strFilePath, CFile::modeRead | CFile::typeBinary)) return; const int nBufSize 4096; // 每次发 4KB兼顾效率和内存 BYTE* pBuf new BYTE[nBufSize]; UINT nRead 0; // 先发文件名和长度服务端好知道存成什么、收多少 CString strName file.GetFileName(); m_client.Send(strName, strName.GetLength() * sizeof(TCHAR)); ULONGLONG ullLen file.GetLength(); m_client.Send(ullLen, sizeof(ullLen)); while ((nRead file.Read(pBuf, nBufSize)) 0) { m_client.Send(pBuf, nRead); // 注意Send 返回值可能小于 nRead } delete[] pBuf; file.Close(); m_client.Close(); }这里有个血泪经验Send的返回值必须检查。TCP 是流式协议你发 4096 字节底层可能只发出去 2000剩下 2096 需要你继续发。源码里如果直接忽略返回值大文件传到一半就会丢数据而且服务端不报错只是文件损坏。正确做法是循环发送直到全部写完或者把发送逻辑放进OnSend回调里做流量控制。另外文件名用CString直接 Send 有个坑GetLength()返回的是字符数sizeof(TCHAR)在 Unicode 下是 2所以字节数是字符数乘 2服务端接收时也要按同样规则解析否则文件名乱码。3. 服务端接收与落盘怎么保证文件不丢不坏3.1 接收缓冲区的动态管理与粘包处理服务端OnReceive被触发时只说明“有数据到了”不代表“一条完整消息到了”。TCP 没有消息边界客户端分三次发的文件名、长度、内容服务端可能一次全收到也可能分五次收到。源码里如果假设一次 Receive 就是一条完整消息那只能传小文件稍微大一点就翻车。常见做法是维护一个接收缓冲区每次 Receive 追加到缓冲区尾部然后按协议格式解析先读文件名长度再读文件名再读 8 字节文件长度然后持续收直到收满这个长度。下面是一个简化的状态机思路// 服务端接收状态机简化示意 enum RecvState { RS_FILENAME, RS_FILELEN, RS_FILEDATA }; void CClientSock::OnReceive(int nErrorCode) { BYTE buf[4096]; int nRet Receive(buf, sizeof(buf)); if (nRet 0) { Close(); return; } m_recvBuf.Append(buf, nRet); // 追加到动态缓冲区 while (TRUE) { if (m_state RS_FILENAME) { if (m_recvBuf.GetSize() sizeof(int)) break; int nNameLen *(int*)m_recvBuf.GetData(); if (m_recvBuf.GetSize() sizeof(int) nNameLen) break; m_strFileName (LPCTSTR)(m_recvBuf.GetData() sizeof(int)); m_recvBuf.RemoveHead(sizeof(int) nNameLen); m_state RS_FILELEN; } else if (m_state RS_FILELEN) { if (m_recvBuf.GetSize() sizeof(ULONGLONG)) break; m_ullFileLen *(ULONGLONG*)m_recvBuf.GetData(); m_recvBuf.RemoveHead(sizeof(ULONGLONG)); m_file.Open(m_strFileName, CFile::modeCreate | CFile::modeWrite | CFile::typeBinary); m_state RS_FILEDATA; } else // RS_FILEDATA { ULONGLONG nRemain m_ullFileLen - m_ullReceived; ULONGLONG nTake min(nRemain, m_recvBuf.GetSize()); if (nTake 0) { m_file.Write(m_recvBuf.GetData(), (UINT)nTake); m_recvBuf.RemoveHead((INT_PTR)nTake); m_ullReceived nTake; } if (m_ullReceived m_ullFileLen) { m_file.Close(); m_state RS_FILENAME; // 复位准备收下一个文件 m_ullReceived 0; } break; } } CAsyncSocket::OnReceive(nErrorCode); }m_recvBuf用CByteArray或std::vectorBYTE都行关键是RemoveHead之后要正确移动剩余数据。min那一步是防止把下一个文件的数据误写进当前文件。状态机复位后同一个连接可以连续传多个文件不用每次重连。3.2 文件写入的边界什么时候 Close什么时候 FlushCFile在析构时会自动 Close但如果你在OnReceive里反复 Open/Close性能很差。源码的做法是收到文件长度后 Open 一次收满后 Close 一次。这里有个容易忽略的点CFile::Write是带缓冲的如果程序在收文件中途崩溃缓冲区里的数据不会落盘文件就是残缺的。对可靠性要求高的场景可以在每次 Write 后调用Flush但会牺牲吞吐。折中方案是每收 1MB Flush 一次或者用FILE_FLAG_WRITE_THROUGH打开文件。另外服务端保存路径要提前校验。如果客户端发来的文件名带..\或绝对路径直接拼接会写到系统目录这是典型的路径穿越问题。常见做法是只取文件名部分忽略客户端传来的任何目录信息CString strSafeName PathFindFileName(m_strFileName); // 只取最后一段 CString strSavePath m_strSaveDir _T(\\) strSafeName;PathFindFileName在shlwapi.h里MFC 工程直接包含即可。如果不想引额外头文件自己找最后一个\或/截断也行。4. 避坑与排查MFC Socket 实验里最容易翻车的五件事4.1 现象客户端显示发送成功服务端文件大小对不上原因Send返回值没检查大文件只发出去了前几 KB。TCP 发送缓冲区满了之后Send会返回实际写入的字节数剩余数据被丢弃。解决循环发送直到nSent nTotal或者把剩余数据缓存起来在OnSend里继续发。调试时可以在每次 Send 后打印返回值和预期字节数对比。4.2 现象服务端只能接一个客户端第二个连不上原因在监听 socket 上直接 Receive没有在OnAccept里 Accept 出新对象。监听 socket 的职责只有 Accept一旦你拿它收数据它就不再触发OnAccept了。解决严格区分监听对象和通信对象OnAccept里new一个派生类实例用Accept(*pNew)接管连接。4.3 现象Unicode 工程下文件名乱码或长度算错原因CString::GetLength()返回字符数不是字节数。Unicode 下每个字符 2 字节如果按字符数当字节数发送服务端解析时偏移量全错。解决发送长度统一用GetLength() * sizeof(TCHAR)接收端按同样规则还原。或者干脆把文件名转成 UTF-8 的char数组再发跨平台更稳。4.4 现象程序退出时崩溃提示内存错误原因OnAccept里new出来的 socket 对象没有在连接关闭时delete。OnClose回调触发后对象还在链表里下次遍历时访问已释放的内存。解决在OnClose里从链表移除并delete同时把m_clientList里对应的 POSITION 清掉。注意delete之后不要再调用该对象的任何方法。4.5 现象本机测试正常两台机器连不上原因服务端Bind时绑定了127.0.0.1只监听回环地址外部机器根本连不进来。解决Create时地址参数传INADDR_ANY或本机实际 IP不要写127.0.0.1。另外检查 Windows 防火墙是否放行了对应端口入站规则里加一条 TCP 端口例外即可。提示调试 Socket 程序时先用telnet 服务端IP 端口测一下端口通不通能排除一半的网络配置问题。5. 从能跑到好用给这份源码加一个进度反馈与断点续传思路源码跑通之后最影响体验的是“不知道传到哪了”。加进度反馈不需要改协议客户端在发送循环里每发一块就更新进度条服务端在接收循环里每收一块就更新状态栏。MFC 里跨线程更新 UI 要用PostMessage不要直接调SetWindowText否则消息泵冲突会卡死。// 客户端发送循环里更新进度 ULONGLONG ullSent 0; while ((nRead file.Read(pBuf, nBufSize)) 0) { int nOffset 0; while (nOffset nRead) { int nRet m_client.Send(pBuf nOffset, nRead - nOffset); if (nRet SOCKET_ERROR) { /* 错误处理 */ break; } nOffset nRet; } ullSent nRead; int nPercent (int)(ullSent * 100 / ullLen); // 自定义消息 WM_UPDATE_PROGRESSwParam 传百分比 GetParent()-PostMessage(WM_UPDATE_PROGRESS, nPercent, 0); }断点续传的思路也不复杂客户端发送前先问服务端“这个文件你收到多少字节了”服务端查一下已保存文件的大小返回客户端从该偏移量继续发。协议上加一个“查询请求”和“查询响应”两个消息类型即可。注意服务端返回已收长度时要确保文件确实完整写入了否则续传会从错误位置开始文件永久损坏。我一般会在服务端落盘时额外写一个.tmp临时文件收完再重命名为正式文件这样续传时只认正式文件临时文件一律当没收到处理。从那以后我每次做文件传输实验都强制先跑一遍“小文件→大文件→断网重连→续传”四步验证少一步都不敢说这代码能交付。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑