Windows跨机实时文件传输:如何避免短生命周期文件丢失?
解决方案
核心问题分析
你的代码存在几个关键缺陷导致文件丢失:
FileSystemWatcher的Created事件触发时,文件可能仍处于写入状态,直接拷贝会得到不完整的文件,甚至因文件被占用而失败Created和Changed事件重复入队,导致同一文件被多次处理,浪费资源- 异常被静默吞掉,无法排查拷贝失败(比如文件已被删除)的问题
- 没有等待文件写入完成就开始拷贝,大概率在文件被删除前还没完成操作
改进后的A机本地缓存+传输实现
下面是修复上述问题的代码,同时增加了文件写入完成的判断、重复处理拦截和异常日志:
using System; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Threading.Tasks; namespace DirListener { internal class Program { // 存储待处理文件队列 static ConcurrentQueue<string> _fileQueue = new ConcurrentQueue<string>(); // 记录已处理/正在处理的文件,避免重复操作 static ConcurrentDictionary<string, bool> _processingFiles = new ConcurrentDictionary<string, bool>(); // 文件写入完成的重试次数(适配100-200ms的文件生命周期) const int _maxRetryCount = 3; // 重试间隔(毫秒) const int _retryDelayMs = 50; public static void Main(string[] args) { // 启动文件处理线程 _ = Task.Run(ProcessFileQueue); string watchDir = @"C:\test2"; string localCacheDir = @"C:\test3"; // 确保缓存目录存在 Directory.CreateDirectory(localCacheDir); var watcher = new FileSystemWatcher(watchDir, "*.*") { NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite, IncludeSubdirectories = false, EnableRaisingEvents = true }; // 只监听Created事件,避免重复触发 watcher.Created += (s, e) => EnqueueFile(e.FullPath); Console.WriteLine("监听已启动,按回车键退出..."); Console.ReadLine(); } static void EnqueueFile(string filePath) { // 避免重复入队 if (_processingFiles.TryAdd(filePath, false)) { _fileQueue.Enqueue(filePath); } } static async Task ProcessFileQueue() { while (true) { if (_fileQueue.TryDequeue(out string filePath)) { try { // 等待文件写入完成 if (!await WaitForFileReady(filePath)) { Console.WriteLine($"文件{filePath}未就绪就被删除,跳过"); _processingFiles.TryRemove(filePath, out _); continue; } string cachePath = Path.Combine(@"C:\test3", Path.GetFileName(filePath)); // 拷贝到本地缓存 File.Copy(filePath, cachePath, true); Console.WriteLine($"已缓存文件:{cachePath}"); // 从本地缓存传输到B机共享目录(替换为B机的可访问路径,比如\\B\SharedDir) string bMachinePath = @"\\B\SharedTransferDir"; Directory.CreateDirectory(bMachinePath); File.Copy(cachePath, Path.Combine(bMachinePath, Path.GetFileName(filePath)), true); Console.WriteLine($"已传输文件到B机:{Path.Combine(bMachinePath, Path.GetFileName(filePath))}"); } catch (FileNotFoundException) { Console.WriteLine($"文件{filePath}已被删除,传输失败"); } catch (IOException ex) { Console.WriteLine($"文件{filePath}处理失败:{ex.Message}"); } finally { // 标记文件处理完成,移除记录 _processingFiles.TryRemove(filePath, out _); } } else { // 无文件时异步等待,避免线程阻塞 await Task.Delay(10); } } } /// <summary> /// 等待文件可被只读打开,判断是否写入完成 /// </summary> static async Task<bool> WaitForFileReady(string filePath) { int retryCount = 0; while (retryCount < _maxRetryCount) { try { // 尝试以只读方式打开文件,成功则说明写入完成 using (var stream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { return true; } } catch (IOException) { // 文件被占用,等待后重试 await Task.Delay(_retryDelayMs); retryCount++; } catch (FileNotFoundException) { // 文件已被删除,直接返回失败 return false; } } // 重试多次仍无法打开,视为文件已被删除或无法访问 return false; } } }
关键优化点
- 文件就绪判断:通过尝试只读打开文件的方式,循环重试确认文件写入完成,适配100-200ms的文件生命周期
- 重复处理拦截:用
ConcurrentDictionary记录待处理文件,避免同一文件被多次入队 - 异常可见性:移除静默吞异常的逻辑,改为打印错误日志,方便排查问题
- 高效等待:用
Task.Delay替代Thread.Sleep,避免线程阻塞浪费资源 - 可靠传输:先缓存到A机本地,再从本地缓存传输到B机,避免原文件被删除时传输中断
额外注意事项
- 确保B机的目标目录(比如
\\B\SharedTransferDir)已开启共享权限,且A机的运行账号有读写权限 - 如果文件生成速率极高,可以调整
_maxRetryCount和_retryDelayMs参数,或者增加多个处理线程并行处理 - 可以考虑增加缓存文件的清理逻辑,比如传输完成后删除本地缓存文件,避免磁盘占用过高
内容的提问来源于stack exchange,提问作者cookier
相关产品推荐
相关产品推荐

