使用 WCF 从远程 FTP 服务器下载和流式传输文件

发布于 2024-12-23 19:27:18 字数 4719 浏览 0 评论 0原文

我正在构建一个解决方案,其中 WCF 服务充当必须通过 FTP 协议(​​Linux 服务器)远程访问的 FTP 服务器和 Windows 客户端应用程序之间的网关。该服务本身将托管在 Windows IIS 服务器上。

我的模型基于一篇关于使用 WCF 通过 http 传输文件的文章,但问题是:

我必须先等待文件下载到 Windows 服务器上,然后再将其传输到客户端,这可能是一个主要的性能问题。我想将文件从 FTP 服务器直接流式传输到客户端,而无需先下载。

这是代码..

public class TransferService : ITransferService{
Starksoft.Net.Ftp.FtpClient ftp = new Starksoft.Net.Ftp.FtpClient();
public RemoteFileInfo DownloadFile(DownloadRequest request)
{
    RemoteFileInfo result = new RemoteFileInfo();
    try
    {
        string filePath = System.IO.Path.Combine(@"C:\UploadFiles\ServerDownloadFiles", request.FileName);
        System.IO.FileInfo fileInfo = new System.IO.FileInfo(filePath);

        ftp = new Starksoft.Net.Ftp.FtpClient("127.0.0.1"); //remote ftp address
        ftp.Open("user", "pass");

        // here is waiting for the file to get downloaded from ftp server
        System.IO.FileStream stream = new System.IO.FileStream(filePath, System.IO.FileMode.Create, System.IO.FileAccess.Write);

        ftp.GetFileAsync(request.FileName, stream,  true);

        stream.Close();
        stream.Dispose();

        // this will read and be streamed to client
        System.IO.FileStream stream2 = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read);

        result.FileName = request.FileName;
        result.Length = stream2.Length;
        result.FileByteStream = stream2;

    }
    catch (Exception ex)
    {

    }
    return result;

 }

客户是这样的:

// start service client
            FileTransferClient.TransferServiceClient client = new FileTransferClient.TransferServiceClient();

            LogText("Start");

            // kill target file, if already exists
            string filePath = System.IO.Path.Combine("Download", textBox1.Text);
            if (System.IO.File.Exists(filePath)) System.IO.File.Delete(filePath);

            // get stream from server
            System.IO.Stream inputStream;
            string fileName = textBox1.Text;
            long length = client.DownloadFile(ref fileName, out inputStream);

            // write server stream to disk
            using (System.IO.FileStream writeStream = new System.IO.FileStream(filePath, System.IO.FileMode.CreateNew, System.IO.FileAccess.Write))
            {
                int chunkSize = 2048;
                byte[] buffer = new byte[chunkSize];

                do
                {
                    // read bytes from input stream
                    int bytesRead = inputStream.Read(buffer, 0, chunkSize);
                    if (bytesRead == 0) break;

                    // write bytes to output stream
                    writeStream.Write(buffer, 0, bytesRead);

                    // report progress from time to time
                    progressBar1.Value = (int)(writeStream.Position * 100 / length);
                } while (true);

                // report end of progress
                LogText("Done!");

                writeStream.Close();
            }

            // close service client
            inputStream.Dispose();
            client.Close();

你觉得怎么样?

采取 2:

Stream stream;
public Stream GetStream(string filename)
{
    Starksoft.Net.Ftp.FtpClient ftp = new Starksoft.Net.Ftp.FtpClient();
    //string filePath = System.IO.Path.Combine(@"C:\UploadFiles\ServerDownloadFiles", filename);
    //System.IO.FileInfo fileInfo = new System.IO.FileInfo(filePath);

    ftp = new Starksoft.Net.Ftp.FtpClient("127.0.0.1");
    ftp.Open("testuser", "123456");

    stream = new MemoryStream();

    ftp.GetFileAsyncCompleted += new EventHandler<Starksoft.Net.Ftp.GetFileAsyncCompletedEventArgs>(ftp_GetFileAsyncCompleted);
    this.IsBusy = true;

    ftp.GetFileAsync(filename, stream, true);
    return stream;
}

服务合同:

[ServiceContract]
public interface IStreamingService
{
    [OperationContract]
    Stream GetStream(string filename);

    [OperationContract]
    Boolean GetBusyState();
}

服务配置(绑定):

<basicHttpBinding>
            <binding name="TransferService" maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" transferMode="Streamed">
                <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
                <security mode="None">
                </security>
            </binding>
        </basicHttpBinding>

I'm building a solution where a WCF service acts as a gateway between an FTP server that it must access remotely via FTP protocol (linux server) and a windows client application. the service itself will be hosted on a windows IIS server.

I based my model on an article about streaming files over http using WCF, but the problem is:

I have to wait for the file to download on the windows server first prior straming it to the client and that could be a major performance problem. I want to direct stream the files from the FTP Sever to the client without having to download it first.

here's the code..

public class TransferService : ITransferService{
Starksoft.Net.Ftp.FtpClient ftp = new Starksoft.Net.Ftp.FtpClient();
public RemoteFileInfo DownloadFile(DownloadRequest request)
{
    RemoteFileInfo result = new RemoteFileInfo();
    try
    {
        string filePath = System.IO.Path.Combine(@"C:\UploadFiles\ServerDownloadFiles", request.FileName);
        System.IO.FileInfo fileInfo = new System.IO.FileInfo(filePath);

        ftp = new Starksoft.Net.Ftp.FtpClient("127.0.0.1"); //remote ftp address
        ftp.Open("user", "pass");

        // here is waiting for the file to get downloaded from ftp server
        System.IO.FileStream stream = new System.IO.FileStream(filePath, System.IO.FileMode.Create, System.IO.FileAccess.Write);

        ftp.GetFileAsync(request.FileName, stream,  true);

        stream.Close();
        stream.Dispose();

        // this will read and be streamed to client
        System.IO.FileStream stream2 = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read);

        result.FileName = request.FileName;
        result.Length = stream2.Length;
        result.FileByteStream = stream2;

    }
    catch (Exception ex)
    {

    }
    return result;

 }

The Client like this:

// start service client
            FileTransferClient.TransferServiceClient client = new FileTransferClient.TransferServiceClient();

            LogText("Start");

            // kill target file, if already exists
            string filePath = System.IO.Path.Combine("Download", textBox1.Text);
            if (System.IO.File.Exists(filePath)) System.IO.File.Delete(filePath);

            // get stream from server
            System.IO.Stream inputStream;
            string fileName = textBox1.Text;
            long length = client.DownloadFile(ref fileName, out inputStream);

            // write server stream to disk
            using (System.IO.FileStream writeStream = new System.IO.FileStream(filePath, System.IO.FileMode.CreateNew, System.IO.FileAccess.Write))
            {
                int chunkSize = 2048;
                byte[] buffer = new byte[chunkSize];

                do
                {
                    // read bytes from input stream
                    int bytesRead = inputStream.Read(buffer, 0, chunkSize);
                    if (bytesRead == 0) break;

                    // write bytes to output stream
                    writeStream.Write(buffer, 0, bytesRead);

                    // report progress from time to time
                    progressBar1.Value = (int)(writeStream.Position * 100 / length);
                } while (true);

                // report end of progress
                LogText("Done!");

                writeStream.Close();
            }

            // close service client
            inputStream.Dispose();
            client.Close();

what do you think?

Take 2:

Stream stream;
public Stream GetStream(string filename)
{
    Starksoft.Net.Ftp.FtpClient ftp = new Starksoft.Net.Ftp.FtpClient();
    //string filePath = System.IO.Path.Combine(@"C:\UploadFiles\ServerDownloadFiles", filename);
    //System.IO.FileInfo fileInfo = new System.IO.FileInfo(filePath);

    ftp = new Starksoft.Net.Ftp.FtpClient("127.0.0.1");
    ftp.Open("testuser", "123456");

    stream = new MemoryStream();

    ftp.GetFileAsyncCompleted += new EventHandler<Starksoft.Net.Ftp.GetFileAsyncCompletedEventArgs>(ftp_GetFileAsyncCompleted);
    this.IsBusy = true;

    ftp.GetFileAsync(filename, stream, true);
    return stream;
}

Service Contract:

[ServiceContract]
public interface IStreamingService
{
    [OperationContract]
    Stream GetStream(string filename);

    [OperationContract]
    Boolean GetBusyState();
}

Service Config (Binding):

<basicHttpBinding>
            <binding name="TransferService" maxReceivedMessageSize="2147483647" maxBufferSize="2147483647" transferMode="Streamed">
                <readerQuotas maxDepth="2147483647" maxStringContentLength="2147483647" maxArrayLength="2147483647" maxBytesPerRead="2147483647" maxNameTableCharCount="2147483647"/>
                <security mode="None">
                </security>
            </binding>
        </basicHttpBinding>

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

-黛色若梦 2024-12-30 19:27:18

更新BlockingStream 实现我最初链接的文章足以让这个对我有用。

服务:

public Stream DownloadFile(string remotePath)
{
    // initialize FTP client...

    BlockingStream blockingStream = new BlockingStream();

    // Assign self-removing TransferComplete handler.
    EventHandler<TransferCompleteEventArgs> transferCompleteDelegate = null;
    transferCompleteDelegate = delegate(object sender, TransferCompleteEventArgs e)
    {
        // Indicate to waiting readers that 'end of stream' is reached.
        blockingStream.SetEndOfStream();
        ftp.TransferComplete -= transferCompleteDelegate;
        // Next line may or may not be necessary and/or safe.  Please test thoroughly.
        blockingStream.Close();
        // Also close the ftp client here, if it is a local variable.
    };
    ftp.TransferComplete += transferCompleteDelegate;

    // Returns immediately.  Download is still in progress.
    ftp.GetFileAsync(remotePath, blockingStream);

    return blockingStream;
}

客户端:

StreamingService.Service1Client client = new StreamingService.Service1Client("BasicHttpBinding_IService1");
Stream inputStream = client.GetFile(remotePath);
//long length = inputStream.Length; // << not available with streaming

// write server stream to disk 
using (FileStream writeStream = new FileStream(localPath, FileMode.CreateNew, FileAccess.Write))
{
    int chunkSize = 2048;
    byte[] buffer = new byte[chunkSize];
    do
    {
        // read bytes from input stream 
        int bytesRead = inputStream.Read(buffer, 0, chunkSize);

        // etc.  The rest like yours, but without progress reporting b/c length unknown.

注意:

  • 我直接从该文章复制了 BlockingStream 代码,并将其粘贴到我的服务项目中,没有进行任何修改。
  • 我在 BlockingStream 的 Read() 和 Write() 方法中的 lock(_lockForAll) 语句后面设置了断点,并在客户端代码的读取循环中设置了断点。我必须使用相当大的文件(至少是 FTP 客户端缓冲区大小的 20 倍)才能查看流式传输的证据。在 FTP 客户端进行大约 8 次直接写入后,服务的另一个线程开始从流中读取。几轮之后,服务调用返回,客户端也开始读取。三个断点交替被击中,直到只有客户端赶上,然后最终完成下载。
  • 我在测试中没有使用真正的 Starksoft FTP 客户端。我编写了一个从本地磁盘异步读取文件的类,主要使用直接从 Starksoft 来源
  • 我还更改了服务方法签名,以将 Web 方法的最简单情况与流式响应相匹配 - 更接近您的“take 2”。如果你能让它像这样工作,那么你应该能够稍后添加其他功能(MessageContract、文件长度等)。
  • 如果您的 FtpClient 是服务类的成员,则 TransferComplete 事件处理程序也应该是。
  • 确保客户端绑定中有transferMode=StreamedResponse,否则即使服务尝试传输数据,客户端也会缓冲数据。
  • 请像检查互联网上的任何内容一样仔细检查和测试 BlockingStream!

我在研究中也遇到过这些,您可能会感兴趣:
可以强制流方法缓冲其响应的功能列表< br>
问题包括一些提高流媒体速度的建议
基本流式传输的完整示例应用程序


该实现实际上是否将文件流式传输回来给客户?除非 RemoteFileInfo 实现 IXmlSerialized,否则我认为它不满足流方法的要求。来自 MSDN

流式传输的限制

使用流式传输模式会导致运行时间强制执行
额外的限制。

跨流传输发生的操作可以有一个契约
至多有一个输入或输出参数。该参数对应
消息的整个正文,并且必须是消息,派生的
Stream 类型,或 IXmlSerialized 实现。有回报
操作的值相当于有一个输出参数。

我认为您的实现实际上将数据缓冲三次:一次缓冲到服务器上的文件,再次缓冲到结果的 FileByteStream 属性,第三次缓冲在客户端,甚至在服务调用返回之前。您可以通过在绑定上启用流式传输来消除其中两个缓冲延迟并直接从您的服务方法返回可读的 FileStream 对象。其他属性可以设置为返回消息中的标头。有关示例,请参阅此答案

不过,也许你可以做得更好。根据 Starksoft 文档< /a>,在对 GetFileAsync 的调用中,“输出流必须是可写的并且可以可以是任何流对象”。您可能可以创建一个Stream的实现,允许您将单个流对象用于所有目的。您可以创建流对象,将其直接传递给 GetFileAsync 方法,然后将其直接返回给客户端,而无需等待整个文件下载完毕。这可能有点过分了,但是这里是阻塞读取的实现-写入您可以尝试的流。

Update: The BlockingStream implementation from the article I originally linked was enough to get this working for me.

Service:

public Stream DownloadFile(string remotePath)
{
    // initialize FTP client...

    BlockingStream blockingStream = new BlockingStream();

    // Assign self-removing TransferComplete handler.
    EventHandler<TransferCompleteEventArgs> transferCompleteDelegate = null;
    transferCompleteDelegate = delegate(object sender, TransferCompleteEventArgs e)
    {
        // Indicate to waiting readers that 'end of stream' is reached.
        blockingStream.SetEndOfStream();
        ftp.TransferComplete -= transferCompleteDelegate;
        // Next line may or may not be necessary and/or safe.  Please test thoroughly.
        blockingStream.Close();
        // Also close the ftp client here, if it is a local variable.
    };
    ftp.TransferComplete += transferCompleteDelegate;

    // Returns immediately.  Download is still in progress.
    ftp.GetFileAsync(remotePath, blockingStream);

    return blockingStream;
}

Client:

StreamingService.Service1Client client = new StreamingService.Service1Client("BasicHttpBinding_IService1");
Stream inputStream = client.GetFile(remotePath);
//long length = inputStream.Length; // << not available with streaming

// write server stream to disk 
using (FileStream writeStream = new FileStream(localPath, FileMode.CreateNew, FileAccess.Write))
{
    int chunkSize = 2048;
    byte[] buffer = new byte[chunkSize];
    do
    {
        // read bytes from input stream 
        int bytesRead = inputStream.Read(buffer, 0, chunkSize);

        // etc.  The rest like yours, but without progress reporting b/c length unknown.

Notes:

  • I copied the BlockingStream code directly from that article and pasted it into my service project with no modifications.
  • I set breakpoints after the lock(_lockForAll) statements in the Read() and Write() methods of BlockingStream, plus a breakpoint in the read loop of the client-side code. I had to use a pretty big file (at least 20x the FTP client's buffer size) to see proof of the streaming. After about 8 straight writes from the FTP client, the service's other thread starting reading from the stream. After a few rounds of that, the service call returned, and the client started reading, too. All three breakpoints were hit alternately, until only the client was catching up, and then finally completed the download.
  • I did not use the real Starksoft FTP client in my testing. I wrote a class that reads a file from the local disk asynchronously, using mainly code taken directly from the Starksoft source.
  • I also changed the service method signature to match the simplest case of a web method with a streamed response - closer to your 'take 2'. If you can make it work like this, then you should be able to add your other features (MessageContract, file length, etc.) later.
  • If your FtpClient is a member of your service class, the TransferComplete event handler should be, as well.
  • Make sure you have transferMode=StreamedResponse in your client's binding, otherwise the client will buffer the data even while the service is trying to stream it.
  • Please review and test the BlockingStream as carefully as you would anything found on the Internet!

I also came across these in my research, which may be of interest to you:
List of features which can force a streaming method to buffer its response
Question including some suggestions for improving streaming speed
Complete sample application of basic streaming


Is that implementation actually streaming the file back to the client? Unless RemoteFileInfo implements IXmlSerializable, I don't think it meets the requirements for a streaming method. From MSDN:

Restrictions on Streamed Transfers

Using the streamed transfer mode causes the run time to enforce
additional restrictions.

Operations that occur across a streamed transport can have a contract
with at most one input or output parameter. That parameter corresponds
to the entire body of the message and must be a Message, a derived
type of Stream, or an IXmlSerializable implementation. Having a return
value for an operation is equivalent to having an output parameter.

I think your implementation is actually buffering the data three times: once to a file on the server, again to the FileByteStream property of the result, and the third time at the client, before the service call even returns. You could remove two of those buffering delays by enabling streaming on the binding and returning the readable FileStream object directly from your service method. The other properties could be set as headers in the return message. See this answer for an example.

Maybe you can do one better, though. According to the Starksoft doc, in the call to GetFileAsync, "The output stream must be writeable and can be any stream object". It might be possible for you to create an implementation of Stream that allows you to use a single stream object for all purposes. You would create the stream object, pass it directly to the GetFileAsync method, and then return it directly to the client without waiting for the entire file to be downloaded. This might be overkill, but here is an implementation of a blocking read-write stream that you could try.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文