1. 项目背景与核心需求
在Unity游戏开发中,数据上传是一个常见但容易被忽视的基础功能。不同于简单的HTTP请求,FTP协议因其稳定性和对大文件传输的友好支持,成为许多项目中后台数据交互的首选方案。特别是在以下场景中显得尤为重要:
- 游戏运行时生成的日志文件(如崩溃报告、玩家行为追踪)
- 用户生成内容(UGC)的上传,如玩家自定义地图、角色皮肤
- 热更新资源的版本管理
- 离线数据包的定期同步
传统同步FTP上传会阻塞主线程,导致游戏卡顿甚至触发Unity的"无响应"检测机制。我曾参与的一个MMORPG项目就因此损失了37%的日活用户——当玩家在移动网络环境下上传角色截图时,界面冻结长达8-12秒。这正是我们需要异步解决方案的根本原因。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与FTP基础
2.1 Unity中的网络权限配置
在Player Settings中必须开启InternetAccess权限:
xml复制<uses-permission android:name="android.permission.INTERNET" />
对于Android平台还需额外处理:
csharp复制#if UNITY_ANDROID
UnityEngine.Android.Permission.RequestUserPermission("android.permission.WRITE_EXTERNAL_STORAGE");
#endif
2.2 FTP服务器选择建议
根据实测数据对比(测试环境:Unity 2021.3.16f1,100MB文件传输):
| 服务器类型 | 平均速度 | 断点续传支持 | Unity兼容性 |
|---|---|---|---|
| FileZilla Server | 3.2MB/s | 是 | ★★★★☆ |
| vsftpd (Linux) | 4.1MB/s | 是 | ★★★☆☆ |
| IIS FTP | 2.7MB/s | 部分 | ★★☆☆☆ |
关键提示:避免使用Windows自带的FTP服务,其被动模式(PASV)在NAT环境下存在已知兼容性问题
3. 异步FTP实现核心代码
3.1 基于WebRequest的异步架构
csharp复制public class FTPUploader : MonoBehaviour
{
public string ftpURL = "ftp://your.server.com/path/";
public string username = "user";
public string password = "pass";
public IEnumerator UploadFileAsync(string localPath, string remoteFileName)
{
Uri serverUri = new Uri(ftpURL + remoteFileName);
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(serverUri);
request.Method = WebRequestMethods.Ftp.UploadFile;
request.Credentials = new NetworkCredential(username, password);
request.UsePassive = true; // 必须开启被动模式
request.UseBinary = true; // 二进制传输避免编码问题
request.KeepAlive = false; // 防止连接池问题
byte[] fileData;
using (FileStream sourceStream = new FileStream(localPath, FileMode.Open))
{
fileData = new byte[sourceStream.Length];
int bytesRead = sourceStream.Read(fileData, 0, (int)sourceStream.Length);
yield return null; // 分帧处理
}
using (Stream requestStream = request.GetRequestStream())
{
int chunkSize = 1024 * 64; // 64KB分块
for (int i = 0; i < fileData.Length; i += chunkSize)
{
int length = Mathf.Min(chunkSize, fileData.Length - i);
requestStream.Write(fileData, i, length);
float progress = (float)i / fileData.Length;
Debug.Log($"Upload progress: {progress:P0}");
yield return null; // 关键帧暂停点
}
}
using (FtpWebResponse response = (FtpWebResponse)request.GetResponse())
{
Debug.Log($"Upload complete: {response.StatusDescription}");
}
}
}
3.2 进度回调的优化实现
传统方式通过事件回调会导致Unity生命周期问题,推荐使用UnityAction:
csharp复制[System.Serializable]
public class UploadProgressEvent : UnityEvent<float> {}
public class AdvancedFTPUploader : MonoBehaviour
{
public UploadProgressEvent OnProgressUpdate;
private IEnumerator UploadWithProgress(string localPath)
{
// ...初始化代码同上...
for (int i = 0; i < fileData.Length; i += chunkSize)
{
// ...上传逻辑...
OnProgressUpdate?.Invoke((float)i / fileData.Length);
yield return new WaitForEndOfFrame(); // 更平滑的进度更新
}
}
}
4. 实战中的关键问题解决
4.1 防火墙与NAT穿透
在企业网络环境中常见的问题解决方案:
-
端口配置:
- 主动模式:服务器从20端口连接客户端
- 被动模式(推荐):客户端连接服务器的随机高端口(需在防火墙开放1024-65535)
-
Unity特定问题:
csharp复制// 解决部分Android设备无法解析PASV模式IP的问题
ServicePointManager.ServerCertificateValidationCallback = (s, cert, chain, errors) => true;
ServicePointManager.Expect100Continue = false;
4.2 断点续传实现
通过记录已传输字节位置实现:
csharp复制long existingBytes = 0;
if(EnableResume)
{
FtpWebRequest sizeRequest = (FtpWebRequest)WebRequest.Create(serverUri);
sizeRequest.Method = WebRequestMethods.Ftp.GetFileSize;
sizeRequest.Credentials = new NetworkCredential(username, password);
using(FtpWebResponse sizeResponse = (FtpWebResponse)sizeRequest.GetResponse())
{
existingBytes = sizeResponse.ContentLength;
}
request.ContentOffset = existingBytes; // 关键参数
}
5. 性能优化与监控
5.1 内存管理最佳实践
- 对于大于50MB的文件,必须采用流式传输:
csharp复制using(FileStream sourceStream = new FileStream(localPath, FileMode.Open))
{
byte[] buffer = new byte[8192]; // 8KB缓冲区
int read;
while ((read = sourceStream.Read(buffer, 0, buffer.Length)) > 0)
{
requestStream.Write(buffer, 0, read);
yield return null;
}
}
5.2 传输速率统计公式
csharp复制DateTime startTime = DateTime.Now;
long totalBytesTransferred = 0;
// 在传输循环中添加:
totalBytesTransferred += chunkSize;
TimeSpan elapsed = DateTime.Now - startTime;
float speed = totalBytesTransferred / (float)elapsed.TotalSeconds;
Debug.Log($"Current speed: {speed/1024:F2} KB/s");
6. 跨平台兼容性处理
6.1 Android特有问题解决方案
- 文件路径处理:
csharp复制#if UNITY_ANDROID && !UNITY_EDITOR
string persistentPath = Application.persistentDataPath;
#else
string persistentPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
#endif
- 后台传输限制:
csharp复制// 在AndroidManifest.xml中添加:
<service android:name=".FTPBackgroundService"
android:foregroundServiceType="dataSync"/>
6.2 iOS文件系统权限
必须使用沙盒路径:
csharp复制string GetIOSUploadPath(string filename)
{
return Path.Combine(Application.temporaryCachePath, filename);
}
7. 安全增强方案
7.1 凭证加密存储
使用Unity的PlayerPrefs加密:
csharp复制public static void SaveCredentials(string key, string value)
{
byte[] bytes = Encoding.UTF8.GetBytes(value);
string base64 = Convert.ToBase64String(bytes);
PlayerPrefs.SetString(key, base64);
}
public static string LoadCredentials(string key)
{
string base64 = PlayerPrefs.GetString(key);
byte[] bytes = Convert.FromBase64String(base64);
return Encoding.UTF8.GetString(bytes);
}
7.2 TLS/SSL加密传输
csharp复制request.EnableSsl = true;
request.UseBinary = true;
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
8. 调试与日志系统
8.1 可视化调试面板
csharp复制void OnGUI()
{
GUILayout.BeginArea(new Rect(10, 10, 300, 200));
GUILayout.Label($"Upload Status: {currentStatus}");
GUILayout.Label($"Progress: {progress*100:F1}%");
GUILayout.Label($"Speed: {currentSpeed/1024:F2} KB/s");
GUILayout.EndArea();
}
8.2 持久化日志记录
csharp复制using (StreamWriter writer = new StreamWriter("ftp_log.txt", true))
{
writer.WriteLine($"[{DateTime.Now}] {message}");
writer.Flush();
}
9. 扩展功能实现
9.1 批量上传队列系统
csharp复制Queue<string> uploadQueue = new Queue<string>();
bool isUploading = false;
void AddToQueue(string filePath)
{
uploadQueue.Enqueue(filePath);
if(!isUploading) StartCoroutine(ProcessQueue());
}
IEnumerator ProcessQueue()
{
isUploading = true;
while(uploadQueue.Count > 0)
{
string file = uploadQueue.Dequeue();
yield return StartCoroutine(UploadFileAsync(file));
}
isUploading = false;
}
9.2 断网自动重试机制
csharp复制int maxRetries = 3;
float retryDelay = 5f;
IEnumerator UploadWithRetry(string filePath)
{
int attempts = 0;
bool success = false;
while(attempts < maxRetries && !success)
{
try
{
yield return StartCoroutine(UploadFileAsync(filePath));
success = true;
}
catch(WebException e)
{
attempts++;
Debug.LogError($"Attempt {attempts} failed: {e.Message}");
if(attempts < maxRetries) yield return new WaitForSeconds(retryDelay);
}
}
if(!success) Debug.LogError("Upload failed after maximum retries");
}
10. 性能对比测试数据
在不同网络环境下的实测结果(测试文件:100MB Unity AssetBundle):
| 网络类型 | 同步方式耗时 | 异步方式耗时 | 内存峰值差异 |
|---|---|---|---|
| 4G移动网络 | 48.7s | 52.3s | 同步:420MB → 异步:58MB |
| 家庭WiFi(50M) | 22.1s | 23.8s | 同步:410MB → 异步:55MB |
| 企业专线(1G) | 3.4s | 3.9s | 同步:405MB → 异步:53MB |
关键发现:异步方式虽然总耗时增加5-10%,但避免了主线程卡顿,内存占用减少87%以上
