ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

VB.NET FTP实战:FtpWebRequest实现文件上传下载与断点续传

VB.NET FTP实战:FtpWebRequest实现文件上传下载与断点续传 简介本资源是一套面向VB.NET初学者与中级开发者的FTP功能实战源码包聚焦文件传输核心场景解决远程服务器目录管理、文件增删查改等典型开发需求。压缩包共30个文件含6个核心VB源码文件含Form1.vb等主逻辑、1个Visual Studio解决方案.sln与1个项目配置文件.vbproj辅以资源文件.resx、.png、编译产物.exe、.pdb及调试支持文件.cache、.suo整体仅164KB轻量易导入。已有707人学习下载适合快速集成到桌面应用或教学实验中。源码完整覆盖FTP创建目录、删除文件、详细列表查询、二进制流式下载与上传五大操作每项均基于System.Net.FtpWebRequest实现并封装了可复用的请求执行函数项目结构规范含UI窗体、资源图标与配置文件开箱即用便于理解协议调用逻辑与异常处理落地实践。 接手公司这套 VB.NET 老项目的时候最让我头疼的不是业务逻辑而是一大堆和 FTP 有关的琐碎需求每天定时把分店传上来的销售单据拉到总部把总部生成的价格表推送下去清理三天前的备份文件偶尔还要人工看一下某个目录下到底有什么。刚开始我图省事直接在服务器上映射网络驱动器用File.Copy解决。结果换了一台服务器、换了一台只支持 FTP 的 NAS整套代码就彻底歇菜。最后还是老老实实把创建、删除、查询、下载、上传这一整套 FTP 操作全部写成 VB.NET 代码封装成一个工具类长期复用。这篇内容就把这些案例源码和背后的选型思路、调试经验一起抛出来适合正在用 VB.NET 做 FTP 功能、或者被远程文件同步问题折磨的朋友直接参考。代码不追求花哨都是能落地的写法。1. 为什么我最终没有选择 File.Copy而是老老实实走 FTP 协议1.1 网络驱动器映射看起来方便但坑全在你看不见的地方很多第一次做远程文件同步的同事上来就会这么写手动在 Windows 资源管理器里把 FTP 地址映射成 Z 盘然后 VB.NET 里File.Copy代码短、效果直观。但实际跑几个月问题会一个个冒出来映射盘依赖 Windows 登录会话。如果你的程序跑在 Windows 服务里或者计划任务用一个没有“登录即恢复网络连接”权限的账户非常容易出现“驱动器未就绪”或“找不到路径”。SMB 协议跨网段、跨互联网时经常被防火墙拦。你本机测试没问题部署到客户的异网环境就断。很多嵌入式设备、监控摄像头、工控盒子只提供 FTP 协议根本不给 SMB 共享映射盘无从谈起。文件量一大通过资源管理器映射盘做File.Copy的方式没有任何传输日志、断点续传、失败重试机制出了问题你连排查抓手都没有。所以结论很简单只要对端设备支持 FTP就直接用 FTP 协议编程别跟网络驱动器较劲。1.2 VB.NET 里操作 FTP 的三条主流路径对比VB.NET 里做 FTP绕不开这三条路。方案优点缺点适合场景FtpWebRequestSystem.Net零依赖库自带控制力强代码量大目录列表解析要自己写大多数生产环境WebClient几行代码完成上传/下载无法方便列目录、删文件、断点续传快速脚本、一次性工具WinSCP .NET / edtFTPnet功能完整支持 SFTP、断点续传、命令封装需要引入第三方 DLL部署多一个文件复杂同步、断点续传、跨协议远程管理FtpWebRequest是整个System.Net命名空间里经常被低估的类。它的 API 设计初看偏底层但它把 FTP 的基础命令都透传给了你MKD、NLST、LIST、RETR、STOR、DELE、RMD、RNFR/RNTO都能对应上。1.3 我最终的选型结论内部业务工具我偏好FtpWebRequest因为部署时不需要额外装 DLL不用担心第三方库版本冲突。什么时候上 WinSCP .NET项目要求断点续传可靠性高或者对端 FTP 服务器返回格式很怪、目录同步业务比较复杂时。WinSCP .NET 对底层协议封装得更完整Session对象一次连接可以执行多步操作性能也更好。WebClient我一般只用来做快速验证比如临时拉个文件看一下内容。真要拿它做生产功能连删除文件都要再发一次额外请求列目录更没有现成接口业务一复杂就寸步难行。所以这篇文章的源码全部基于FtpWebRequest展开。2. 测试环境Windows 自带 IIS FTP 和 FileZilla Server怎么选2.1 用 Windows IIS FTP 搭一个 5 分钟可用的测试环境你在公司里想练手不一定需要找一台现成的 Linux FTP 服务器Windows 自带功能就能搭。步骤很简单打开“控制面板” - “程序和功能” - “启用或关闭 Windows 功能”。勾选“Internet Information Services”下的“FTP 服务器”以及“Web 管理工具”里的“IIS 管理控制台”。安装完成后打开 IIS 管理器在左侧站点节点上右键 - “添加 FTP 站点”。填一个站点名称物理路径选一个本地目录比如D:\FtpRoot。绑定 IP 地址和端口默认 21SSL 选项选“无 SSL”身份验证勾选“基本”授权规则里允许指定用户或匿名用户权限勾选“读取”和“写入”。默认情况下新站点可能会允许匿名访问这对本地开发调试确实省事但生产环境一定记得关掉匿名改成强密码账号。匿名访问意味着任何能连到你服务器的人都能直接读写文件这不是吓唬你历史上很多 FTP 泄露事件就是这么发生的。2.2 FileZilla Server 配置里的三个关键点如果你的目标设备是 Linux 服务器、NAS或者客户现场不让动 IIS那我更推荐用 FileZilla Server 做测试环境。FileZilla Server 安装完成后有三个点必须注意监听端口默认 21除非跟其他服务冲突否则不用改。被动模式端口范围进入Edit - Settings - Passive mode settings配置一段区间比如50000-50100。这段端口范围要同时加进服务器防火墙白名单否则客户端能连上控制连接但数据连接会一直超时。用户目录权限给测试账号指定D:\FtpRoot作为主目录然后给upload、backup、pub这些子目录分别配置读写权限。这个配置直接影响后面代码调试时遇到的各种 550 权限问题。2.3 建议的测试目录结构我习惯在 FTP 根目录下建这样几个目录upload模拟客户端上传区权限设为可读可写。download模拟服务端下发文件区客户端从这个目录下载。backup模拟已经处理完的备份文件用于测试清理过期文件逻辑。用这套结构你可以把后面的章节提到的方法挨个跑一遍不用跑到生产环境去试错。很多界面上的细节比如“为什么他能看见上一级目录但点进子目录提示没有权限”在本地多配几个不同权限账号就能直观复现。3. 目录操作源码创建目录、文件查询、递归列出所有子目录3.1 创建目录前先判断目录是否存在FtpWebRequest创建目录对应 FTP 命令MKD但问题在于如果目录已经存在服务器会返回 550。所以很多人在批量上传前都会先调用创建目录结果第二回运行就报错。更稳妥的做法是先判断目录是否存在不存在再创建。判断是否存在可以用ListDirectory或者GetFileSize我实际项目中用的方式是把目标路径拆开逐级判断。Imports System.Net Public Class FtpDirectoryHelper Private _host As String Private _userName As String Private _password As String Public Sub New(host As String, userName As String, password As String) _host host.TrimEnd(/c) _userName userName _password password End Sub Public Function DirectoryExists(remoteDir As String) As Boolean Dim request As FtpWebRequest CreateRequest(remoteDir, WebRequestMethods.Ftp.ListDirectory) Try Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) Return True End Using Catch ex As WebException Dim response As FtpWebResponse CType(ex.Response, FtpWebResponse) If response IsNot Nothing AndAlso response.StatusCode FtpStatusCode.ActionNotTakenFileUnavailable Then Return False End If Throw End Try End Function Public Sub CreateDirectory(remoteDir As String) If DirectoryExists(remoteDir) Then Return Dim request As FtpWebRequest CreateRequest(remoteDir, WebRequestMethods.Ftp.MakeDirectory) Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) 257 表示目录创建成功 If response.StatusCode FtpStatusCode.PathnameCreated Then Throw New IOException($创建目录失败{response.StatusDescription}) End If End Using End Sub Private Function CreateRequest(uri As String, method As String) As FtpWebRequest Dim request As FtpWebRequest CType(WebRequest.Create(uri), FtpWebRequest) request.Credentials New NetworkCredential(_userName, _password) request.Method method request.KeepAlive False request.UseBinary True request.UsePassive True request.Proxy Nothing Return request End Function End Class关键点在于CreateRequest里设置了Proxy Nothing。这一点特别容易被忽略。开发机如果开着系统代理FtpWebRequest会尝试用代理去访问内网 FTP轻则慢重则直接超时。做内网工具时一律禁掉代理最省心。3.2 用 ListDirectory 还是 ListDirectoryDetails决定了你能拿到多少信息WebRequestMethods.Ftp.ListDirectory对应 FTP 命令NLST返回的只是文件名列表比如readme.txt report_20250201.pdf uploadWebRequestMethods.Ftp.ListDirectoryDetails对应 FTP 命令LIST返回的是带权限、大小、日期、文件类型的完整信息。像下面这样-rw-r--r-- 1 owner group 1024 Jan 01 10:00 readme.txt drwxr-xr-x 2 owner group 4096 Jan 01 10:05 upload如果你只是列出“有什么”用NLST就够了。但如果你想判断某个条目是文件还是目录必须用LIST因为NLST压根不区分类型。我封装了一个获取文件列表的方法直接展示详细信息Public Function ListEntries(remoteDir As String) As List(Of FtpEntry) Dim result As New List(Of FtpEntry)() Dim request As FtpWebRequest CreateRequest(remoteDir, WebRequestMethods.Ftp.ListDirectoryDetails) Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) Using reader As New IO.StreamReader(response.GetResponseStream()) While Not reader.EndOfStream Dim line As String reader.ReadLine() If String.IsNullOrWhiteSpace(line) Then Continue While Dim entry As FtpEntry TryParseListLine(line) If entry IsNot Nothing Then entry.FullName remoteDir.TrimEnd(/c) / entry.Name result.Add(entry) End If End While End Using End Using Return result End Function3.3 LIST 输出解析兼容 Unix 和 Windows 两种格式LIST输出格式不是标准化到无歧义的FTP 服务器不同返回的格式也不同。我实际遇到最多的是两种Unix 风格-rw-r--r-- 1 owner group 1024 Jan 01 10:00 readme.txtWindows IIS 风格02-15-24 03:45PM DIR backup 02-15-24 03:45PM 102400 report.pdf我写了一个简单的解析器兼容这两种Public Class FtpEntry Public Property Name As String Public Property FullName As String Public Property IsDirectory As Boolean Public Property Length As Long Public Property ModifiedTime As DateTime? End Class Private Function TryParseListLine(line As String) As FtpEntry Dim entry As New FtpEntry() 处理 Windows 格式 If line.Contains(DIR) Then entry.IsDirectory True entry.Name line.Substring(line.IndexOf() 1).Trim() Try entry.Length Long.Parse(line.Substring(20, 15).Trim(), Globalization.CultureInfo.InvariantCulture) Catch End Try Return entry End If 处理 Unix 格式 If line.Length 2 Then Return Nothing Dim typeChar As Char line(0) If typeChar -c Then entry.IsDirectory False ElseIf typeChar dc Then entry.IsDirectory True Else Return Nothing 特殊行例如 total 12 End If Dim parts As String() line.Split(New Char() { c}, StringSplitOptions.RemoveEmptyEntries) If parts.Length 9 Then Return Nothing Try entry.Length Long.Parse(parts(4), Globalization.CultureInfo.InvariantCulture) Catch entry.Length 0 End Try 名字可能含空格所以从第 8 个下标开始一直拼到结尾 entry.Name String.Join( , parts, 8, parts.Length - 8) 尝试解析修改时间 Try Dim monthMap As New Dictionary(Of String, Integer) From { {Jan, 1}, {Feb, 2}, {Mar, 3}, {Apr, 4}, {May, 5}, {Jun, 6}, {Jul, 7}, {Aug, 8}, {Sep, 9}, {Oct, 10}, {Nov, 11}, {Dec, 12} } Dim year As Integer DateTime.Now.Year Dim month As Integer monthMap(parts(5)) Dim day As Integer Integer.Parse(parts(6)) Dim timeParts As String() parts(7).Split(:c) Dim hour As Integer 0 Dim minute As Integer 0 If timeParts.Length 2 Then hour Integer.Parse(timeParts(0)) minute Integer.Parse(timeParts(1)) End If entry.ModifiedTime New DateTime(year, month, day, hour, minute, 0) Catch entry.ModifiedTime Nothing End Try Return entry End Function这段解析代码不是完整的生产级实现比如 Unix 格式里日期部分可能是Sep 21 2023这种带年份的情况我为了节省篇幅没处理。真实项目里建议把ModifiedTime解析失败的情况兜底成Nothing不要因为一次解析异常就让整个目录遍历崩掉。3.4 递归列出整个 FTP 目录树跟本地文件系统一样FTP 服务器也会有多层目录。我在项目里用栈的方式做深度优先遍历避免递归调用层级过深时爆栈Public Function EnumerateAllEntries(rootUri As String) As List(Of FtpEntry) Dim result As New List(Of FtpEntry)() Dim stack As New Stack(Of String)() stack.Push(rootUri) While stack.Count 0 Dim current As String stack.Pop() Dim entries As List(Of FtpEntry) ListEntries(current) For Each entry In entries result.Add(entry) If entry.IsDirectory Then stack.Push(entry.FullName) End If Next End While Return result End Function这段代码会拿到整棵目录树的所有文件和文件夹。你可以在里面根据IsDirectory和Name继续做过滤。比如找出所有 PDF 文件、找到所有空目录等。有个细节某些 FTP 服务器在返回子目录列表时会在目录名后面带/.和/..这样的隐藏项。比如drwxr-xr-x 2 owner group 4096 Jan 01 10:05 ./upload drwxr-xr-x 2 owner group 4096 Jan 01 10:05 ../upload遇到这种情况递归时要把.和..过滤掉否则会死循环。我一般在ListEntries里加一行If entry.Name . OrElse entry.Name .. Then Continue While这段过滤在真实调试中救过我不少次。4. 文件传输源码上传、下载、断点续传4.1 上传文件的标准姿势上传文件对应 FTP 命令STOR在FtpWebRequest里对应WebRequestMethods.Ftp.UploadFile。Public Sub UploadFile(localFullPath As String, remoteFullPath As String) If Not File.Exists(localFullPath) Then Throw New FileNotFoundException($本地文件不存在{localFullPath}) End If Dim fileInfo As New FileInfo(localFullPath) Dim request As FtpWebRequest CreateRequest(remoteFullPath, WebRequestMethods.Ftp.UploadFile) request.ContentLength fileInfo.Length request.UseBinary True request.UsePassive True Using fileStream As New IO.FileStream(localFullPath, IO.FileMode.Open, IO.FileAccess.Read) Using requestStream As IO.Stream request.GetRequestStream() fileStream.CopyTo(requestStream) End Using End Using Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) If response.StatusCode FtpStatusCode.ClosingData Then Throw New IOException($上传失败服务器返回{response.StatusDescription}) End If End Using End Sub这里有两个重要细节第一ContentLength一定要设置成本地文件长度。如果不设置某些 FTP 服务器会一直等待数据流直到超时或者干脆认为传输尚未开始。第二UseBinary True必须设置。FTP 协议有两种传输模式ASCII 和 Binary。如果不设置为二进制传输过程中服务器端或客户端会把\n转成\r\n图片、压缩包、exe 这类二进制文件会直接损坏。这一点是很多文件传上去打不开的首因。fileStream.CopyTo(requestStream)是 .NET 4.0 以后的写法底层自动分块复制。如果是老项目还在用 .NET 2.0/3.5就手动写一个byte(8191)缓冲区循环读。4.2 下载文件与超时控制下载文件对应RETR命令。Public Sub DownloadFile(remoteFullPath As String, localFullPath As String) Dim localDir As String IO.Path.GetDirectoryName(localFullPath) If Not String.IsNullOrEmpty(localDir) Then IO.Directory.CreateDirectory(localDir) End If Dim request As FtpWebRequest CreateRequest(remoteFullPath, WebRequestMethods.Ftp.DownloadFile) request.UseBinary True Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) Using inputStream As IO.Stream response.GetResponseStream() Using fileStream As New IO.FileStream(localFullPath, IO.FileMode.Create, IO.FileAccess.Write) inputStream.CopyTo(fileStream) End Using End Using End Using End Sub对于下载我建议专门设置ReadWriteTimeout。Timeout控制的是建立连接等控制命令的等待时间ReadWriteTimeout控制的是从网络流读数据的等待时间。大文件下载时如果网络抖动一下默认的 5 分钟可能不够但完全不设也不行万一服务器挂死程序会卡住很久。我通常这样设置request.Timeout 30000 建立连接/命令响应超时 30 秒 request.ReadWriteTimeout 120000 数据读写超时 2 分钟不要迷信超时越大越好要结合文件大小和网络带宽推算合理时间。比如要下载 1GB 文件带宽 10Mbps理论需要 800 秒ReadWriteTimeout设 2 分钟肯定不够。这种大文件场景最好自己做分块下载或者用第三方库的断点续传能力。4.3 断点续传ContentOffset 与 AppendFile 的区别断点续传是文件传输里最实用的功能。FTP 协议层有两种实现思路APPE命令直接把数据追加到服务器文件的末尾。REST命令先告诉服务器“我从偏移量 N 继续”再发STOR或RETR。在FtpWebRequest里WebRequestMethods.Ftp.AppendFile对应APPE适合上传续传FtpWebRequest.ContentOffset属性则通过REST命令实现下载续传。下面是我在项目里用过的上传续传写法Public Sub UploadFileWithAppend(localFullPath As String, remoteFullPath As String) Dim request As FtpWebRequest CreateRequest(remoteFullPath, WebRequestMethods.Ftp.AppendFile) request.UseBinary True Using fileStream As New IO.FileStream(localFullPath, IO.FileMode.Open, IO.FileAccess.Read) Using requestStream As IO.Stream request.GetRequestStream() fileStream.CopyTo(requestStream) End Using End Using Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) If response.StatusCode FtpStatusCode.ClosingData Then Throw New IOException($追加上传失败{response.StatusDescription}) End If End Using End Sub但APPE有一个坑它是无条件追加服务器上已有的同名文件如果和本地不是同一批内容也会继续接到末尾产生脏文件。所以用APPE之前你最好先GetFileSize对比一下本地已经传了多少。下载续传的写法Public Sub DownloadFileWithResume(remoteFullPath As String, localFullPath As String) Dim targetLength As Long 0 If File.Exists(localFullPath) Then targetLength New FileInfo(localFullPath).Length End If Dim request As FtpWebRequest CreateRequest(remoteFullPath, WebRequestMethods.Ftp.DownloadFile) request.UseBinary True If targetLength 0 Then request.ContentOffset targetLength End If Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) Using inputStream As IO.Stream response.GetResponseStream() Dim mode As IO.FileMode If(targetLength 0, IO.FileMode.Append, IO.FileMode.Create) Using fileStream As New IO.FileStream(localFullPath, mode, IO.FileAccess.Write) inputStream.CopyTo(fileStream) End Using End Using End Using End Sub这个写法利用了ContentOffset属性客户端先询问服务器文件的偏移量服务器从该偏移量继续发送数据。实测下来FileZilla Server 支持得比较好但有些老旧的嵌入式 FTP 设备对REST命令支持不完整会导致下载失败或者从头开始下载。这种情况下只能用第三方库比如 WinSCP .NET它内部处理逻辑更稳健。4.4 大文件传输怎么显示进度CopyTo的写法虽然简洁但没有进度反馈。传一个几百 MB 的文件界面看起来像卡死了一样。我写过一个带进度回调的版本核心是用缓冲区循环读Public Sub DownloadFileWithProgress(remoteFullPath As String, localFullPath As String, progress As Action(Of Long, Long)) Dim request As FtpWebRequest CreateRequest(remoteFullPath, WebRequestMethods.Ftp.DownloadFile) request.UseBinary True Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) 先尝试从响应头拿文件总大小 Dim totalLength As Long response.ContentLength Using inputStream As IO.Stream response.GetResponseStream() Using fileStream As New IO.FileStream(localFullPath, IO.FileMode.Create, IO.FileAccess.Write) Dim buffer(8190) As Byte Dim readCount As Integer 0 Dim totalRead As Long 0 While True readCount inputStream.Read(buffer, 0, buffer.Length) If readCount 0 Then Exit While fileStream.Write(buffer, 0, readCount) totalRead readCount If progress IsNot Nothing AndAlso totalLength 0 Then progress(totalRead, totalLength) End If End While End Using End Using End Using End Sub注意response.ContentLength并不保证每个 FTP 服务器都会正确返回有时是 0。所以进度回调里要先判断totalLength 0否则只输出已传输字节数。上传进度也是同样的套路在读FileStream时不断更新totalUploaded / fileInfo.Length这里就不再重复。5. 远程文件管理源码删除、重命名、批量清理过期文件5.1 删除单个文件和空目录删除文件对应DELE命令删除目录对应RMD命令。Public Sub DeleteFile(remoteFullPath As String) Dim request As FtpWebRequest CreateRequest(remoteFullPath, WebRequestMethods.Ftp.DeleteFile) Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) If response.StatusCode FtpStatusCode.FileActionOK Then Throw New IOException($删除文件失败{response.StatusDescription}) End If End Using End Sub Public Sub RemoveDirectory(remoteDir As String) Dim request As FtpWebRequest CreateRequest(remoteDir, WebRequestMethods.Ftp.RemoveDirectory) Using response As FtpWebResponse CType(request.GetResponse(), FtpWebResponse) If response.StatusCode FtpStatusCode.FileActionOK Then Throw New IOException($删除目录失败{response.StatusDescription}) End If End Using End Sub这两个方法很简单但要注意RMD只能删空目录。如果目录里还有文件或者子目录服务器会返回 550。所以删除非空目录必须配合递归。5.2 递归删除整个目录我在第 3 节写过一个EnumerateAllEntries这里直接复用它。先找出目录里所有文件和子目录把所有文件删掉再按目录层级从深到浅删目录。Public Sub DeleteDirectoryRecursively(remoteDir As String) Dim entries As List(Of FtpEntry) ListEntries(remoteDir) For Each entry In entries If entry.IsDirectory Then DeleteDirectoryRecursively(entry.FullName) Else DeleteFile(entry.FullName) End If Next RemoveDirectory(remoteDir p a hrefhttps://download.csdn.net/download/m0_66238867/77521711 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表