Partilhar via


Canal Chunking

O exemplo ChunkingChannel mostra como um protocolo personalizado ou canal em camadas pode ser usado para fazer chunking e dechunking de mensagens 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. O próprio aplicativo poderia fazer esse chunking e de-chunking ou 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 estar sempre em camadas abaixo de um canal de segurança e 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 sessionful. As sessões podem ser fornecidas pelo transporte (por exemplo, transporte TCP) ou por um canal de protocolo 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 Chunk

<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, em um alto nível, segue a arquitetura 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 de envio, o canal usa um personalizado XmlDictionaryWriter chamado ChunkingWriter que faz o chunking real. ChunkingWriter usa o canal interno diretamente para enviar partes. O uso de um personalizado XmlDictionaryWriter 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 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 dos blocos 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 a mensagem de saída passando-a assim ChunkingWriter. O ChunkingWriter then faz o fragmentamento da mensagem (incluindo a cópia dos cabeçalhos da mensagem original para a mensagem do bloco inicial) e envia partes 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 nome sendingDone que é redefinido quando uma mensagem em partes está sendo 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 para evitar alterações de estado sincronizadas durante o CommunicationObject.ThisLock 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 o corpo inteiro seria tamponado. Em vez disso, temos um costume XmlDictionaryWriter que é passado 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 recebimento

Em um nível alto, a operação Receber primeiro verifica se a mensagem recebida não null é e se sua ação é o ChunkingAction. Se não atender aos dois critérios, a mensagem será retornada inalterada de 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 usa um thread threadpool para executar ReceiveChunkLoop, que chama innerChannel.Receive em um loop e entrega blocos para a ChunkingReader mensagem de bloco até que o final seja recebido 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, Receive usa um ManualResetEvent nome currentMessageCompleted, que é definido quando a mensagem do bloco final é recebida e redefinido quando uma nova mensagem do 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 consumindo o corpo da mensagem a uma taxa menor do que a taxa de mensagens de bloco de entrada, os ChunkingReader buffers desses blocos de entrada até o 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 chamadas innerChannel.Open para abrir o canal interno.

OnClose

OnClose primeiro define stopReceive para true sinalizar o pendente ReceiveChunkLoop para parar. Em seguida, aguarda o receiveStopped ManualResetEvent, que é definido quando ReceiveChunkLoop para. Supondo as ReceiveChunkLoop paradas dentro do tempo limite especificado, OnClose as chamadas innerChannel.Close com o tempo limite restante.

OnAbort

OnAbort chamadas 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.

Implementando o Channel Factory

O ChunkingChannelFactory é responsável pela criação de instâncias e por transições de ChunkingDuplexSessionChannel 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-o este canal interno, juntamente com a lista de ações de mensagem a serem fragmentadas e o número máximo de blocos para buffer após receber. 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, OnCloseOnAbort e seus equivalentes assíncronos chamam o método de transição de estado correspondente na fábrica de canal interno.

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 canais aceitos do 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.

Implementando o elemento de vinculação e a vinculação

ChunkingBindingElement é responsável pela criação do ChunkingChannelFactory e ChunkingChannelListener. O ChunkingBindingElement verifica se T em<CanBuildChannelFactory 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 passagem para 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 para buffer. O número máximo de partes vem de uma propriedade chamada MaxBufferedChunks exposed by the ChunkingBindingElement.

BuildChannelListener<T> tem uma implementação semelhante para criar ChunkingChannelListener e passar o ouvinte 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 (define como ChunkingUtils.ChunkSize + 100KB bytes para cabeçalhos).

TcpChunkingBinding também implementa IBindingRuntimePreferences e retorna true do ReceiveSynchronously método indicando que apenas as chamadas de recebimento 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 isso 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 ChunkingBindingElementimplementação do BuildChannelFactory<T> 's e BuildChannelListener<T> retirar isso ChunkingBindingParameter do 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 instalação única para os 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 Service.exe primeiro, depois execute Client.exe e observe as duas janelas do console para 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