Отображение прогресса загрузки данных в сервисах WCF

Платформу Windows Communication Foundation можно использовать не только для простого вызова операций, но и для передачи большого объема данных (например, файлов в несколько мегабайт). Иногда такие сценарии применимы и на медленных каналах. В последнем случае зачастую очень не хватает индикатора выполнения операции (progress bar). Когда-то давно я задавался такой задачей, но довести ее до реализации не хватило времени. Лучше поздно, чем никогда. Поэтому сегодня я предлагаю рассмотреть способ реализации прогресса выполнения на базе WCF.

Для простоты мы будем использовать привязку BasicHttpBinding. Поскольку протокол HTTP не имеет принципиальный противоречий для отображение прогресса выполнения, то этот способ нам подойдет. Ключевым моментом при реализации данного сценария является установка параметра TransferMode и привязки в значение Streamed. Таким образом, данные будут передаваться в виде потока.

После этого определим контракт взаимодействия клиента и сервера. На клиентскую сторону требуется передать имя файла, его размер и содержимое. Поэтому определим контракт следующим образом.

[ServiceContract]
public interface IFileTransferService
{
    [OperationContract]
    RemoteFileInfo DownloadFile(DownloadRequest request);
}

[MessageContract]
public class DownloadRequest
{
    [MessageBodyMember]
    public string FileName;
}

[MessageContract]
public class RemoteFileInfo : IDisposable
{
    [MessageHeader(MustUnderstand = true)]
    public string FileName;

    [MessageHeader(MustUnderstand = true)]
    public long Length;

    [MessageBodyMember(Order = 1)]
    public Stream FileByteStream;

    public void Dispose()
    {
        if (FileByteStream != null)
        {
            FileByteStream.Close();
            FileByteStream = null;
        }
    }
}

Как видно, операция DownloadFile возвращает объект RemoteFileInfo, который содержит необходимую информацию. Реализация данного сервиса, я думаю, вопросов не вызывает — для этого нужно создать объект RemoteFileInfo и заполнить его всеми необходимыми данными.

public class FileTransferService : IFileTransferService
{
    public RemoteFileInfo DownloadFile(DownloadRequest request)
    {
        var filePath = request.FileName;
        var fileInfo = new FileInfo(filePath);

        if (fileInfo.Exists==false)
        {
            throw new FileNotFoundException("File not found", request.FileName);
        }

        var stream = new FileStream(filePath, FileMode.Open, FileAccess.Read);

        var result = new RemoteFileInfo
        {
            FileName = request.FileName, 
            Length = fileInfo.Length, 
            FileByteStream = stream
        };

        return result;
    }
}

На этом с серверной стороны мы сделали все необходимое, осталось только сформировать конфигурационный файл и определить хост-среду для сервиса. Для простоты в качестве хоста будем использовать консольное приложение. Тогда код для размещения сервиса будет выглядеть следующим образом.

class Program
{
    static void Main()
    {
        var myServiceHost = new ServiceHost(typeof(FileService.FileTransferService));
        myServiceHost.Open();

        Console.ReadKey();

        myServiceHost.Close();
    }
}

При этом конфигурационный файл будет иметь следующий вид.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="FileTransferServicesBinding"
                         transferMode="Streamed"
                         messageEncoding="Mtom"
                         maxReceivedMessageSize="20134217728" />
            </basicHttpBinding>
        </bindings>
        <services>
            <service behaviorConfiguration="MyServiceTypeBehaviors"
                     name="FileService.FileTransferService">
                <endpoint address="getfile"
                  binding="basicHttpBinding"
                  bindingConfiguration="FileTransferServicesBinding"
                  contract="FileService.IFileTransferService" />
                <host>
                    <baseAddresses>
                        <add baseAddress="http://localhost/" />
                    </baseAddresses>
                </host>
            </service>
        </services>
        <behaviors>
            <serviceBehaviors>
                <behavior name="MyServiceTypeBehaviors">
                    <serviceMetadata httpGetEnabled="true" />
                    <serviceDebug includeExceptionDetailInFaults="true" />
                </behavior>
            </serviceBehaviors>
        </behaviors>
    </system.serviceModel>
</configuration>

Теперь самое время заняться клиентской частью. Во-первых, определим конфигурацию для данного сервиса.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <bindings>
            <basicHttpBinding>
                <binding name="fileBinding" maxReceivedMessageSize="20134217728"
                         messageEncoding="Mtom" transferMode="Streamed" />
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="http://localhost/getfile"
                      binding="basicHttpBinding"
                      bindingConfiguration="fileBinding"
                      contract="Client.FileTransferClient.IFileTransferService" />
        </client>
    </system.serviceModel>
</configuration>

После генерации клиентского прокси он выглядит следующим образом.

[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public partial class FileTransferServiceClient : 
    System.ServiceModel.ClientBase<Client.FileTransferClient.IFileTransferService>, 
    Client.FileTransferClient.IFileTransferService
{
   public FileTransferServiceClient()
   {
   }
   
   public FileTransferServiceClient(string endpointConfigurationName) : 
           base(endpointConfigurationName)
   {
   }
   
   public FileTransferServiceClient(string endpointConfigurationName, string remoteAddress) : 
           base(endpointConfigurationName, remoteAddress)
   {
   }
   
   public FileTransferServiceClient(string endpointConfigurationName, System.ServiceModel.EndpointAddress remoteAddress) : 
           base(endpointConfigurationName, remoteAddress)
   {
   }
   
   public FileTransferServiceClient(System.ServiceModel.Channels.Binding binding, System.ServiceModel.EndpointAddress remoteAddress) : 
           base(binding, remoteAddress)
   {
   }
   
   Client.FileTransferClient.RemoteFileInfo Client.FileTransferClient.IFileTransferService.DownloadFile(Client.FileTransferClient.DownloadRequest request)
   {
       return base.Channel.DownloadFile(request);
   }
}

Как мы помним, операция сервиса DownloadFile возвращает информацию о файле и его поток. В этом виде не очень удобно работать с операцией, поэтому создадим еще один перегруженный вариант.

public long DownloadFile(ref string fileName, out System.IO.Stream fileByteStream)
{
    Client.FileTransferClient.DownloadRequest inValue = new Client.FileTransferClient.DownloadRequest();
    inValue.FileName = FileName;
    Client.FileTransferClient.RemoteFileInfo retVal = ((Client.FileTransferClient.IFileTransferService)(this)).DownloadFile(inValue);
    FileName = retVal.FileName;
    FileByteStream = retVal.FileByteStream;
    return retVal.Length;
}

Таким образом, мы получили метод, который немедленно возвращает длину файла и его содержимое в виде потока (Stream). Теперь мы должны получить этот поток и считывать из него информацию порциями. При этом после каждого считывания необходимо обновлять информацию о прогрессе передачи данных. Полученные данные можно записывать, например, в локальный файл.

var client = new FileTransferClient.FileTransferServiceClient();

Stream inputStream;
long length = client.DownloadFile(ref fileName, out inputStream);

using (var writeStream = new FileStream(fileName, FileMode.CreateNew, FileAccess.Write))
{
    const int bufferSize = 2048;
    var buffer = new byte[bufferSize];

    do
    {
        int bytesRead = inputStream.Read(buffer, 0, bufferSize);
        if (bytesRead == 0)
        {
            break;
        }

        writeStream.Write(buffer, 0, bytesRead);

        progressBar1.Value = (int)(writeStream.Position * 100 / length);
    }
    while (true);

    writeStream.Close();
}

inputStream.Dispose();
client.Close();

Отлично, теперь мы получили приложение, которое будет отображать прогресс загрузки данных на клиент. Аналогичным образом можно сделать загрузку данных на сервер.