c# - 如何在不重新启动进程的情况下重新启动与发回重置数据包的 FTP 服务器的通信?

标签 c# ftp tcp

我们有一个(长期运行的)Windows 服务,除其他外,它使用 FtpWebRequest 定期与嵌入在第三方设备上的 FTP 服务器通信。这在大多数情况下都很好用,但有时我们的服务会停止与设备通信,但只要您重新启动我们的服务,一切都会重新开始。

我花了一些时间使用 MCVE(包括在下面)对此进行调试,并通过 Wireshark 发现一旦通信开始失败,就没有网络流量流向外部 FTP 服务器(根本没有数据包显示流向该 IP Wireshark )。如果我尝试从同一台机器上的另一个应用程序(如 Windows 资源管理器)连接到同一个 FTP,一切正常。

在一切停止工作之前查看数据包,我看到来自设备的设置了重置 (RST) 标志的数据包,所以我怀疑这可能是问题所在。一旦我们运行的服务在计算机上的网络堆栈的某个部分接收到重置数据包,它就会执行 this article 的 TCP 重置部分中描述的操作,并阻止从我们的进程到设备的所有进一步通信。

据我所知,我们与设备通信的方式没有任何问题,而且大多数情况下,完全相同的代码也能正常工作。重现该问题的最简单方法(请参阅下面的 MCVE)似乎是同时与 FTP 建立大量单独的连接,因此我怀疑当与 FTP 建立大量连接时可能会出现该问题(不是全部由我们)同时进行。

问题是,如果我们重启进程,一切正常,我们确实需要重新建立与设备的通信。有没有办法重新建立通信(经过适当的时间后)而不必重新启动整个过程?

不幸的是,FTP 服务器嵌入在一个相当老旧的第三方设备上运行,该设备不太可能更新以解决此问题,即使更新了我们仍然希望/需要与所有已经存在的设备进行通信如果可能,无需我们的客户更新字段。

我们知道的选项:

  1. 使用命令行 FTP 客户端,例如 Windows 中内置的客户端。

    • 这样做的一个缺点是我们需要列出一个目录中的所有文件,然后只下载其中的一部分,因此我们必须编写逻辑来解析对此的响应。
    • 我们还必须将文件下载到临时文件,而不是像现在这样下载到流。
  2. 创建另一个应用程序来处理我们在每个请求完成后拆除的 FTP 通信部分。

    • 这里的主要缺点是进程间通信有点麻烦。

MCVE

这在 LINQPad 中运行并相当可靠地重现了问题。通常前几个任务成功,然后问题出现,之后所有任务开始超时。在 Wireshark 中,我可以看到我的计算机和设备之间没有发生任何通信。

如果我再次运行该脚本,则所有 任务都会失败,直到我重新启动 LINQPad 或执行“取消所有线程并重置”以重新启动 LINQPad 用于运行查询的进程。如果我做了其中任何一件事情,那么我们就会回到前几项成功的任务。

async Task Main() {
    var tasks = new List<Task>();
    var numberOfBatches = 3;
    var numberOfTasksPerBatch = 10;
    foreach (var batchNumber in Enumerable.Range(1, numberOfBatches)) {
        $"Starting tasks in batch {batchNumber}".Dump();
        tasks.AddRange(Enumerable.Range(1, numberOfTasksPerBatch).Select(taskNumber => Connect(batchNumber, taskNumber)));
        await Task.Delay(TimeSpan.FromSeconds(5));
    }

    await Task.WhenAll(tasks);
}

async Task Connect(int batchNumber, int taskNumber) {
    try {
        var client = new FtpClient();
        var result = await client.GetFileAsync(new Uri("ftp://192.168.0.191/logging/20140620.csv"), TimeSpan.FromSeconds(10));
        result.Count.Dump($"Task {taskNumber} in batch {batchNumber} succeeded");
    } catch (Exception e) {
        e.Dump($"Task {taskNumber} in batch {batchNumber} failed");
    }
}

public class FtpClient {

    public virtual async Task<ImmutableList<Byte>> GetFileAsync(Uri fileUri, TimeSpan timeout) {
        if (fileUri == null) {
            throw new ArgumentNullException(nameof(fileUri));
        }

        FtpWebRequest ftpWebRequest = (FtpWebRequest)WebRequest.Create(fileUri);
        ftpWebRequest.Method = WebRequestMethods.Ftp.DownloadFile;
        ftpWebRequest.UseBinary = true;
        ftpWebRequest.KeepAlive = false;

        using (var source = new CancellationTokenSource(timeout)) {
            try {
                using (var response = (FtpWebResponse)await ftpWebRequest.GetResponseAsync()
                    .WithWaitCancellation(source.Token)) {
                    using (Stream ftpStream = response.GetResponseStream()) {
                        if (ftpStream == null) {
                            throw new InvalidOperationException("No response stream");
                        }

                        using (var dataStream = new MemoryStream()) {
                            await ftpStream.CopyToAsync(dataStream, 4096, source.Token)
                                .WithWaitCancellation(source.Token);

                            return dataStream.ToArray().ToImmutableList();
                        }
                    }
                }
            } catch (OperationCanceledException) {
                throw new WebException(
                    String.Format("Operation timed out after {0} seconds.", timeout.TotalSeconds),
                    WebExceptionStatus.Timeout);
            } finally {
                ftpWebRequest.Abort();
            }
        }
    }
}

public static class TaskCancellationExtensions {
    /// http://stackoverflow.com/a/14524565/1512
    public static async Task<T> WithWaitCancellation<T>(
        this Task<T> task,
        CancellationToken cancellationToken) {
        // The task completion source. 
        var tcs = new TaskCompletionSource<Boolean>();

        // Register with the cancellation token.
        using (cancellationToken.Register(
            s => ((TaskCompletionSource<Boolean>)s).TrySetResult(true),
            tcs)) {
            // If the task waited on is the cancellation token...
            if (task != await Task.WhenAny(task, tcs.Task)) {
                throw new OperationCanceledException(cancellationToken);
            }
        }

        // Wait for one or the other to complete.
        return await task;
    }

    /// http://stackoverflow.com/a/14524565/1512
    public static async Task WithWaitCancellation(
        this Task task,
        CancellationToken cancellationToken) {
        // The task completion source. 
        var tcs = new TaskCompletionSource<Boolean>();

        // Register with the cancellation token.
        using (cancellationToken.Register(
            s => ((TaskCompletionSource<Boolean>)s).TrySetResult(true),
            tcs)) {
            // If the task waited on is the cancellation token...
            if (task != await Task.WhenAny(task, tcs.Task)) {
                throw new OperationCanceledException(cancellationToken);
            }
        }

        // Wait for one or the other to complete.
        await task;
    }
}

最佳答案

这让我想起了旧的(?)IE 行为,即使在 N 次不成功的尝试后网络恢复时也不会重新加载页面。

您应该尝试将 FtpWebRequest 的缓存策略设置为 BypassCache

HttpRequestCachePolicy bypassPolicy = new HttpRequestCachePolicy(
    HttpRequestCacheLevel.BypassCache
);
ftpWebRequest.CachePolicy = bypassPolicy;

设置KeepAlive后。

关于c# - 如何在不重新启动进程的情况下重新启动与发回重置数据包的 FTP 服务器的通信?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/33044415/

相关文章:

c# - Web.config 递归拒绝访问子文件夹

c# - 调用多个任务

c# - 通过 FTP 上传文件 - 服务器返回错误 (550) 文件不可用,找不到文件

php - 使用 PHP/CURL 通过 FTP 获取修改日期

php - 通过 ftp 双向同步 Netbeans 中的项目

networking - TCP 和 Go Back N 的区别

java - 停止我的 TCP 服务器 java 时抛出 SocketException

c# - 反向 PInvoke 并创建一个完整的非托管 C# 程序

c# - 使用 ASP.NET 在 IE8 中通过 https 下载文件

c# - 解析来自 TCP/IP 连接的 XML 字符串