VB.NET FTP操作指南:FtpWebRequest实现上传下载与文件管理

VB.NET FTP操作指南:FtpWebRequest实现上传下载与文件管理 简介本资源是一套面向VB.NET初学者与中级开发者的FTP功能实战源码包聚焦文件传输核心场景解决远程服务器目录管理、文件增删查传等典型开发需求。压缩包共30个文件含6个关键VB源码文件含主窗体逻辑与FTP操作封装、3个可执行exe程序便于快速验证功能、2个resx与2个resources资源文件支持多语言与界面适配以及sln解决方案、vbproj项目配置等完整工程结构总大小仅164KB轻量易集成。已有707人下载学习代码严格基于System.Net.FtpWebRequest实现涵盖创建目录、删除文件、详列目录、断点续传式下载及二进制流上传五大功能模块并内置异常处理框架与请求执行通用方法所有案例均通过实际调试验证可直接嵌入企业级文件同步、运维工具或教学实训项目中复用。 做VB.NET的FTP操作很多人第一反应是去网上抄一段System.Net.FtpWebRequest的代码结果发现连不上、上传大文件超时、中文文件名乱码、被动模式配不对各种坑踩一遍。这篇博文我直接给你一套可以跑的案例源码覆盖创建目录、删除文件、查询列表、上传下载这五个最常用的操作每段代码后面都会解释关键点最后还会聊聊我在实际项目里踩过的坑和排查思路。适用对象是刚接触VB.NET的桌面开发新手以及公司内部要做文件传输小工具的运维和业务人员不需要你去学一堆底层协议照着抄就能用。先说清楚一个概念FTP听起来是“文件传输协议”的缩写但实际开发时你要区分控制连接和数据连接。控制连接走21端口用来发指令比如USER、PASS、CWD、LIST、PASV而真正的文件内容走数据连接端口可能是20也可能是一个随机高端口取决于你用的是主动模式还是被动模式。VB.NET里的FtpWebRequest把这两层封装了但你仍然需要理解主动和被动模式的区别不然换了网络环境就掉线。1. 整体设计思路为什么直接操作FtpWebRequest而不是用第三方库1.1 项目需求拆解标题里列了五个核心操作创建目录、删除文件或目录、查询文件列表、下载文件、上传文件。这三个动作就是FTP日常使用的全部高频场景说白了一个业务系统里的FTP模块无非就是这五件事来回组合。以前我做公司内部的文件同步工具也是先梳理出这五个操作再外层包一层业务逻辑比如定时轮询远端目录、对比本地文件MD5后增量下载、下载完回传确认文件等。1.2 选型理由自带库的取舍VB.NET里做FTP方案无非三种直接用System.Net.FtpWebRequest、用WinINetAPI、引用第三方库如Rebex、edtFTPnet。第三方库封装度高但牵涉到授权、DLL搬运和小概率兼容性问题而FtpWebRequest是.NET框架自带的不需要额外引用代码逻辑直观所有FTP指令都看得见摸得着对学习协议和排查问题都更友好。所以我的建议是功能简单、数量不大的场景优先用自带库遇到需要断点续传、SFTP、并发高吞吐时再考虑上第三方。1.3 目录结构与代码骨架我在案例源码里设计了一个FtpHelper类把五个操作封装成公共方法用起来就是一个类名加方法名比如FtpHelper.UploadFile(ftp://192.168.1.10/a.txt, C:\\temp\\a.txt, user, pass)。类内部的公共字段包括服务器地址、端口、用户名、密码、是否使用被动模式、连接超时时间等方法内部统一创建FtpWebRequest设置好必要属性后调用GetResponse执行。这样业务层不需要关心FTP细节。2. 核心细节解析FtpWebRequest的每个属性都是干什么的2.1 核心参数URI格式、凭据、KeepAlive与UsePassiveFtpWebRequest创建的入口是WebRequest.Create但必须转成FtpWebRequest类型。URI格式必须是ftp://主机地址/路径/文件名注意如果不写路径默认去用户的根目录不同FTP服务器表现可能不一样。凭据通过request.Credentials New NetworkCredential(user, pass)设置如果FTP服务器允许匿名访问可以传new NetworkCredential(anonymous, anonymousexample.com)。KeepAlive这个属性容易踩坑。默认值是true表示一个请求结束后底层的控制连接不关闭复用给下一条指令这样效率高。但如果你在循环里批量处理文件又显式把KeepAlivefalse那每条指令都会重新建立TCP连接握手三次加身份认证几百个文件会让你怀疑人生。反过来如果你用true但某个操作异常没有关闭请求控制连接会悬挂一段时间所以要搭配finally块里的request.Abort()。UsePassive决定数据连接的建立方向。true表示被动模式客户端连服务器的随机端口绝大多数局域网和公网环境都是这种因为它能绕过客户端防火墙限制只要客户端能出去就行。false表示主动模式服务器反过来连客户端的一个随机端口这在很多企业防火墙下直接失败。我的经验是默认永远设true只有遇到服务器特别老、或者运维明确要求主动模式时才改。2.2 五个核心操作的命令映射每个FTP操作其实都是发一个协议指令。创建目录对应MKD删除文件对应DELE删除空目录对应RMD查询列表对应LIST或NLST上传对应STOR下载对应RETR。FtpWebRequest通过request.Method属性来指定比如WebRequestMethods.Ftp.MakeDirectory、DeleteFile、RemoveDirectory、ListDirectory、UploadFile、DownloadFile。因此你写代码时其实是在拼装一个FTP客户端搞懂命令的语义有助于排查返回的异常码。2.3 四类异常处理FTP操作经常抛异常需要分类处理。WebException是最常见的它的Response属性如果是FtpWebResponse可以通过StatusDescription拿到FTP服务器返回的状态码和说明比如550表示文件不存在或没有权限553表示文件名非法530表示认证失败421表示服务不可用。IOException一般出现在网络中断、连接超时的情况。UnauthorizedAccessException是本地文件问题。InvalidCastException多半是创建请求时没有正确转成FtpWebRequest。代码里统一用一个自定义异常把FtpStatusCode包进去方便业务层显示给用户看。3. 实操过程完整案例源码与逐段解读3.1 公共配置类首先定义一个FtpConfig类用来承载配置项这样比在方法参数里堆十几个参数清晰得多。Public Class FtpConfig Public Property Host As String 192.168.1.100 Public Property Port As Integer 21 Public Property Username As String ftpuser Public Property Password As String ftppass Public Property UsePassive As Boolean True Public Property UseBinary As Boolean True Public Property Timeout As Integer 30000 Public Function GetBaseUri() As String Return $ftp://{Host}:{Port} End Function End Class这里的端口默认21如果FTP服务器换了端口直接改Port。UseBinary表示二进制模式上传图片、压缩包、Excel文件时必须为True否则会以ASCII模式传输导致文件损坏。3.2 创建目录创建目录的代码看起来很短但有几个细节Public Shared Function CreateDirectory(config As FtpConfig, remotePath As String) As Boolean Dim request As FtpWebRequest CType(WebRequest.Create(${config.GetBaseUri()}/{remotePath}), FtpWebRequest) request.Method WebRequestMethods.Ftp.MakeDirectory request.Credentials New NetworkCredential(config.Username, config.Password) request.UsePassive config.UsePassive request.UseBinary config.UseBinary request.KeepAlive True request.Timeout config.Timeout Try Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) If response.StatusCode FtpStatusCode.PathnameCreated Then Return True End If End Using Catch ex As WebException Dim ftpResponse As FtpWebResponse CType(ex.Response, FtpWebResponse) If ftpResponse IsNot Nothing AndAlso ftpResponse.StatusCode FtpStatusCode.ActionNotTakenFileUnavailable Then Throw New Exception($目录可能已存在或路径非法: {remotePath}, ex) End If Throw End Try Return False End Function路径分隔符最好用/FTP协议标准就是斜杠Windows本地才是反斜杠。如果你想递归创建多级目录比如/2025/04/report但服务器上2025和04都不存在MKD一次是建不了多级的。我封装时做了递归逻辑先把路径拆成数组从第一级开始逐层创建遇到550就认为是已存在继续下一层这样可以避免重复报错。3.3 删除文件与删除目录删除文件用的是DeleteFile删除空目录用RemoveDirectory两者代码结构类似。具体代码如下Public Shared Function DeleteFile(config As FtpConfig, remoteFilePath As String) As Boolean Dim request As FtpWebRequest CType(WebRequest.Create(${config.GetBaseUri()}/{remoteFilePath}), FtpWebRequest) request.Method WebRequestMethods.Ftp.DeleteFile request.Credentials New NetworkCredential(config.Username, config.Password) request.UsePassive config.UsePassive request.Timeout config.Timeout Try Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) Return response.StatusCode FtpStatusCode.FileActionOK End Using Catch ex As WebException Dim ftpResponse As FtpWebResponse CType(ex.Response, FtpWebResponse) If ftpResponse IsNot Nothing AndAlso ftpResponse.StatusCode FtpStatusCode.ActionNotTakenFileUnavailable Then Throw New FileNotFoundException($远端文件不存在: {remoteFilePath}, ex) End If Throw End Try End Function一个我踩过很多次的坑DeleteFile只能删除文件不能删除非空目录。如果你想删除整个目录树就得先列出目录下的所有条目递归删除。我在实际项目里做过一个DeleteDirectoryRecursive函数用ListDirectoryDetails拿到每个子项的类型标记识别出DIR就递归进去否则DeleteFile。这套递归逻辑虽然代码量不大但能解决运维场景里90%的“清理过期历史数据”需求。3.4 查询文件列表查询列表有两种方式一是ListDirectory拿到的是文件名清单二是ListDirectoryDetails拿到的是服务器返回的详细信息文本类似DOS的dir命令包含日期、大小、权限标记。推荐用ListDirectoryDetails因为光有文件名你不知道它是文件还是目录也不知道大小。Public Shared Function GetFileList(config As FtpConfig, remoteDir As String) As List(Of FtpFileInfo) Dim result As New List(Of FtpFileInfo)() Dim request As FtpWebRequest CType(WebRequest.Create(${config.GetBaseUri()}/{remoteDir}), FtpWebRequest) request.Method WebRequestMethods.Ftp.ListDirectoryDetails request.Credentials New NetworkCredential(config.Username, config.Password) request.UsePassive config.UsePassive request.UseBinary True request.Timeout config.Timeout Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) Using streamReader As New StreamReader(response.GetResponseStream(), Encoding.UTF8) While True Dim line As String streamReader.ReadLine() If String.IsNullOrEmpty(line) Then Exit While Dim info As New FtpFileInfo() 这里需要根据FTP服务器类型做解析示例简化为按空格分割 Dim parts line.Split(New Char() { c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length 9 Then Dim nameIndex parts.Length - 1 info.Name parts(nameIndex) info.IsDirectory line.StartsWith(d) info.Size GetSizeFromListLine(line) result.Add(info) End If End While End Using End Using Return result End Function解析ListDirectoryDetails是最让人抓狂的部分因为不同FTP服务器的文本格式不一样。Windows IIS的列表是个-a---- 2025-01-01 10:00 1234 file.txtLinux vsftpd的列表是-rw-r--r-- 1 ftp ftp 1234 Jan 1 10:00 file.txt用空格Split后字段位置完全不同。所以我封装了一个FtpFileInfo类并在解析方法里同时兼容两种格式先判断是否包含DIR如果没有尝试用Regex提取Unix格式的日期字段。如果你目标服务器是固定的可以直接简化成像示例那样的按空格解析。3.5 下载文件下载文件流程就是把FtpWebRequest的响应流拷贝到本地文件流。为了小文件和大文件都稳定我用CopyTo或Read循环两种方式这里展示常用版本Public Shared Sub DownloadFile(config As FtpConfig, remoteFilePath As String, localFilePath As String) Dim request As FtpWebRequest CType(WebRequest.Create(${config.GetBaseUri()}/{remoteFilePath}), FtpWebRequest) request.Method WebRequestMethods.Ftp.DownloadFile request.Credentials New NetworkCredential(config.Username, config.Password) request.UsePassive config.UsePassive request.UseBinary True request.Timeout config.Timeout Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) Using inputStream As Stream response.GetResponseStream() Using outputStream As New FileStream(localFilePath, FileMode.Create, FileAccess.Write) Dim buffer(8190) As Byte Dim bytesRead As Integer While True bytesRead inputStream.Read(buffer, 0, buffer.Length) If bytesRead 0 Then Exit While outputStream.Write(buffer, 0, bytesRead) End While End Using End Using End Using End Sub下载时有个重要细节FileMode.Create直接覆盖已有文件如果本地文件正在被Excel或另一个进程占用会抛IOException。所以生产环境我建议先下载临时文件比如.download后缀下载完成并校验大小无误后再File.Move成正式文件名这样可以避免“下载了一半目标文件被占用”的恶心场景。8KB的缓冲区是经验值数据吞吐基本能跑满局域网带宽网络上很多教程用CopyTo(stream)对于大文件也没有问题但从流式读取角度自己用Read循环更便于中途加进度事件。3.6 上传文件上传文件的核心是获取请求流然后把本地文件流写进去Public Shared Sub UploadFile(config As FtpConfig, localFilePath As String, remoteFilePath As String) Dim request As FtpWebRequest CType(WebRequest.Create(${config.GetBaseUri()}/{remoteFilePath}), FtpWebRequest) request.Method WebRequestMethods.Ftp.UploadFile request.Credentials New NetworkCredential(config.Username, config.Password) request.UsePassive config.UsePassive request.UseBinary True request.KeepAlive True request.Timeout config.Timeout Dim fileInfo As New FileInfo(localFilePath) request.ContentLength fileInfo.Length Using inputStream As New FileStream(localFilePath, FileMode.Open, FileAccess.Read) Using requestStream As Stream request.GetRequestStream() Dim buffer(8190) As Byte Dim bytesRead As Integer While True bytesRead inputStream.Read(buffer, 0, buffer.Length) If bytesRead 0 Then Exit While requestStream.Write(buffer, 0, bytesRead) End While End Using End Using Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) If response.StatusCode FtpStatusCode.ClosingData AndAlso response.StatusCode FtpStatusCode.FileActionOK Then Throw New Exception($上传失败状态码: {response.StatusCode}, 描述: {response.StatusDescription}) End If End Using End Sub上传时最容易漏的是request.ContentLength fileInfo.Length。如果不设置FtpWebRequest在写入流时无法确定内容长度部分服务器会报错或挂起。还有一点如果你用同一个FtpWebRequest实例在GetRequestStream之后必须写入完并关闭请求流再调用GetResponse顺序不能变反过来先GetResponse再GetRequestStream会抛异常因为服务器的数据连接还没建立。3.7 查询文件大小额外加一个很实用的方法下载前先获取远端文件大小用于判断文件是否存在或做进度条Public Shared Function GetFileSize(config As FtpConfig, remoteFilePath As String) As Long Dim request As FtpWebRequest CType(WebRequest.Create(${config.GetBaseUri()}/{remoteFilePath}), FtpWebRequest) request.Method WebRequestMethods.Ftp.GetFileSize request.Credentials New NetworkCredential(config.Username, config.Password) request.UsePassive config.UsePassive request.Timeout config.Timeout Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) Return response.ContentLength End Using End Function注意这里返回的ContentLength在部分服务器上不稳定尤其当服务器返回的FTP响应里没有213消息时可能是一个很奇怪的数字。我在项目里没有完全依赖它而是列表解析时把大小字段一起存到FtpFileInfo里这样更可靠。4. 常见问题与排查技巧实录4.1 被动模式换成主动模式的挣扎第一次在公司内网部署工具时FTP服务器是Server-U客户端能Ping通、能Telnet 21端口但socket连接建立后一列目录就超时。我排查半天最后发现是防火墙把服务器主动连客户端的高端口给拦了。解决方案就是把UsePassiveTrue。从那时起我就把默认值写死成被动模式只在连接老版本AIX服务器时临时改主动。这个经验对新手特别重要如果上传下载卡住先确认两边的防火墙规则再检查代码里UsePassive的设置。4.2 中文文件名乱码FTP协议没有强制规定文件名编码Windows自带FTP默认用系统ANSIGBKLinux服务器默认UTF-8。你用Encoding.UTF8去解码一个GBK文件名就会出现“鏂囦欢”这种乱码反过来用GBK解码UTF8也会乱码。解决办法是拿到ListDirectoryDetails的响应流时根据服务器系统选择GetEncoding(GBK)或Encoding.UTF8。但也不是那么绝对比如FileZilla Server新版默认UTF-8那你用UTF8解码没问题如果客户端代码用Encoding.Default在中国大陆Windows上就是GBK反而会乱码。我的建议是既然是用VB.NET做工具首先确认服务器默认编码然后在实体类里统一用Encoding.UTF8解析不要用Encoding.Default因为默认编码会随系统区域设置变化部署到其他机器就跑偏。4.3 上传大文件超时我在做一个视频素材上传功能时文件大概2GB发现每次传到一半就抛WebException: The operation has timed out。原因有两个一是Timeout属性控制的是发送请求和接收响应的等待时间不是整个传输过程传输大文件时如果中途网络停顿超过这个值就会超时二是服务器端的DataConnectionOpen超时和客户端KeepAlive设置冲突。解决思路不是把Timeout设到10分钟这么粗暴而是把文件分块上传或者延长超时时间到合适值并且每次读写循环里主动输出进度这样即使网络卡顿也能看出是卡在传输中还是真的超时。实际项目中我把超时调到120秒同时把缓冲区从4KB改成64KB传输速率明显提升。4.4 返回550却不知道是文件不存在还是没权限550 Requested action not taken. File unavailable这个错误其实有两种可能一是路径不对、文件不存在二是没有权限访问。我刚入行时总是默认是权限问题一顿调用户权限最后发现是路径里多写了一个空格。所以排查顺序应该是先用FileZilla之类的图形客户端手动登录同账号看同一个路径能不能访问、能不能上传然后用FtpWebRequest做最简单的ListDirectory看基础权限是否通再一步步调试到具体文件。图形客户端能通说明账号权限没问题问题出在代码的路径或编码图形客户端也不通那就先去服务器侧查目录权限和SELinux。4.5 无法显示列表但能上传文件有一次遇到能上传、能下载但一调ListDirectoryDetails就报错。后来发现是请求的URI末尾没有加斜杠服务器把请求解读成了RETR 目录名而不是LIST 目录。目录查询的URI建议这样拼${config.GetBaseUri()}/{remoteDir.TrimEnd(/)}/确保以斜杠结尾能减少很多歧义。这个问题在Windows IIS FTP上特别明显因为它对命令解析比较死板。4.6 客户端报“无法连接到远程服务器”但浏览器能访问FTP浏览器访问FTP可能会被Windows资源管理器接管也可能用IE内核的WebDAV方式跟真实FTP命令不一样。排查时用telnet ip 21远比浏览器可靠。如果你在VB.NET里WebRequest.Create直接连不上检查服务器防火墙是否只放行了特定客户端IP比如Windows防火墙里FTP服务的“允许连接”可能只对域网络生效。另外如果FTP服务器域名解析到了IPv6地址而你的客户端网络没有IPv6路由也会连不上。遇到这种情况直接在Host里填IP地址能绕过DNS解析。5. 案例源码的扩展玩法把工具类变成完整软件5.1 增加进度条事件上面每个方法都是同步阻塞的UI线程调用会直接卡死窗口进度条根本动不了。解决方法是把FTP操作放到Task.Run或BackgroundWorker里并给FtpHelper类加一个Public Event ProgressChanged(percent As Integer)。在读写循环里每隔一段字节数触发一次事件UI层Invoke更新进度条。这个改造虽然多了十几行代码但用户体验是质的飞跃。5.2 监控FTP文件夹变化热搜词里有“ftp监控”说明很多人想做“远端目录变了程序自动下载”的轮询功能。最简单可靠的方案不是用FileSystemWatcher监控FTP因为FTP服务器不在本地FileSystemWatcher只认本地路径。正确做法是用一个Timer每隔10秒或30秒调用一次GetFileList把返回的文件列表和上一次的列表做对比增量部分触发下载。为了避免重复处理可以在本地维护一个“已处理文件清单”放在database或文本文件里。这个方案我已经在多个项目里跑过缺点是轮询有延迟但胜在实现简单、稳定不会漏文件。5.3 与其他桌面功能联动这套FTP工具类天然适合嵌入进WinForms或WPF程序比如做一个“文件全览”窗口左侧是本地目录树右侧是FTP远程目录树双击上传、右键下载。配合VB.NET的FolderBrowserDialog、OpenFileDialog和ListView半天就能搭出一个小型FTP客户端。再往上加功能比如记录历史连接配置、保存多个站点、托盘运行、调度任务就能替代一部分FlashFXP的工作。提示企业环境下使用FTP明文传输时密码和文件内容都是明文暴露在局域网里的如果传的是敏感数据建议改用FTPSFTP over SSL或SFTP。改成FTPS时FtpWebRequest只需要设置request.EnableSsl True并处理证书校验逻辑SFTP则需要换用第三方库因为是不同的协议。6. 写在最后的排错心法调试FTP代码时不要靠猜最有效的办法是抓包。用Wireshark过滤ftp或tcp.port 21可以清晰看到每一次控制命令和响应码甚至能看出数据连接是否建立。比如上传时只有STOR指令、后面没有数据包说明服务端没有打开数据端口多半就是被动模式或防火墙问题。如果你不想用Wireshark也可以在代码里临时打印response.StatusDescription和request.Method关键路径上把KeepAlive、UsePassive、Timeout这三个值打到日志里问题范围一下子就缩小了。还有一个小技巧很多FTP服务器对并发连接数有限制你在本地调试时开了多个窗口连着同一账号代码里又用了KeepAliveTrue就可能出现“上一个连接未释放下一个连接被拒绝”。所以我建议工具类里加一个静态锁对象确保某个账号的指令排队执行或者干脆每次操作完用request.Abort()释放连接。测试时如果发现连接数是问题根源去服务器侧调大最大连接数省心很多。我个人在实际项目里最常用的组合是UsePassiveTrue、UseBinaryTrue、KeepAliveTrue、Timeout30000解析列表时统一UTF8遇到报错先看StatusDescription再看抓包。这套思路帮我解决过上百次FTP相关的线上问题希望你抄完代码也能少走弯路。本文还有配套的精品资源点击获取