尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Unity FTP上传全攻略:方案选型、核心代码与避坑指南

Unity FTP上传全攻略:方案选型、核心代码与避坑指南 做Unity开发时间久了你一定会遇到一个需求把本地的文件传到服务器上。可能是玩家头像、游戏截图、日志文件、存档数据也可能是一批热更资源包。我在项目里就踩过不少网络传输的坑从HTTP上传到WebSocket都试过最后在稳定性要求高、服务器资源有限的场景里FTP反而是最省心的方案之一。这篇博文就围绕Unity里的FTP上传数据展开聊聊方案选型、核心代码、实操过程中的坑以及我能给到的一些经验。不管你是刚接触Unity网络编程的新手还是已经在项目里折腾过HTTP上传的老手这篇内容都能帮你快速理清FTP在Unity里的落地方式。尤其是那些服务器端是Linux或Windows自带FTP服务的场景学会这套方案后数据上传就会变得非常可控。文章不会从头科普FTP协议的历史而是直接切入Unity开发中真正用得到的部分。1. 场景定位一个正经项目为什么需要FTP上传1.1 Unity项目里的典型数据上传场景我见过的Unity项目里FTP上传最多出现在这几类场景中用户生成内容上传比如玩家自定义头像、皮肤、关卡地图、截图分享。这类文件一般不大但数量可能很多用FTP管理目录结构非常直观。日志与崩溃报告上报App运行中产生的日志文件、崩溃dump通过FTP定期上传到指定服务器。相比HTTP接口FTP服务器端基本不需要开发运维成本很低。游戏存档与进度同步部分单机或弱联网游戏会把存档文件备份到用户自己的FTP空间比如用户配置了一个私有NAS。资源热更新小体量项目的热更包、配置文件、美术资源用FTP做分发源。服务器上只需要一个FTP服务端软件不需要额外写API。内容发布管道开发期或运营期策划配表、活动图、音频文件从本地上传到CDN或内容服务器前的暂存区。可以看到FTP很适合“原始文件同步”这一类的需求不承载复杂业务逻辑它就是老老实实把文件送过去。如果项目本身已经有完善的后端API体系那么HTTP上传可能更合理但如果你只是想快速、稳定地把文件丢到一台服务器上FTP永远是可用性最高的备选方案之一。1.2 为什么不是HTTP、不是WebSocket而是FTP聊方案选型之前先分享一个我踩过的坑有段时间我图省事直接在一个项目里用HTTP接口上传图片后端程序员也配合写了接口。结果上线后一旦并发量大一点后端Tomcat就频繁报内存溢出最后发现是文件流没处理好。后来换了一台专门的FTP文件服务器上传逻辑全部走FTP问题一下解决了。选FTP的关键理由如下服务器端零代码不管是Linux的vsftpd还是Windows的IIS FTP服务装好配置一下就能用。不需要写Controller、不需要指定存储路径天然就是为文件传输设计的。断点续传与传输队列成熟FTP协议自带REST命令可以很方便地做断点续传。Unity端只需要处理文件流的偏移量即可。异步与低资源占用FTP协议建立数据连接后文件数据直接在数据连接中传输应用服务器本身几乎没有额外开销。内网穿透与同网段自适应在局域网环境下比如工作室内部工具FTP的传输效率非常稳定不受HTTP代理、Nginx超时等因素干扰。生态成熟、排查工具多几乎所有系统都能用FTP命令行直接调试出了问题用FileZilla或者WinSCP先测一下服务器就能区分是服务端问题还是客户端问题。当然FTP也有缺点比如明文传输需要考虑SFTP/FTPS、主动/被动模式带来的防火墙问题、以及复杂目录权限管理不够灵活。但从Unity客户端角度来说这些都有成熟的应对手段后面会细讲。2. 三种主流实现方案从系统API到第三方库2.1 方案一C#内置FtpWebRequest最推荐Unity基于Mono或IL2CPP运行其C#运行时对.NET框架类库有较好兼容性。System.Net.FtpWebRequest就是.NET类库中用于FTP操作的专用类不需要安装任何第三方包Unity 2018及以上版本都能直接用。这个方案的优势纯C#实现不需要额外插件跨平台能力好Windows、macOS、Linux、Android、iOS都能跑。开发者对连接、认证、传输过程的可控性高。配合async/await或协程可以做到流畅的异步上传。用法上核心就是四个步骤创建一个指向FTP地址的FtpWebRequest对象。设置Method为WebRequestMethods.Ftp.UploadFile。带上用户名密码等凭据。把文件流写入GetRequestStream()等待响应。后面我会把完整代码放出来这里先建立整体印象。2.2 方案二UnityWebRequest配合FTP部分场景可用Unity引擎自带的UnityWebRequest是一个上层API一般用于HTTP/HTTPS。但它也实现了一部分FTP的PUT语义可以发送PUT请求到FTP地址。我当时为了少写代码试过用UnityWebRequest.Put()传文件到FTP结果发现两个限制不支持自定义FTP命令比如获取文件列表、重命名、删除远端文件都得另想办法。对于大文件上传UnityWebRequest的内存占用较高因为它内部缓冲了整个上传数据。所以这个方案适合那种“临时传个小文件”的轻量场景不适合做成正式的上传组件。2.3 方案三第三方FTP库如果你需要的功能比较多比如目录递归上传、断点续传、SFTP支持、进度事件齐全那可以考虑第三方库。我常用的第三方库有两个库名特点适用场景FluentFTP功能全面支持SFTP/FTPS/FTP进度事件完善文档丰富正式项目功能要求高SSH.NET主要做SFTP支持密钥登录安全要求高的项目WinSCP .NET库封装了WinSCP命令Windows平台稳定只在Windows上运行的编辑器工具如果项目只需要普通FTP上传用FluentFTP就够了。它的NuGet包可以直接导入Unity需要自己处理DLL依赖API设计也比FtpWebRequest顺手得多。不过我的建议是先学会方案一因为FtpWebRequest不依赖任何外部DLL出问题了你还能从底层排查用第三方库一旦和Unity的IL2CPP兼容性出问题调试成本反而更高。3. 实操全流程从搭建测试服务器到写出上传组件3.1 搭建一个本地FTP测试服务器正式写Unity代码之前强烈建议你在本机先搭一个FTP服务器这样开发调试才能闭环。我平时最常用的两种方式方式一Windows IIS FTP服务Windows系统自带IIS FTP图形化界面配置很方便。步骤是控制面板 - 启用或关闭Windows功能 - 勾选“Internet Information Services”下的“FTP服务器”。用IIS管理器新建FTP站点指定物理目录。设置SSL策略为“允许”绑定本机IP。设置身份验证为“基本”授权用户选一个Windows账号。方式二Linux vsftpd如果你用的是Mac或者Linux开发环境vsftpd是轻量级FTP服务端的首选。Ubuntu上的安装步骤大致是sudo apt update sudo apt install vsftpd sudo systemctl start vsftp # 配置文件位置 /etc/vsftpd.conf需要重点确认的配置项anonymous_enableNO local_enableYES write_enableYES local_umask022 # 为了让Unity客户端能被动模式连接还要加上 pasv_enableYES pasv_min_port30000 pasv_max_port31000测试的时候就创建一个登录账号在浏览器或FileZilla里连一次确认能正常上传下载。服务器端通了后面Unity端写代码就不用再怀疑是环境问题了。3.2 一个可复用的FtpUploader组件下面是核心代码。我先给一个简版再去优化异步、进度和错误处理。完整组件我放在项目里用过的版本注释写得比较详细你可以直接复制参考。using System; using System.IO; using System.Net; using System.Threading.Tasks; using UnityEngine; /// summary /// Unity FTP 上传组件FtpWebRequest 方案 /// 支持异步上传、取消、进度回调 /// /summary public class FtpUploader : MonoBehaviour { [Header(FTP服务器配置)] public string ftpHost 192.168.1.100; public string ftpPort 21; public string ftpUserName unity; public string ftpPassword 123456; public bool usePassive true; // 被动模式默认开启 public bool useBinary true; // 二进制传输 public bool enableSsl false; // FTPS一般内网测试不用 /// summary /// 上传文件异步 /// /summary /// param namelocalFilePath本地文件完整路径/param /// param nameremoteRelativePath远程相对路径如 upload/avatar.png/param /// param nameonProgress进度回调 [0,1]/param /// param nameonComplete完成回调参数为是否成功和错误信息/param public async Task UploadFileAsync( string localFilePath, string remoteRelativePath, Actionfloat onProgress null, Actionbool, string onComplete null) { try { // 1. 参数校验 if (!File.Exists(localFilePath)) { onComplete?.Invoke(false, 本地文件不存在: localFilePath); return; } string remoteUrl BuildFtpUrl(remoteRelativePath); Debug.Log([FtpUploader] 开始上传: remoteUrl); // 2. 创建FTP请求 FtpWebRequest request (FtpWebRequest)WebRequest.Create(remoteUrl); request.Method WebRequestMethods.Ftp.UploadFile; request.Credentials new NetworkCredential(ftpUserName, ftpPassword); // ASCII/Binary模式切换 request.UseBinary useBinary; // 被动模式客户端发起数据连接不容易被防火墙拦截 request.UsePassive usePassive; // 上传完成后不保持连接 request.KeepAlive false; // 超时时间连接超时15秒 request.Timeout 15000; request.ReadWriteTimeout 30000; // 3. 打开本地文件流写入请求流 using (FileStream fs new FileStream(localFilePath, FileMode.Open, FileAccess.Read)) { // 获取总字节数 long totalBytes fs.Length; request.ContentLength totalBytes; using (Stream requestStream request.GetRequestStream()) { byte[] buffer new byte[8192]; int bytesRead 0; long uploadedBytes 0; while ((bytesRead fs.Read(buffer, 0, buffer.Length)) 0) { await requestStream.WriteAsync(buffer, 0, bytesRead); uploadedBytes bytesRead; // 计算进度 float progress (float)uploadedBytes / totalBytes; onProgress?.Invoke(progress); } } } // 4. 获取响应并检查状态 using (FtpWebResponse response (FtpWebResponse)request.GetResponse()) { Debug.Log($[FtpUploader] 上传成功服务器返回: {response.StatusDescription}); onComplete?.Invoke(true, response.StatusDescription); } } catch (WebException ex) { HandleWebException(ex, onComplete); } catch (Exception ex) { Debug.LogError([FtpUploader] 上传异常: ex.Message); onComplete?.Invoke(false, ex.Message); } } private string BuildFtpUrl(string remoteRelativePath) { // 规范路径避免反斜杠 string normalizedPath remoteRelativePath.Replace(\\, /); // 去掉开头的斜杠 normalizedPath normalizedPath.TrimStart(/); return $ftp://{ftpHost}:{ftpPort}/{normalizedPath}; } private void HandleWebException(WebException ex, Actionbool, string onComplete) { if (ex.Response is FtpWebResponse ftpResp) { string statusDesc ftpResp.StatusDescription; Debug.LogError($[FtpUploader] FTP错误: {(int)ftpResp.StatusCode} - {statusDesc}); onComplete?.Invoke(false, $FTP错误码: {(int)ftpResp.StatusCode} - {statusDesc}); } else { Debug.LogError([FtpUploader] 网络异常: ex.Message); onComplete?.Invoke(false, 网络异常: ex.Message); } } }这段代码有几个细节要专门说明ContentLength必须设置如果不设置部分FTP服务器会报错或者无法正确处理上传流。这和HTTP上传不一样FTP协议需要知道文件大小。WriteAsync比Write好使用异步写入可以避免在Unity主线程上阻塞配合await不会卡UI。UsePassive要设为true默认情况下FtpWebRequest.UsePassive是true但如果你在局域网里折腾过可能会被改成false结果怎么都连不上。被动模式是客户端主动去连服务器的数据端口这个对NAT、防火墙都更友好。KeepAlive置为false每次上传完就关闭连接防止连接池占用过多文件句柄。如果你连续上传多个文件保持KeepAlivetrue能省下重新握手的时间但会占用端口资源看场景取舍。3.3 在MonoBehaviour里调用上传组件写好之后在场景里挂一个空物体把服务器参数填好然后在任意脚本里调用public class TestFtpUpload : MonoBehaviour { public FtpUploader uploader; void Start() { // 示例上传一个截图 string filePath Application.persistentDataPath /screenshot.png; ScreenCapture.CaptureScreenshot(filePath); uploader.UploadFileAsync( filePath, gameData/screenshot_001.png, progress Debug.Log($上传进度: {progress:P0}), (success, msg) { if (success) { Debug.Log(上传完成); } else { Debug.LogError(上传失败: msg); } } ); } }这里再提一个Unity特有的细节Application.persistentDataPath在Android和iOS上就是应用沙盒目录开发者不需要申请存储权限也可以读写。如果是从相册选图上传走的是另外一套原生产品流程不在本文范围内。4. 进阶优化大文件上传、进度事件与断点续传4.1 大文件上传时最容易犯的错一次性读入内存很多教程会让你这样做byte[] fileData File.ReadAllBytes(localFilePath); request.ContentLength fileData.Length; requestStream.Write(fileData, 0, fileData.Length);这段代码在文件只有几MB时没问题但一旦文件超过100MBUnity进程内存直接飙升移动端很容易闪退。所以第三部分的代码里我用的是FileStream循环分块写入每次只读8KB这个Buffer大小你可以调8KB16KB适合网络环境一般的情况每块传输时间短进度流畅。64KB256KB适合内网高速传输减少多次Write的系统调用开销但进度回调会稍显迟钝。1MB以上不推荐因为一次分配1MB以上的byte[]在Mono的GC堆里很容易触发频繁垃圾回收。我内网实测过64KB的Buffer比8KB整体快约20%~30%但CPU占用也略高。移动端网络没这么快用16KB就比较稳。4.2 进度的坑不要每帧刷新UI代码里onProgress回调是每写一个Buffer触发一次。如果Buffer是8KB一个10MB的文件会触发1280次回调。如果这些回调里直接去更新UGUI的Text或Slider主线程压力很大。推荐做法是在回调里只存一个字段然后每帧或每0.1秒去UI上刷新一次。private float _uploadProgress; async void OnUploadButtonClicked() { _uploadProgress 0f; string filePath Path.Combine(Application.persistentDataPath, bigfile.zip); await uploader.UploadFileAsync(filePath, upload/bigfile.zip, progress _uploadProgress progress, (ok, msg) Debug.Log(ok ? 完成 : msg)); } void Update() { progressSlider.value _uploadProgress; progressText.text (_uploadProgress * 100f).ToString(F1) %; }这样的方案既能实时显示进度又不会因为回调频率过高拖慢帧率。4.3 断点续传的实现思路断点续传是FTP协议的一大优势对应命令是REST。在.NET的FtpWebRequest中写法是这样的request.Method WebRequestMethods.Ftp.UploadFile; // 关键设置内容偏移量 request.ContentOffset uploadedBytes; // 从上次断点继续使用ContentOffset时要注意以下事项服务器端必须支持REST命令。vsftpd默认支持Windows IIS FTP也支持。你需要先获取远端文件的大小。可以发送GetFileSize请求或者解析List响应。这样才知道从哪个字节继续。断点续传不能和ContentLength混用。你只需要把本地文件流的位置Seek到偏移量然后写剩余部分即可。配合本地记录上传进度PlayerPrefs或文件这样一次大文件上传断网后重连可以从断点继续不用重新传。我做过一个视频文件上传工具配合这个方案几百MB的文件在弱网下也能慢慢传完。4.4 多个文件的上传队列如果场景是批量上传比如一键上传10张截图直接开10个UploadFileAsync协程会把服务器打满也会导致FTP服务器拒绝连接。正确做法是用队列挨个上传。我一般会写一个FtpUploadQueue类public class FtpUploadQueue { private QueueKeyValuePairstring, string _queue new QueueKeyValuePairstring, string(); private FtpUploader _uploader; private bool _isUploading false; public void Enqueue(string localPath, string remotePath) { _queue.Enqueue(new KeyValuePairstring, string(localPath, remotePath)); TryProcessNext(); } private async void TryProcessNext() { if (_isUploading) return; if (_queue.Count 0) return; _isUploading true; var item _queue.Dequeue(); await _uploader.UploadFileAsync(item.Key, item.Value, null, (ok, msg) { Debug.Log($队列上传{(ok ? 成功 : 失败)}: {item.Key} - {msg}); _isUploading false; TryProcessNext(); }); } }这样既控制了并发数又能保证文件的先后顺序。我在日志上传需求里就是这样做的一天几千个文件稳定上传没出过问题。5. 避坑手册Unity FTP上传的常见问题与排查5.1 连接超时或无法连接最常见的报错是Unable to connect to the remote server。排查看这几个地方服务器地址/端口对不对ftp://192.168.1.100:21端口号默认是21有些服务器起在2121确认一下。服务器服务是否启动Linux上用ps -ef | grep vsftpdWindows上看服务管理器里的FTP服务。防火墙拦截21端口是控制连接被动模式下数据端口是30000-31000区间以vsftpd配置为准都需要放行。Unity编辑器所在机器能不能FTP连通先用FileZilla或命令行试一下。5.2 中文文件名乱码FTP协议默认传输文件名一般按UTF-8或本地编码。Windows IIS FTP服务器默认使用系统本地编码比如GBK而Unity这边传中文路径时默认UTF-8两边对不上就乱码。处理办法服务器端尽量用vsftpd同时配置utf8_filesystemYES。客户端方面在创建FtpWebRequest后设置request.Headers.Add(HttpRequestHeader.AcceptCharset, utf-8);但不能保证所有服务器都支持。更稳妥的办法是上传文件时用英文/拼音文件名文件名映射关系存在本地数据库或清单文件里。我在多个项目里都采用的这个思路彻底避开乱码问题。5.3 认证失败530 Login incorrect用户名密码都对但登录失败大概率是以下原因服务器账号主目录权限没给对。Linux上要确保账号对目标目录有写权限比如chown -R ftpuser:ftpuser /var/ftp/uploads。Windows IIS FTP里授权规则没加“写入”权限。有些FTP服务器要求用“域名\用户名”格式比如localhost\username而不是单纯的username。另外一点容易被忽略Unity的NetworkCredential类在跨平台时如果传的是空字符串密码有些服务器会直接拒绝。空密码场景建议直接用匿名账号逻辑。5.4 主动模式和被动模式导致的卡死我用FtpWebRequest时遇到过一种诡异情况上传小文件没问题上传大文件到一半卡死。后来发现是服务器在主动模式下回连客户端的数据端口时被防火墙拦了。解决办法很简单把UsePassive硬编码为true。被动模式下是客户端去连接服务器的端口防火墙只需要放行服务器侧的端口就行。5.5 上传成功但服务器上文件大小为0这个坑非常经典。我在测试时一不小心就踩到request.ContentLength 0; // 忘了设置导致服务器不知道要接收多少字节有些FTP服务器就会直接当成0字节文件处理。所以ContentLength一定要设置成文件流的长度别偷懒。另一个原因是没有把GetRequestStream()里写入的数据Flush()或没等数据写完就调用了GetResponse()。用using语句块包裹requestStream会在离开作用域时自动Flush和Close这是最安全的方式。5.6 FTPS/TLS相关的警告热词里有一条warning: ftp over tls is not enabled, users cannot securely log in.这是vsftpd的提示。如果服务器配置了ssl_enableYES但Unity客户端没启用FTPS就会报TLS相关错误。如果你的服务器支持FTPSUnity端可以在FtpWebRequest上启用request.EnableSsl true;但注意非加密FTP是明文传密码的正式环境如果数据敏感还是优先考虑FTPS或SFTP。Unity对SFTP没有内置支持需要引入SSH.NET等库。5.7 上传过程中断线怎么办移动端网络切换、Wi-Fi休眠、弱网抖动都可能导致上传中断。除了做断点续传还可以在业务层加一个“重试机制”断线后延迟3~5秒重试。连续重试3次失败后把文件路径记录到待重传列表等下次App启动再传。超时时间别设太长连接超时15秒足够读写超时30秒。这样设计之后用户在网络环境差的时候也能有比较好的体验。6. 关于工具链与生态的扩展思考写到这里FTP上传的核心内容基本讲完了。不过有几个周边工具和建议我觉得值得一并分享因为它们在实际项目里也经常要配合使用。6.1 用FTP客户端做联调无论服务端是vsftpd还是Windows IIS FTP建议你在开发机上常备一个FileZilla客户端。它能查看服务器目录结构、测试用户权限、手动上传下载文件Unity代码写好后直接用FileZilla验证服务器返回的目录结构和文件名能快速定位问题在哪一端。6.2 数据安全推荐AES加密后再上传在公开场合用FTP传敏感数据我强烈建议先在Unity端做AES加密之前热词里也提到过Unity AES加密GCM模式得到一个加密后的临时文件再上传。这样即便FTP账号泄露攻击者看到的也是一堆密文安全性会好很多。虽然多了一步加解密开销但配合异步流处理性能影响很小。6.3 和“Unity安装与开发环境”的关联如果你刚从零起步可能连Unity环境都还没装好。Unity Hub管理多个Unity版本很方便FTP上传组件不依赖Unity版本但不同版本对应的.NET API级别会有些差异。Unity 2020及以上版本默认支持netstandard2.1FtpWebRequest这个类在netstandard2.0里就有所以基本不用担心兼容性。6.4 嵌入式或物联网场景如果你看过热词里的“单片机FTP协议”、“S7-200 SMART通过MQTT协议上传数据”会发现FTP上传并不仅仅是游戏开发里才用。很多嵌入式设备会把采集到的数据文件CSV、日志通过FTP上传到网关或服务器。Unity常用于这些设备的可视化上位机上位机从设备拿数据、再整合上传一套FTP逻辑其实是完全可行的。我做过一个类似的工控上位机项目Unity侧直接从PLC的OPC UA服务把数据读出来生成CSV再通过FTP传到远程服务器整个链路跑得很稳。6.5 上传后的远端处理FTP只负责把文件送到不负责后续处理。文件上传到服务器后可以结合一些ETL任务、定时任务去转发、入库、分发。比如你上传一个配置文件到特定目录服务器上有个监控脚本一旦发现新文件就自动解析这就形成了简单的自动化流程。7. 我踩过的几个坑最后说几句自己的体会。第一个坑是ContentLength。第一次写FTP上传时照着一篇博客抄的对方没设置这个字段我也没设置。小文件没感觉后来传一个几百MB的压缩包服务器上文件大小永远只有0KB。当时排查了很久打开FTP日志才明白服务器一直在等EOF但客户端又没告诉它总长度。从那以后我每次都显式设置Length。第二个坑是移动端切后台。用协程或await做上传时用户一旦把App切到后台Unity的协程默认不会继续跑。iOS尤其明显。如果上传必须执行完需要调用Application.runInBackground或使用原生的后台任务接口。不过通常我们只是上传日志不太需要保证这一点但你要知道有这个问题。第三个坑是服务器目录权限。我在Linux服务器上配vsftpd时经常会忘记给FTP用户对上传目录的写权限结果用FileZilla测试没问题因为FileZilla用的是管理员账号登录但Unity用只读账号登录后全部报权限错误。所以联调时尽量用一个权限受限的测试账号这样能提前暴露权限问题。总的来说Unity里做FTP上传并不复杂核心就是FtpWebRequest的封装、文件流的读写、异步处理以及服务器端的配置。这一套东西用熟了无论是做游戏客户端、AR展示工具还是工控上位机都能很快落地。如果你打算在项目里正式使用建议你用一个独立的管理类来封装所有FTP操作不要在每个页面里散落连接逻辑维护成本会低很多。
返回列表