Compartilhar via


Canal de agrupamento

O exemplo de ChunkingChannel mostra como um protocolo personalizado ou canal em camadas pode ser usado para fazer a separação em partes e agrupamento das partes de mensagens aleatoriamente grandes.

Ao enviar mensagens grandes usando o WCF (Windows Communication Foundation), costuma ser desejável limitar a quantidade de memória usada para armazenar essas mensagens em buffer. Uma solução possível é transmitir o corpo da mensagem (supondo que a maior parte dos dados estejam no corpo). No entanto, alguns protocolos exigem o buffer de toda a mensagem. Mensagens confiáveis e segurança são dois exemplos desse tipo. Outra solução possível é dividir a mensagem grande em mensagens menores chamadas de partes, enviar essas partes individualmente e reconstituir a mensagem grande no lado receptor. O próprio aplicativo pode fazer essa separação e agrupamento em partes ou pode usar um canal personalizado para fazer isso.

A separação em partes sempre deve ser empregada somente depois que toda a mensagem a ser enviada tiver sido construída. Um canal de separação em partes sempre deve estar em camadas abaixo de um canal de segurança e de um canal de sessão confiável.

Observação

Os procedimentos de instalação e as instruções de build desse exemplo estão localizadas no final deste tópico.

Suposições e limitações do canal de separação em partes

Estrutura da mensagem

O canal de separação em partes pressupõe a seguinte estrutura para separação das mensagens em partes:

<soap:Envelope>
  <!-- headers -->
  <soap:Body>
    <operationElement>
      <paramElement>data to be chunked</paramElement>
    </operationElement>
  </soap:Body>
</soap:Envelope>

Ao usar o ServiceModel, as operações de contrato que têm um parâmetro de entrada estão em conformidade com essa forma de mensagem para a mensagem de entrada. Da mesma forma, as operações de contrato que têm um parâmetro de saída ou valor retornado estão em conformidade com essa forma de mensagem para a mensagem de saída. Veja a seguir exemplos dessas operações:

[ServiceContract]
interface ITestService
{
    [OperationContract]
    Stream EchoStream(Stream stream);

    [OperationContract]
    Stream DownloadStream();

    [OperationContract(IsOneWay = true)]
    void UploadStream(Stream stream);
}

Sessões

O canal de separação em partes exige que as mensagens sejam entregues exatamente uma vez, em entrega ordenada de mensagens (partes). Isso significa que a pilha de canais subjacente deve ter uma sessão. As sessões podem ser fornecidas pelo transporte (por exemplo, transporte TCP) ou por um canal de protocolo com sessão (por exemplo, canal ReliableSession).

Envio e recebimento assíncronos

Os métodos de envio e recebimento assíncronos não são implementados nesta versão do exemplo de canal de separação de partes.

Protocolo de separação de partes

O canal de separação de partes define um protocolo que indica o início e o fim de uma série de partes, bem como o número de sequência de cada parte. As três mensagens de exemplo a seguir demonstram as mensagens de início, de parte e de término com comentários que descrevem os principais aspectos de cada uma.

Mensagem de início

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"
            xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
<!--Original message action is replaced with a chunking-specific action. -->
    <a:Action s:mustUnderstand="1">http://samples.microsoft.com/chunkingAction</a:Action>
<!--
Original message is assigned a unique id that is transmitted
in a MessageId header. Note that this is different from the WS-Addressing MessageId header.
-->
    <MessageId s:mustUnderstand="1" xmlns="http://samples.microsoft.com/chunking">
53f183ee-04aa-44a0-b8d3-e45224563109
</MessageId>
<!--
ChunkingStart header signals the start of a chunked message.
-->
    <ChunkingStart s:mustUnderstand="1" i:nil="true" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://samples.microsoft.com/chunking" />
<!--
Original message action is transmitted in OriginalAction.
This is required to re-create the original message on the other side.
-->
    <OriginalAction xmlns="http://samples.microsoft.com/chunking">
http://tempuri.org/ITestService/EchoStream
    </OriginalAction>
   <!--
    All original message headers are included here.
   -->
  </s:Header>
  <s:Body>
<!--
Chunking assumes this structure of Body content:
<element>
  <childelement>large data to be chunked<childelement>
</element>
The start message contains just <element> and <childelement> without
the data to be chunked.
-->
    <EchoStream xmlns="http://tempuri.org/">
      <stream />
    </EchoStream>
  </s:Body>
</s:Envelope>

Mensagem de parte

<s:Envelope
  xmlns:a="http://www.w3.org/2005/08/addressing"
  xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
   <!--
    All chunking protocol messages have this action.
   -->
    <a:Action s:mustUnderstand="1">
      http://samples.microsoft.com/chunkingAction
    </a:Action>
<!--
Same as MessageId in the start message. The GUID indicates which original message this chunk belongs to.
-->
    <MessageId s:mustUnderstand="1"
               xmlns="http://samples.microsoft.com/chunking">
      53f183ee-04aa-44a0-b8d3-e45224563109
    </MessageId>
<!--
The sequence number of the chunk.
This number restarts at 1 with each new sequence of chunks.
-->
    <ChunkNumber s:mustUnderstand="1"
                 xmlns="http://samples.microsoft.com/chunking">
      1096
    </ChunkNumber>
  </s:Header>
  <s:Body>
<!--
The chunked data is wrapped in a chunk element.
The encoding of this data (and the entire message)
depends on the encoder used. The chunking channel does not mandate an encoding.
-->
    <chunk xmlns="http://samples.microsoft.com/chunking">
kfSr2QcBlkHTvQ==
    </chunk>
  </s:Body>
</s:Envelope>

Mensagem de término

<s:Envelope xmlns:a="http://www.w3.org/2005/08/addressing"
            xmlns:s="http://www.w3.org/2003/05/soap-envelope">
  <s:Header>
    <a:Action s:mustUnderstand="1">
      http://samples.microsoft.com/chunkingAction
    </a:Action>
<!--
Same as MessageId in the start message. The GUID indicates which original message this chunk belongs to.
-->
    <MessageId s:mustUnderstand="1"
               xmlns="http://samples.microsoft.com/chunking">
      53f183ee-04aa-44a0-b8d3-e45224563109
    </MessageId>
<!--
ChunkingEnd header signals the end of a chunk sequence.
-->
    <ChunkingEnd s:mustUnderstand="1" i:nil="true"
                 xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
                 xmlns="http://samples.microsoft.com/chunking" />
<!--
ChunkingEnd messages have a sequence number.
-->
    <ChunkNumber s:mustUnderstand="1"
                 xmlns="http://samples.microsoft.com/chunking">
      79
    </ChunkNumber>
  </s:Header>
  <s:Body>
<!--
The ChunkingEnd message has the same <element><childelement> structure
as the ChunkingStart message.
-->
    <EchoStream xmlns="http://tempuri.org/">
      <stream />
    </EchoStream>
  </s:Body>
</s:Envelope>

Arquitetura do canal de separação em partes

O canal de separação em partes é um IDuplexSessionChannel que, em um nível alto, segue a arquitetura típica do canal. Há um ChunkingBindingElement que pode criar um ChunkingChannelFactory e um ChunkingChannelListener. O ChunkingChannelFactory cria instâncias de ChunkingChannel quando recebe a solicitação. O ChunkingChannelListener cria instâncias de ChunkingChannel quando um novo canal interno é aceito. O ChunkingChannel em si é responsável por enviar e receber mensagens.

No próximo nível inferior, ChunkingChannel depende de vários componentes para implementar o protocolo de separação em partes. No lado do envio, o canal usa um XmlDictionaryWriter personalizado chamado ChunkingWriter que faz a separação em partes. ChunkingWriter usa o canal interno diretamente para enviar partes. Usar um XmlDictionaryWriter personalizado nos permite enviar partes à medida que o corpo grande da mensagem original está sendo escrito. Isso significa que não armazenamos em buffer toda a mensagem original.

Diagrama que mostra a arquitetura de envio do canal de separação em partes.

No lado do receptor, ChunkingChannel efetua pull de mensagens do canal interno e as entrega a um XmlDictionaryReader personalizado chamado ChunkingReader, que reconstitui a mensagem original das partes de entrada. ChunkingChannel encapsula isso ChunkingReader em uma implementação Message personalizada chamada ChunkingMessage e retorna essa mensagem para a camada acima. Essa combinação de ChunkingReader e ChunkingMessage nos permite agrupar as partes do corpo da mensagem original, pois ele está sendo lido pela camada acima, em vez de ter que armazenar em buffer todo o corpo da mensagem original. ChunkingReader tem uma fila em que armazena em buffer as partes de entrada até um número máximo configurável de partes armazenadas em buffer. Quando esse limite máximo é atingido, o leitor aguarda que as mensagens sejam drenadas da fila pela camada acima (ou seja, apenas lendo do corpo da mensagem original) ou até que o tempo limite máximo de recebimento seja atingido.

Diagrama que mostra a arquitetura de recebimento do canal de separação em partes.

Modelo de programação da separação em partes

Os desenvolvedores de serviço podem especificar quais mensagens devem ser separadas em partes aplicando o atributo ChunkingBehavior às operações dentro do contrato. O atributo expõe uma propriedade AppliesTo que permite que o desenvolvedor especifique se a separação em partes se aplica à mensagem de entrada, à mensagem de saída ou às duas. O exemplo a seguir mostra o uso do atributo ChunkingBehavior:

[ServiceContract]
interface ITestService
{
    [OperationContract]
    [ChunkingBehavior(ChunkingAppliesTo.Both)]
    Stream EchoStream(Stream stream);

    [OperationContract]
    [ChunkingBehavior(ChunkingAppliesTo.OutMessage)]
    Stream DownloadStream();

    [OperationContract(IsOneWay=true)]
    [ChunkingBehavior(ChunkingAppliesTo.InMessage)]
    void UploadStream(Stream stream);

}

Nesse modelo de programação, ChunkingBindingElement compila uma lista de URIs de ação que identificam mensagens a serem agrupadas. A ação de cada mensagem de saída é comparada com essa lista para determinar se a mensagem deve ser separada em partes ou enviada diretamente.

Implementação da operação Send

Em um alto nível, a operação Send primeiro verifica se a mensagem de saída deve ser separada em partes e, em caso negativo, envia a mensagem diretamente usando o canal interno.

Se a mensagem precisar ser agrupada, Send criará um novo ChunkingWriter e chamará WriteBodyContents na mensagem de saída passando a ela este ChunkingWriter. Em seguida, o ChunkingWriter faz a separação em partes da mensagem (incluindo a cópia de cabeçalhos da mensagem original na mensagem de parte inicial) e envia as partes usando o canal interno.

Vale notar alguns detalhes:

  • Primeiro, Send chama ThrowIfDisposedOrNotOpened para garantir que CommunicationState esteja aberto.

  • O envio é sincronizado para que apenas uma mensagem possa ser enviada por vez para cada sessão. Há um ManualResetEvent nomeado sendingDone que é redefinido quando uma mensagem em partes está sendo enviada. Após o envio da mensagem de parte final, esse evento é definido. O método Send aguarda a definição desse evento antes de tentar enviar a mensagem de saída.

  • Send bloqueia CommunicationObject.ThisLock para evitar alterações de estado sincronizadas durante o envio. Confira a documentação CommunicationObject para saber mais sobre estados CommunicationObject e máquina de estado.

  • O tempo limite passado para Send é usado como o tempo limite para toda a operação de envio, que inclui o envio de todas as partes.

  • O design personalizado XmlDictionaryWriter foi escolhido para evitar o buffer de todo o corpo da mensagem original. Se tivéssemos um XmlDictionaryReader no corpo usando message.GetReaderAtBodyContents, todo o corpo seria armazenado em buffer. Em vez disso, temos um XmlDictionaryWriter personalizado que é passado para message.WriteBodyContents. Como a mensagem chama WriteBase64 no gravador, o gravador empacota partes em mensagens e as envia usando o canal interno. WriteBase64 gera um bloqueio até que a parte seja enviada.

Implementação da operação Receive

Em um alto nível, a operação Receive primeiro verifica se a mensagem de entrada não é null e se a ação dela é ChunkingAction. Se não atender aos dois critérios, a mensagem retornará inalterada de Receive. Caso contrário, Receive criará um novo ChunkingReader e um novo ChunkingMessage encapsulado em torno dele (chamando GetNewChunkingMessage). Antes de retornar esse novo ChunkingMessage, Receive usa um thread de threadpool para executar ReceiveChunkLoop, que chama innerChannel.Receive em um loop e entrega partes para o ChunkingReader até que a mensagem da parte final seja recebida ou o tempo limite de recebimento seja atingido.

Vale notar alguns detalhes:

  • Assim como Send, Receive chama primeiro ThrowIfDisposedOrNotOpened para garantir que CommunicationState esteja Aberto.

  • Receive também é sincronizado para que apenas uma mensagem possa ser recebida por vez da sessão. Isso é especialmente importante porque, depois que uma mensagem de parte inicial é recebida, espera-se que todas as mensagens recebidas na sequência sejam partes dentro dessa nova sequência de partes, até que uma mensagem de parte final seja recebida. O recebimento não poderá efetuar pull de mensagens do canal interno até que todas as partes que pertencem à mensagem que está sendo agrupada sejam recebidas. Para fazer isso, Receive usa um ManualResetEvent chamado currentMessageCompleted, que é definido quando a mensagem de parte final é recebida, e redefinida quando uma nova mensagem de parte inicial é recebida.

  • Ao contrário de Send, Receive não impede transições de estado sincronizadas durante o recebimento. Por exemplo, é possível chamar Close durante o recebimento e aguardar até que o recebimento pendente da mensagem original seja concluído ou o valor de tempo limite especificado seja atingido.

  • O tempo limite passado para Receive é usado como o tempo limite para toda a operação de recebimento, que inclui o recebimento de todas as partes.

  • Se a camada que consome a mensagem estiver consumindo o corpo da mensagem a uma taxa menor do que a taxa de mensagens de partes de entrada, o ChunkingReader armazenará em buffer essas partes de entrada até o limite especificado por ChunkingBindingElement.MaxBufferedChunks. Depois que esse limite for atingido, nenhuma outra parte será extraída da camada inferior até que uma parte armazenada em buffer seja consumida ou o tempo limite de recebimento seja atingido.

Substituições de CommunicationObject

OnOpen

OnOpen chama innerChannel.Open para abrir o canal interno.

OnClose

OnClose primeiro define stopReceive como true para sinalizar o ReceiveChunkLoop pendente para parar. Em seguida, ele aguardará o receiveStopped ManualResetEvent, que será definido quando ReceiveChunkLoop parar. Supondo que ReceiveChunkLoop pare dentro do tempo limite especificado, OnClose chama innerChannel.Close com o tempo limite restante.

OnAbort

OnAbort chama innerChannel.Abort para anular o canal interno. Se houver um ReceiveChunkLoop pendente, ele obterá uma exceção da chamada innerChannel.Receive pendente.

OnFaulted

ChunkingChannel não exige um comportamento especial quando o canal tem falha, portanto OnFaulted não é substituído.

Implementação da fábrica de canais

ChunkingChannelFactory é responsável por criar instâncias de ChunkingDuplexSessionChannel e para transições de estado em cascata para a fábrica de canais interno.

OnCreateChannel usa a fábrica de canais interno para criar um canal interno IDuplexSessionChannel. Em seguida, cria um novo ChunkingDuplexSessionChannel passando esse canal interno junto com a lista de ações de mensagem a serem separadas em partes e o número máximo de partes a serem armazenadas em buffer no recebimento. A lista de ações de mensagem a serem separadas em partes e o número máximo de partes a serem armazenadas em buffer são dois parâmetros passados para ChunkingChannelFactory em seu construtor. A seção sobre ChunkingBindingElement descreve de onde esses valores vêm.

Os OnOpen, OnCloseOnAbort e seus equivalentes assíncronos chamam o método de transição de estado correspondente na fábrica de canais interno.

Implementação do ouvinte de canais

ChunkingChannelListener é um wrapper em torno de um ouvinte de canal interno. Sua função principal, além de delegar chamadas para esse ouvinte de canal interno, é encapsular novos ChunkingDuplexSessionChannels em torno de canais aceitos do ouvinte de canal interno. Isso é feito em OnAcceptChannel e OnEndAcceptChannel. O recém-criado ChunkingDuplexSessionChannel é passado pelo canal interno junto com os outros parâmetros descritos anteriormente.

Implementação do elemento de associação e da associação

Um ChunkingBindingElement é responsável por criar o ChunkingChannelFactory e o ChunkingChannelListener. O ChunkingBindingElement verifica se T em CanBuildChannelFactory<T> e CanBuildChannelListener<T> é do tipo IDuplexSessionChannel (o único canal com suporte pelo canal de separação em partes) e se os outros elementos de associação na associação dão suporte a esse tipo de canal.

Primeiro, BuildChannelFactory<T> verifica se a forma de canal solicitada pode ser criada e, em seguida, obtém uma lista de ações de mensagem a serem separadas em partes. Para saber mais, veja a seção a seguir. Em seguida, ele cria um novo ChunkingChannelFactory passando-lhe a fábrica de canais internos (conforme retornado de context.BuildInnerChannelFactory<IDuplexSessionChannel>), a lista de ações de mensagem e o número máximo de partes para buffer. O número máximo de partes vem de uma propriedade chamada MaxBufferedChunks exposta por ChunkingBindingElement.

BuildChannelListener<T> tem uma implementação semelhante para criar ChunkingChannelListener e passar o ouvinte de canal interno.

Há um exemplo de associação incluído neste exemplo chamado TcpChunkingBinding. Essa associação é formada por dois elementos de associação: TcpTransportBindingElement e ChunkingBindingElement. Além de expor a propriedade MaxBufferedChunks, a associação também define algumas das propriedades, TcpTransportBindingElement, tais como MaxReceivedMessageSize (define como ChunkingUtils.ChunkSize + 100 KB para cabeçalhos).

TcpChunkingBinding também implementa IBindingRuntimePreferences e retorna true do método ReceiveSynchronously que indica que apenas as chamadas síncronas de Receive são implementadas.

Determinar quais mensagens separar em partes

O canal de separação em partes separa apenas as mensagens identificadas por meio do atributo ChunkingBehavior. A classe ChunkingBehavior implementa IOperationBehavior e é implementada chamando o método AddBindingParameter. Nesse método, ChunkingBehavior examina o valor de sua propriedade AppliesTo (InMessageOutMessage ou ambos) para determinar quais mensagens devem ser separadas em partes. Em seguida, obtém a ação de cada uma dessas mensagens (da coleção Messages em OperationDescription) e a adiciona a uma coleção de cadeias de caracteres contida em uma instância do ChunkingBindingParameter. Em seguida, ele adiciona isso ChunkingBindingParameter ao BindingParameterCollection fornecido.

Isso BindingParameterCollection é passado dentro do BindingContext para cada elemento de associação na associação quando esse elemento de associação cria a fábrica de canais ou o ouvinte do canal. A implementação de ChunkingBindingElement de BuildChannelFactory<T> e BuildChannelListener<T> efetua pull desse ChunkingBindingParameter dos BindingContext's BindingParameterCollection. Em seguida, a coleção de ações contidas no ChunkingBindingParameter é passada para o ChunkingChannelFactory ou ChunkingChannelListener, que, por sua vez, passa para o ChunkingDuplexSessionChannel.

Executando o exemplo

Para configurar, compilar, e executar o exemplo

  1. Instale o ASP.NET 4.0 usando o seguinte comando.

    %windir%\Microsoft.NET\Framework\v4.0.XXXXX\aspnet_regiis.exe /i /enable
    
  2. Verifique se você executou o Procedimento de instalação única para os exemplos do Windows Communication Foundation.

  3. Para compilar a solução, siga as instruções contidas em Como compilar as amostras do Windows Communication Foundation.

  4. Para executar a amostra em uma configuração de computador único ou entre computadores, siga as instruções contidas em Como executar as amostras do Windows Communication Foundation.

  5. Execute o Service.exe primeiro, execute Client.exe em seguida e observe as duas janelas do console para obter saída.

Ao executar o exemplo, a saída a seguir é esperada.

Cliente:

Press enter when service is available

 > Sent chunk 1 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 2 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 3 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 4 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 5 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 6 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 7 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 8 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 9 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 10 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 1 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 2 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 3 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 4 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 5 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 6 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 7 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 8 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 9 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 < Received chunk 10 of message 5b226ad5-c088-4988-b737-6a565e0563dd

Servidor:

Service started, press enter to exit
 < Received chunk 1 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 2 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 3 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 4 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 5 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 6 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 7 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 8 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 9 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 < Received chunk 10 of message 867c1fd1-d39e-4be1-bc7b-32066d7ced10
 > Sent chunk 1 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 2 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 3 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 4 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 5 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 6 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 7 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 8 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 9 of message 5b226ad5-c088-4988-b737-6a565e0563dd
 > Sent chunk 10 of message 5b226ad5-c088-4988-b737-6a565e0563dd