news 2026/7/14 5:10:19

VC++使用WinINet API实现FTP文件上传:从原理到实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VC++使用WinINet API实现FTP文件上传:从原理到实战

1. 项目概述:为什么选择VC++实现FTP文件上传?

在当前的开发环境中,HTTP API和云存储服务大行其道,但FTP(文件传输协议)作为一种经典、稳定且被广泛支持的协议,依然在大量企业内部系统、工业控制、设备管理和遗留系统集成中扮演着不可替代的角色。比如,你可能需要将一台工控机上的日志文件定时上传到中央服务器,或者为某个桌面应用添加一个“备份到远程服务器”的功能。这时,一个轻量级、无需依赖复杂运行时环境的本地FTP客户端就显得尤为实用。

我选择使用VC++(Visual C++)来实现这个功能,而不是C#或Python,主要基于几个核心考量。首先,运行时的零依赖是关键。一个用VC++编译的MFC或Win32程序,可以打包成一个独立的EXE文件,扔到任何一台Windows机器上(哪怕是Windows XP或嵌入式系统)都能直接运行,无需安装.NET Framework或Python解释器。这对于部署环境受限或需要“开箱即用”的场景是巨大的优势。其次,对Windows系统API和网络栈的底层控制能力更强。我们可以直接使用WinINet API,这是Windows原生提供的网络功能接口,与系统深度集成,稳定性和兼容性有保障。最后,性能与资源占用。对于需要高频、大文件传输或集成在资源紧张的设备中的应用,原生C++程序的效率优势是显而易见的。

网上很多关于FTP的代码片段要么过于简单(只实现了最基础的命令),要么依赖第三方库如libcurl,增加了复杂度。本文将带你从零开始,使用VC++和WinINet API,实现一个健壮、可用的FTP文件上传功能,并附带完整的、可编译的源码。我们会深入每个步骤背后的原理,并分享在实际开发中踩过的坑和调试技巧。

2. 核心思路与方案选型:WinINet vs. libcurl

在VC++中实现FTP客户端,主流有两种技术路线:使用Windows自带的WinINet API,或者集成开源的libcurl库。我们需要根据项目需求做出选择。

2.1 WinINet API方案深度解析

WinINet是Microsoft Internet Explorer底层使用的网络API,它封装了HTTP、FTP和Gopher协议。对于FTP操作,它提供了一组非常直观的函数。

为什么选择WinINet?

  1. 原生与轻量:它是Windows SDK的一部分,无需额外下载和链接第三方库。你的项目配置会非常干净。
  2. 与Windows认证无缝集成:它能够自动处理当前用户的登录凭据(如果服务器支持Windows集成认证),也支持代理设置,这些都与IE/Edge浏览器的设置相通。
  3. 简化了复杂操作:例如,FtpPutFile一个函数调用就能完成本地文件到远程服务器的上传,包括建立连接、认证、传输模式设置、数据传输和连接关闭等一系列操作。
  4. 适合桌面集成:如果你的VC++程序本身就是一个MFC或Win32桌面应用,使用WinINet能让网络功能与你的UI线程、消息循环更好地结合,例如方便地实现上传进度回调(虽然WinINet原生不支持进度回调,但可以通过其他方式模拟,后文会讲)。

它的局限性:

  • 功能相对基础:WinINet的FTP函数主要面向常规的文件传输。对于需要精细控制(如断点续传、自定义命令、SSL/TLS加密的FTPS)的场景,它可能力有不逮。
  • 进度反馈不友好:没有提供标准的、方便的回调函数来报告上传进度,需要开发者自己“想办法”。
  • 阻塞式操作:默认的FtpPutFile是同步的,会阻塞调用线程直到文件传输完成或失败。在大文件上传时,需要将其放入工作线程以避免界面“假死”。

2.2 libcurl库方案简介

libcurl是一个功能极其强大且跨平台的网络传输库,支持数十种协议,包括FTP、FTPS、SFTP、HTTP、SCP等。

什么情况下应该考虑libcurl?

  • 你需要支持FTPS(基于SSL/TLS的FTP)或SFTP。
  • 你需要断点续传功能。
  • 你的项目未来可能需要跨平台(Linux, macOS)。
  • 你需要极其精细的控制和丰富的状态信息回调(包括实时上传/下载速度、进度)。

对于本次“实现上传文件到FTP服务器”的目标,WinINet完全够用,且能保持项目的简洁。因此,本文将聚焦于使用WinINet API的实现方案。理解了这个方案,你也能更容易地理解libcurl的工作模式。

2.3 项目整体架构设计

我们的程序逻辑将遵循以下清晰流程:

  1. 初始化:使用InternetOpen初始化WinINet库,获取一个根句柄。
  2. 建立连接:使用InternetConnect,通过根句柄、服务器地址、端口、用户名和密码,建立一个FTP会话句柄。
  3. 设置传输模式(可选):使用FtpSetCurrentDirectory切换远程目录(如果需要)。使用FtpCommand或其他方式设置传输模式为二进制(IMAGE)或ASCII。
  4. 执行上传:核心步骤,使用FtpPutFile函数上传文件。我们将重点处理如何将其放入线程,以及如何获取传输状态。
  5. 错误处理与资源清理:每一步都需要检查函数返回值(通常是NULL或FALSE),并通过GetLastError获取详细错误码。最后,必须按顺序关闭所有Internet句柄。

3. 核心代码实现与逐行解析

接下来,我们进入实战环节。我将提供一个封装成CFtpUploader类的完整源码,并逐部分进行详细解释。这个类将上传操作放在独立线程中,并提供了简单的状态查询接口。

3.1 头文件定义 (FtpUploader.h)

// FtpUploader.h #pragma once #include <windows.h> #include <wininet.h> #include <string> #include <thread> #include <atomic> class CFtpUploader { public: CFtpUploader(); ~CFtpUploader(); // 设置FTP服务器连接参数 void SetServerInfo(const std::wstring& strServer, int nPort, const std::wstring& strUser, const std::wstring& strPass); // 设置要上传的本地文件路径和远程目标路径 void SetFileInfo(const std::wstring& strLocalFile, const std::wstring& strRemotePath); // 开始异步上传 bool StartUpload(); // 停止上传 void StopUpload(); // 获取当前状态 enum class UploadStatus { Idle, Connecting, Uploading, Success, Failed, Cancelled }; UploadStatus GetStatus() const; // 获取错误信息(如果状态为Failed) std::wstring GetLastError() const; private: // 实际执行上传的工作线程函数 void UploadThreadProc(); // WinINet句柄 HINTERNET m_hInternet; HINTERNET m_hFtpSession; // 连接参数 std::wstring m_strServer; int m_nPort; std::wstring m_strUser; std::wstring m_strPass; // 文件参数 std::wstring m_strLocalFile; std::wstring m_strRemotePath; // 控制与状态 std::atomic<UploadStatus> m_eStatus; std::atomic<bool> m_bStopRequested; std::thread m_workerThread; std::wstring m_strLastError; // 内部辅助函数 bool ConnectToServer(); void Disconnect(); void SetLastError(DWORD dwError); };

关键点解析:

  • 使用宽字符std::wstring:WinINet API的Unicode版本函数(后缀为W,如InternetConnectW)要求宽字符串。使用std::wstring能避免字符集转换的麻烦,也与现代Windows编程习惯一致。
  • 原子操作std::atomic:状态m_eStatus和停止标志m_bStopRequested会被主线程和工作线程同时访问。使用std::atomic可以确保线程安全,无需引入复杂的锁。
  • 异步设计StartUpload会启动一个std::thread来执行UploadThreadProc,这样主线程(通常是UI线程)就不会被阻塞。

3.2 源文件实现 (FtpUploader.cpp) - 第一部分:连接与断开

// FtpUploader.cpp #include "FtpUploader.h" #include <iostream> // 实际项目中可替换为日志库 #pragma comment(lib, "wininet.lib") // 链接WinINet库 CFtpUploader::CFtpUploader() : m_hInternet(NULL) , m_hFtpSession(NULL) , m_nPort(21) // FTP默认端口 , m_eStatus(UploadStatus::Idle) , m_bStopRequested(false) { } CFtpUploader::~CFtpUploader() { StopUpload(); // 确保工作线程停止 Disconnect(); // 清理网络资源 } void CFtpUploader::SetServerInfo(const std::wstring& strServer, int nPort, const std::wstring& strUser, const std::wstring& strPass) { // 简单起见,这里没有加锁。如果会在上传过程中调用,需要加锁保护。 m_strServer = strServer; m_nPort = (nPort > 0) ? nPort : 21; m_strUser = strUser; m_strPass = strPass; } void CFtpUploader::SetFileInfo(const std::wstring& strLocalFile, const std::wstring& strRemotePath) { m_strLocalFile = strLocalFile; m_strRemotePath = strRemotePath; } bool CFtpUploader::ConnectToServer() { // 1. 初始化WinINet m_hInternet = InternetOpenW( L"FtpUploader/1.0", // User Agent,标识客户端 INTERNET_OPEN_TYPE_DIRECT, // 直接连接,不使用代理 NULL, // 代理地址(直接连接时为NULL) NULL, // 代理bypass列表 0); // 标志位,通常为0 if (m_hInternet == NULL) { SetLastError(GetLastError()); return false; } // 2. 建立FTP连接 m_hFtpSession = InternetConnectW( m_hInternet, // 上一步获取的根句柄 m_strServer.c_str(), // FTP服务器地址或域名 m_nPort, // 端口号 m_strUser.empty() ? NULL : m_strUser.c_str(), // 用户名,为空表示匿名登录 m_strPass.empty() ? NULL : m_strPass.c_str(), // 密码 INTERNET_SERVICE_FTP, // 服务类型:FTP m_strUser.empty() ? INTERNET_FLAG_PASSIVE : 0, // 标志:匿名登录时通常用PASV模式 0); // 上下文,通常为0 if (m_hFtpSession == NULL) { DWORD dwErr = GetLastError(); InternetCloseHandle(m_hInternet); m_hInternet = NULL; SetLastError(dwErr); return false; } return true; } void CFtpUploader::Disconnect() { if (m_hFtpSession) { InternetCloseHandle(m_hFtpSession); m_hFtpSession = NULL; } if (m_hInternet) { InternetCloseHandle(m_hInternet); m_hInternet = NULL; } }

注意:连接模式(主动 vs. 被动):这是FTP初学者最容易困惑和出错的地方。在InternetConnectW的标志位参数中,我设置了一个条件:m_strUser.empty() ? INTERNET_FLAG_PASSIVE : 0。这表示匿名登录时默认使用被动模式(PASV)。为什么?

  • 主动模式(PORT):服务器主动连接客户端的一个随机端口来传输数据。这在客户端位于防火墙或NAT之后时常常失败,因为外部服务器无法连接到客户端内部的随机端口。
  • 被动模式(PASV):服务器打开一个数据端口并告知客户端,由客户端去连接服务器。这种方式对位于防火墙后的客户端更友好。 在实际企业环境中,服务器可能强制要求某种模式。如果连接失败,错误码可能是ERROR_FTP_TRANSFER_IN_PROGRESS或连接超时,这时你需要尝试修改这个标志(INTERNET_FLAG_PASSIVE)来切换模式。更健壮的实现应该提供选项让调用者指定模式。

3.3 源文件实现 - 第二部分:上传线程与核心上传逻辑

void CFtpUploader::UploadThreadProc() { m_eStatus = UploadStatus::Connecting; if (!ConnectToServer()) { m_eStatus = UploadStatus::Failed; return; } // 可选:设置传输模式为二进制。这对于上传图片、压缩包、可执行文件等至关重要。 // 如果不设置,默认可能是ASCII模式,会导致非文本文件损坏。 if (!FtpSetCurrentDirectoryW(m_hFtpSession, L"/")) { // 切换目录失败不一定是致命错误,记录但继续 // 实际可记录日志 } // WinINet的FtpPutFile在内部似乎已处理模式,但显式设置是好习惯。 // 更底层的控制需使用InternetSetOption,此处从简。 m_eStatus = UploadStatus::Uploading; // 核心上传调用 BOOL bResult = FtpPutFileW( m_hFtpSession, // FTP会话句柄 m_strLocalFile.c_str(), // 本地文件完整路径 m_strRemotePath.c_str(), // 远程文件路径(可包含文件名) FTP_TRANSFER_TYPE_BINARY, // 传输类型:二进制 INTERNET_FLAG_NO_CACHE_WRITE // 标志:不缓存 ); // 处理上传结果 if (m_bStopRequested) { m_eStatus = UploadStatus::Cancelled; } else if (bResult) { m_eStatus = UploadStatus::Success; } else { SetLastError(GetLastError()); m_eStatus = UploadStatus::Failed; } Disconnect(); } bool CFtpUploader::StartUpload() { if (m_eStatus == UploadStatus::Uploading || m_eStatus == UploadStatus::Connecting) { m_strLastError = L"Upload is already in progress."; return false; } if (m_strLocalFile.empty() || m_strServer.empty()) { m_strLastError = L"Local file or server address is not set."; return false; } m_bStopRequested = false; m_strLastError.clear(); m_eStatus = UploadStatus::Idle; // 启动工作线程 try { m_workerThread = std::thread(&CFtpUploader::UploadThreadProc, this); } catch (const std::exception&) { m_strLastError = L"Failed to create worker thread."; return false; } m_workerThread.detach(); // 分离线程,让其自行结束 return true; } void CFtpUploader::StopUpload() { m_bStopRequested = true; // 注意:FtpPutFile是阻塞的,无法从外部中断其网络操作。 // 这里的停止标志更多是用于状态标识。要实现真正的可中断上传,需要更复杂的方案(如使用WinINet的异步模式或libcurl)。 } CFtpUploader::UploadStatus CFtpUploader::GetStatus() const { return m_eStatus; } std::wstring CFtpUploader::GetLastError() const { if (m_eStatus == UploadStatus::Failed) { return m_strLastError; } return L""; } void CFtpUploader::SetLastError(DWORD dwError) { wchar_t szMsg[256]; if (FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, dwError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), szMsg, sizeof(szMsg) / sizeof(wchar_t), NULL)) { m_strLastError = szMsg; } else { m_strLastError = L"Unknown error occurred."; } // 也可以将错误码附加到信息后,便于调试 // m_strLastError += L" (Error Code: " + std::to_wstring(dwError) + L")"; }

核心函数FtpPutFileW详解:这个函数是上传的核心,它封装了建立数据连接、传输文件、关闭连接的全过程。

  • 参数FTP_TRANSFER_TYPE_BINARY:这是重中之重。FTP有两种传输模式:ASCII(用于文本文件,会进行换行符转换)和BINARYIMAGE(逐字节传输,不做任何更改)。上传任何非纯文本文件(如图片、视频、ZIP、EXE)都必须使用BINARY模式,否则文件会损坏。WinINet默认行为可能不安全,所以务必显式指定。
  • 阻塞性:该函数是同步调用,直到整个文件传输完成或发生错误才会返回。这就是为什么我们要把它放到独立线程中。
  • 远程路径strRemotePath可以像/folder/subfolder/myfile.zip这样包含路径和文件名。如果远程目录不存在,FtpPutFile通常会失败。更健壮的做法是先使用FtpCreateDirectoryW递归创建目录。

4. 实战应用与进阶技巧

有了上面的核心类,我们可以在一个MFC对话框或Win32窗口中轻松使用它。

4.1 在MFC对话框中集成上传功能

假设你有一个对话框,上面有服务器地址、用户名、密码等编辑框,一个“浏览”按钮选择本地文件,一个“上传”按钮和一个进度条(用于显示状态)。

// 在对话框头文件中 private: CFtpUploader m_ftpUploader; CProgressCtrl m_progressCtrl; CStatic m_statusText; // “上传”按钮的响应函数 void CMyFtpDlg::OnBnClickedButtonUpload() { UpdateData(TRUE); // 获取控件数据到变量 CString strLocalFile, strServer, strUser, strPass, strRemote; m_editLocalFile.GetWindowText(strLocalFile); m_editServer.GetWindowText(strServer); // ... 获取其他编辑框内容 m_ftpUploader.SetServerInfo(strServer.GetString(), 21, strUser.GetString(), strPass.GetString()); // 远程路径可以写死,或由用户输入。这里示例为上传到服务器根目录,文件名不变。 CString strRemotePath = L"/" + CPath(strLocalFile).GetFileName(); m_ftpUploader.SetFileInfo(strLocalFile.GetString(), strRemotePath.GetString()); if (!m_ftpUploader.StartUpload()) { AfxMessageBox(L"启动上传失败!"); return; } m_progressCtrl.SetState(PBST_NORMAL); m_progressCtrl.SetPos(0); SetTimer(1, 200, NULL); // 启动一个定时器,用于轮询状态更新UI } // 定时器响应函数,用于更新进度和状态 void CMyFtpDlg::OnTimer(UINT_PTR nIDEvent) { if (nIDEvent == 1) { auto status = m_ftpUploader.GetStatus(); switch (status) { case CFtpUploader::UploadStatus::Uploading: // 由于WinINet没有提供进度回调,我们无法获取精确进度。 // 这里可以将进度条设为Marquee模式(滚动条),表示正在进行中。 m_progressCtrl.SetMarquee(TRUE, 50); m_statusText.SetWindowText(L"正在上传..."); break; case CFtpUploader::UploadStatus::Success: KillTimer(1); m_progressCtrl.SetMarquee(FALSE, 0); m_progressCtrl.SetPos(100); m_statusText.SetWindowText(L"上传成功!"); AfxMessageBox(L"文件上传成功。"); break; case CFtpUploader::UploadStatus::Failed: KillTimer(1); m_progressCtrl.SetState(PBST_ERROR); m_statusText.SetWindowText(L"上传失败!"); AfxMessageBox(m_ftpUploader.GetLastError().c_str()); break; case CFtpUploader::UploadStatus::Cancelled: KillTimer(1); m_progressCtrl.SetState(PBST_PAUSED); m_statusText.SetWindowText(L"上传已取消。"); break; default: break; } } CDialogEx::OnTimer(nIDEvent); }

4.2 实现上传进度反馈(模拟方案)

如前所述,FtpPutFile没有进度回调。但我们可以通过一个“曲线救国”的方法来模拟进度,提升用户体验。思路是:在独立线程中,一边上传,一边定期查询本地已传输文件的大小变化。

这需要放弃FtpPutFile,改用更底层的FtpOpenFileWInternetWriteFileFtpCloseHandle组合。这里给出简化后的伪代码思路:

void CFtpUploader::UploadWithProgress() { // ... 建立连接代码同上 ... // 1. 打开本地文件 HANDLE hLocalFile = CreateFile(m_strLocalFile.c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); LARGE_INTEGER liFileSize; GetFileSizeEx(hLocalFile, &liFileSize); const DWORD dwBufferSize = 64 * 1024; // 64KB缓冲区 std::vector<BYTE> buffer(dwBufferSize); // 2. 打开远程文件(用于写入) HINTERNET hRemoteFile = FtpOpenFileW(m_hFtpSession, m_strRemotePath.c_str(), GENERIC_WRITE, FTP_TRANSFER_TYPE_BINARY, 0); if (!hRemoteFile) { /* 错误处理 */ } DWORD dwTotalRead = 0; DWORD dwRead = 0, dwWritten = 0; BOOL bResult; while (!m_bStopRequested && dwTotalRead < liFileSize.QuadPart) { // 3. 从本地文件读取一块数据 bResult = ReadFile(hLocalFile, buffer.data(), dwBufferSize, &dwRead, NULL); if (!bResult || dwRead == 0) break; // 4. 写入到远程FTP文件 bResult = InternetWriteFile(hRemoteFile, buffer.data(), dwRead, &dwWritten); if (!bResult) break; dwTotalRead += dwWritten; // 5. 计算并更新进度(此处需要线程安全的方式通知主线程) int nPercent = static_cast<int>((dwTotalRead * 100) / liFileSize.QuadPart); // PostMessage或回调函数通知UI更新进度条 } // 6. 清理 InternetCloseHandle(hRemoteFile); CloseHandle(hLocalFile); // ... 状态设置和断开连接 ... }

这种方法给了我们完全的控制权,可以实现进度报告、暂停和恢复(需要服务器支持断点续传命令REST)。但代码复杂度显著增加,需要处理更多的错误边界。对于大多数“上传单个文件”的需求,简单的FtpPutFile配合线程和状态轮询已经足够。

5. 常见问题、错误排查与调试技巧实录

在实际开发和部署中,你会遇到各种各样的问题。下面是我总结的常见错误码、原因及解决方法。

5.1 连接阶段错误

错误现象/代码可能原因排查与解决思路
ERROR_INTERNET_CANNOT_CONNECT(12029)网络不通,服务器地址或端口错误,防火墙阻止。1. 用pingtelnet [服务器] 21检查网络连通性。
2. 确认服务器IP和端口(默认21)。
3. 检查Windows防火墙或杀毒软件是否阻止了你的程序。
ERROR_INTERNET_LOGIN_FAILURE(12015)用户名或密码错误,或该用户权限不足。1. 使用FileZilla等标准FTP客户端测试同一套凭据能否登录。
2. 确认服务器是否允许匿名登录(用户名填anonymous,密码填邮箱或任意字符)。
ERROR_FTP_TRANSFER_IN_PROGRESS(12110)连接模式冲突。常见于服务器被动模式配置与客户端不匹配。InternetConnectW调用中,尝试切换INTERNET_FLAG_PASSIVE标志。先尝试被动模式(添加此标志),如果不行再尝试主动模式(移除此标志)。

5.2 上传阶段错误

错误现象/代码可能原因排查与解决思路
ERROR_INTERNET_EXTENDED_ERROR(12003)服务器返回了具体的FTP错误。需要获取扩展错误信息。调用InternetGetLastResponseInfoW函数获取服务器返回的详细错误字符串,这能提供最直接的线索,如“550 Permission denied”(无写入权限)或“553 Could not create file”(路径不存在或磁盘满)。
ERROR_GEN_FAILURE(31) 或上传后文件损坏传输模式错误。在ASCII模式下上传了二进制文件。确保FtpPutFiledwFlags参数包含FTP_TRANSFER_TYPE_BINARY。这是最容易被忽略但后果最严重的一点。
上传成功,但文件大小为0远程路径目录不存在,或程序对目标目录没有写权限。1. 使用FtpCreateDirectoryWFtpSetCurrentDirectoryW确保远程目录存在。
2. 检查服务器端该目录的FTP用户写入权限。
程序在上传大文件时“卡住”无响应FtpPutFile阻塞主线程。确认你已经将上传操作放在了工作线程中执行。UI线程只负责触发和状态更新。

5.3 调试与日志记录技巧

  1. 启用WinINet诊断日志:在程序开始时调用InternetSetOption(NULL, INTERNET_OPTION_DEBUG_FLAGS, (LPVOID)&dwFlag, sizeof(dwFlag)),可以将WinINet的内部调试信息输出到调试器(如Visual Studio的Output窗口)。这对于诊断复杂的网络问题非常有帮助。
  2. 使用网络抓包工具:如Wireshark。过滤ftp或指定端口,你可以清晰地看到客户端与服务器之间的所有FTP命令(USER, PASS, PASV/PORT, STOR等)和数据传输。当错误信息模糊时,抓包是终极定位手段。
  3. 实现简单的日志系统:在你的CFtpUploader类中添加一个日志函数,将关键步骤(“开始连接”、“登录成功”、“开始上传文件XXX”、“上传完成”、“错误:XXX”)写入文件或内存。在用户报告问题时,这些日志是首要的分析依据。
  4. 处理路径中的空格和特殊字符:如果本地或远程文件路径包含空格或中文,确保你的字符串处理正确。使用宽字符版本API并检查路径是否被正确引用。有时需要将路径中的反斜杠\转换为斜杠/

5.4 关于“VC++崩溃生成调试文件”的额外提示

在开发过程中,程序崩溃是常事。确保你的VC++项目配置生成了调试信息。

  • 项目属性 -> 链接器 -> 调试中,设置“生成调试信息”为“是”。
  • C/C++ -> 常规中,设置“调试信息格式”为“程序数据库(/Zi)”。 这样,当程序在客户机器上崩溃时,可以收集生成的.dmp(转储)文件,在你自己装有相同PDB文件的开发机上用Visual Studio打开,可以定位到崩溃的代码行,极大提升排查效率。

6. 项目构建与源码集成指南

  1. 创建新项目:打开Visual Studio,创建一个新的“MFC应用程序”或“Win32控制台应用程序”项目。
  2. 添加文件:将我们编写的FtpUploader.hFtpUploader.cpp添加到项目中。
  3. 配置项目属性
    • 项目属性 -> 高级中,将“字符集”设置为“使用Unicode字符集”。这很重要,因为我们使用的是宽字符API。
    • WinINet库通常会自动链接,如果遇到链接错误,可以在链接器 -> 输入 -> 附加依赖项中手动添加wininet.lib
  4. 编写测试代码:在MFC对话框的按钮事件中,或控制台程序的main函数里,实例化CFtpUploader,设置参数并调用StartUpload
  5. 测试:准备一个FTP测试服务器(可以用FileZilla Server在本地快速搭建一个),使用你的程序上传一个小文件进行测试。从连接、认证到文件传输,一步步验证。

这个CFtpUploader类提供了一个坚实、可扩展的起点。你可以根据需要增加更多功能,如目录列表、文件下载、删除、重命名,或者集成到更复杂的文件同步任务中。记住,网络编程的核心在于细致的错误处理和资源管理,每一处InternetCloseHandle都至关重要,避免句柄泄漏。希望这份结合了原理、代码和实战经验的指南,能帮助你顺利实现VC++下的FTP文件上传功能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/14 5:09:37

Unity龙卷风特效插件实战:从粒子系统到性能优化全解析

1. 项目概述与核心价值最近在做一个开放世界冒险游戏&#xff0c;里面有个天气系统&#xff0c;需要实现那种毁天灭地的龙卷风效果。一开始我头都大了&#xff0c;自己用粒子系统从零开始堆&#xff0c;调了三天三夜&#xff0c;结果做出来的东西像个原地打转的灰色麻绳&#x…

作者头像 李华
网站建设 2026/7/14 5:09:33

担心IP暴露隐私?用安全IP查询工具自查,三步配置网络出口

你是不是也有过这种顾虑&#xff1a;查了一下自己的IP&#xff0c;归属地显示“北京市朝阳区”&#xff0c;心里就咯噔一下——“别人是不是也能查到我住哪个小区&#xff1f;”其实&#xff0c;单凭一个IP地址&#xff0c;不能直接暴露你的家庭住址。IP地址本质上只是网络通信…

作者头像 李华
网站建设 2026/7/14 5:08:09

脑电信号(EEG)分析全流程:从采集、预处理到解码应用

1. 脑电信号&#xff08;EEG&#xff09;基础与采集技术脑电信号&#xff08;EEG&#xff09;是大脑神经元电活动的宏观表现&#xff0c;就像一群人在广场上同时说话产生的声波。想象一下&#xff0c;每个神经元都是一个微型发电机&#xff0c;当它们同步放电时&#xff0c;就会…

作者头像 李华
网站建设 2026/7/14 5:03:09

双系统适配 OpenClaw 搭建,解决网关离线各类报错

本教程专为 Windows 11 和 macOS 系统用户设计&#xff0c;针对安装过程中常见的权限不足、Defender 拦截、中文路径报错以及 SmartScreen 阻拦等问题&#xff0c;提供了详细的解决方案。采用一键式安装设计&#xff0c;即便是零基础用户&#xff0c;也能在 10 分钟内完成配置并…

作者头像 李华
网站建设 2026/7/14 5:00:26

学 Simulink——基于滞环比较器的电流瞬时值控制双向逆变器仿真

目录 手把手教你学 Simulink——基于滞环比较器的电流瞬时值控制双向逆变器仿真 一、滞环电流控制是什么?为什么用?** 1.1 基本原理 1.2 特点 二、系统拓扑(教学级)** 三、控制框图(单相示意,三相同理每相独立)** 四、关键参数(典型教学值)** 五、Simulink 建…

作者头像 李华