Partilhar via


Canal Chunking

O ChunkingChannel sample mostra como um protocolo personalizado ou canal em camadas pode ser usado para realizar a segmentação e a dessegmentação de mensagens de tamanho arbitrariamente grandes.

Ao enviar mensagens grandes usando o Windows Communication Foundation (WCF), geralmente é 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 (assumindo que a maior parte dos dados está no corpo). No entanto, alguns protocolos exigem buffering de toda a mensagem. Mensagens confiáveis e segurança são dois desses exemplos. Outra solução possível é dividir a mensagem grande em mensagens menores chamadas blocos, enviar esses blocos um pedaço de cada vez e reconstituir a mensagem grande no lado recetor. A própria aplicação poderia fazer essa segmentação e dessegmentação ou então poderia usar um canal personalizado para fazê-lo.

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

Nota

O procedimento de configuração e as instruções de compilação para este exemplo estão localizados no final deste tópico.

Pressupostos e limitações do canal de fragmentação

Estrutura da mensagem

O canal de fragmentação assume a seguinte estrutura de mensagens para que as mensagens sejam fragmentadas:

<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 1 parâmetro de entrada estão em conformidade com essa forma de mensagem para sua mensagem de entrada. Da mesma forma, as operações de contrato que têm 1 parâmetro de saída ou valor de retorno estão em conformidade com essa forma de mensagem para sua mensagem de saída. Seguem-se exemplos de tais 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 fragmentação requer que as mensagens sejam entregues exatamente uma vez, na entrega ordenada de mensagens (blocos). Isso significa que a pilha de canais subjacente deve ser com estado de sessão. As sessões podem ser fornecidas pelo transporte (por exemplo, transporte TCP) ou por um canal de protocolo com estado de sessão (por exemplo, canal ReliableSession).

Envio e recebimento assíncronos

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

Protocolo de Fragmentação

O canal de fragmentação define um protocolo que indica o início e o fim de uma série de blocos, bem como o número de sequência de cada bloco. As três mensagens de exemplo a seguir demonstram as mensagens de início, parte e fim com comentários que descrevem os principais aspetos 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 em Bloco

<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 final

<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 Chunking

O canal chunking é um IDuplexSessionChannel que, a um nível elevado, segue a estrutura típica do canal. Há um ChunkingBindingElement que pode construir um ChunkingChannelFactory e um ChunkingChannelListener. O ChunkingChannelFactory cria instâncias de ChunkingChannel quando é solicitado. O ChunkingChannelListener cria instâncias de ChunkingChannel quando um novo canal interno é aceito. O ChunkingChannel próprio é responsável por enviar e receber mensagens.

No próximo nível abaixo, ChunkingChannel depende de vários componentes para implementar o protocolo de fragmentação. No lado remetente, o canal utiliza um elemento personalizado XmlDictionaryWriter denominado ChunkingWriter que realiza a segmentação propriamente dita. ChunkingWriter usa o canal interno diretamente para enviar fragmentos. O uso de um componente personalizado XmlDictionaryWriter permite-nos enviar partes enquanto o grande corpo da mensagem original está a ser escrito. Isso significa que não armazenamos em buffer toda a mensagem original.

Diagrama que mostra a arquitetura de envio do canal de fragmentação.

No lado de receção, ChunkingChannel puxa mensagens do canal interno e entrega-as a um personalizado XmlDictionaryReader chamado ChunkingReader, que reconstitui a mensagem original a partir dos fragmentos recebidos. ChunkingChannel Encapsula isso ChunkingReader em uma implementação personalizada Message chamada ChunkingMessage e retorna essa mensagem para a camada acima. Essa combinação de ChunkingReader e ChunkingMessage nos permite desfragmentar o corpo da mensagem original à medida que ela está sendo lida pela camada acima, em vez de ter que armazenar em buffer todo o corpo da mensagem original. ChunkingReader tem uma fila onde armazena em buffer os blocos de entrada até um número máximo configurável de blocos em buffer. Quando esse limite máximo é atingido, o leitor espera que as mensagens sejam drenadas da fila pela camada acima (ou seja, apenas lendo o 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 fragmentação.

Modelo de programação de fragmentação

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

[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);

}

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

Implementando a operação de envio

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

Se a mensagem precisar ser fragmentada, Enviar criará uma nova ChunkingWriter e chamará WriteBodyContents na mensagem de saída, passando-lhe este ChunkingWriter. Em seguida, o ChunkingWriter realiza o fragmentamento da mensagem (incluindo a cópia dos cabeçalhos da mensagem original para o fragmento inicial) e envia os fragmentos usando o canal interno.

Alguns detalhes que merecem destaque:

  • Envie as primeiras chamadas ThrowIfDisposedOrNotOpened para garantir que o CommunicationState está aberto.

  • O envio é sincronizado para que apenas uma mensagem possa ser enviada de cada vez para cada sessão. Há um ManualResetEvent chamado sendingDone que é redefinido quando uma mensagem em partes está a ser enviada. Uma vez que a mensagem de bloco final é enviada, esse evento é definido. O método Send aguarda que esse evento seja definido antes de tentar enviar a mensagem de saída.

  • Enviar bloqueia o CommunicationObject.ThisLock para evitar alterações de estado sincronizadas durante o envio. Consulte a CommunicationObject documentação para obter mais informações sobre CommunicationObject estados e máquina de estado.

  • O tempo limite passado para Enviar é usado como o tempo limite para toda a operação de envio, que inclui o envio de todos os blocos.

  • O design personalizado XmlDictionaryWriter foi escolhido para evitar o buffer de todo o corpo da mensagem original. Se fôssemos obter um XmlDictionaryReader no corpo usando message.GetReaderAtBodyContents, todo o corpo seria armazenado em buffer. Em vez disso, temos um XmlDictionaryWriter personalizado que é transferido para message.WriteBodyContents. À medida que a mensagem chama WriteBase64 no gravador, o gravador empacota partes em mensagens e as envia usando o canal interno. WriteBase64 bloqueia até que o bloco seja enviado.

Implementando a operação de receção

De forma geral, a operação Receber primeiro verifica se a mensagem recebida não é null e se sua ação é o ChunkingAction. Caso não cumpra ambos os critérios, a mensagem será devolvida inalterada a partir do componente Receber. Caso contrário, Receive criará um novo ChunkingReader e um novo ChunkingMessage embrulhado em torno dele (chamando GetNewChunkingMessage). Antes de retornar esse novo ChunkingMessage, Receive utiliza uma thread do threadpool para executar ReceiveChunkLoop, que chama innerChannel.Receive em um loop e transfere pedaços para o ChunkingReader até que a mensagem de fim do pedaço seja recebida ou o tempo limite de recebimento seja atingido.

Alguns detalhes que merecem destaque:

  • Como o Enviar, receba as primeiras chamadas ThrowIfDisposedOrNotOpened para garantir que o CommunicationState seja Aberto.

  • O recebimento também é sincronizado para que apenas uma mensagem possa ser recebida por vez da sessão. Isso é especialmente importante porque, uma vez que uma mensagem de bloco inicial é recebida, espera-se que todas as mensagens recebidas subsequentes sejam partes dentro dessa nova sequência de blocos até que uma mensagem de bloco final seja recebida. Receber não pode extrair mensagens do canal interno até que todas as partes que pertencem à mensagem que está sendo desfragmentada no momento sejam recebidas. Para fazer isso, o Receive utiliza um ManualResetEvent denominado currentMessageCompleted, que é configurado quando a mensagem de bloco final é recebida e é redefinido quando uma nova mensagem de bloco inicial é recebida.

  • Ao contrário do Envio, o Recebimento não impede transições de estado sincronizadas durante o recebimento. Por exemplo, Close pode ser chamado durante o recebimento e aguarda 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 Receber é 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 a consumir o corpo da mensagem a uma taxa inferior à taxa de fragmentos de entrada, o ChunkingReader armazena esses fragmentos de entrada até ao limite especificado por ChunkingBindingElement.MaxBufferedChunks. Uma vez atingido esse limite, não são retirados mais pedaços da camada inferior até que um pedaço em buffer seja consumido 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 configura stopReceive para true sinalizar a ação pendente ReceiveChunkLoop para parar. Em seguida, aguarda o receiveStoppedManualResetEvent, que é definido quando ReceiveChunkLoop para. Supondo que ReceiveChunkLoop pare dentro do tempo limite especificado, OnClose chama innerChannel.Close com o tempo limite restante.

OnAbort

OnAbort chama innerChannel.Abort para abortar o canal interno. Se houver uma pendente ReceiveChunkLoop , ela receberá uma exceção da chamada pendente innerChannel.Receive .

OnFaulted

O ChunkingChannel não requer comportamento especial quando o canal está com defeito, portanto, OnFaulted não é substituído.

Implementação do Channel Factory

O ChunkingChannelFactory é responsável pela criação de instâncias de ChunkingDuplexSessionChannel e pelas transições de estado em cascata para a fábrica de canais internos.

OnCreateChannel Usa a fábrica de canais internos para criar um IDuplexSessionChannel canal interno. Em seguida, ele cria um novo ChunkingDuplexSessionChannel, passando este canal interno para ele, juntamente com a lista de ações de mensagem a serem fragmentadas e o número máximo de blocos para armazenamento temporário após a recepção. A lista de ações de mensagem a serem fragmentadas 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 em ChunkingBindingElement descreve de onde vêm esses valores.

O OnOpen, o OnClose, OnAbort e seus equivalentes assíncronos chamam o método de transição de estado correspondente na fábrica interna do canal.

Implementando o Channel Listener

O ChunkingChannelListener é um invólucro em torno de um ouvinte de canal interno. Sua principal função, além de delegar chamadas para esse ouvinte do canal interno, é envolver novos ChunkingDuplexSessionChannels em torno dos canais aceitos por esse ouvinte do canal interno. Isto é feito em OnAcceptChannel e OnEndAcceptChannel. O recém-criado ChunkingDuplexSessionChannel é passado o canal interno juntamente com os outros parâmetros descritos anteriormente.

Implementação do Elemento de Ligação e Ligação

ChunkingBindingElement é responsável pela criação do ChunkingChannelFactory e ChunkingChannelListener. O ChunkingBindingElement verifica se T emCanBuildChannelFactory< T> e CanBuildChannelListener<T> é do tipo IDuplexSessionChannel (o único canal suportado pelo canal de fragmentação) e se os outros elementos de ligação na associação suportam esse tipo de canal.

BuildChannelFactory <T> primeiro verifica se o tipo de canal solicitado pode ser criado e, em seguida, obtém uma lista de ações de mensagem a serem divididas. Para obter mais informações, consulte a seção a seguir. Em seguida, ele cria uma nova ChunkingChannelFactory, passando-lhe a fábrica do canal interno (conforme retornado de context.BuildInnerChannelFactory<IDuplexSessionChannel>), a lista de ações de mensagem e o número máximo de blocos a serem armazenados em buffer. O número máximo de blocos vem de uma propriedade chamada MaxBufferedChunks, exposta pelo ChunkingBindingElement.

BuildChannelListener<T> tem uma implementação semelhante para criar ChunkingChannelListener e passar a escuta do canal interno.

Há um exemplo de vinculação incluído neste exemplo chamado TcpChunkingBinding. Esta ligação consiste em dois elementos de ligação: TcpTransportBindingElement e ChunkingBindingElement. Além de expor a MaxBufferedChunks propriedade, a associação também define algumas das TcpTransportBindingElement propriedades, como MaxReceivedMessageSize (definindo-a como ChunkingUtils.ChunkSize + 100KB para os cabeçalhos).

TcpChunkingBinding também implementa IBindingRuntimePreferences e retorna true do método ReceiveSynchronously, indicando que apenas as operações de receção síncronas são implementadas.

Determinando quais mensagens devem ser fragmentadas

O canal de fragmentação fragmenta apenas as mensagens identificadas através do ChunkingBehavior atributo. A ChunkingBehavior classe implementa IOperationBehavior e é implementada chamando o AddBindingParameter método. Neste método, o ChunkingBehavior examina o valor de sua AppliesTo propriedade (InMessage, OutMessage ou ambos) para determinar quais mensagens devem ser divididas. Em seguida, ele obtém a ação de cada uma dessas mensagens (da coleção Messages em OperationDescription) e a adiciona a uma coleção de cadeia de caracteres contida em uma instância de ChunkingBindingParameter. Em seguida, acrescenta este ChunkingBindingParameter ao fornecido BindingParameterCollection.

Isso BindingParameterCollection é passado dentro do BindingContext para cada elemento de ligação na associação quando esse elemento de ligação cria a fábrica de canais ou o ouvinte de canal. A implementação de BuildChannelFactory<T> e BuildChannelListener<T> pelo ChunkingBindingElement retira este ChunkingBindingParameter dos BindingContext's BindingParameterCollection. A coleção de ações contidas no ChunkingBindingParameter é então 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. Certifique-se de ter executado o procedimento de configuração única dos exemplos do Windows Communication Foundation.

  3. Para criar a solução, siga as instruções em Criando os exemplos do Windows Communication Foundation.

  4. Para executar o exemplo em uma configuração de máquina única ou cruzada, siga as instruções em Executando os exemplos do Windows Communication Foundation.

  5. Execute primeiro o Service.exe, depois execute o Client.exe e observe as duas janelas do console para verificar a 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