我正在使用 FluentFTP(版本 35.0.5)在本地深度复制大型 FTP 服务器(我需要对其进行格式化以进行结构化搜索,这是不可行的)。服务器似乎很不稳定:
- 即使对于单个连接并列出包含一个或两个元素的文件夹,我也可以让客户端超时
- 在超时的情况下,我可以断开我的客户端,但是当我尝试重新连接它时,服务器说我已经达到了这个用户的最大连接数(5),
- 等等
话虽如此,我编写了一个客户端池,我设法从该服务器连接、列出和下载文件。它在边缘情况下工作得很好(比如在重新连接之前等待客户端超时等),但我仍然有一个我无法弄清楚的问题。示意性地,我有一个内部池连接的实例,例如:
public class FtpClient
{
// all new clients are create with this methods and then connected
// and enqueued to the queue.
private FluentFTP.FtpClient CreateFluentFtpClient() =>
new()
{
Host = ...,
Port = ...,
Credentials = ...,
EncryptionMode = ...,
ReadTimeout = 60000,
ConnectTimeout = 60000,
DataConnectionReadTimeout = 60000,
DataConnectionConnectTimeout = 60000,
DataConnectionType = FtpDataConnectionType.PASV,
SocketKeepAlive = true
};
// clients are connected before being enqueued, and when hitting
// timeouts, I disconnect all of them, wait and try to reconnect
// newer ones, quite ugly. but I guess not the matter of this issue
private readonly ConcurrentQueue<FluentFTP.FtpClient> _clients = new();
public async Task<Stream> DownloadFileAsync(
string path, CancellationToken cancellationToken)
{
var tries = 0;
Exception? traceException = null;
while (tries++ < Settings.MaxRetries)
{
cancellationToken.ThrowIfCancellationRequested();
var client = await AcquireClient(cancellationToken)
.ConfigureAwait(false);
try
{
var stream = new MemoryStream();
if (!await client.DownloadAsync(
stream, path, 0, null, cancellationToken).ConfigureAwait(false))
throw new InvalidOperationException(
"Downloading failed without throwing inner exception"
);
ReleaseClient(client);
return stream;
}
catch (Exception ex)
{
ReleaseClient(client);
traceException = ex;
_logger.LogWarning(
ex,
$"File {path} cannot be downloaded, retrying... ({tries - 1}/{Settings.MaxRetries})"
);
// not really the matter of this issue but I have another
// function for waiting and reseting connections when there
// are timeouts, etc.
if (tries < Settings.MaxRetries)
await WaitAsync(ex, cancellationToken)
.ConfigureAwait(false);
}
}
throw new InvalidOperationException(
$"File {path} cannot be downloaded",
traceException
);
}
private async Task<FluentFTP.FtpClient> AcquireClient(CancellationToken cancellationToken)
{
FluentFTP.FtpClient? client;
while (!_clients.TryDequeue(out client))
await Task.Delay(100, cancellationToken)
.ConfigureAwait(false);
return client!;
}
private void ReleaseClient(FluentFTP.FtpClient client)
{
_clients.Enqueue(client);
}
}
我有几个同时调用的任务(大约比允许的最大连接数多一点,即上面实现中的队列大小)DownloadFileAsync
:
using var stream = await _ftpClient.DownloadFileAsync(
"Path/to/file", cancellationToken
).ConfigureAwait(false);
有了所有这些配置(我想我显然错过了什么?)有时我会得到奇怪的下载内容,比如另一个文件的内容,甚至流内容包含另一个 FTP 命令,比如"-r--r----- 1 ftp ftp 491 Jan 3 2018 file_name.csv"
. 这有点像我可以有一个最终在两个任务之间共享的客户端,并且下载内容因此会从套接字流中复制一些完全不合适的东西。
如果你们中的一些人遇到这样的问题或直接从我的代码中看到明显的错误,将不胜感激:)
我已经更仔细地跟踪了客户端,并且我意识到这更有可能是由于如何正确地将套接字流复制到内存流,例如我有典型的日志:
[2021-11-22 16:12:09] info: ***
[client:745f16ad-9569-4708-906f-6930a0e98538] Downloading file /path/to/xxx.csv
[2021-11-22 16:12:09] info: ***
[client:745f16ad-9569-4708-906f-6930a0e98538] Listing files at /other/path
[2021-11-22 16:12:09] warn: ***
Error on file /path/to/xxx.csv (n°1), retrying...
IReader state:
["dr-xr-x--- 2 ftp ftp 4096 Jan 3 2018 163"]
即我们首先正确下载文件,但是当我们处理它时,我们同时重用客户端以在另一个路径上列出文件,并且它似乎追溯影响我复制到我的流MemoryStream
(参见我的方法DownloadAsync
中提供的流)。
我不知道这是由于我对流的使用不当,是由于并发问题和糟糕的设计,还是由于从 FluentFTP 读取套接字的内部问题(我对此表示怀疑,我宁愿打赌 99%机会是我的^^)。