You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Windows跨机实时文件传输:如何避免短生命周期文件丢失?

解决方案

核心问题分析

你的代码存在几个关键缺陷导致文件丢失:

  1. FileSystemWatcher的Created事件触发时,文件可能仍处于写入状态,直接拷贝会得到不完整的文件,甚至因文件被占用而失败
  2. Created和Changed事件重复入队,导致同一文件被多次处理,浪费资源
  3. 异常被静默吞掉,无法排查拷贝失败(比如文件已被删除)的问题
  4. 没有等待文件写入完成就开始拷贝,大概率在文件被删除前还没完成操作

改进后的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机,避免原文件被删除时传输中断

额外注意事项

  1. 确保B机的目标目录(比如\\B\SharedTransferDir)已开启共享权限,且A机的运行账号有读写权限
  2. 如果文件生成速率极高,可以调整_maxRetryCount和_retryDelayMs参数,或者增加多个处理线程并行处理
  3. 可以考虑增加缓存文件的清理逻辑,比如传输完成后删除本地缓存文件,避免磁盘占用过高

内容的提问来源于stack exchange,提问作者cookier

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 17:23:32