做Unity客户端开发,总会碰到一些看起来简单、真上手才发现到处是坑的需求。比如把玩家生成的截图、存档、日志文件传到服务器上。我最早碰到这个需求,是在给一个游戏项目做素材上传功能,当时后端接口还没埋好,运维团队丢过来一个FTP地址说“你先传到这儿,我们后续自己拉”。于是我在Unity里折腾了一轮FTP上传,发现网上资料散得很,能直接跑通的例子不多。这篇文章就围绕Unity网络基础里的FTP上传数据展开,包括协议原理、Unity可用的实现方案、能直接复制的最小代码、异步上传与进度反馈、常见报错排查,以及凭据保护与安全加固。适合同样被这个需求卡住的客户端开发者,独立开发者也可以直接拿去搭工具链。
1. 思路拆解:Unity里为什么绕不开FTP
1.1 我的应用场景
先说说我当时的具体场景,方便你判断这套方案适不适合自己。项目是一个面向移动端的内容社区类应用,玩家会生成一批本地素材文件,比如截图、语音包和自定义皮肤压缩包,需要回传到服务器做审核和展示。按常规思路,后端给几个HTTP接口,客户端用UnityWebRequest POST过去就行了。但现实是服务端由运维团队托管在一台老机器上,没有开发HTTP上传接口的排期,只有一台现成的FTP服务器,目录权限都开好了。所以客户端这边的任务就变成:走FTP,把文件从手机存储传到服务器的指定目录里。
类似的场景其实不少。独立开发者做本地工具,比如关卡编辑器导出资源包后一键传到团队成员共享的NAS;项目组内部做自动化测试,测试报告需要回传到一个固定FTP仓库;甚至有的智能硬件Demo,用Unity做上位机界面,设备生成的数据文件也要走FTP归档。只要服务器端没有改成现代Web框架,FTP仍然是最稳的“最后一百米”。
1.2 为什么不用HTTP和SFTP?
这是选型时第一个要回答的问题。Unity自带的UnityWebRequest原生支持HTTP和HTTPS,但不支持FTP协议,所以走FTP必须自己写或者引第三方库。那为什么不干脆让服务器加个HTTP接口?如果服务端是你能说了算的,当然HTTP更好,UnityWebRequest封装的下载上传、断点续传、进度回调都成熟,还有HTTPS加密。问题是很多场景下服务器端不在你的掌控里,或者只是临时过渡方案,加接口要排期、要改安全组规则,FTP服务器现成可用。
为什么不直接用SFTP?SFTP走SSH通道,安全性比FTP高不少,.NET的SSH.NET库在Unity里也能跑。但SFTP要求服务端安装并启用SSH服务,公司和云主机默认配置里,SSH端口经常被安全组限制,开通流程比FTP慢。另外运维团队给的凭证往往就是FTP账号。在低敏感场景下,先跑通FTP上传,后续再升级FTPS或者换SFTP,是成本最低的路径。
选型这里还有一个容易踩的坑:FTP和SFTP是两种不同协议,FTP没有加密、SFTP加密,端口也不同。客户端别因为“sftp”和“ftp”长得像就把地址混用,否则会得到一堆莫名其妙的握手失败报错。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. FTP协议核心机制与基础环境准备
2.1 控制连接和数据连接
FTP和HTTP最大的区别在于,它不是一个“一次请求一次响应”的简单协议,而是用两条连接完成一次传输。第一条是控制连接,走21端口,用来发命令、收状态码;第二条是数据连接,端口和方向不固定,用来传输文件内容。理解这一点,对后面排查各种坑会很有帮助。
数据连接的建立分两种模式。主动模式下,客户端告诉服务器“我开了一个端口等你来连”,服务器主动连客户端的这个端口;被动模式下,服务器返回一个IP和端口,客户端主动去连。这个差别在Unity移动端是致命的:手机在运营商NAT后面,主动模式下服务器根本连不进手机内部端口;在办公网里,客户端电脑的防火墙也会拦截服务器主动发起的连接。所以Unity客户端里应该统一使用被动模式,也就是FTP被动端口段需要在服务器防火墙放行。
FtpWebRequest的UsePassive属性就是干这个的,通常设成true。如果遇到“连接已建立但传输卡住”的情况,十有八九是被动模式端口段没开,或者服务器返回了内网地址客户端连不上,这类问题我会在第5章详细展开。
2.2 服务端基础配置清单
客户端跑不通,很多时候问题出在服务端,所以服务端最小配置必须有数。以Linux上常见的vsftpd为例,至少要确认三件事:一是FTP服务有没有跑起来,21端口有没有监听;二是被动模式端口段有没有开放,比如vsftpd里配置pasv_min_port=30000和pasv_max_port=31000,然后防火墙和安全组都要放行这段端口;三是用户目录权限,给专用账号一个隔离目录,别把账号放在根目录上,否则客户端能翻到整个文件系统。
这里特别提醒一句,很多云服务器有“TLS警告”,比如用FtpWebRequest连vsftpd时会收到“FTP over TLS is not enabled, users cannot securely log in.”这类提示。这是vsftpd在告知客户端当前没有启用加密。如果只是传不敏感的文件,忽略也行;如果要传账号信息或用户资料,务必开启FTPS支持。安全加固的细节我放到第6章。
如果服务器是Windows,用IIS的FTP服务或者Serv-U之类也行,配置思路类似:指定物理路径、创建专用账号、开被动端口段、设置读写权限。为了方便客户端联调,我建议先手动用FileZilla或者其他FTP客户端连一次,确认可以列出目录、上传文件,再开始写Unity代码。这个步骤能帮你少排查一半以上的问题。
2.3 Unity工程环境准备
Unity里面用FTP,核心是.NET的FtpWebRequest类,它位于System.dll命名空间System.Net下,Unity的Mono和IL2CPP后端都支持。但有一点必须注意:Project Settings里Player的Api Compatibility Level要选.NET Framework或者.NET Standard 2.1,如果选的是.NET Standard 2.0,部分网络API可能缺失。FtpWebRequest在2.0里也能用,但像某些异步方法、连接管理相关功能可能不全,遇到编译报错先查这里。
另外一个平台差异是WebGL。WebGL构建运行在浏览器沙箱里,没有传统的Socket能力,FtpWebRequest在WebGL下不可用。如果你要做Unity WebGL版本,走FTP基本不可行,应该让服务器提供一个HTTP代理接口,或者用WebSocket转发。我记得之前有个项目坚持要在WebGL里用FTP,最后折腾了半天还是换成了HTTP,这个坑没必要踩。
第三方库方面,如果不想用FtpWebRequest原生类,推荐FluentFTP这个库,它支持.NET Standard,Unity里通过Package Manager引进来就能用,封装了主动/被动模式、FTPS、进度回调、断点续传,API比FtpWebRequest友好很多。我会在第4章展示FluentFTP的用法,两边对比着看,你就能选一个最适合自己项目的。
3. 核心实现:最小可用的FTP上传代码
3.1 用FtpWebRequest写一个最小上传器
先给一段直接用FtpWebRequest的最小实现。为了演示,我这里把逻辑写成一个静态方法,传入FTP地址、账户名、密码、本地文件路径,返回上传是否成功。
csharp复制using System;
using System.IO;
using System.Net;
using UnityEngine;
public static class FtpUploader
{
public static bool UploadFile(string ftpUrl, string username, string password, string localPath)
{
try
{
var request = (FtpWebRequest)WebRequest.Create(ftpUrl);
request.Method = WebRequestMethods.Ftp.UploadFile;
request.Credentials = new NetworkCredential(username, password);
request.UseBinary = true;
request.UsePassive = true;
request.KeepAlive = false;
request.Timeout = 15000;
request.ReadWriteTimeout = 10000;
var fileInfo = new FileInfo(localPath);
request.ContentLength = fileInfo.Length;
using (var fileStream = File.OpenRead(localPath))
using (var requestStream = request.GetRequestStream())
{
fileStream.CopyTo(requestStream);
}
using (var response = (FtpWebResponse)request.GetResponse())
{
Debug.Log($"FTP上传完成,状态码: {response.StatusDescription}");
return true;
}
}
catch (Exception ex)
{
Debug.LogError($"FTP上传失败: {ex.Message}");
return false;
}
}
}
调用方式很简单:
csharp复制bool ok = FtpUploader.UploadFile(
"ftp://192.168.1.100/uploads/player_20250416.jpg",
"uploader",
"your_password",
Application.persistentDataPath + "/player_20250416.jpg");
这段代码从功能上说已经能跑了。不过里面有几点值得说清楚:第一,ftpUrl必须以ftp://开头,而且最好把文件名包含在URL里,否则服务器会认为你要上传到目录而不是具体文件;第二,Credentials如果为空,FtpWebRequest会尝试匿名登录,大多数生产环境会拒绝;第三,GetRequestStream和GetResponse之间不能颠倒顺序,先写内容,后拿响应。
3.2 参数设置背后的“为什么”
不少人在网上抄到这段代码后,直接改了地址就扔进项目,一跑发现各种问题。原因在于这几个参数每个都有讲究。
request.UsePassive = true是最重要的一个。前面已经说了,移动端在NAT后面,主动模式很难连上。如果服务器不支持被动模式,或者被动端口段没开,这里会表现为超时。
request.UseBinary = true表示以二进制方式传输。如果设成false,就会走ASCII文本模式,文件在传输过程中可能被做换行符转换,导致图片、压缩包损坏。只要是上传图片、音频、视频、压缩包,必须用二进制模式,这是最基础的一条。
request.KeepAlive = false表示每次上传完成后关闭控制连接。把它设成false,是为了避免连续上传多个文件时,FtpWebRequest复用了内部连接池里的旧连接,服务器端会话过期导致“451 Requested action aborted”之类的报错。如果频繁上传,可以设成true来复用连接,但要记得显式调用request.Abort()或等待连接空闲。
request.Timeout和request.ReadWriteTimeout要分开理解。Timeout是建立连接的超时时间,单位毫秒;ReadWriteTimeout是读写流时的超时时间。传统IT系统里,这两个值经常被忽略,但在弱网环境里,连接能建立、数据传不完的情况很常见,所以建议两个都设,避免UI卡死等半天。
request.ContentLength = fileInfo.Length也很关键。如果不知道文件大小,FtpWebRequest在写流时不会报错,但某些FTP服务器会拒绝上传,或者上传后的文件变成0字节。设上ContentLength,等于提前告诉服务器“我要传这么大一坨东西”,服务器才能正确处理。
3.3 大文件上传的缓冲区设置
上面的最小代码里用了fileStream.CopyTo(requestStream),CopyTo内部默认缓冲区是81920字节,也就是80KB左右。这个值对大多数场景是合适的,但如果你的文件有几百MB甚至几GB,建议显式控制缓冲区大小,避免内存和CPU开销异常。
csharp复制using (var fileStream = File.OpenRead(localPath))
using (var requestStream = request.GetRequestStream())
{
var buffer = new byte[81920];
int bytesRead;
while ((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) > 0)
{
requestStream.Write(buffer, 0, bytesRead);
}
}
缓冲区选太大没用,选太小则会把大量时间花在系统调用上。我试过从1024字节调到81920字节,同样的文件上传时间能缩短20%以上。如果你要限制上传速度,比如怕上传占满带宽影响游戏主流程,可以在循环里加一个简单的限速逻辑,根据时间戳控制每次循环之间的间隔。这里先不展开,第6章会再提。
还有一点容易忽略:FTP服务器对单文件大小通常有限制。上传前最好先确认服务器配置,否则文件写了一半返回“550 Maximum file size exceeded”,客户端这边只拿到一个错误码,很难定位是网络问题还是服务器限制。
4. 异步上传、进度反馈和断点续传
4.1 主线程卡死问题
前面那段代码是同步的,这意味着GetRequestStream和GetResponse执行期间,Unity主线程会一直阻塞,轻则游戏画面卡顿,重则触发Android的ANR弹窗。尤其是在弱网环境下,一个几十MB的文件能卡好几秒甚至几分钟,体验非常糟糕。所以在Unity里
